diff --git a/.eslintrc.js b/.eslintrc.js index 7bc7d0960..f8f46d5fa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -37,24 +37,6 @@ module.exports = { 'dot-notation': 'error' }, overrides: [ - // Introduce prettier. but ignore files to avoid conflicts with PR. - { - files: [ - // https://github.com/vuejs/eslint-plugin-vue/pull/819 - 'lib/rules/attributes-order.js', - 'tests/lib/rules/attributes-order.js' - ], - extends: [ - 'plugin:eslint-plugin/recommended', - 'plugin:vue-libs/recommended' - ], - rules: { - 'prettier/prettier': 'off', - - 'rest-spread-spacing': 'error', - 'no-mixed-operators': 'error' - } - }, { files: ['lib/rules/*.js'], rules: { diff --git a/.gitignore b/.gitignore index 688618a31..e1401b951 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ yarn.lock yarn-error.log docs/.vuepress/dist +typings/eslint/lib/rules diff --git a/.vscode/settings.json b/.vscode/settings.json index 7bd96f8d9..841a29074 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "eslint.validate": [ "javascript", "javascriptreact", - { "language": "vue", "autoFix": true } - ] + "vue" + ], + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/docs/developer-guide/README.md b/docs/developer-guide/README.md index bdc148105..658a4bf66 100644 --- a/docs/developer-guide/README.md +++ b/docs/developer-guide/README.md @@ -47,3 +47,10 @@ Check out an [example rule](https://github.com/vuejs/eslint-plugin-vue/blob/mast Please be aware that regarding what kind of code examples you'll write in tests, you'll have to accordingly setup the parser in `RuleTester` (you can do it on per test case basis though). [See an example here](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/attribute-hyphenation.js#L19) If you'll stuck, remember there are plenty of rules you can learn from already, and if you can't find the right solution - don't hesitate to reach out in issues. We're happy to help! + +## :white_check_mark: JSDoc type checking with TypeScript + +We have type checking enabled via TypeScript and JSDoc. +The command to perform type checking is: `npm run tsc` + +This is just to help you write the rules, not to do strict type checking. If you find it difficult to resolve type checking warnings, feel free to suppress warnings using the `// @ts-nocheck` and `// @ts-ignore` comment. diff --git a/lib/processor.js b/lib/processor.js index b018e0bb8..81ab94784 100644 --- a/lib/processor.js +++ b/lib/processor.js @@ -13,6 +13,7 @@ */ module.exports = { + /** @param {string} code */ preprocess(code) { return [code] }, @@ -34,6 +35,7 @@ module.exports = { disableRuleKeys: new Map() } } + /** @type {string[]} */ const usedDisableDirectiveKeys = [] /** @type {Map} */ const unusedDisableDirectiveReports = new Map() @@ -88,15 +90,15 @@ module.exports = { if (state.line.disableAllKeys.size) { disableDirectiveKeys.push(...state.line.disableAllKeys) } - if (state.block.disableRuleKeys.has(message.ruleId)) { - disableDirectiveKeys.push( - ...state.block.disableRuleKeys.get(message.ruleId) - ) - } - if (state.line.disableRuleKeys.has(message.ruleId)) { - disableDirectiveKeys.push( - ...state.line.disableRuleKeys.get(message.ruleId) - ) + if (message.ruleId) { + const block = state.block.disableRuleKeys.get(message.ruleId) + if (block) { + disableDirectiveKeys.push(...block) + } + const line = state.line.disableRuleKeys.get(message.ruleId) + if (line) { + disableDirectiveKeys.push(...line) + } } if (disableDirectiveKeys.length) { @@ -153,8 +155,8 @@ function messageToKey(message) { /** * Compares the locations of two objects in a source file - * @param {{line: number, column: number}} itemA The first object - * @param {{line: number, column: number}} itemB The second object + * @param {Position} itemA The first object + * @param {Position} itemB The second object * @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if * itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location. */ diff --git a/lib/rules/arrow-spacing.js b/lib/rules/arrow-spacing.js index 6586e8389..3d626c614 100644 --- a/lib/rules/arrow-spacing.js +++ b/lib/rules/arrow-spacing.js @@ -5,5 +5,5 @@ const { wrapCoreRule } = require('../utils') -// eslint-disable-next-line +// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule(require('eslint/lib/rules/arrow-spacing')) diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js index ed5ab1904..1fd7b5e9b 100644 --- a/lib/rules/attribute-hyphenation.js +++ b/lib/rules/attribute-hyphenation.js @@ -45,7 +45,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() const option = context.options[0] @@ -61,6 +61,10 @@ module.exports = { useHyphenated ? 'kebab-case' : 'camelCase' ) + /** + * @param {VDirective | VAttribute} node + * @param {string} name + */ function reportIssue(node, name) { const text = sourceCode.getText(node.key) @@ -78,6 +82,9 @@ module.exports = { }) } + /** + * @param {string} value + */ function isIgnoredAttribute(value) { const isIgnored = ignoredAttributes.some((attr) => { return value.indexOf(attr) !== -1 @@ -101,7 +108,9 @@ module.exports = { const name = !node.directive ? node.key.rawName : node.key.name.name === 'bind' - ? node.key.argument && node.key.argument.rawName + ? node.key.argument && + node.key.argument.type === 'VIdentifier' && + node.key.argument.rawName : /* otherwise */ false if (!name || isIgnoredAttribute(name)) return diff --git a/lib/rules/attributes-order.js b/lib/rules/attributes-order.js index 2f3d54f09..794628a5c 100644 --- a/lib/rules/attributes-order.js +++ b/lib/rules/attributes-order.js @@ -22,14 +22,29 @@ const ATTRS = { CONTENT: 'CONTENT' } -function getAttributeName (attribute, sourceCode) { - const isBind = attribute.directive && attribute.key.name.name === 'bind' - return isBind - ? (attribute.key.argument ? sourceCode.getText(attribute.key.argument) : '') - : (attribute.directive ? getDirectiveKeyName(attribute.key, sourceCode) : attribute.key.name) +/** + * @param {VAttribute | VDirective} attribute + * @param {SourceCode} sourceCode + */ +function getAttributeName(attribute, sourceCode) { + if (attribute.directive) { + if (attribute.key.name.name === 'bind') { + return attribute.key.argument + ? sourceCode.getText(attribute.key.argument) + : '' + } else { + return getDirectiveKeyName(attribute.key, sourceCode) + } + } else { + return attribute.key.name + } } -function getDirectiveKeyName (directiveKey, sourceCode) { +/** + * @param {VDirectiveKey} directiveKey + * @param {SourceCode} sourceCode + */ +function getDirectiveKeyName(directiveKey, sourceCode) { let text = `v-${directiveKey.name.name}` if (directiveKey.argument) { text += `:${sourceCode.getText(directiveKey.argument)}` @@ -40,56 +55,92 @@ function getDirectiveKeyName (directiveKey, sourceCode) { return text } -function getAttributeType (attribute, sourceCode) { - const isBind = attribute.directive && attribute.key.name.name === 'bind' - const name = isBind - ? (attribute.key.argument ? sourceCode.getText(attribute.key.argument) : '') - : (attribute.directive ? attribute.key.name.name : attribute.key.name) - - if (attribute.directive && !isBind) { - if (name === 'for') { - return ATTRS.LIST_RENDERING - } else if (name === 'if' || name === 'else-if' || name === 'else' || name === 'show' || name === 'cloak') { - return ATTRS.CONDITIONALS - } else if (name === 'pre' || name === 'once') { - return ATTRS.RENDER_MODIFIERS - } else if (name === 'model') { - return ATTRS.TWO_WAY_BINDING - } else if (name === 'on') { - return ATTRS.EVENTS - } else if (name === 'html' || name === 'text') { - return ATTRS.CONTENT - } else if (name === 'slot') { - return ATTRS.UNIQUE - } else { - return ATTRS.OTHER_DIRECTIVES +/** + * @param {VAttribute | VDirective} attribute + * @param {SourceCode} sourceCode + */ +function getAttributeType(attribute, sourceCode) { + let propName + if (attribute.directive) { + if (attribute.key.name.name !== 'bind') { + const name = attribute.key.name.name + if (name === 'for') { + return ATTRS.LIST_RENDERING + } else if ( + name === 'if' || + name === 'else-if' || + name === 'else' || + name === 'show' || + name === 'cloak' + ) { + return ATTRS.CONDITIONALS + } else if (name === 'pre' || name === 'once') { + return ATTRS.RENDER_MODIFIERS + } else if (name === 'model') { + return ATTRS.TWO_WAY_BINDING + } else if (name === 'on') { + return ATTRS.EVENTS + } else if (name === 'html' || name === 'text') { + return ATTRS.CONTENT + } else if (name === 'slot') { + return ATTRS.UNIQUE + } else { + return ATTRS.OTHER_DIRECTIVES + } } + propName = attribute.key.argument + ? sourceCode.getText(attribute.key.argument) + : '' } else { - if (name === 'is') { - return ATTRS.DEFINITION - } else if (name === 'id') { - return ATTRS.GLOBAL - } else if (name === 'ref' || name === 'key' || name === 'slot' || name === 'slot-scope') { - return ATTRS.UNIQUE - } else { - return ATTRS.OTHER_ATTR - } + propName = attribute.key.name + } + if (propName === 'is') { + return ATTRS.DEFINITION + } else if (propName === 'id') { + return ATTRS.GLOBAL + } else if ( + propName === 'ref' || + propName === 'key' || + propName === 'slot' || + propName === 'slot-scope' + ) { + return ATTRS.UNIQUE + } else { + return ATTRS.OTHER_ATTR } } -function getPosition (attribute, attributePosition, sourceCode) { +/** + * @param {VAttribute | VDirective} attribute + * @param { { [key: string]: number } } attributePosition + * @param {SourceCode} sourceCode + */ +function getPosition(attribute, attributePosition, sourceCode) { const attributeType = getAttributeType(attribute, sourceCode) - return attributePosition.hasOwnProperty(attributeType) ? attributePosition[attributeType] : -1 + return attributePosition.hasOwnProperty(attributeType) + ? attributePosition[attributeType] + : -1 } -function isAlphabetical (prevNode, currNode, sourceCode) { - const isSameType = getAttributeType(prevNode, sourceCode) === getAttributeType(currNode, sourceCode) +/** + * @param {VAttribute | VDirective} prevNode + * @param {VAttribute | VDirective} currNode + * @param {SourceCode} sourceCode + */ +function isAlphabetical(prevNode, currNode, sourceCode) { + const isSameType = + getAttributeType(prevNode, sourceCode) === + getAttributeType(currNode, sourceCode) if (isSameType) { const prevName = getAttributeName(prevNode, sourceCode) const currName = getAttributeName(currNode, sourceCode) if (prevName === currName) { - const prevIsBind = Boolean(prevNode.directive && prevNode.key.name.name === 'bind') - const currIsBind = Boolean(currNode.directive && currNode.key.name.name === 'bind') + const prevIsBind = Boolean( + prevNode.directive && prevNode.key.name.name === 'bind' + ) + const currIsBind = Boolean( + currNode.directive && currNode.key.name.name === 'bind' + ) return prevIsBind <= currIsBind } return prevName < currName @@ -97,24 +148,57 @@ function isAlphabetical (prevNode, currNode, sourceCode) { return true } -function create (context) { +/** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ +function create(context) { const sourceCode = context.getSourceCode() - let attributeOrder = [ATTRS.DEFINITION, ATTRS.LIST_RENDERING, ATTRS.CONDITIONALS, ATTRS.RENDER_MODIFIERS, ATTRS.GLOBAL, ATTRS.UNIQUE, ATTRS.TWO_WAY_BINDING, ATTRS.OTHER_DIRECTIVES, ATTRS.OTHER_ATTR, ATTRS.EVENTS, ATTRS.CONTENT] + let attributeOrder = [ + ATTRS.DEFINITION, + ATTRS.LIST_RENDERING, + ATTRS.CONDITIONALS, + ATTRS.RENDER_MODIFIERS, + ATTRS.GLOBAL, + ATTRS.UNIQUE, + ATTRS.TWO_WAY_BINDING, + ATTRS.OTHER_DIRECTIVES, + ATTRS.OTHER_ATTR, + ATTRS.EVENTS, + ATTRS.CONTENT + ] if (context.options[0] && context.options[0].order) { attributeOrder = context.options[0].order } + const alphabetical = Boolean( + context.options[0] && context.options[0].alphabetical + ) + + /** @type { { [key: string]: number } } */ const attributePosition = {} attributeOrder.forEach((item, i) => { - if (item instanceof Array) { + if (Array.isArray(item)) { item.forEach((attr) => { attributePosition[attr] = i }) } else attributePosition[item] = i }) - let currentPosition - let previousNode - function reportIssue (node, previousNode) { + /** + * @typedef {object} State + * @property {number} currentPosition + * @property {VAttribute | VDirective} previousNode + */ + /** + * @type {State | null} + */ + let state + + /** + * @param {VAttribute | VDirective} node + * @param {VAttribute | VDirective} previousNode + */ + function reportIssue(node, previousNode) { const currentNode = sourceCode.getText(node.key) const prevNode = sourceCode.getText(previousNode.key) context.report({ @@ -125,12 +209,18 @@ function create (context) { currentNode }, - fix (fixer) { + fix(fixer) { const attributes = node.parent.attributes - const shiftAttrs = attributes.slice(attributes.indexOf(previousNode), attributes.indexOf(node) + 1) + const shiftAttrs = attributes.slice( + attributes.indexOf(previousNode), + attributes.indexOf(node) + 1 + ) return shiftAttrs.map((attr, i) => { - const text = attr === previousNode ? sourceCode.getText(node) : sourceCode.getText(shiftAttrs[i - 1]) + const text = + attr === previousNode + ? sourceCode.getText(node) + : sourceCode.getText(shiftAttrs[i - 1]) return fixer.replaceText(attr, text) }) } @@ -138,20 +228,26 @@ function create (context) { } return utils.defineTemplateBodyVisitor(context, { - 'VStartTag' () { - currentPosition = -1 - previousNode = null + VStartTag() { + state = null }, - 'VAttribute' (node) { + VAttribute(node) { let inAlphaOrder = true - if (currentPosition !== -1 && (context.options[0] && context.options[0].alphabetical)) { - inAlphaOrder = isAlphabetical(previousNode, node, sourceCode) + if (state && alphabetical) { + inAlphaOrder = isAlphabetical(state.previousNode, node, sourceCode) } - if ((currentPosition === -1) || ((currentPosition <= getPosition(node, attributePosition, sourceCode)) && inAlphaOrder)) { - currentPosition = getPosition(node, attributePosition, sourceCode) - previousNode = node + if ( + !state || + (state.currentPosition <= + getPosition(node, attributePosition, sourceCode) && + inAlphaOrder) + ) { + state = { + currentPosition: getPosition(node, attributePosition, sourceCode), + previousNode: node + } } else { - reportIssue(node, previousNode) + reportIssue(node, state.previousNode) } } }) diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index c0ca04175..4aebfabab 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -5,5 +5,5 @@ const { wrapCoreRule } = require('../utils') -// eslint-disable-next-line +// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule(require('eslint/lib/rules/camelcase')) diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js index cfd0a4185..aa6c83c5e 100644 --- a/lib/rules/comma-dangle.js +++ b/lib/rules/comma-dangle.js @@ -5,5 +5,5 @@ const { wrapCoreRule } = require('../utils') -// eslint-disable-next-line +// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule(require('eslint/lib/rules/comma-dangle')) diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js index 40772dc37..e7313b8ac 100644 --- a/lib/rules/comma-style.js +++ b/lib/rules/comma-style.js @@ -11,6 +11,7 @@ module.exports = wrapCoreRule(require('eslint/lib/rules/comma-style'), { return { VSlotScopeExpression(node) { if (coreHandlers.FunctionExpression) { + // @ts-expect-error -- Process params of VSlotScopeExpression as FunctionExpression. coreHandlers.FunctionExpression(node) } } diff --git a/lib/rules/comment-directive.js b/lib/rules/comment-directive.js index 74d528a96..9ff9d2fdf 100644 --- a/lib/rules/comment-directive.js +++ b/lib/rules/comment-directive.js @@ -5,17 +5,17 @@ 'use strict' -/** - * @typedef {import('eslint').Rule.RuleContext} RuleContext - * @typedef {import('vue-eslint-parser').AST.VDocumentFragment} VDocumentFragment - * @typedef {import('vue-eslint-parser').AST.VElement} VElement - * @typedef {import('vue-eslint-parser').AST.Token} Token - * @typedef {import('vue-eslint-parser').AST.Location} Location - */ +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + /** * @typedef {object} RuleAndLocation * @property {string} RuleAndLocation.ruleId * @property {number} RuleAndLocation.index + * @property {string} [RuleAndLocation.key] */ // ----------------------------------------------------------------------------- // Helpers @@ -218,7 +218,7 @@ function reportUnused(context, comment, kind) { * @param {Token} comment The comment token to report. * @param {string} kind The comment directive kind. * @param {RuleAndLocation[]} rules To report rule. - * @returns { { ruleId: string; key: string; }[] } + * @returns { { ruleId: string, key: string }[] } */ function reportUnusedRules(context, comment, kind, rules) { const sourceCode = context.getSourceCode() @@ -245,7 +245,7 @@ function reportUnusedRules(context, comment, kind, rules) { /** * Gets the key of location - * @param {Location} location The location + * @param {Position} location The location * @returns {string} The key */ function locToKey(location) { @@ -258,15 +258,7 @@ function locToKey(location) { * @returns {VElement[]} The top-level elements */ function extractTopLevelHTMLElements(documentFragment) { - return documentFragment.children.filter(isVElement) - - /** - * @param {any} e - * @returns {e is VElement} - */ - function isVElement(e) { - return e.type === 'VElement' - } + return documentFragment.children.filter(utils.isVElement) } /** * Extracts the top-level comments in document fragment. @@ -324,7 +316,10 @@ module.exports = { "Unused {{kind}} directive (no problems were reported from '{{rule}}')." } }, - + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { const options = context.options[0] || {} /** @type {boolean} */ diff --git a/lib/rules/component-definition-name-casing.js b/lib/rules/component-definition-name-casing.js index 453e3943e..1f2e59a85 100644 --- a/lib/rules/component-definition-name-casing.js +++ b/lib/rules/component-definition-name-casing.js @@ -28,7 +28,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] const caseType = @@ -38,15 +38,20 @@ module.exports = { // Public // ---------------------------------------------------------------------- + /** + * @param {Literal | TemplateLiteral} node + */ function convertName(node) { + /** @type {string} */ let nodeValue + /** @type {Range} */ let range if (node.type === 'TemplateLiteral') { const quasis = node.quasis[0] nodeValue = quasis.value.cooked range = quasis.range } else { - nodeValue = node.value + nodeValue = `${node.value}` range = node.range } @@ -67,6 +72,10 @@ module.exports = { } } + /** + * @param {Expression | SpreadElement} node + * @returns {node is (Literal | TemplateLiteral)} + */ function canConvert(node) { return ( node.type === 'Literal' || @@ -88,14 +97,10 @@ module.exports = { } }), utils.executeOnVue(context, (obj) => { - const node = obj.properties.find( - (item) => - item.type === 'Property' && - item.key.name === 'name' && - canConvert(item.value) - ) + const node = utils.findProperty(obj, 'name') if (!node) return + if (!canConvert(node.value)) return convertName(node.value) }) ) diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js index 90eb48d84..a4771c0bf 100644 --- a/lib/rules/component-name-in-template-casing.js +++ b/lib/rules/component-name-in-template-casing.js @@ -55,18 +55,20 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const caseOption = context.options[0] const options = context.options[1] || {} const caseType = allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase + /** @type {RegExp[]} */ const ignores = (options.ignores || []).map(toRegExp) const registeredComponentsOnly = options.registeredComponentsOnly !== false const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore() + /** @type { string[] } */ const registeredComponents = [] /** @@ -148,20 +150,18 @@ module.exports = { } } }, - Object.assign( - { - Program(node) { - hasInvalidEOF = utils.hasInvalidEOF(node) - } + { + Program(node) { + hasInvalidEOF = utils.hasInvalidEOF(node) }, - registeredComponentsOnly + ...(registeredComponentsOnly ? utils.executeOnVue(context, (obj) => { registeredComponents.push( ...utils.getRegisteredComponents(obj).map((n) => n.name) ) }) - : {} - ) + : {}) + } ) } } diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js index 6fa6f211e..b2a2692a8 100644 --- a/lib/rules/component-tags-order.js +++ b/lib/rules/component-tags-order.js @@ -48,13 +48,17 @@ module.exports = { 'The <{{name}}> should be above the <{{firstUnorderedName}}> on line {{line}}.' } }, + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { - /** @type {Map} */ const orderMap = new Map() - ;( - (context.options[0] && context.options[0].order) || - DEFAULT_ORDER - ).forEach((nameOrNames, index) => { + /** @type {(string|string[])[]} */ + const orderOptions = + (context.options[0] && context.options[0].order) || DEFAULT_ORDER + orderOptions.forEach((nameOrNames, index) => { if (Array.isArray(nameOrNames)) { for (const name of nameOrNames) { orderMap.set(name, index) @@ -63,17 +67,29 @@ module.exports = { orderMap.set(nameOrNames, index) } }) + + /** + * @param {string} name + */ + function getOrderPosition(name) { + const num = orderMap.get(name) + return num == null ? -1 : num + } const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment() function getTopLevelHTMLElements() { if (documentFragment) { - return documentFragment.children.filter((e) => e.type === 'VElement') + return documentFragment.children.filter(utils.isVElement) } return [] } + /** + * @param {VElement} element + * @param {VElement} firstUnorderedElement + */ function report(element, firstUnorderedElement) { context.report({ node: element, @@ -87,33 +103,29 @@ module.exports = { }) } - return utils.defineTemplateBodyVisitor( - context, - {}, - { - Program(node) { - if (utils.hasInvalidEOF(node)) { + return { + Program(node) { + if (utils.hasInvalidEOF(node)) { + return + } + const elements = getTopLevelHTMLElements() + + elements.forEach((element, index) => { + const expectedIndex = getOrderPosition(element.name) + if (expectedIndex < 0) { return } - const elements = getTopLevelHTMLElements() - - elements.forEach((element, index) => { - const expectedIndex = orderMap.get(element.name) - if (expectedIndex < 0) { - return - } - const firstUnordered = elements - .slice(0, index) - .filter((e) => expectedIndex < orderMap.get(e.name)) - .sort( - (e1, e2) => orderMap.get(e1.name) - orderMap.get(e2.name) - )[0] - if (firstUnordered) { - report(element, firstUnordered) - } - }) - } + const firstUnordered = elements + .slice(0, index) + .filter((e) => expectedIndex < getOrderPosition(e.name)) + .sort( + (e1, e2) => getOrderPosition(e1.name) - getOrderPosition(e2.name) + )[0] + if (firstUnordered) { + report(element, firstUnordered) + } + }) } - ) + } } } diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js index cec6f55d7..cd7f73443 100644 --- a/lib/rules/custom-event-name-casing.js +++ b/lib/rules/custom-event-name-casing.js @@ -4,11 +4,6 @@ */ 'use strict' -/** - * @typedef {import('vue-eslint-parser').AST.ESLintLiteral} Literal - * @typedef {import('vue-eslint-parser').AST.ESLintCallExpression} CallExpression - */ - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ @@ -33,7 +28,7 @@ function isValidEventName(name) { /** * Get the name param node from the given CallExpression * @param {CallExpression} node CallExpression - * @returns { Literal & { value: string } } + * @returns { Literal & { value: string } | null } */ function getNameParamNode(node) { const nameLiteralNode = node.arguments[0] @@ -46,7 +41,7 @@ function getNameParamNode(node) { return null } - return nameLiteralNode + return /** @type {Literal & { value: string }} */ (nameLiteralNode) } /** * Get the callee member node from the given CallExpression @@ -82,7 +77,7 @@ module.exports = { unexpected: "Custom event name '{{name}}' must be kebab-case." } }, - + /** @param {RuleContext} context */ create(context) { const setupContexts = new Map() @@ -121,28 +116,26 @@ module.exports = { utils.compositingVisitors( utils.defineVueVisitor(context, { onSetupFunctionEnter(node, { node: vueNode }) { - const contextParam = node.params[1] + const contextParam = utils.unwrapAssignmentPattern(node.params[1]) if (!contextParam) { // no arguments return } - if (contextParam.type === 'RestElement') { - // cannot check - return - } - if (contextParam.type === 'ArrayPattern') { + if ( + contextParam.type === 'RestElement' || + contextParam.type === 'ArrayPattern' + ) { // cannot check return } const contextReferenceIds = new Set() const emitReferenceIds = new Set() if (contextParam.type === 'ObjectPattern') { - const emitProperty = contextParam.properties.find( - (p) => - p.type === 'Property' && - utils.getStaticPropertyName(p) === 'emit' + const emitProperty = utils.findAssignmentProperty( + contextParam, + 'emit' ) - if (!emitProperty) { + if (!emitProperty || emitProperty.value.type !== 'Identifier') { return } const emitParam = emitProperty.value diff --git a/lib/rules/dot-location.js b/lib/rules/dot-location.js index c8889bb63..14ab075cd 100644 --- a/lib/rules/dot-location.js +++ b/lib/rules/dot-location.js @@ -5,5 +5,5 @@ const { wrapCoreRule } = require('../utils') -// eslint-disable-next-line +// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule(require('eslint/lib/rules/dot-location')) diff --git a/lib/rules/eqeqeq.js b/lib/rules/eqeqeq.js index 5e9d4d8b9..e749e41f7 100644 --- a/lib/rules/eqeqeq.js +++ b/lib/rules/eqeqeq.js @@ -5,5 +5,5 @@ const { wrapCoreRule } = require('../utils') -// eslint-disable-next-line +// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule(require('eslint/lib/rules/eqeqeq')) diff --git a/lib/rules/html-closing-bracket-newline.js b/lib/rules/html-closing-bracket-newline.js index 20d7fbd20..6963de8f1 100644 --- a/lib/rules/html-closing-bracket-newline.js +++ b/lib/rules/html-closing-bracket-newline.js @@ -15,6 +15,9 @@ const utils = require('../utils') // Helpers // ------------------------------------------------------------------------------ +/** + * @param {number} lineBreaks + */ function getPhrase(lineBreaks) { switch (lineBreaks) { case 0: @@ -51,7 +54,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = Object.assign( {}, @@ -66,6 +69,7 @@ module.exports = { context.parserServices.getTemplateBodyTokenStore() return utils.defineTemplateBodyVisitor(context, { + /** @param {VStartTag | VEndTag} node */ 'VStartTag, VEndTag'(node) { const closingBracketToken = template.getLastToken(node) if ( @@ -98,6 +102,7 @@ module.exports = { actual: getPhrase(actualLineBreaks) }, fix(fixer) { + /** @type {Range} */ const range = [prevToken.range[1], closingBracketToken.range[0]] const text = '\n'.repeat(expectedLineBreaks) return fixer.replaceTextRange(range, text) diff --git a/lib/rules/html-closing-bracket-spacing.js b/lib/rules/html-closing-bracket-spacing.js index 9b70dfb09..95de6d01f 100644 --- a/lib/rules/html-closing-bracket-spacing.js +++ b/lib/rules/html-closing-bracket-spacing.js @@ -14,40 +14,49 @@ const utils = require('../utils') // Helpers // ----------------------------------------------------------------------------- +/** + * @typedef { {startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"} } Options + */ + /** * Normalize options. - * @param {{startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"}} options The options user configured. - * @param {TokenStore} tokens The token store of template body. - * @returns {{startTag:"always"|"never",endTag:"always"|"never",selfClosingTag:"always"|"never"}} The normalized options. + * @param {Options} options The options user configured. + * @param {ParserServices.TokenStore} tokens The token store of template body. + * @returns {Options & { detectType: (node: VStartTag | VEndTag) => 'never' | 'always' | null }} The normalized options. */ function parseOptions(options, tokens) { - return Object.assign( + const opts = Object.assign( { startTag: 'never', endTag: 'never', - selfClosingTag: 'always', - - detectType(node) { - const openType = tokens.getFirstToken(node).type - const closeType = tokens.getLastToken(node).type - - if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') { - return this.endTag - } - if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') { - return this.startTag - } - if ( - openType === 'HTMLTagOpen' && - closeType === 'HTMLSelfClosingTagClose' - ) { - return this.selfClosingTag - } - return null - } + selfClosingTag: 'always' }, options ) + return Object.assign(opts, { + /** + * @param {VStartTag | VEndTag} node + * @returns {'never' | 'always' | null} + */ + detectType(node) { + const openType = tokens.getFirstToken(node).type + const closeType = tokens.getLastToken(node).type + + if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') { + return opts.endTag + } + if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') { + return opts.startTag + } + if ( + openType === 'HTMLTagOpen' && + closeType === 'HTMLSelfClosingTagClose' + ) { + return opts.selfClosingTag + } + return null + } + }) } // ----------------------------------------------------------------------------- @@ -75,7 +84,7 @@ module.exports = { ], fixable: 'whitespace' }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() const tokens = @@ -84,6 +93,7 @@ module.exports = { const options = parseOptions(context.options[0], tokens) return utils.defineTemplateBodyVisitor(context, { + /** @param {VStartTag | VEndTag} node */ 'VStartTag, VEndTag'(node) { const type = options.detectType(node) const lastToken = tokens.getLastToken(node) diff --git a/lib/rules/html-comment-content-newline.js b/lib/rules/html-comment-content-newline.js index e5e436490..3985f7d52 100644 --- a/lib/rules/html-comment-content-newline.js +++ b/lib/rules/html-comment-content-newline.js @@ -11,13 +11,16 @@ const htmlComments = require('../utils/html-comments') /** - * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment + * @typedef { import('../utils/html-comments').ParsedHTMLComment } ParsedHTMLComment */ // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ +/** + * @param {any} param + */ function parseOption(param) { if (param && typeof param === 'string') { return { @@ -87,22 +90,24 @@ module.exports = { unexpectedBeforeHTMLCommentOpen: "Unexpected line breaks before '-->'." } }, - + /** @param {RuleContext} context */ create(context) { const option = parseOption(context.options[0]) return htmlComments.defineVisitor( context, context.options[1], (comment) => { - if (!comment.value) { + const { value, openDecoration, closeDecoration } = comment + if (!value) { return } - const startLine = comment.openDecoration - ? comment.openDecoration.loc.end.line - : comment.value.loc.start.line - const endLine = comment.closeDecoration - ? comment.closeDecoration.loc.start.line - : comment.value.loc.end.line + + const startLine = openDecoration + ? openDecoration.loc.end.line + : value.loc.start.line + const endLine = closeDecoration + ? closeDecoration.loc.start.line + : value.loc.end.line const newlineType = startLine === endLine ? option.singleline : option.multiline if (newlineType === 'ignore') { @@ -115,92 +120,94 @@ module.exports = { /** * Reports the newline before the contents of a given comment if it's invalid. - * @param {HTMLComment} comment - comment data. + * @param {ParsedHTMLComment} comment - comment data. * @param {boolean} requireNewline - `true` if line breaks are required. * @returns {void} */ function checkCommentOpen(comment, requireNewline) { - const beforeToken = comment.openDecoration || comment.open + const { value, openDecoration, open } = comment + if (!value) { + return + } + const beforeToken = openDecoration || open if (requireNewline) { - if (beforeToken.loc.end.line < comment.value.loc.start.line) { + if (beforeToken.loc.end.line < value.loc.start.line) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: comment.value.loc.start + end: value.loc.start }, - messageId: comment.openDecoration + messageId: openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', - fix: comment.openDecoration + fix: openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, '\n') }) } else { - if (beforeToken.loc.end.line === comment.value.loc.start.line) { + if (beforeToken.loc.end.line === value.loc.start.line) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: comment.value.loc.start + end: value.loc.start }, messageId: 'unexpectedAfterHTMLCommentOpen', fix: (fixer) => - fixer.replaceTextRange( - [beforeToken.range[1], comment.value.range[0]], - ' ' - ) + fixer.replaceTextRange([beforeToken.range[1], value.range[0]], ' ') }) } } /** * Reports the space after the contents of a given comment if it's invalid. - * @param {HTMLComment} comment - comment data. + * @param {ParsedHTMLComment} comment - comment data. * @param {boolean} requireNewline - `true` if line breaks are required. * @returns {void} */ function checkCommentClose(comment, requireNewline) { - const afterToken = comment.closeDecoration || comment.close + const { value, closeDecoration, close } = comment + if (!value) { + return + } + const afterToken = closeDecoration || close if (requireNewline) { - if (comment.value.loc.end.line < afterToken.loc.start.line) { + if (value.loc.end.line < afterToken.loc.start.line) { // Is valid return } context.report({ loc: { - start: comment.value.loc.end, + start: value.loc.end, end: afterToken.loc.start }, - messageId: comment.closeDecoration + messageId: closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', - fix: comment.closeDecoration + fix: closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, '\n') }) } else { - if (comment.value.loc.end.line === afterToken.loc.start.line) { + if (value.loc.end.line === afterToken.loc.start.line) { // Is valid return } context.report({ loc: { - start: comment.value.loc.end, + start: value.loc.end, end: afterToken.loc.start }, messageId: 'unexpectedBeforeHTMLCommentOpen', fix: (fixer) => - fixer.replaceTextRange( - [comment.value.range[1], afterToken.range[0]], - ' ' - ) + fixer.replaceTextRange([value.range[1], afterToken.range[0]], ' ') }) } } diff --git a/lib/rules/html-comment-content-spacing.js b/lib/rules/html-comment-content-spacing.js index 250fd7ae9..b4f31672a 100644 --- a/lib/rules/html-comment-content-spacing.js +++ b/lib/rules/html-comment-content-spacing.js @@ -11,7 +11,7 @@ const htmlComments = require('../utils/html-comments') /** - * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment + * @typedef { import('../utils/html-comments').ParsedHTMLComment } ParsedHTMLComment */ // ------------------------------------------------------------------------------ @@ -54,7 +54,7 @@ module.exports = { unexpectedBeforeHTMLCommentOpen: "Unexpected space before '-->'." } }, - + /** @param {RuleContext} context */ create(context) { // Unless the first option is never, require a space const requireSpace = context.options[0] !== 'never' @@ -62,9 +62,6 @@ module.exports = { context, context.options[1], (comment) => { - if (!comment.value) { - return - } checkCommentOpen(comment) checkCommentClose(comment) }, @@ -73,100 +70,108 @@ module.exports = { /** * Reports the space before the contents of a given comment if it's invalid. - * @param {HTMLComment} comment - comment data. + * @param {ParsedHTMLComment} comment - comment data. * @returns {void} */ function checkCommentOpen(comment) { - const beforeToken = comment.openDecoration || comment.open - if (beforeToken.loc.end.line !== comment.value.loc.start.line) { + const { value, openDecoration, open } = comment + if (!value) { + return + } + const beforeToken = openDecoration || open + if (beforeToken.loc.end.line !== value.loc.start.line) { // Ignore newline return } if (requireSpace) { - if (beforeToken.range[1] < comment.value.range[0]) { + if (beforeToken.range[1] < value.range[0]) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: comment.value.loc.start + end: value.loc.start }, - messageId: comment.openDecoration + messageId: openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', - fix: comment.openDecoration + fix: openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, ' ') }) } else { - if (comment.openDecoration) { + if (openDecoration) { // Ignore expection block return } - if (beforeToken.range[1] === comment.value.range[0]) { + if (beforeToken.range[1] === value.range[0]) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: comment.value.loc.start + end: value.loc.start }, messageId: 'unexpectedAfterHTMLCommentOpen', fix: (fixer) => - fixer.removeRange([beforeToken.range[1], comment.value.range[0]]) + fixer.removeRange([beforeToken.range[1], value.range[0]]) }) } } /** * Reports the space after the contents of a given comment if it's invalid. - * @param {HTMLComment} comment - comment data. + * @param {ParsedHTMLComment} comment - comment data. * @returns {void} */ function checkCommentClose(comment) { - const afterToken = comment.closeDecoration || comment.close - if (comment.value.loc.end.line !== afterToken.loc.start.line) { + const { value, closeDecoration, close } = comment + if (!value) { + return + } + const afterToken = closeDecoration || close + if (value.loc.end.line !== afterToken.loc.start.line) { // Ignore newline return } if (requireSpace) { - if (comment.value.range[1] < afterToken.range[0]) { + if (value.range[1] < afterToken.range[0]) { // Is valid return } context.report({ loc: { - start: comment.value.loc.end, + start: value.loc.end, end: afterToken.loc.start }, - messageId: comment.closeDecoration + messageId: closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', - fix: comment.closeDecoration + fix: closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, ' ') }) } else { - if (comment.closeDecoration) { + if (closeDecoration) { // Ignore expection block return } - if (comment.value.range[1] === afterToken.range[0]) { + if (value.range[1] === afterToken.range[0]) { // Is valid return } context.report({ loc: { - start: comment.value.loc.end, + start: value.loc.end, end: afterToken.loc.start }, messageId: 'unexpectedBeforeHTMLCommentOpen', fix: (fixer) => - fixer.removeRange([comment.value.range[1], afterToken.range[0]]) + fixer.removeRange([value.range[1], afterToken.range[0]]) }) } } diff --git a/lib/rules/html-comment-indent.js b/lib/rules/html-comment-indent.js index c3b60bfcc..d5992b59b 100644 --- a/lib/rules/html-comment-indent.js +++ b/lib/rules/html-comment-indent.js @@ -10,10 +10,6 @@ const htmlComments = require('../utils/html-comments') -/** - * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment - */ - // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ @@ -21,16 +17,17 @@ const htmlComments = require('../utils/html-comments') /** * Normalize options. * @param {number|"tab"|undefined} type The type of indentation. - * @returns {Object} Normalized options. + * @returns { { indentChar: string, indentSize: number, indentText: string } } Normalized options. */ function parseOptions(type) { const ret = { indentChar: ' ', - indentSize: 2 + indentSize: 2, + indentText: '' } if (Number.isSafeInteger(type)) { - ret.indentSize = type + ret.indentSize = Number(type) } else if (type === 'tab') { ret.indentChar = '\t' ret.indentSize = 1 @@ -40,6 +37,10 @@ function parseOptions(type) { return ret } +/** + * @param {string} s + * @param {string} [unitChar] + */ function toDisplay(s, unitChar) { if (s.length === 0 && unitChar) { return `0 ${toUnit(unitChar)}s` @@ -54,6 +55,7 @@ function toDisplay(s, unitChar) { return JSON.stringify(s) } +/** @param {string} char */ function toUnit(char) { if (char === '\t') { return 'tab' @@ -96,7 +98,7 @@ module.exports = { 'Expected relative indentation of {{expected}} but found {{actual}}.' } }, - + /** @param {RuleContext} context */ create(context) { const options = parseOptions(context.options[0]) const sourceCode = context.getSourceCode() @@ -158,7 +160,7 @@ module.exports = { * @param {number} line The number of line. * @param {string} actualIndentText The actual indentation text. * @param {string} expectedIndentText The expected indentation text. - * @returns {function} The defined function. + * @returns { (fixer: RuleFixer) => Fix } The defined function. */ function defineFix(line, actualIndentText, expectedIndentText) { return (fixer) => { diff --git a/lib/rules/html-end-tags.js b/lib/rules/html-end-tags.js index b40a53ce0..59d2e874f 100644 --- a/lib/rules/html-end-tags.js +++ b/lib/rules/html-end-tags.js @@ -26,7 +26,7 @@ module.exports = { fixable: 'code', schema: [] }, - + /** @param {RuleContext} context */ create(context) { let hasInvalidEOF = false diff --git a/lib/rules/html-indent.js b/lib/rules/html-indent.js index 9cbea60c7..6e6149ef5 100644 --- a/lib/rules/html-indent.js +++ b/lib/rules/html-indent.js @@ -17,6 +17,7 @@ const utils = require('../utils') // ------------------------------------------------------------------------------ module.exports = { + /** @param {RuleContext} context */ create(context) { const tokenStore = context.parserServices.getTemplateBodyTokenStore && diff --git a/lib/rules/html-quotes.js b/lib/rules/html-quotes.js index 05bb272d5..9f741b21b 100644 --- a/lib/rules/html-quotes.js +++ b/lib/rules/html-quotes.js @@ -37,7 +37,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() const double = context.options[0] !== 'single' @@ -45,6 +45,7 @@ module.exports = { context.options[1] && context.options[1].avoidEscape === true const quoteChar = double ? '"' : "'" const quoteName = double ? 'double quotes' : 'single quotes' + /** @type {boolean} */ let hasInvalidEOF return utils.defineTemplateBodyVisitor( diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js index f2fa0b277..f8841f782 100644 --- a/lib/rules/html-self-closing.js +++ b/lib/rules/html-self-closing.js @@ -18,54 +18,63 @@ const utils = require('../utils') /** * These strings wil be displayed in error messages. */ -const ELEMENT_TYPE = Object.freeze({ +const ELEMENT_TYPE_MESSAGES = Object.freeze({ NORMAL: 'HTML elements', VOID: 'HTML void elements', COMPONENT: 'Vue.js custom components', SVG: 'SVG elements', - MATH: 'MathML elements' + MATH: 'MathML elements', + UNKNOWN: 'unknown elements' }) +/** + * @typedef {object} Options + * @property {'always' | 'never'} NORMAL + * @property {'always' | 'never'} VOID + * @property {'always' | 'never'} COMPONENT + * @property {'always' | 'never'} SVG + * @property {'always' | 'never'} MATH + * @property {null} UNKNOWN + */ + /** * Normalize the given options. - * @param {Object|undefined} options The raw options object. - * @returns {Object} Normalized options. + * @param {any} options The raw options object. + * @returns {Options} Normalized options. */ function parseOptions(options) { return { - [ELEMENT_TYPE.NORMAL]: - (options && options.html && options.html.normal) || 'always', - [ELEMENT_TYPE.VOID]: - (options && options.html && options.html.void) || 'never', - [ELEMENT_TYPE.COMPONENT]: - (options && options.html && options.html.component) || 'always', - [ELEMENT_TYPE.SVG]: (options && options.svg) || 'always', - [ELEMENT_TYPE.MATH]: (options && options.math) || 'always' + NORMAL: (options && options.html && options.html.normal) || 'always', + VOID: (options && options.html && options.html.void) || 'never', + COMPONENT: (options && options.html && options.html.component) || 'always', + SVG: (options && options.svg) || 'always', + MATH: (options && options.math) || 'always', + UNKNOWN: null } } /** * Get the elementType of the given element. * @param {VElement} node The element node to get. - * @returns {string} The elementType of the element. + * @returns {keyof Options} The elementType of the element. */ function getElementType(node) { if (utils.isCustomComponent(node)) { - return ELEMENT_TYPE.COMPONENT + return 'COMPONENT' } if (utils.isHtmlElementNode(node)) { if (utils.isHtmlVoidElementName(node.name)) { - return ELEMENT_TYPE.VOID + return 'VOID' } - return ELEMENT_TYPE.NORMAL + return 'NORMAL' } if (utils.isSvgElementNode(node)) { - return ELEMENT_TYPE.SVG + return 'SVG' } if (utils.isMathMLElementNode(node)) { - return ELEMENT_TYPE.MATH + return 'MATH' } - return 'unknown elements' + return 'UNKNOWN' } /** @@ -124,7 +133,7 @@ module.exports = { maxItems: 1 } }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() const options = parseOptions(context.options[0]) @@ -150,7 +159,10 @@ module.exports = { node, loc: node.loc, message: 'Require self-closing on {{elementType}} (<{{name}}>).', - data: { elementType, name: node.rawName }, + data: { + elementType: ELEMENT_TYPE_MESSAGES[elementType], + name: node.rawName + }, fix: (fixer) => { const tokens = context.parserServices.getTemplateBodyTokenStore() const close = tokens.getLastToken(node.startTag) @@ -171,14 +183,17 @@ module.exports = { loc: node.loc, message: 'Disallow self-closing on {{elementType}} (<{{name}}/>).', - data: { elementType, name: node.rawName }, + data: { + elementType: ELEMENT_TYPE_MESSAGES[elementType], + name: node.rawName + }, fix: (fixer) => { const tokens = context.parserServices.getTemplateBodyTokenStore() const close = tokens.getLastToken(node.startTag) if (close.type !== 'HTMLSelfClosingTagClose') { return null } - if (elementType === ELEMENT_TYPE.VOID) { + if (elementType === 'VOID') { return fixer.replaceText(close, '>') } // If only `close` is targeted for replacement, it conflicts with `component-name-in-template-casing`, diff --git a/lib/rules/jsx-uses-vars.js b/lib/rules/jsx-uses-vars.js index 2d335dde1..0da5e486f 100644 --- a/lib/rules/jsx-uses-vars.js +++ b/lib/rules/jsx-uses-vars.js @@ -44,18 +44,21 @@ module.exports = { }, schema: [] }, - + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { return { JSXOpeningElement(node) { let name - if (node.name.name) { + if (node.name.type === 'JSXIdentifier') { // name = node.name.name - } else if (node.name.object) { + } else if (node.name.type === 'JSXMemberExpression') { // let parent = node.name.object - while (parent.object) { + while (parent.type === 'JSXMemberExpression') { parent = parent.object } name = parent.name diff --git a/lib/rules/match-component-file-name.js b/lib/rules/match-component-file-name.js index 45d603727..8428516fb 100644 --- a/lib/rules/match-component-file-name.js +++ b/lib/rules/match-component-file-name.js @@ -45,7 +45,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] const shouldMatchCase = (options && options.shouldMatchCase) || false @@ -57,6 +57,7 @@ module.exports = { const extension = path.extname(context.getFilename()) const filename = path.basename(context.getFilename(), extension) + /** @type {Rule.ReportDescriptor[]} */ const errors = [] let componentCount = 0 @@ -68,6 +69,10 @@ module.exports = { // Private // ---------------------------------------------------------------------- + /** + * @param {string} name + * @param {string} filename + */ function compareNames(name, filename) { if (shouldMatchCase) { return name === filename @@ -79,13 +84,16 @@ module.exports = { ) } + /** + * @param {Literal | TemplateLiteral} node + */ function verifyName(node) { let name if (node.type === 'TemplateLiteral') { const quasis = node.quasis[0] name = quasis.value.cooked } else { - name = node.value + name = `${node.value}` } if (!compareNames(name, filename)) { @@ -98,6 +106,10 @@ module.exports = { } } + /** + * @param {Expression | SpreadElement} node + * @returns {node is (Literal | TemplateLiteral)} + */ function canVerify(node) { return ( node.type === 'Literal' || @@ -119,16 +131,12 @@ module.exports = { } }), utils.executeOnVue(context, (object) => { - const node = object.properties.find( - (item) => - item.type === 'Property' && - item.key.name === 'name' && - canVerify(item.value) - ) + const node = utils.findProperty(object, 'name') componentCount++ if (!node) return + if (!canVerify(node.value)) return verifyName(node.value) }), { diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js index 29b6a4e6b..8cec63617 100644 --- a/lib/rules/max-attributes-per-line.js +++ b/lib/rules/max-attributes-per-line.js @@ -65,7 +65,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() const configuration = parseOptions(context.options[0]) @@ -107,6 +107,9 @@ module.exports = { // ---------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------- + /** + * @param {any} options + */ function parseOptions(options) { const defaults = { singleline: 1, @@ -139,32 +142,40 @@ module.exports = { return defaults } + /** + * @param {(VDirective | VAttribute)[]} attributes + */ function showErrors(attributes) { attributes.forEach((prop, i) => { - const fix = (fixer) => { - if (i !== 0) return null - - // Find the closest token before the current prop - // that is not a white space - const prevToken = template.getTokenBefore(prop, { - filter: (token) => token.type !== 'HTMLWhitespace' - }) - - const range = [prevToken.range[1], prop.range[0]] - - return fixer.replaceTextRange(range, '\n') - } - context.report({ node: prop, loc: prop.loc, message: "'{{name}}' should be on a new line.", data: { name: sourceCode.getText(prop.key) }, - fix + fix(fixer) { + if (i !== 0) return null + + // Find the closest token before the current prop + // that is not a white space + const prevToken = /** @type {Token} */ (template.getTokenBefore( + prop, + { + filter: (token) => token.type !== 'HTMLWhitespace' + } + )) + + /** @type {Range} */ + const range = [prevToken.range[1], prop.range[0]] + + return fixer.replaceTextRange(range, '\n') + } }) }) } + /** + * @param {(VDirective | VAttribute)[]} attributes + */ function groupAttrsByLine(attributes) { const propsPerLine = [[attributes[0]]] diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js index 8f1b07b87..6de8f8953 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -82,20 +82,23 @@ const OPTIONS_OR_INTEGER_SCHEMA = { * Computes the length of a line that may contain tabs. The width of each * tab will be the number of spaces to the next tab stop. * @param {string} line The line. - * @param {int} tabWidth The width of each tab stop in spaces. - * @returns {int} The computed line length. + * @param {number} tabWidth The width of each tab stop in spaces. + * @returns {number} The computed line length. * @private */ function computeLineLength(line, tabWidth) { let extraCharacterCount = 0 - line.replace(/\t/gu, (match, offset) => { + const re = /\t/gu + let ret + while ((ret = re.exec(line))) { + const offset = ret.index const totalOffset = offset + extraCharacterCount const previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0 const spaceCount = tabWidth - previousTabStopOffset - extraCharacterCount += spaceCount - 1 // -1 for the replaced tab - }) + } + return Array.from(line).length + extraCharacterCount } @@ -104,16 +107,16 @@ function computeLineLength(line, tabWidth) { * extends to or past the end of the current line. * @param {string} line The source line we want to check for a trailing comment on * @param {number} lineNumber The one-indexed line number for line - * @param {ASTNode} comment The comment to inspect - * @returns {boolean} If the comment is trailing on the given line + * @param {Token | null} comment The comment to inspect + * @returns {comment is Token} If the comment is trailing on the given line */ function isTrailingComment(line, lineNumber, comment) { - return ( + return Boolean( comment && - comment.loc.start.line === lineNumber && - lineNumber <= comment.loc.end.line && - (comment.loc.end.line > lineNumber || - comment.loc.end.column === line.length) + comment.loc.start.line === lineNumber && + lineNumber <= comment.loc.end.line && + (comment.loc.end.line > lineNumber || + comment.loc.end.column === line.length) ) } @@ -121,10 +124,13 @@ function isTrailingComment(line, lineNumber, comment) { * Tells if a comment encompasses the entire line. * @param {string} line The source line with a trailing comment * @param {number} lineNumber The one-indexed line number this is on - * @param {ASTNode} comment The comment to remove + * @param {Token | null} comment The comment to remove * @returns {boolean} If the comment covers the entire line */ function isFullLineComment(line, lineNumber, comment) { + if (!comment) { + return false + } const start = comment.loc.start const end = comment.loc.end const isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim() @@ -142,7 +148,7 @@ function isFullLineComment(line, lineNumber, comment) { * Gets the line after the comment and any remaining trailing whitespace is * stripped. * @param {string} line The source line with a trailing comment - * @param {ASTNode} comment The comment to remove + * @param {Token} comment The comment to remove * @returns {string} Line without comment and trailing whitepace */ function stripTrailingComment(line, comment) { @@ -153,9 +159,9 @@ function stripTrailingComment(line, comment) { /** * Ensure that an array exists at [key] on `object`, and add `value` to it. * - * @param {Object} object the object to mutate - * @param {string} key the object's key - * @param {*} value the value to add + * @param { { [key: number]: Token[] } } object the object to mutate + * @param {number} key the object's key + * @param {Token} value the value to add * @returns {void} * @private */ @@ -169,9 +175,9 @@ function ensureArrayAndPush(object, key, value) { /** * A reducer to group an AST node by line number, both start and end. * - * @param {Object} acc the accumulator - * @param {ASTNode} node the AST node in question - * @returns {Object} the modified accumulator + * @param { { [key: number]: Token[] } } acc the accumulator + * @param {Token} node the AST node in question + * @returns { { [key: number]: Token[] } } the modified accumulator * @private */ function groupByLineNumber(acc, node) { @@ -209,7 +215,10 @@ module.exports = { 'This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}.' } }, - + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { /* * Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however: @@ -222,8 +231,11 @@ module.exports = { const URL_REGEXP = /[^:/?#]:\/\/[^?#]/u const sourceCode = context.getSourceCode() + /** @type {Token[]} */ const tokens = [] + /** @type {(HTMLComment | HTMLBogusComment | Comment)[]} */ const comments = [] + /** @type {VLiteral[]} */ const htmlAttributeValues = [] // The options object must be the last option specified… @@ -241,9 +253,11 @@ module.exports = { if (typeof context.options[1] === 'number') { options.tabWidth = context.options[1] } - + /** @type {number} */ const scriptMaxLength = typeof options.code === 'number' ? options.code : 80 + /** @type {number} */ const tabWidth = typeof options.tabWidth === 'number' ? options.tabWidth : 2 // default value of `vue/html-indent` + /** @type {number} */ const templateMaxLength = typeof options.template === 'number' ? options.template : scriptMaxLength const ignoreComments = !!options.ignoreComments @@ -255,7 +269,9 @@ module.exports = { const ignoreUrls = !!options.ignoreUrls const ignoreHTMLAttributeValues = !!options.ignoreHTMLAttributeValues const ignoreHTMLTextContents = !!options.ignoreHTMLTextContents + /** @type {number} */ const maxCommentLength = options.comments + /** @type {RegExp} */ let ignorePattern = options.ignorePattern || null if (ignorePattern) { @@ -269,7 +285,7 @@ module.exports = { /** * Retrieves an array containing all strings (" or ') in the source code. * - * @returns {ASTNode[]} An array of string nodes. + * @returns {Token[]} An array of string nodes. */ function getAllStrings() { return tokens.filter( @@ -284,7 +300,7 @@ module.exports = { /** * Retrieves an array containing all template literals in the source code. * - * @returns {ASTNode[]} An array of template literal nodes. + * @returns {Token[]} An array of template literal nodes. */ function getAllTemplateLiterals() { return tokens.filter((token) => token.type === 'Template') @@ -293,7 +309,7 @@ module.exports = { /** * Retrieves an array containing all RegExp literals in the source code. * - * @returns {ASTNode[]} An array of RegExp literal nodes. + * @returns {Token[]} An array of RegExp literal nodes. */ function getAllRegExpLiterals() { return tokens.filter((token) => token.type === 'RegularExpression') @@ -302,7 +318,7 @@ module.exports = { /** * Retrieves an array containing all HTML texts in the source code. * - * @returns {ASTNode[]} An array of HTML text nodes. + * @returns {Token[]} An array of HTML text nodes. */ function getAllHTMLTextContents() { return tokens.filter((token) => token.type === 'HTMLText') @@ -310,7 +326,7 @@ module.exports = { /** * Check the program for max length - * @param {ASTNode} node Node to examine + * @param {Program} node Node to examine * @returns {void} * @private */ @@ -351,6 +367,7 @@ module.exports = { } } + /** @type {Range} */ let scriptLinesRange if (scriptTokens.length) { if (scriptComments.length) { @@ -458,7 +475,7 @@ module.exports = { if (commentsByLine[lineNumber]) { const commentList = [...commentsByLine[lineNumber]] - let comment = commentList.pop() + let comment = commentList.pop() || null if (isFullLineComment(line, lineNumber, comment)) { lineIsComment = true @@ -470,7 +487,7 @@ module.exports = { textToMeasure = stripTrailingComment(line, comment) // ignore multiple trailing comments in the same line - comment = commentList.pop() + comment = commentList.pop() || null while (isTrailingComment(textToMeasure, lineNumber, comment)) { textToMeasure = stripTrailingComment(textToMeasure, comment) @@ -527,19 +544,18 @@ module.exports = { // Public API // -------------------------------------------------------------------------- - const bodyVisitor = utils.defineTemplateBodyVisitor(context, { - 'VAttribute[directive=false] > VLiteral'(node) { - htmlAttributeValues.push(node) - } - }) - - return Object.assign({}, bodyVisitor, { - 'Program:exit'(node) { - if (bodyVisitor['Program:exit']) { - bodyVisitor['Program:exit'](node) + return utils.compositingVisitors( + utils.defineTemplateBodyVisitor(context, { + /** @param {VLiteral} node */ + 'VAttribute[directive=false] > VLiteral'(node) { + htmlAttributeValues.push(node) + } + }), + { + 'Program:exit'(node) { + checkProgramForMaxLength(node) } - checkProgramForMaxLength(node) } - }) + ) } } diff --git a/lib/rules/multiline-html-element-content-newline.js b/lib/rules/multiline-html-element-content-newline.js index b2332ae5d..49a675c4b 100644 --- a/lib/rules/multiline-html-element-content-newline.js +++ b/lib/rules/multiline-html-element-content-newline.js @@ -16,10 +16,16 @@ const INLINE_ELEMENTS = require('../utils/inline-non-void-elements.json') // Helpers // ------------------------------------------------------------------------------ +/** + * @param {VElement & { endTag: VEndTag }} element + */ function isMultilineElement(element) { return element.loc.start.line < element.endTag.loc.start.line } +/** + * @param {any} options + */ function parseOptions(options) { return Object.assign( { @@ -31,6 +37,9 @@ function parseOptions(options) { ) } +/** + * @param {number} lineBreaks + */ function getPhrase(lineBreaks) { switch (lineBreaks) { case 0: @@ -42,7 +51,7 @@ function getPhrase(lineBreaks) { /** * Check whether the given element is empty or not. * This ignores whitespaces, doesn't ignore comments. - * @param {VElement} node The element node to check. + * @param {VElement & { endTag: VEndTag }} node The element node to check. * @param {SourceCode} sourceCode The source code object of the current context. * @returns {boolean} `true` if the element is empty. */ @@ -94,7 +103,7 @@ module.exports = { 'Expected 1 line break before closing tag (``), but {{actual}} line breaks found.' } }, - + /** @param {RuleContext} context */ create(context) { const options = parseOptions(context.options[0]) const ignores = options.ignores @@ -105,8 +114,12 @@ module.exports = { context.parserServices.getTemplateBodyTokenStore() const sourceCode = context.getSourceCode() - let inIgnoreElement + /** @type {VElement | null} */ + let inIgnoreElement = null + /** + * @param {VElement} node + */ function isIgnoredElement(node) { return ( ignores.includes(node.name) || @@ -115,6 +128,9 @@ module.exports = { ) } + /** + * @param {number} lineBreaks + */ function isInvalidLineBreaks(lineBreaks) { if (allowEmptyLines) { return lineBreaks === 0 @@ -138,73 +154,83 @@ module.exports = { return } - if (!isMultilineElement(node)) { + const element = /** @type {VElement & { endTag: VEndTag }} */ (node) + + if (!isMultilineElement(element)) { return } + /** + * @type {SourceCode.CursorWithCountOptions} + */ const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' } if ( ignoreWhenEmpty && - node.children.length === 0 && + element.children.length === 0 && template.getFirstTokensBetween( - node.startTag, - node.endTag, + element.startTag, + element.endTag, getTokenOption ).length === 0 ) { return } - const contentFirst = template.getTokenAfter( - node.startTag, + const contentFirst = /** @type {Token} */ (template.getTokenAfter( + element.startTag, + getTokenOption + )) + const contentLast = /** @type {Token} */ (template.getTokenBefore( + element.endTag, getTokenOption - ) - const contentLast = template.getTokenBefore(node.endTag, getTokenOption) + )) const beforeLineBreaks = - contentFirst.loc.start.line - node.startTag.loc.end.line + contentFirst.loc.start.line - element.startTag.loc.end.line const afterLineBreaks = - node.endTag.loc.start.line - contentLast.loc.end.line + element.endTag.loc.start.line - contentLast.loc.end.line if (isInvalidLineBreaks(beforeLineBreaks)) { context.report({ - node: template.getLastToken(node.startTag), + node: template.getLastToken(element.startTag), loc: { - start: node.startTag.loc.end, + start: element.startTag.loc.end, end: contentFirst.loc.start }, messageId: 'unexpectedAfterClosingBracket', data: { - name: node.rawName, + name: element.rawName, actual: getPhrase(beforeLineBreaks) }, fix(fixer) { - const range = [node.startTag.range[1], contentFirst.range[0]] + /** @type {Range} */ + const range = [element.startTag.range[1], contentFirst.range[0]] return fixer.replaceTextRange(range, '\n') } }) } - if (isEmpty(node, sourceCode)) { + if (isEmpty(element, sourceCode)) { return } if (isInvalidLineBreaks(afterLineBreaks)) { context.report({ - node: template.getFirstToken(node.endTag), + node: template.getFirstToken(element.endTag), loc: { start: contentLast.loc.end, - end: node.endTag.loc.start + end: element.endTag.loc.start }, messageId: 'unexpectedBeforeOpeningBracket', data: { - name: node.name, + name: element.name, actual: getPhrase(afterLineBreaks) }, fix(fixer) { - const range = [contentLast.range[1], node.endTag.range[0]] + /** @type {Range} */ + const range = [contentLast.range[1], element.endTag.range[0]] return fixer.replaceTextRange(range, '\n') } }) diff --git a/lib/rules/mustache-interpolation-spacing.js b/lib/rules/mustache-interpolation-spacing.js index 83fb7886c..8522c8ad9 100644 --- a/lib/rules/mustache-interpolation-spacing.js +++ b/lib/rules/mustache-interpolation-spacing.js @@ -29,7 +29,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || 'always' const template = @@ -41,6 +41,7 @@ module.exports = { // ---------------------------------------------------------------------- return utils.defineTemplateBodyVisitor(context, { + /** @param {VExpressionContainer} node */ 'VExpressionContainer[expression!=null]'(node) { const openBrace = template.getFirstToken(node) const closeBrace = template.getLastToken(node) diff --git a/lib/rules/name-property-casing.js b/lib/rules/name-property-casing.js index 6e5a52190..d86435a95 100644 --- a/lib/rules/name-property-casing.js +++ b/lib/rules/name-property-casing.js @@ -30,7 +30,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] const caseType = @@ -41,28 +41,28 @@ module.exports = { // ---------------------------------------------------------------------- return utils.executeOnVue(context, (obj) => { - const node = obj.properties.find( - (item) => - item.type === 'Property' && - item.key.name === 'name' && - item.value.type === 'Literal' - ) + const node = utils.findProperty(obj, 'name') if (!node) return + const valueNode = node.value + if (valueNode.type !== 'Literal') return - if (!casing.getChecker(caseType)(node.value.value)) { - const value = casing.getExactConverter(caseType)(node.value.value) + if (!casing.getChecker(caseType)(`${valueNode.value}`)) { + const value = casing.getExactConverter(caseType)(`${valueNode.value}`) context.report({ - node: node.value, + node: valueNode, message: 'Property name "{{value}}" is not {{caseType}}.', data: { - value: node.value.value, + value: `${valueNode.value}`, caseType }, fix: (fixer) => fixer.replaceText( - node.value, - node.value.raw.replace(node.value.value, value) + valueNode, + context + .getSourceCode() + .getText(valueNode) + .replace(`${valueNode.value}`, value) ) }) } diff --git a/lib/rules/no-arrow-functions-in-watch.js b/lib/rules/no-arrow-functions-in-watch.js index 2fce3f87f..77bb6787e 100644 --- a/lib/rules/no-arrow-functions-in-watch.js +++ b/lib/rules/no-arrow-functions-in-watch.js @@ -16,11 +16,10 @@ module.exports = { fixable: null, schema: [] }, + /** @param {RuleContext} context */ create(context) { return utils.executeOnVue(context, (obj) => { - const watchNode = obj.properties.find( - (property) => utils.getStaticPropertyName(property) === 'watch' - ) + const watchNode = utils.findProperty(obj, 'watch') if (watchNode == null) { return } diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 34849d1c6..e3080d7d5 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -6,6 +6,11 @@ const utils = require('../utils') +/** + * @typedef {import('../utils').VueObjectData} VueObjectData + * @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty + */ + const PROMISE_FUNCTIONS = ['then', 'catch', 'finally'] const PROMISE_METHODS = ['all', 'race', 'reject', 'resolve'] @@ -17,6 +22,9 @@ const TIMED_FUNCTIONS = [ 'requestAnimationFrame' ] +/** + * @param {CallExpression} node + */ function isTimedFunction(node) { return ( ((node.type === 'CallExpression' && @@ -26,11 +34,15 @@ function isTimedFunction(node) { node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'window' && + node.callee.property.type === 'Identifier' && TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1)) && node.arguments.length ) } +/** + * @param {CallExpression} node + */ function isPromise(node) { if ( node.type === 'CallExpression' && @@ -42,6 +54,7 @@ function isPromise(node) { PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1) || // Promise.PROMISE_METHOD() (node.callee.object.type === 'Identifier' && node.callee.object.name === 'Promise' && + node.callee.property.type === 'Identifier' && PROMISE_METHODS.indexOf(node.callee.property.name) !== -1) ) } @@ -63,10 +76,17 @@ module.exports = { fixable: null, schema: [] }, - + /** @param {RuleContext} context */ create(context) { + /** + * @typedef {object} ScopeStack + * @property {ScopeStack} upper + * @property {BlockStatement | Expression} body + */ + /** @type {Map} */ const computedPropertiesMap = new Map() - let scopeStack = { upper: null, body: null } + /** @type {ScopeStack} */ + let scopeStack const expressionTypes = { promise: 'asynchronous action', @@ -76,6 +96,10 @@ module.exports = { timed: 'timed function' } + /** + * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node + * @param {VueObjectData} data + */ function onFunctionEnter(node, { node: vueNode }) { if (node.async) { verify(node, node.body, 'async', computedPropertiesMap.get(vueNode)) @@ -88,7 +112,13 @@ module.exports = { scopeStack = scopeStack.upper } - function verify(node, targetBody, type, computedProperties) { + /** + * @param {ESNode} node + * @param {BlockStatement | Expression} targetBody + * @param {keyof expressionTypes} type + * @param {ComponentComputedProperty[]} computedProperties + */ + function verify(node, targetBody, type, computedProperties = []) { computedProperties.forEach((cp) => { if ( cp.value && @@ -102,7 +132,7 @@ module.exports = { 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.', data: { expressionName: expressionTypes[type], - propertyName: cp.key + propertyName: cp.key || 'unknown' } }) } @@ -116,7 +146,10 @@ module.exports = { ':function:exit': onFunctionExit, NewExpression(node, { node: vueNode }) { - if (node.callee.name === 'Promise') { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'Promise' + ) { verify( node, scopeStack.body, diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js index 8fea1625c..d0426e3a5 100644 --- a/lib/rules/no-bare-strings-in-template.js +++ b/lib/rules/no-bare-strings-in-template.js @@ -12,18 +12,8 @@ const utils = require('../utils') const regexp = require('../utils/regexp') const casing = require('../utils/casing') -/** - * @typedef {import('vue-eslint-parser').AST.VAttribute} VAttribute - * @typedef {import('vue-eslint-parser').AST.VDirective} VDirective - * @typedef {import('vue-eslint-parser').AST.VElement} VElement - * @typedef {import('vue-eslint-parser').AST.VIdentifier} VIdentifier - * @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer - * @typedef {import('vue-eslint-parser').AST.VText} VText - */ - /** * @typedef { { names: { [tagName in string]: Set }, regexps: { name: RegExp, attrs: Set }[], cache: { [tagName in string]: Set } } } TargetAttrs - * @typedef { { upper: ElementStack, name: string, attrs: Set } } ElementStack */ // ------------------------------------------------------------------------------ @@ -82,6 +72,7 @@ const DEFAULT_DIRECTIVES = ['v-text'] /** * Parse attributes option + * @param {any} options * @returns {TargetAttrs} */ function parseTargetAttrs(options) { @@ -104,7 +95,7 @@ function parseTargetAttrs(options) { /** * Get a string from given expression container node - * @param {VExpressionContainer} node + * @param {VExpressionContainer} value * @returns { string | null } */ function getStringValue(value) { @@ -166,8 +157,13 @@ module.exports = { unexpectedInAttr: 'Unexpected non-translated string used in `{{attr}}`.' } }, + /** @param {RuleContext} context */ create(context) { + /** + * @typedef { { upper: ElementStack, name: string, attrs: Set } } ElementStack + */ const opts = context.options[0] || {} + /** @type {string[]} */ const whitelist = opts.whitelist || DEFAULT_WHITELIST const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES) const directives = opts.directives || DEFAULT_DIRECTIVES @@ -177,8 +173,8 @@ module.exports = { 'gu' ) - /** @type {ElementStack | null} */ - let elementStack = null + /** @type {ElementStack} */ + let elementStack /** * Gets the bare string from given string * @param {string} str diff --git a/lib/rules/no-boolean-default.js b/lib/rules/no-boolean-default.js index 1f1e192df..e278e3011 100644 --- a/lib/rules/no-boolean-default.js +++ b/lib/rules/no-boolean-default.js @@ -6,10 +6,18 @@ const utils = require('../utils') +/** + * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp + * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp + */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ +/** + * @param {Property | SpreadElement} prop + */ function isBooleanProp(prop) { return ( prop.type === 'Property' && @@ -20,23 +28,32 @@ function isBooleanProp(prop) { ) } +/** + * @typedef {ComponentObjectProp & { value : ObjectExpression }} ObjectExpressionProp + */ + +/** + * @param {(ComponentArrayProp | ComponentObjectProp)[]} props + * @returns {ObjectExpressionProp[]} + */ function getBooleanProps(props) { return props.filter( + /** + * @param {ComponentArrayProp | ComponentObjectProp} prop + * @returns {prop is ObjectExpressionProp} + */ (prop) => - prop.value && - prop.value.properties && - prop.value.properties.find(isBooleanProp) + prop.value != null && + prop.value.type === 'ObjectExpression' && + prop.value.properties.some(isBooleanProp) ) } +/** + * @param {ObjectExpressionProp} propDef + */ function getDefaultNode(propDef) { - return propDef.value.properties.find((p) => { - return ( - p.type === 'Property' && - p.key.type === 'Identifier' && - p.key.name === 'default' - ) - }) + return utils.findProperty(propDef.value, 'default') } module.exports = { @@ -54,7 +71,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { return utils.executeOnVueComponent(context, (obj) => { const props = utils.getComponentProps(obj) @@ -66,20 +83,24 @@ module.exports = { booleanProps.forEach((propDef) => { const defaultNode = getDefaultNode(propDef) + if (!defaultNode) { + return + } switch (booleanType) { case 'no-default': - if (defaultNode) { - context.report({ - node: defaultNode, - message: - 'Boolean prop should not set a default (Vue defaults it to false).' - }) - } + context.report({ + node: defaultNode, + message: + 'Boolean prop should not set a default (Vue defaults it to false).' + }) break case 'default-false': - if (defaultNode && defaultNode.value.value !== false) { + if ( + defaultNode.value.type !== 'Literal' || + defaultNode.value.value !== false + ) { context.report({ node: defaultNode, message: 'Boolean prop should only be defaulted to false.' diff --git a/lib/rules/no-confusing-v-for-v-if.js b/lib/rules/no-confusing-v-for-v-if.js index 43951a090..4135f163a 100644 --- a/lib/rules/no-confusing-v-for-v-if.js +++ b/lib/rules/no-confusing-v-for-v-if.js @@ -17,16 +17,19 @@ const utils = require('../utils') /** * Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive. - * @param {ASTNode} vIf The `v-if` attribute node to check. + * @param {VDirective} vIf The `v-if` attribute node to check. * @returns {boolean} `true` if the `v-if` is using the variable which is defined by the `v-for` directive. */ function isUsingIterationVar(vIf) { const element = vIf.parent.parent - return vIf.value.references.some((reference) => - element.variables.some( - (variable) => - variable.id.name === reference.id.name && variable.kind === 'v-for' - ) + return Boolean( + vIf.value && + vIf.value.references.some((reference) => + element.variables.some( + (variable) => + variable.id.name === reference.id.name && variable.kind === 'v-for' + ) + ) ) } @@ -47,7 +50,7 @@ module.exports = { fixable: null, schema: [] }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { "VAttribute[directive=true][key.name.name='if']"(node) { diff --git a/lib/rules/no-custom-modifiers-on-v-model.js b/lib/rules/no-custom-modifiers-on-v-model.js index ea80fb7a6..6670be1da 100644 --- a/lib/rules/no-custom-modifiers-on-v-model.js +++ b/lib/rules/no-custom-modifiers-on-v-model.js @@ -35,6 +35,7 @@ module.exports = { "'v-model' directives don't support the modifier '{{name}}'." } }, + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { "VAttribute[directive=true][key.name.name='model']"(node) { diff --git a/lib/rules/no-deprecated-data-object-declaration.js b/lib/rules/no-deprecated-data-object-declaration.js index 02422c7e5..37d280ef6 100644 --- a/lib/rules/no-deprecated-data-object-declaration.js +++ b/lib/rules/no-deprecated-data-object-declaration.js @@ -10,14 +10,20 @@ const utils = require('../utils') +/** @param {Token} token */ function isOpenParen(token) { return token.type === 'Punctuator' && token.value === '(' } +/** @param {Token} token */ function isCloseParen(token) { return token.type === 'Punctuator' && token.value === ')' } +/** + * @param {Expression} node + * @param {SourceCode} sourceCode + */ function getFirstAndLastTokens(node, sourceCode) { let first = sourceCode.getFirstToken(node) let last = sourceCode.getLastToken(node) @@ -56,35 +62,34 @@ module.exports = { "Object declaration on 'data' property is deprecated. Using function declaration instead." } }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() return utils.executeOnVue(context, (obj) => { - obj.properties - .filter( - (p) => - p.type === 'Property' && - p.key.type === 'Identifier' && - p.key.name === 'data' && - p.value.type !== 'FunctionExpression' && - p.value.type !== 'ArrowFunctionExpression' && - p.value.type !== 'Identifier' - ) - .forEach((p) => { - context.report({ - node: p, - messageId: 'objectDeclarationIsDeprecated', - fix(fixer) { - const tokens = getFirstAndLastTokens(p.value, sourceCode) + const invalidData = utils.findProperty( + obj, + 'data', + (p) => + p.value.type !== 'FunctionExpression' && + p.value.type !== 'ArrowFunctionExpression' && + p.value.type !== 'Identifier' + ) + + if (invalidData) { + context.report({ + node: invalidData, + messageId: 'objectDeclarationIsDeprecated', + fix(fixer) { + const tokens = getFirstAndLastTokens(invalidData.value, sourceCode) - return [ - fixer.insertTextBefore(tokens.first, 'function() {\nreturn '), - fixer.insertTextAfter(tokens.last, ';\n}') - ] - } - }) + return [ + fixer.insertTextBefore(tokens.first, 'function() {\nreturn '), + fixer.insertTextAfter(tokens.last, ';\n}') + ] + } }) + } }) } } diff --git a/lib/rules/no-deprecated-dollar-listeners-api.js b/lib/rules/no-deprecated-dollar-listeners-api.js index 7c60cbbe5..97d98359c 100644 --- a/lib/rules/no-deprecated-dollar-listeners-api.js +++ b/lib/rules/no-deprecated-dollar-listeners-api.js @@ -29,7 +29,7 @@ module.exports = { deprecated: 'The `$listeners` is deprecated.' } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor( context, diff --git a/lib/rules/no-deprecated-dollar-scopedslots-api.js b/lib/rules/no-deprecated-dollar-scopedslots-api.js index f369c70ff..bdfa0f844 100644 --- a/lib/rules/no-deprecated-dollar-scopedslots-api.js +++ b/lib/rules/no-deprecated-dollar-scopedslots-api.js @@ -30,7 +30,7 @@ module.exports = { deprecated: 'The `$scopedSlots` is deprecated.' } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor( context, diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js index 05d50bac1..a09f36bf0 100644 --- a/lib/rules/no-deprecated-events-api.js +++ b/lib/rules/no-deprecated-events-api.js @@ -29,9 +29,10 @@ module.exports = { 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.' } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineVueVisitor(context, { + /** @param {MemberExpression & {parent: CallExpression}} node */ 'CallExpression > MemberExpression'(node) { const call = node.parent if ( diff --git a/lib/rules/no-deprecated-filter.js b/lib/rules/no-deprecated-filter.js index 0229d6e2e..87b61a5b3 100644 --- a/lib/rules/no-deprecated-filter.js +++ b/lib/rules/no-deprecated-filter.js @@ -29,7 +29,7 @@ module.exports = { noDeprecatedFilter: 'Filters are deprecated.' } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { VFilterSequenceExpression(node) { diff --git a/lib/rules/no-deprecated-functional-template.js b/lib/rules/no-deprecated-functional-template.js index cfaa8d957..e489841b0 100644 --- a/lib/rules/no-deprecated-functional-template.js +++ b/lib/rules/no-deprecated-functional-template.js @@ -30,7 +30,10 @@ module.exports = { unexpected: 'The `functional` template are deprecated.' } }, - + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { return { Program(program) { diff --git a/lib/rules/no-deprecated-html-element-is.js b/lib/rules/no-deprecated-html-element-is.js index d62c3b479..e07f493a5 100644 --- a/lib/rules/no-deprecated-html-element-is.js +++ b/lib/rules/no-deprecated-html-element-is.js @@ -29,9 +29,10 @@ module.exports = { unexpected: 'The `is` attribute on HTML element are deprecated.' } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VDirective | VAttribute} node */ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=false][key.name='is']"( node ) { diff --git a/lib/rules/no-deprecated-inline-template.js b/lib/rules/no-deprecated-inline-template.js index 69bdfda46..647121638 100644 --- a/lib/rules/no-deprecated-inline-template.js +++ b/lib/rules/no-deprecated-inline-template.js @@ -29,9 +29,10 @@ module.exports = { unexpected: '`inline-template` are deprecated.' } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VIdentifier} node */ "VAttribute[directive=false] > VIdentifier[rawName='inline-template']"( node ) { diff --git a/lib/rules/no-deprecated-scope-attribute.js b/lib/rules/no-deprecated-scope-attribute.js index 24ef41042..e7e22c3d3 100644 --- a/lib/rules/no-deprecated-scope-attribute.js +++ b/lib/rules/no-deprecated-scope-attribute.js @@ -21,6 +21,7 @@ module.exports = { forbiddenScopeAttribute: '`scope` attributes are deprecated.' } }, + /** @param {RuleContext} context */ create(context) { const templateBodyVisitor = scopeAttribute.createTemplateBodyVisitor( context diff --git a/lib/rules/no-deprecated-slot-attribute.js b/lib/rules/no-deprecated-slot-attribute.js index c7174c12a..6459cdff4 100644 --- a/lib/rules/no-deprecated-slot-attribute.js +++ b/lib/rules/no-deprecated-slot-attribute.js @@ -21,6 +21,7 @@ module.exports = { forbiddenSlotAttribute: '`slot` attributes are deprecated.' } }, + /** @param {RuleContext} context */ create(context) { const templateBodyVisitor = slotAttribute.createTemplateBodyVisitor(context) return utils.defineTemplateBodyVisitor(context, templateBodyVisitor) diff --git a/lib/rules/no-deprecated-slot-scope-attribute.js b/lib/rules/no-deprecated-slot-scope-attribute.js index 1356016ed..793b1ed6d 100644 --- a/lib/rules/no-deprecated-slot-scope-attribute.js +++ b/lib/rules/no-deprecated-slot-scope-attribute.js @@ -23,6 +23,7 @@ module.exports = { forbiddenSlotScopeAttribute: '`slot-scope` are deprecated.' } }, + /** @param {RuleContext} context */ create(context) { const templateBodyVisitor = slotScopeAttribute.createTemplateBodyVisitor( context, diff --git a/lib/rules/no-deprecated-v-bind-sync.js b/lib/rules/no-deprecated-v-bind-sync.js index b84ebf4de..97dfe51bd 100644 --- a/lib/rules/no-deprecated-v-bind-sync.js +++ b/lib/rules/no-deprecated-v-bind-sync.js @@ -30,6 +30,7 @@ module.exports = { "'.sync' modifier on 'v-bind' directive is deprecated. Use 'v-model:propName' instead." } }, + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { "VAttribute[directive=true][key.name.name='bind']"(node) { @@ -39,10 +40,13 @@ module.exports = { loc: node.loc, messageId: 'syncModifierIsDeprecated', fix: (fixer) => { - const isUsingSpreadSyntax = node.key.argument == null - const hasMultipleModifiers = node.key.modifiers.length > 1 - if (isUsingSpreadSyntax || hasMultipleModifiers) { - return + if (node.key.argument == null) { + // is using spread syntax + return null + } + if (node.key.modifiers.length > 1) { + // has multiple modifiers + return null } const bindArgument = context diff --git a/lib/rules/no-deprecated-v-on-native-modifier.js b/lib/rules/no-deprecated-v-on-native-modifier.js index 16f1987c5..4f60210b0 100644 --- a/lib/rules/no-deprecated-v-on-native-modifier.js +++ b/lib/rules/no-deprecated-v-on-native-modifier.js @@ -30,9 +30,10 @@ module.exports = { deprecated: "'.native' modifier on 'v-on' directive is deprecated." } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VIdentifier & {parent:VDirectiveKey} } node */ "VAttribute[directive=true][key.name.name='on'] > VDirectiveKey > VIdentifier[name='native']"( node ) { diff --git a/lib/rules/no-deprecated-v-on-number-modifiers.js b/lib/rules/no-deprecated-v-on-number-modifiers.js index bf1fb88bd..544c1ee9f 100644 --- a/lib/rules/no-deprecated-v-on-number-modifiers.js +++ b/lib/rules/no-deprecated-v-on-number-modifiers.js @@ -32,9 +32,10 @@ module.exports = { "'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead." } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VDirectiveKey} node */ "VAttribute[directive=true][key.name.name='on'] > VDirectiveKey"(node) { const modifier = node.modifiers.find((mod) => Number.isInteger(parseInt(mod.name, 10)) @@ -48,7 +49,7 @@ module.exports = { messageId: 'numberModifierIsDeprecated', fix: (fixer) => { const key = keyCodeToKey[keyCodes] - if (!key) return + if (!key) return null return fixer.replaceText(modifier, `${key}`) } diff --git a/lib/rules/no-deprecated-vue-config-keycodes.js b/lib/rules/no-deprecated-vue-config-keycodes.js index 0de109ef1..b30db3fed 100644 --- a/lib/rules/no-deprecated-vue-config-keycodes.js +++ b/lib/rules/no-deprecated-vue-config-keycodes.js @@ -24,9 +24,10 @@ module.exports = { unexpected: '`Vue.config.keyCodes` are deprecated.' } }, - + /** @param {RuleContext} context */ create(context) { return { + /** @param {MemberExpression} node */ "MemberExpression[property.type='Identifier'][property.name='keyCodes']"( node ) { diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js index dfbebdedf..8b5e8e54c 100644 --- a/lib/rules/no-dupe-keys.js +++ b/lib/rules/no-dupe-keys.js @@ -6,10 +6,14 @@ const utils = require('../utils') +/** + * @typedef {import('../utils').GroupName} GroupName + */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - +/** @type {GroupName[]} */ const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup'] module.exports = { @@ -33,7 +37,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const groups = new Set(GROUP_NAMES.concat(options.groups || [])) diff --git a/lib/rules/no-duplicate-attr-inheritance.js b/lib/rules/no-duplicate-attr-inheritance.js index 78e00b326..08426c352 100644 --- a/lib/rules/no-duplicate-attr-inheritance.js +++ b/lib/rules/no-duplicate-attr-inheritance.js @@ -25,23 +25,21 @@ module.exports = { // fill in your schema ] }, - + /** @param {RuleContext} context */ create(context) { + /** @type {string | number | boolean | RegExp | BigInt | null} */ let inheritsAttrs = true return Object.assign( utils.executeOnVue(context, (node) => { - const inheritAttrsProp = node.properties.find( - (prop) => - prop.type === 'Property' && - utils.getStaticPropertyName(prop) === 'inheritAttrs' - ) + const inheritAttrsProp = utils.findProperty(node, 'inheritAttrs') if (inheritAttrsProp && inheritAttrsProp.value.type === 'Literal') { inheritsAttrs = inheritAttrsProp.value.value } }), utils.defineTemplateBodyVisitor(context, { + /** @param {VExpressionContainer} node */ "VAttribute[directive=true][key.name.name='bind'][key.argument=null] > VExpressionContainer"( node ) { diff --git a/lib/rules/no-duplicate-attributes.js b/lib/rules/no-duplicate-attributes.js index 023dacf42..83cc52d83 100644 --- a/lib/rules/no-duplicate-attributes.js +++ b/lib/rules/no-duplicate-attributes.js @@ -17,15 +17,20 @@ const utils = require('../utils') /** * Get the name of the given attribute node. - * @param {ASTNode} attribute The attribute node to get. - * @returns {string} The name of the attribute. + * @param {VAttribute | VDirective} attribute The attribute node to get. + * @returns {string | null} The name of the attribute. */ function getName(attribute) { if (!attribute.directive) { return attribute.key.name } if (attribute.key.name.name === 'bind') { - return (attribute.key.argument && attribute.key.argument.name) || null + return ( + (attribute.key.argument && + attribute.key.argument.type === 'VIdentifier' && + attribute.key.argument.name) || + null + ) } return null } @@ -58,15 +63,21 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const allowCoexistStyle = options.allowCoexistStyle !== false const allowCoexistClass = options.allowCoexistClass !== false + /** @type {Set} */ const directiveNames = new Set() + /** @type {Set} */ const attributeNames = new Set() + /** + * @param {string} name + * @param {boolean} isDirective + */ function isDuplicate(name, isDirective) { if ( (allowCoexistStyle && name === 'style') || diff --git a/lib/rules/no-empty-pattern.js b/lib/rules/no-empty-pattern.js index daa5b1c26..2d8223876 100644 --- a/lib/rules/no-empty-pattern.js +++ b/lib/rules/no-empty-pattern.js @@ -5,5 +5,5 @@ const { wrapCoreRule } = require('../utils') -// eslint-disable-next-line +// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule(require('eslint/lib/rules/no-empty-pattern')) diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 4cc865a5b..3e04f193e 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -12,13 +12,6 @@ module.exports = wrapCoreRule(require('eslint/lib/rules/no-extra-parens'), { create: createForVueSyntax }) -/** - * @typedef {import('vue-eslint-parser').AST.Token} Token - * @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression - * @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer - * @typedef {import('vue-eslint-parser').AST.VFilterSequenceExpression} VFilterSequenceExpression - */ - /** * Check whether the given token is a left parenthesis. * @param {Token} token The token to check. @@ -75,8 +68,8 @@ function isRightBracket(token) { /** * Determines if a given expression node is an IIFE - * @param {ASTNode} node The node to check - * @returns {boolean} `true` if the given node is an IIFE + * @param {Expression} node The node to check + * @returns {node is CallExpression & { callee: FunctionExpression } } `true` if the given node is an IIFE */ function isIIFE(node) { return ( @@ -84,14 +77,19 @@ function isIIFE(node) { ) } +/** + * @param {RuleContext} context - The rule context. + * @returns {TemplateListener} AST event handlers. + */ function createForVueSyntax(context) { - const tokenStore = - context.parserServices.getTemplateBodyTokenStore && - context.parserServices.getTemplateBodyTokenStore() + if (!context.parserServices.getTemplateBodyTokenStore) { + return {} + } + const tokenStore = context.parserServices.getTemplateBodyTokenStore() /** * Checks if the given node turns into a filter when unwraped. - * @param {Expression} node node to evaluate + * @param {Expression} expression node to evaluate * @returns {boolean} `true` if the given node turns into a filter when unwraped. */ function isUnwrapChangeToFilter(expression) { @@ -118,17 +116,17 @@ function createForVueSyntax(context) { return false } /** - * @param {VExpressionContainer} node + * @param {VExpressionContainer & { expression: Expression | VFilterSequenceExpression | null }} node */ function verify(node) { - let expression = node.expression - if (!expression) { + if (!node.expression) { return } - if (expression.type === 'VFilterSequenceExpression') { - expression = expression.expression - } + const expression = + node.expression.type === 'VFilterSequenceExpression' + ? node.expression.expression + : node.expression if (!isParenthesized(expression, tokenStore)) { return diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js index babd2d6e1..7c05083b3 100644 --- a/lib/rules/no-irregular-whitespace.js +++ b/lib/rules/no-irregular-whitespace.js @@ -71,9 +71,13 @@ module.exports = { disallow: 'Irregular whitespace not allowed.' } }, - + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { // Module store of error indexes that we have found + /** @type {number[]} */ let errorIndexes = [] // Lookup the `skipComments` option, which defaults to `false`. @@ -89,7 +93,7 @@ module.exports = { /** * Removes errors that occur inside a string node - * @param {ASTNode} node to check for matching errors. + * @param {ASTNode | Token} node to check for matching errors. * @returns {void} * @private */ @@ -103,7 +107,7 @@ module.exports = { /** * Checks literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors - * @param {ASTNode} node to check for matching errors. + * @param {Literal} node to check for matching errors. * @returns {void} * @private */ @@ -113,7 +117,7 @@ module.exports = { if (shouldCheckStrings || shouldCheckRegExps) { // If we have irregular characters remove them from the errors list - if (ALL_IRREGULARS.test(node.raw)) { + if (ALL_IRREGULARS.test(sourceCode.getText(node))) { removeWhitespaceError(node) } } @@ -121,7 +125,7 @@ module.exports = { /** * Checks template string literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors - * @param {ASTNode} node to check for matching errors. + * @param {TemplateElement} node to check for matching errors. * @returns {void} * @private */ @@ -133,7 +137,7 @@ module.exports = { /** * Checks HTML attribute value nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors - * @param {ASTNode} node to check for matching errors. + * @param {VLiteral} node to check for matching errors. * @returns {void} * @private */ @@ -145,7 +149,7 @@ module.exports = { /** * Checks HTML text content nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors - * @param {ASTNode} node to check for matching errors. + * @param {VText} node to check for matching errors. * @returns {void} * @private */ @@ -157,7 +161,7 @@ module.exports = { /** * Checks comment nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors - * @param {ASTNode} node to check for matching errors. + * @param {Comment | HTMLComment | HTMLBogusComment} node to check for matching errors. * @returns {void} * @private */ diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js index cafcde97c..2b5304c9b 100644 --- a/lib/rules/no-lifecycle-after-await.js +++ b/lib/rules/no-lifecycle-after-await.js @@ -6,6 +6,10 @@ const { ReferenceTracker } = require('eslint-utils') const utils = require('../utils') +/** + * @typedef {import('eslint-utils').TYPES.TraceMap} TraceMap + */ + const LIFECYCLE_HOOKS = [ 'onBeforeMount', 'onBeforeUnmount', @@ -34,17 +38,32 @@ module.exports = { forbidden: 'The lifecycle hooks after `await` expression are forbidden.' } }, + /** @param {RuleContext} context */ create(context) { + /** + * @typedef {object} SetupFunctionData + * @property {Property} setupProperty + * @property {boolean} afterAwait + */ + /** + * @typedef {object} ScopeStack + * @property {ScopeStack} upper + * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} functionNode + */ + /** @type {Set} */ const lifecycleHookCallNodes = new Set() + /** @type {Map} */ const setupFunctions = new Map() - let scopeStack = { upper: null, functionNode: null } + /** @type {ScopeStack} */ + let scopeStack return Object.assign( { Program() { const tracker = new ReferenceTracker(context.getScope()) const traceMap = { + /** @type {TraceMap} */ vue: { [ReferenceTracker.ESM]: true } @@ -71,14 +90,18 @@ module.exports = { }) }, AwaitExpression() { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + const setupFunctionData = setupFunctions.get( + scopeStack && scopeStack.functionNode + ) if (!setupFunctionData) { return } setupFunctionData.afterAwait = true }, CallExpression(node) { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + const setupFunctionData = setupFunctions.get( + scopeStack && scopeStack.functionNode + ) if (!setupFunctionData || !setupFunctionData.afterAwait) { return } diff --git a/lib/rules/no-multi-spaces.js b/lib/rules/no-multi-spaces.js index 3c79ee7d0..ef1fee678 100644 --- a/lib/rules/no-multi-spaces.js +++ b/lib/rules/no-multi-spaces.js @@ -8,6 +8,10 @@ // Rule Definition // ------------------------------------------------------------------------------ +/** + * @param {RuleContext} context + * @param {Token} node + */ const isProperty = (context, node) => { const sourceCode = context.getSourceCode() return node.type === 'Punctuator' && sourceCode.getText(node) === ':' @@ -37,7 +41,7 @@ module.exports = { /** * @param {RuleContext} context - The rule context. - * @returns {Object} AST event handlers. + * @returns {RuleListener} AST event handlers. */ create(context) { const options = context.options[0] || {} @@ -62,7 +66,7 @@ module.exports = { includeComments: true }) - let prevToken = tokens.shift() + let prevToken = /** @type {Token} */ (tokens.shift()) for (const token of tokens) { const spaces = token.range[0] - prevToken.range[1] const shouldIgnore = diff --git a/lib/rules/no-multiple-slot-args.js b/lib/rules/no-multiple-slot-args.js index e8bcae96c..f98e058ec 100644 --- a/lib/rules/no-multiple-slot-args.js +++ b/lib/rules/no-multiple-slot-args.js @@ -11,11 +11,6 @@ const utils = require('../utils') const { findVariable } = require('eslint-utils') -/** - * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression - * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier - */ - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -35,7 +30,7 @@ module.exports = { unexpectedSpread: 'Unexpected spread argument.' } }, - + /** @param {RuleContext} context */ create(context) { /** * Verify the given node @@ -88,7 +83,6 @@ module.exports = { * @param {Identifier} node The node to verify */ function verifyReferences(node) { - // @ts-ignore const variable = findVariable(context.getScope(), node) if (!variable) { return diff --git a/lib/rules/no-multiple-template-root.js b/lib/rules/no-multiple-template-root.js index 4fd0df0eb..1bbad01ca 100644 --- a/lib/rules/no-multiple-template-root.js +++ b/lib/rules/no-multiple-template-root.js @@ -25,7 +25,10 @@ module.exports = { fixable: null, schema: [] }, - + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { const sourceCode = context.getSourceCode() diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js index 4498fc9f5..b8eeb17e4 100644 --- a/lib/rules/no-mutating-props.js +++ b/lib/rules/no-mutating-props.js @@ -7,18 +7,6 @@ const utils = require('../utils') const { findVariable } = require('eslint-utils') -/** - * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression - * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression - * @typedef {import('vue-eslint-parser').AST.ESLintProperty} Property - * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier - * @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer - * @typedef {import('vue-eslint-parser').AST.ESLintObjectPattern} ObjectPattern - * @typedef {import('vue-eslint-parser').AST.ESLintArrayPattern} ArrayPattern - * @typedef {import('vue-eslint-parser').AST.ESLintPattern} Pattern - * @typedef {import('vue-eslint-parser').AST.Node} Node - */ - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -36,7 +24,7 @@ module.exports = { // fill in your schema ] }, - + /** @param {RuleContext} context */ create(context) { /** @type {Map>} */ const propsMap = new Map() @@ -44,7 +32,7 @@ module.exports = { let vueObjectData = null /** - * @param {Node} node + * @param {ASTNode} node * @param {string} name */ function report(node, name) { @@ -58,18 +46,18 @@ module.exports = { } /** - * @param {Node} node + * @param {ASTNode} node * @returns {VExpressionContainer} */ function getVExpressionContainer(node) { let n = node while (n.type !== 'VExpressionContainer') { - n = n.parent + n = /** @type {ASTNode} */ (n.parent) } return n } /** - * @param {MemberExpression|Property} node + * @param {MemberExpression|AssignmentProperty} node * @returns {string} */ function getPropertyNameText(node) { @@ -85,7 +73,7 @@ module.exports = { return '?unknown?' } /** - * @param {Node} node + * @param {ASTNode} node * @returns {node is Identifier} */ function isVmReference(node) { @@ -171,11 +159,19 @@ module.exports = { onVueObjectEnter(node) { propsMap.set( node, - new Set(utils.getComponentProps(node).map((p) => p.propName)) + new Set( + utils + .getComponentProps(node) + .map((p) => p.propName) + .filter(utils.isDef) + ) ) }, onVueObjectExit(node, { type }) { - if (!vueObjectData || vueObjectData.type !== 'export') { + if ( + (!vueObjectData || vueObjectData.type !== 'export') && + type !== 'instance' + ) { vueObjectData = { type, object: node @@ -183,7 +179,6 @@ module.exports = { } }, onSetupFunctionEnter(node) { - /** @type {Pattern} */ const propsParam = node.params[0] if (!propsParam) { // no arguments @@ -200,7 +195,6 @@ module.exports = { propsParam, [] )) { - // @ts-ignore const variable = findVariable(context.getScope(), prop) if (!variable) { continue @@ -210,7 +204,6 @@ module.exports = { if (!reference.isRead()) { continue } - /** @type {Identifier} */ const id = reference.identifier const invalid = utils.findMutating(id) @@ -235,6 +228,7 @@ module.exports = { } } }, + /** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */ 'MemberExpression > :matches(Identifier, ThisExpression)'( node, { node: vueNode } @@ -242,32 +236,40 @@ module.exports = { if (!utils.isThis(node, context)) { return } - /** @type {MemberExpression} */ const mem = node.parent if (mem.object !== node) { return } const name = utils.getStaticPropertyName(mem) - if (name && propsMap.get(vueNode).has(name)) { + if ( + name && + /** @type {Set} */ (propsMap.get(vueNode)).has(name) + ) { verifyMutating(mem, name) } } }), utils.defineTemplateBodyVisitor(context, { + /** @param {ThisExpression & { parent: MemberExpression } } node */ 'VExpressionContainer MemberExpression > ThisExpression'(node) { if (!vueObjectData) { return } - /** @type {MemberExpression} */ const mem = node.parent if (mem.object !== node) { return } const name = utils.getStaticPropertyName(mem) - if (name && propsMap.get(vueObjectData.object).has(name)) { + if ( + name && + /** @type {Set} */ (propsMap.get(vueObjectData.object)).has( + name + ) + ) { verifyMutating(mem, name) } }, + /** @param {Identifier } node */ 'VExpressionContainer Identifier'(node) { if (!vueObjectData) { return @@ -276,10 +278,16 @@ module.exports = { return } const name = node.name - if (name && propsMap.get(vueObjectData.object).has(name)) { + if ( + name && + /** @type {Set} */ (propsMap.get(vueObjectData.object)).has( + name + ) + ) { verifyMutating(node, name) } }, + /** @param {ESNode} node */ "VAttribute[directive=true][key.name.name='model'] VExpressionContainer > *"( node ) { @@ -300,7 +308,12 @@ module.exports = { } else { return } - if (name && propsMap.get(vueObjectData.object).has(name)) { + if ( + name && + /** @type {Set} */ (propsMap.get(vueObjectData.object)).has( + name + ) + ) { report(node, name) } } diff --git a/lib/rules/no-parsing-error.js b/lib/rules/no-parsing-error.js index d51fd7f61..9a6b4f869 100644 --- a/lib/rules/no-parsing-error.js +++ b/lib/rules/no-parsing-error.js @@ -70,12 +70,15 @@ module.exports = { properties: Object.keys(DEFAULT_OPTIONS).reduce((ret, code) => { ret[code] = { type: 'boolean' } return ret - }, {}), + }, /** @type { { [key: string]: { type: 'boolean' } } } */ ({})), additionalProperties: false } ] }, - + /** + * @param {RuleContext} context - The rule context. + * @returns {RuleListener} AST event handlers. + */ create(context) { const options = Object.assign({}, DEFAULT_OPTIONS, context.options[0] || {}) diff --git a/lib/rules/no-potential-component-option-typo.js b/lib/rules/no-potential-component-option-typo.js index a29d2acf0..f3ac47363 100644 --- a/lib/rules/no-potential-component-option-typo.js +++ b/lib/rules/no-potential-component-option-typo.js @@ -48,50 +48,64 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const option = context.options[0] || {} const custom = option.custom || [] + /** @type {('all' | 'vue' | 'vue-router' | 'nuxt')[]} */ const presets = option.presets || ['vue'] const threshold = option.threshold || 1 - let candidateOptions - if (presets.includes('all')) { - candidateOptions = Object.keys(vueComponentOptions).reduce((pre, cur) => { - return [...pre, ...vueComponentOptions[cur]] - }, []) - } else { - candidateOptions = presets.reduce((pre, cur) => { - return [...pre, ...vueComponentOptions[cur]] - }, []) + /** @type {Set} */ + const candidateOptionSet = new Set(custom) + for (const preset of presets) { + if (preset === 'all') { + for (const opts of Object.values(vueComponentOptions)) { + for (const opt of opts) { + candidateOptionSet.add(opt) + } + } + } else { + for (const opt of vueComponentOptions[preset]) { + candidateOptionSet.add(opt) + } + } } - const candidateOptionSet = new Set([...candidateOptions, ...custom]) const candidateOptionList = [...candidateOptionSet] if (!candidateOptionList.length) { return {} } return utils.executeOnVue(context, (obj) => { - const componentInstanceOptions = obj.properties.filter( - (p) => p.type === 'Property' && p.key.type === 'Identifier' - ) + const componentInstanceOptions = obj.properties + .map((p) => { + if (p.type === 'Property') { + const name = utils.getStaticPropertyName(p) + if (name != null) { + return { + name, + key: p.key + } + } + } + return null + }) + .filter(utils.isDef) + if (!componentInstanceOptions.length) { - return {} + return } componentInstanceOptions.forEach((option) => { const id = option.key - const name = id.name + const name = option.name if (candidateOptionSet.has(name)) { return } const potentialTypoList = candidateOptionList .map((o) => ({ option: o, distance: utils.editDistance(o, name) })) - .filter( - ({ distance, option }) => distance <= threshold && distance > 0 - ) + .filter(({ distance }) => distance <= threshold && distance > 0) .sort((a, b) => a.distance - b.distance) if (potentialTypoList.length) { context.report({ node: id, - loc: id.loc, message: `'{{name}}' may be a typo, which is similar to option [{{option}}].`, data: { name, diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index cfd09921c..bfb344d24 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -22,9 +22,20 @@ module.exports = { 'Must use `.value` to read or write the value wrapped by `{{method}}()`.' } }, + /** @param {RuleContext} context */ create(context) { + /** + * @typedef {object} ReferenceData + * @property {VariableDeclarator} variableDeclarator + * @property {VariableDeclaration | null} variableDeclaration + * @property {string} method + */ + /** @type {Map} */ const refReferenceIds = new Map() + /** + * @param {Identifier} node + */ function reportIfRefWrapped(node) { const data = refReferenceIds.get(node) if (!data) { @@ -97,30 +108,37 @@ module.exports = { } }, // if (refValue) + /** @param {Identifier} node */ 'IfStatement>Identifier'(node) { reportIfRefWrapped(node) }, // switch (refValue) + /** @param {Identifier} node */ 'SwitchStatement>Identifier'(node) { reportIfRefWrapped(node) }, // -refValue, +refValue, !refValue, ~refValue, typeof refValue + /** @param {Identifier} node */ 'UnaryExpression>Identifier'(node) { reportIfRefWrapped(node) }, // refValue++, refValue-- + /** @param {Identifier} node */ 'UpdateExpression>Identifier'(node) { reportIfRefWrapped(node) }, // refValue+1, refValue-1 + /** @param {Identifier} node */ 'BinaryExpression>Identifier'(node) { reportIfRefWrapped(node) }, // refValue+=1, refValue-=1, foo+=refValue, foo-=refValue + /** @param {Identifier} node */ 'AssignmentExpression>Identifier'(node) { reportIfRefWrapped(node) }, // refValue || other, refValue && other. ignore: other || refValue + /** @param {Identifier & {parent: LogicalExpression}} node */ 'LogicalExpression>Identifier'(node) { if (node.parent.left !== node) { return @@ -139,6 +157,7 @@ module.exports = { reportIfRefWrapped(node) }, // refValue ? x : y + /** @param {Identifier & {parent: ConditionalExpression}} node */ 'ConditionalExpression>Identifier'(node) { if (node.parent.test !== node) { return @@ -146,10 +165,12 @@ module.exports = { reportIfRefWrapped(node) }, // `${refValue}` + /** @param {Identifier} node */ 'TemplateLiteral>Identifier'(node) { reportIfRefWrapped(node) }, // refValue.x + /** @param {Identifier & {parent: MemberExpression}} node */ 'MemberExpression>Identifier'(node) { if (node.parent.object !== node) { return diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js index 258f1c517..e21bf7cd8 100644 --- a/lib/rules/no-reserved-component-names.js +++ b/lib/rules/no-reserved-component-names.js @@ -33,9 +33,14 @@ const vueBuiltInComponents = [ const vue3BuiltInComponents = ['teleport', 'suspense'] -const isLowercase = (word) => /^[a-z]*$/.test(word) -const capitalizeFirstLetter = (word) => - word[0].toUpperCase() + word.substring(1, word.length) +/** @param {string} word */ +function isLowercase(word) { + return /^[a-z]*$/.test(word) +} +/** @param {string} word */ +function capitalizeFirstLetter(word) { + return word[0].toUpperCase() + word.substring(1, word.length) +} const RESERVED_NAMES_IN_HTML = new Set([ ...htmlElements, @@ -93,7 +98,7 @@ module.exports = { reservedInVue3: 'Name "{{name}}" is reserved in Vue.js 3.x.' } }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const disallowVueBuiltInComponents = @@ -108,6 +113,10 @@ module.exports = { ...RESERVED_NAMES_IN_OTHERS ]) + /** + * @param {Expression | SpreadElement} node + * @returns {node is (Literal | TemplateLiteral)} + */ function canVerify(node) { return ( node.type === 'Literal' || @@ -117,19 +126,26 @@ module.exports = { ) } + /** + * @param {Literal | TemplateLiteral} node + */ function reportIfInvalid(node) { let name if (node.type === 'TemplateLiteral') { const quasis = node.quasis[0] name = quasis.value.cooked } else { - name = node.value + name = `${node.value}` } if (reservedNames.has(name)) { report(node, name) } } + /** + * @param {ESNode} node + * @param {string} name + */ function report(node, name) { context.report({ node, @@ -164,14 +180,10 @@ module.exports = { .filter(({ name }) => reservedNames.has(name)) .forEach(({ node, name }) => report(node, name)) - const node = obj.properties.find( - (item) => - item.type === 'Property' && - item.key.name === 'name' && - canVerify(item.value) - ) + const node = utils.findProperty(obj, 'name') if (!node) return + if (!canVerify(node.value)) return reportIfInvalid(node.value) }) ) diff --git a/lib/rules/no-reserved-keys.js b/lib/rules/no-reserved-keys.js index d17ce05df..38b4f410d 100644 --- a/lib/rules/no-reserved-keys.js +++ b/lib/rules/no-reserved-keys.js @@ -6,11 +6,16 @@ const utils = require('../utils') +/** + * @typedef {import('../utils').GroupName} GroupName + */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ const RESERVED_KEYS = require('../utils/vue-reserved.json') +/** @type {GroupName[]} */ const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup'] module.exports = { @@ -37,7 +42,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const reservedKeys = new Set(RESERVED_KEYS.concat(options.reserved || [])) diff --git a/lib/rules/no-restricted-static-attribute.js b/lib/rules/no-restricted-static-attribute.js index 70db824db..24cab62b8 100644 --- a/lib/rules/no-restricted-static-attribute.js +++ b/lib/rules/no-restricted-static-attribute.js @@ -7,9 +7,6 @@ const utils = require('../utils') const regexp = require('../utils/regexp') -/** - * @typedef {import('vue-eslint-parser').AST.VAttribute} VAttribute - */ /** * @typedef {object} ParsedOption * @property { (key: VAttribute) => boolean } test @@ -118,6 +115,7 @@ module.exports = { restrictedAttr: '{{message}}' } }, + /** @param {RuleContext} context */ create(context) { if (!context.options.length) { return {} diff --git a/lib/rules/no-restricted-syntax.js b/lib/rules/no-restricted-syntax.js index 869540001..279f400cd 100644 --- a/lib/rules/no-restricted-syntax.js +++ b/lib/rules/no-restricted-syntax.js @@ -5,5 +5,5 @@ const { wrapCoreRule } = require('../utils') -// eslint-disable-next-line +// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule(require('eslint/lib/rules/no-restricted-syntax')) diff --git a/lib/rules/no-restricted-v-bind.js b/lib/rules/no-restricted-v-bind.js index 7ec586dd5..02ac6e399 100644 --- a/lib/rules/no-restricted-v-bind.js +++ b/lib/rules/no-restricted-v-bind.js @@ -6,11 +6,6 @@ const utils = require('../utils') const regexp = require('../utils/regexp') - -/** - * @typedef {import('vue-eslint-parser').AST.VDirectiveKey} VDirectiveKey - * @typedef {import('vue-eslint-parser').AST.VIdentifier} VIdentifier - */ /** * @typedef {object} ParsedOption * @property { (key: VDirectiveKey) => boolean } test @@ -50,10 +45,10 @@ function parseOption(option) { const matcher = buildMatcher(option) return { test(key) { - return ( + return Boolean( key.argument && - key.argument.type === 'VIdentifier' && - matcher(key.argument.rawName) + key.argument.type === 'VIdentifier' && + matcher(key.argument.rawName) ) }, modifiers: [] @@ -74,7 +69,7 @@ function parseOption(option) { if (!argTest(key)) { return false } - return option.modifiers.every((modName) => { + return /** @type {string[]} */ (option.modifiers).every((modName) => { return key.modifiers.some((mid) => mid.name === modName) }) } @@ -139,6 +134,7 @@ module.exports = { restrictedVBind: '{{message}}' } }, + /** @param {RuleContext} context */ create(context) { /** @type {ParsedOption[]} */ const options = (context.options.length === 0 diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js index c4f04edd4..43b1856ee 100644 --- a/lib/rules/no-setup-props-destructure.js +++ b/lib/rules/no-setup-props-destructure.js @@ -23,9 +23,15 @@ module.exports = { 'Getting a value from the `props` in root scope of `setup()` will cause the value to lose reactivity.' } }, + /** @param {RuleContext} context */ create(context) { + /** @type {Map>} */ const setupScopePropsReferenceIds = new Map() + /** + * @param {ESNode} node + * @param {string} messageId + */ function report(node, messageId) { context.report({ node, @@ -33,6 +39,11 @@ module.exports = { }) } + /** + * @param {Pattern} left + * @param {Expression | null} right + * @param {Set} propsReferenceIds + */ function verify(left, right, propsReferenceIds) { if (!right) { return @@ -46,23 +57,31 @@ module.exports = { return } + /** @type {Expression | Super} */ let rightId = right while (rightId.type === 'MemberExpression') { rightId = rightId.object } - if (propsReferenceIds.has(rightId)) { + if (rightId.type === 'Identifier' && propsReferenceIds.has(rightId)) { report(left, 'getProperty') } } - - let scopeStack = null + /** + * @typedef {object} ScopeStack + * @property {ScopeStack} upper + * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} functionNode + */ + /** + * @type {ScopeStack} + */ + let scopeStack return utils.defineVueVisitor(context, { ':function'(node) { scopeStack = { upper: scopeStack, functionNode: node } }, onSetupFunctionEnter(node) { - const propsParam = node.params[0] + const propsParam = utils.unwrapAssignmentPattern(node.params[0]) if (!propsParam) { // no arguments return diff --git a/lib/rules/no-shared-component-data.js b/lib/rules/no-shared-component-data.js index 2200f0e78..160f41ef9 100644 --- a/lib/rules/no-shared-component-data.js +++ b/lib/rules/no-shared-component-data.js @@ -6,14 +6,20 @@ const utils = require('../utils') +/** @param {Token} token */ function isOpenParen(token) { return token.type === 'Punctuator' && token.value === '(' } +/** @param {Token} token */ function isCloseParen(token) { return token.type === 'Punctuator' && token.value === ')' } +/** + * @param {Expression} node + * @param {SourceCode} sourceCode + */ function getFirstAndLastTokens(node, sourceCode) { let first = sourceCode.getFirstToken(node) let last = sourceCode.getLastToken(node) @@ -46,35 +52,33 @@ module.exports = { fixable: 'code', schema: [] }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() return utils.executeOnVueComponent(context, (obj) => { - obj.properties - .filter( - (p) => - p.type === 'Property' && - p.key.type === 'Identifier' && - p.key.name === 'data' && - p.value.type !== 'FunctionExpression' && - p.value.type !== 'ArrowFunctionExpression' && - p.value.type !== 'Identifier' - ) - .forEach((p) => { - context.report({ - node: p, - message: '`data` property in component must be a function.', - fix(fixer) { - const tokens = getFirstAndLastTokens(p.value, sourceCode) + const invalidData = utils.findProperty( + obj, + 'data', + (p) => + p.value.type !== 'FunctionExpression' && + p.value.type !== 'ArrowFunctionExpression' && + p.value.type !== 'Identifier' + ) + if (invalidData) { + context.report({ + node: invalidData, + message: '`data` property in component must be a function.', + fix(fixer) { + const tokens = getFirstAndLastTokens(invalidData.value, sourceCode) - return [ - fixer.insertTextBefore(tokens.first, 'function() {\nreturn '), - fixer.insertTextAfter(tokens.last, ';\n}') - ] - } - }) + return [ + fixer.insertTextBefore(tokens.first, 'function() {\nreturn '), + fixer.insertTextAfter(tokens.last, ';\n}') + ] + } }) + } }) } } diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js index 59f3b37e7..e6cd548e9 100644 --- a/lib/rules/no-side-effects-in-computed-properties.js +++ b/lib/rules/no-side-effects-in-computed-properties.js @@ -7,8 +7,7 @@ const utils = require('../utils') /** - * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression - * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression + * @typedef {import('../utils').VueObjectData} VueObjectData * @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty */ @@ -28,12 +27,21 @@ module.exports = { fixable: null, schema: [] }, - + /** @param {RuleContext} context */ create(context) { /** @type {Map} */ const computedPropertiesMap = new Map() - let scopeStack = { upper: null, body: null } + /** + * @typedef {object} ScopeStack + * @property {ScopeStack} upper + * @property {BlockStatement | Expression} body + */ + /** + * @type {ScopeStack} + */ + let scopeStack + /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */ function onFunctionEnter(node) { scopeStack = { upper: scopeStack, body: node.body } } @@ -49,21 +57,25 @@ module.exports = { ':function': onFunctionEnter, ':function:exit': onFunctionExit, + /** + * @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node + * @param {VueObjectData} data + */ 'MemberExpression > :matches(Identifier, ThisExpression)'( node, { node: vueNode } ) { const targetBody = scopeStack.body - const computedProperty = computedPropertiesMap - .get(vueNode) - .find((cp) => { - return ( - cp.value && - node.loc.start.line >= cp.value.loc.start.line && - node.loc.end.line <= cp.value.loc.end.line && - targetBody === cp.value - ) - }) + const computedProperty = /** @type {ComponentComputedProperty[]} */ (computedPropertiesMap.get( + vueNode + )).find((cp) => { + return ( + cp.value && + node.loc.start.line >= cp.value.loc.start.line && + node.loc.end.line <= cp.value.loc.end.line && + targetBody === cp.value + ) + }) if (!computedProperty) { return } @@ -71,7 +83,6 @@ module.exports = { if (!utils.isThis(node, context)) { return } - /** @type {MemberExpression} */ const mem = node.parent if (mem.object !== node) { return @@ -82,7 +93,7 @@ module.exports = { context.report({ node: invalid.node, message: 'Unexpected side effect in "{{key}}" computed property.', - data: { key: computedProperty.key } + data: { key: computedProperty.key || 'Unknown' } }) } } diff --git a/lib/rules/no-spaces-around-equal-signs-in-attribute.js b/lib/rules/no-spaces-around-equal-signs-in-attribute.js index 4bb32f03f..8357fbf3d 100644 --- a/lib/rules/no-spaces-around-equal-signs-in-attribute.js +++ b/lib/rules/no-spaces-around-equal-signs-in-attribute.js @@ -26,7 +26,7 @@ module.exports = { fixable: 'whitespace', schema: [] }, - + /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() return utils.defineTemplateBodyVisitor(context, { @@ -34,6 +34,7 @@ module.exports = { if (!node.value) { return } + /** @type {Range} */ const range = [node.key.range[1], node.value.range[0]] const eqText = sourceCode.text.slice(range[0], range[1]) const expect = eqText.trim() diff --git a/lib/rules/no-static-inline-styles.js b/lib/rules/no-static-inline-styles.js index 73421a97b..7dce27e1a 100644 --- a/lib/rules/no-static-inline-styles.js +++ b/lib/rules/no-static-inline-styles.js @@ -30,10 +30,11 @@ module.exports = { forbiddenStyleAttr: '`style` attributes are forbidden.' } }, + /** @param {RuleContext} context */ create(context) { /** * Checks whether if the given property node is a static value. - * @param {AssignmentProperty} prop property node to check + * @param {Property} prop property node to check * @returns {boolean} `true` if the given property node is a static value. */ function isStaticValue(prop) { @@ -55,8 +56,8 @@ module.exports = { * - If all properties are static properties, it returns one root node. * `:style="[ { color: 'red' }, { display: 'flex', width: '16px' } ]"` * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - * @param {VAttribute} node `:style` node to check - * @returns {AssignmentProperty[] | [VAttribute]} the static properties. + * @param {VDirective} node `:style` node to check + * @returns {Property[] | [VDirective]} the static properties. */ function getReportNodes(node) { const { value } = node @@ -108,7 +109,7 @@ module.exports = { /** * Reports if the value is static. - * @param {VAttribute} node `:style` node to check + * @param {VDirective} node `:style` node to check */ function verifyVBindStyle(node) { for (const n of getReportNodes(node)) { @@ -119,7 +120,9 @@ module.exports = { } } + /** @type {TemplateListener} */ const visitor = { + /** @param {VAttribute} node */ "VAttribute[directive=false][key.name='style']"(node) { context.report({ node, diff --git a/lib/rules/no-template-key.js b/lib/rules/no-template-key.js index c7b06ef56..935113820 100644 --- a/lib/rules/no-template-key.js +++ b/lib/rules/no-template-key.js @@ -26,9 +26,10 @@ module.exports = { fixable: null, schema: [] }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VElement} node */ "VElement[name='template']"(node) { if ( utils.hasAttribute(node, 'key') || diff --git a/lib/rules/no-template-shadow.js b/lib/rules/no-template-shadow.js index 73aeca296..c228642f4 100644 --- a/lib/rules/no-template-shadow.js +++ b/lib/rules/no-template-shadow.js @@ -10,10 +10,15 @@ const utils = require('../utils') +/** + * @typedef {import('../utils').GroupName} GroupName + */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ +/** @type {GroupName[]} */ const GROUP_NAMES = ['props', 'computed', 'data', 'methods'] module.exports = { @@ -28,13 +33,18 @@ module.exports = { fixable: null, schema: [] }, - + /** @param {RuleContext} context */ create(context) { + /** @type {Set} */ const jsVars = new Set() - let scope = { - parent: null, - nodes: [] - } + + /** + * @typedef {object} ScopeStack + * @property {ScopeStack} parent + * @property {Identifier[]} nodes + */ + /** @type {ScopeStack} */ + let scope // ---------------------------------------------------------------------- // Public @@ -43,10 +53,13 @@ module.exports = { return utils.defineTemplateBodyVisitor( context, { + /** @param {VElement} node */ VElement(node) { scope = { parent: scope, - nodes: scope.nodes.slice() // make copy + nodes: scope + ? scope.nodes.slice() // make copy + : [] } if (node.variables) { for (const variable of node.variables) { @@ -71,7 +84,7 @@ module.exports = { } } }, - 'VElement:exit'(node) { + 'VElement:exit'() { scope = scope.parent } }, diff --git a/lib/rules/no-template-target-blank.js b/lib/rules/no-template-target-blank.js index f7c1ab864..b671091ce 100644 --- a/lib/rules/no-template-target-blank.js +++ b/lib/rules/no-template-target-blank.js @@ -13,6 +13,7 @@ const utils = require('../utils') // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ +/** @param {VAttribute} node */ function isTargetBlank(node) { return ( node.key && @@ -21,11 +22,17 @@ function isTargetBlank(node) { node.value.value === '_blank' ) } - +/** + * @param {VStartTag} node + * @param {boolean} allowReferrer + */ function hasSecureRel(node, allowReferrer) { return node.attributes.some((attr) => { if (attr.key && attr.key.name === 'rel') { - const tags = attr.value && attr.value.value.toLowerCase().split(' ') + const tags = + attr.value && + attr.value.type === 'VLiteral' && + attr.value.value.toLowerCase().split(' ') return ( tags && tags.includes('noopener') && @@ -37,16 +44,23 @@ function hasSecureRel(node, allowReferrer) { }) } +/** + * @param {VStartTag} node + */ function hasExternalLink(node) { return node.attributes.some( (attr) => attr.key && attr.key.name === 'href' && attr.value && + attr.value.type === 'VLiteral' && /^(?:\w+:|\/\/)/.test(attr.value.value) ) } +/** + * @param {VStartTag} node + */ function hasDynamicLink(node) { return node.attributes.some( (attr) => @@ -55,6 +69,7 @@ function hasDynamicLink(node) { attr.key.name && attr.key.name.name === 'bind' && attr.key.argument && + attr.key.argument.type === 'VIdentifier' && attr.key.argument.name === 'href' ) } @@ -100,7 +115,8 @@ module.exports = { const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always' return utils.defineTemplateBodyVisitor(context, { - VAttribute(node) { + /** @param {VAttribute} node */ + 'VAttribute[directive=false]'(node) { if (!isTargetBlank(node) || hasSecureRel(node.parent, allowReferrer)) { return } diff --git a/lib/rules/no-textarea-mustache.js b/lib/rules/no-textarea-mustache.js index 06dabbf57..abbe01157 100644 --- a/lib/rules/no-textarea-mustache.js +++ b/lib/rules/no-textarea-mustache.js @@ -26,9 +26,10 @@ module.exports = { fixable: null, schema: [] }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VExpressionContainer} node */ "VElement[name='textarea'] VExpressionContainer"(node) { if (node.parent.type !== 'VElement') { return diff --git a/lib/rules/no-unregistered-components.js b/lib/rules/no-unregistered-components.js index d4ac6fa15..205580221 100644 --- a/lib/rules/no-unregistered-components.js +++ b/lib/rules/no-unregistered-components.js @@ -29,7 +29,7 @@ const VUE_BUILT_IN_COMPONENTS = [ * * Includes `suspense` and `teleport` from Vue 3. * - * @param {ASTNode} node The start tag node to check. + * @param {VElement} node The start tag node to check. * @returns {boolean} `true` if the node is a built-in component. */ const isBuiltInComponent = (node) => { @@ -68,16 +68,20 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} + /** @type {string[]} */ const ignorePatterns = options.ignorePatterns || [] + /** @type { { node: VElement | VDirective | VAttribute, name: string }[] } */ const usedComponentNodes = [] + /** @type { { node: Property, name: string }[] } */ const registeredComponents = [] return utils.defineTemplateBodyVisitor( context, { + /** @param {VElement} node */ VElement(node) { if ( (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) || @@ -90,6 +94,7 @@ module.exports = { usedComponentNodes.push({ node, name: node.rawName }) }, + /** @param {VDirective} node */ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"( node ) { @@ -101,11 +106,17 @@ module.exports = { return if (node.value.expression.type === 'Literal') { - if (utils.isHtmlWellKnownElementName(node.value.expression.value)) + if ( + utils.isHtmlWellKnownElementName(`${node.value.expression.value}`) + ) return - usedComponentNodes.push({ node, name: node.value.expression.value }) + usedComponentNodes.push({ + node, + name: `${node.value.expression.value}` + }) } }, + /** @param {VAttribute} node */ "VAttribute[directive=false][key.name='is']"(node) { if ( !node.value || // `` @@ -114,6 +125,7 @@ module.exports = { return usedComponentNodes.push({ node, name: node.value.value }) }, + /** @param {VElement} node */ "VElement[name='template']:exit"() { // All registered components, transformed to kebab-case const registeredComponentNames = registeredComponents.map( diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js index 4089eef94..eaf9ce57a 100644 --- a/lib/rules/no-unsupported-features.js +++ b/lib/rules/no-unsupported-features.js @@ -4,7 +4,7 @@ */ 'use strict' -const { Range } = require('semver') +const semver = require('semver') const utils = require('../utils') const FEATURES = { @@ -22,7 +22,7 @@ const cache = new Map() /** * Get the `semver.Range` object of a given range text. * @param {string} x The text expression for a semver range. - * @returns {Range|null} The range object of a given range text. + * @returns {semver.Range} The range object of a given range text. * It's null if the `x` is not a valid range text. */ function getSemverRange(x) { @@ -31,7 +31,7 @@ function getSemverRange(x) { if (!ret) { try { - ret = new Range(s) + ret = new semver.Range(s) } catch (_error) { // Ignore parsing error. } @@ -41,29 +41,6 @@ function getSemverRange(x) { return ret } -/** - * Merge two visitors. - * @param {Visitor} x The visitor which is assigned. - * @param {Visitor} y The visitor which is assigning. - * @returns {Visitor} `x`. - */ -function merge(x, y) { - for (const key of Object.keys(y)) { - if (typeof x[key] === 'function') { - if (x[key]._handlers == null) { - const fs = [x[key], y[key]] - x[key] = (node) => fs.forEach((h) => h(node)) - x[key]._handlers = fs - } else { - x[key]._handlers.push(y[key]) - } - } else { - x[key] = y[key] - } - } - return x -} - module.exports = { meta: { type: 'suggestion', @@ -106,6 +83,7 @@ module.exports = { '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".' } }, + /** @param {RuleContext} context */ create(context) { const { version, ignores } = Object.assign( { @@ -122,7 +100,7 @@ module.exports = { /** * Check whether a given case object is full-supported on the configured node version. - * @param {{supported:string}} aCase The case object to check. + * @param { { supported?: string | ((range: semver.Range) => boolean) } } aCase The case object to check. * @returns {boolean} `true` if it's supporting. */ function isNotSupportingVersion(aCase) { @@ -131,13 +109,16 @@ module.exports = { } return versionRange.intersects(getSemverRange(`<${aCase.supported}`)) } - const templateBodyVisitor = Object.keys(FEATURES) + + const keys = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES)) + + const templateBodyVisitor = keys .filter((syntaxName) => !ignores.includes(syntaxName)) .filter((syntaxName) => isNotSupportingVersion(FEATURES[syntaxName])) .reduce((result, syntaxName) => { const visitor = FEATURES[syntaxName].createTemplateBodyVisitor(context) if (visitor) { - merge(result, visitor) + return utils.compositingVisitors(result, visitor) } return result }, {}) diff --git a/lib/rules/no-unused-components.js b/lib/rules/no-unused-components.js index 09cad4af7..b76af7198 100644 --- a/lib/rules/no-unused-components.js +++ b/lib/rules/no-unused-components.js @@ -37,7 +37,7 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const ignoreWhenBindingPresent = @@ -45,13 +45,16 @@ module.exports = { ? options.ignoreWhenBindingPresent : true const usedComponents = new Set() + /** @type { { node: Property, name: string }[] } */ let registeredComponents = [] let ignoreReporting = false + /** @type {Position} */ let templateLocation return utils.defineTemplateBodyVisitor( context, { + /** @param {VElement} node */ VElement(node) { if ( (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) || @@ -63,6 +66,7 @@ module.exports = { usedComponents.add(node.rawName) }, + /** @param {VDirective} node */ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"( node ) { @@ -79,17 +83,23 @@ module.exports = { ignoreReporting = true } }, + /** @param {VAttribute} node */ "VAttribute[directive=false][key.name='is']"(node) { + if (!node.value) { + return + } usedComponents.add(node.value.value) }, - "VElement[name='template']"(rootNode) { - templateLocation = templateLocation || rootNode.loc.start + /** @param {VElement} node */ + "VElement[name='template']"(node) { + templateLocation = templateLocation || node.loc.start }, - "VElement[name='template']:exit"(rootNode) { + /** @param {VElement} node */ + "VElement[name='template']:exit"(node) { if ( - rootNode.loc.start !== templateLocation || + node.loc.start !== templateLocation || ignoreReporting || - utils.hasAttribute(rootNode, 'src') + utils.hasAttribute(node, 'src') ) return diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 8dce09b40..951d6a2db 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -11,25 +11,9 @@ const utils = require('../utils') const eslintUtils = require('eslint-utils') -/** - * @typedef {import('vue-eslint-parser').AST.Node} Node - * @typedef {import('vue-eslint-parser').AST.ESLintNode} ASTNode - * @typedef {import('vue-eslint-parser').AST.ESLintObjectPattern} ObjectPattern - * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier - * @typedef {import('vue-eslint-parser').AST.ESLintPattern} Pattern - * @typedef {import('vue-eslint-parser').AST.ESLintThisExpression} ThisExpression - * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression - * @typedef {import('vue-eslint-parser').AST.ESLintFunctionExpression} FunctionExpression - * @typedef {import('vue-eslint-parser').AST.ESLintArrowFunctionExpression} ArrowFunctionExpression - * @typedef {import('vue-eslint-parser').AST.ESLintFunctionDeclaration} FunctionDeclaration - * @typedef {import('vue-eslint-parser').AST.VAttribute} VAttribute - * @typedef {import('vue-eslint-parser').AST.VIdentifier} VIdentifier - * @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer - * @typedef {import('eslint').Scope.Variable} Variable - * @typedef {import('eslint').Rule.RuleContext} RuleContext - */ /** * @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData + * @typedef {import('../utils').VueObjectData} VueObjectData */ /** * @typedef {object} TemplatePropertiesContainer @@ -58,11 +42,12 @@ const GROUP_SETUP = 'setup' const GROUP_WATCHER = 'watch' const PROPERTY_LABEL = { - [GROUP_PROPERTY]: 'property', - [GROUP_DATA]: 'data', - [GROUP_COMPUTED_PROPERTY]: 'computed property', - [GROUP_METHODS]: 'method', - [GROUP_SETUP]: 'property returned from `setup()`' + props: 'property', + data: 'data', + computed: 'computed property', + methods: 'method', + setup: 'property returned from `setup()`', + watch: 'watch' } // ------------------------------------------------------------------------------ @@ -72,27 +57,26 @@ const PROPERTY_LABEL = { /** * Find the variable of a given name. * @param {RuleContext} context The rule context - * @param {ASTNode} node The variable name to find. + * @param {Identifier} node The variable name to find. * @returns {Variable|null} The found variable or null. */ function findVariable(context, node) { - // @ts-ignore return eslintUtils.findVariable(getScope(context, node), node) } /** * Gets the scope for the current node * @param {RuleContext} context The rule context - * @param {ASTNode} currentNode The node to get the scope of - * @returns { import('eslint-scope').Scope } The scope information for this node + * @param {ESNode} currentNode The node to get the scope of + * @returns { import('eslint').Scope.Scope } The scope information for this node */ function getScope(context, currentNode) { // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. const inner = currentNode.type !== 'Program' const scopeManager = context.getSourceCode().scopeManager - // @ts-ignore - for (let node = currentNode; node; node = node.parent) { - // @ts-ignore + /** @type {ESNode | null} */ + let node = currentNode + for (; node; node = /** @type {ESNode | null} */ (node.parent)) { const scope = scopeManager.acquire(node, inner) if (scope) { @@ -108,6 +92,7 @@ function getScope(context, currentNode) { /** * Extract names from references objects. + * @param {VReference[]} references */ function getReferencesNames(references) { return references @@ -169,7 +154,15 @@ function extractObjectPatternProperties(node) { for (const prop of node.properties) { if (prop.type === 'Property') { const name = utils.getStaticPropertyName(prop) - usedNames.add(name, getObjectPatternPropertyPatternTracker(prop.value)) + if (name) { + usedNames.add(name, getObjectPatternPropertyPatternTracker(prop.value)) + } else { + // If cannot trace name, everything is used! + return { + usedNames, + unknown: true + } + } } else { // If use RestElement, everything is used! return { @@ -206,8 +199,6 @@ function getObjectPatternPropertyPatternTracker(pattern) { return result } for (const reference of variable.references) { - /** @type {Identifier} */ - // @ts-ignore const id = reference.identifier const { usedNames, unknown, calls } = extractPatternOrThisProperties( id, @@ -259,8 +250,6 @@ function extractPatternOrThisProperties(node, context) { return result } for (const reference of variable.references) { - /** @type {Identifier} */ - // @ts-ignore const id = reference.identifier const { usedNames, unknown, calls } = extractPatternOrThisProperties( id, @@ -287,7 +276,6 @@ function extractPatternOrThisProperties(node, context) { } return result } else if (parent.type === 'CallExpression') { - // @ts-ignore const argIndex = parent.arguments.indexOf(node) if (argIndex > -1 && parent.callee.type === 'Identifier') { // `foo(arg)` @@ -299,13 +287,12 @@ function extractPatternOrThisProperties(node, context) { const def = calleeVariable.defs[0] if ( def.type === 'Variable' && - def.parent && def.parent.kind === 'const' && + def.node.init && (def.node.init.type === 'FunctionExpression' || def.node.init.type === 'ArrowFunctionExpression') ) { result.calls.push({ - // @ts-ignore node: def.node.init, index: argIndex }) @@ -338,12 +325,14 @@ class UsedProps { */ class ParamUsedProps extends UsedProps { /** - * @param {ASTNode} paramNode + * @param {Pattern} paramNode * @param {RuleContext} context */ constructor(paramNode, context) { super() - + while (paramNode.type === 'AssignmentPattern') { + paramNode = paramNode.left + } if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') { // cannot check return @@ -354,13 +343,14 @@ class ParamUsedProps extends UsedProps { this.unknown = this.unknown || unknown return } + if (paramNode.type !== 'Identifier') { + return + } const variable = findVariable(context, paramNode) if (!variable) { return } for (const reference of variable.references) { - /** @type {Identifier} */ - // @ts-ignore const id = reference.identifier const { usedNames, unknown, calls } = extractPatternOrThisProperties( id, @@ -390,7 +380,7 @@ class ParamsUsedProps { /** * @param {number} index - * @returns {ParamUsedProps} + * @returns {ParamUsedProps | null} */ getParam(index) { const param = this.params[index] @@ -446,7 +436,7 @@ module.exports = { unused: "'{{name}}' of {{group}} found, but never used." } }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const groups = new Set(options.groups || [GROUP_PROPERTY]) @@ -607,7 +597,9 @@ module.exports = { const container = getVueComponentPropertiesContainer(vueData.node) if (node.params[0]) { const paramsUsedProps = getParamsUsedProps(node) - const paramUsedProps = paramsUsedProps.getParam(0) + const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam( + 0 + )) processParamPropsUsed(container, paramUsedProps) } @@ -617,7 +609,9 @@ module.exports = { if (node.params[0]) { // for Vue 3.x render const paramsUsedProps = getParamsUsedProps(node) - const paramUsedProps = paramsUsedProps.getParam(0) + const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam( + 0 + )) processParamPropsUsed(container, paramUsedProps) if (container.unknownProps) { @@ -628,7 +622,9 @@ module.exports = { if (vueData.functional && node.params[1]) { // for Vue 2.x render & functional const paramsUsedProps = getParamsUsedProps(node) - const paramUsedProps = paramsUsedProps.getParam(1) + const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam( + 1 + )) for (const { usedNames, unknown } of iterateUsedProps( paramUsedProps @@ -647,6 +643,10 @@ module.exports = { } } }, + /** + * @param {ThisExpression | Identifier} node + * @param {VueObjectData} vueData + */ 'ThisExpression, Identifier'(node, vueData) { if (!utils.isThis(node, context)) { return @@ -666,6 +666,7 @@ module.exports = { } }), { + /** @param {Program} node */ 'Program:exit'(node) { if (!node.templateBody) { reportUnusedProperties() diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index e808ee7f9..a9939c1ae 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -7,12 +7,7 @@ const utils = require('../utils') /** - * @typedef {import('vue-eslint-parser').AST.Node} Node - * @typedef {import('vue-eslint-parser').AST.VElement} VElement - * @typedef {import('vue-eslint-parser').AST.Variable} Variable - */ -/** - * @typedef {Variable['kind']} VariableKind + * @typedef {VVariable['kind']} VariableKind */ // ------------------------------------------------------------------------------ @@ -22,10 +17,10 @@ const utils = require('../utils') /** * Groups variables by directive kind. * @param {VElement} node The element node - * @returns { { [kind in VariableKind]?: Variable[] } } The variables of grouped by directive kind. + * @returns { { [kind in VariableKind]?: VVariable[] } } The variables of grouped by directive kind. */ function groupingVariables(node) { - /** @type { { [kind in VariableKind]?: Variable[] } } */ + /** @type { { [kind in VariableKind]?: VVariable[] } } */ const result = {} for (const variable of node.variables) { const vars = result[variable.kind] || (result[variable.kind] = []) @@ -36,12 +31,12 @@ function groupingVariables(node) { /** * Checks if the given variable was defined by destructuring. - * @param {Variable} variable the given variable to check + * @param {VVariable} variable the given variable to check * @returns {boolean} `true` if the given variable was defined by destructuring. */ function isDestructuringVar(variable) { const node = variable.id - /** @type {Node} */ + /** @type {ASTNode | null} */ let parent = node.parent while (parent) { if ( @@ -84,10 +79,11 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const option = context.options[0] || {} const ignorePattern = option.ignorePattern + /** @type {RegExp | null} */ let ignoreRegEx = null if (ignorePattern) { ignoreRegEx = new RegExp(ignorePattern, 'u') @@ -99,6 +95,9 @@ module.exports = { VElement(node) { const vars = groupingVariables(node) for (const variables of Object.values(vars)) { + if (!variables) { + continue + } let hasAfterUsed = false for (let i = variables.length - 1; i >= 0; i--) { @@ -122,7 +121,9 @@ module.exports = { node: variable.id, loc: variable.id.loc, message: `'{{name}}' is defined but never used.`, - data: variable.id, + data: { + name: variable.id.name + }, suggest: ignorePattern === '^_' ? [ diff --git a/lib/rules/no-use-v-if-with-v-for.js b/lib/rules/no-use-v-if-with-v-for.js index a12fda3c9..0cc4aba49 100644 --- a/lib/rules/no-use-v-if-with-v-for.js +++ b/lib/rules/no-use-v-if-with-v-for.js @@ -20,14 +20,18 @@ const utils = require('../utils') /** * Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive. - * @param {ASTNode} vIf The `v-if` attribute node to check. + * @param {VDirective} vIf The `v-if` attribute node to check. * @returns {boolean} `true` if the `v-if` is using the variable which is defined by the `v-for` directive. */ function isUsingIterationVar(vIf) { return !!getVForUsingIterationVar(vIf) } +/** @param {VDirective} vIf */ function getVForUsingIterationVar(vIf) { + if (!vIf.value) { + return null + } const element = vIf.parent.parent for (const reference of vIf.value.references) { const targetVFor = element.variables.find( @@ -38,7 +42,7 @@ function getVForUsingIterationVar(vIf) { return targetVFor } } - return undefined + return null } // ------------------------------------------------------------------------------ @@ -65,11 +69,12 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const allowUsingIterationVar = options.allowUsingIterationVar === true // default false return utils.defineTemplateBodyVisitor(context, { + /** @param {VDirective} node */ "VAttribute[directive=true][key.name.name='if']"(node) { const element = node.parent.parent @@ -77,10 +82,13 @@ module.exports = { if (isUsingIterationVar(node)) { if (!allowUsingIterationVar) { const vForVar = getVForUsingIterationVar(node) + if (!vForVar) { + return + } let targetVForExpr = vForVar.id.parent while (targetVForExpr.type !== 'VForExpression') { - targetVForExpr = targetVForExpr.parent + targetVForExpr = /** @type {ASTNode} */ (targetVForExpr.parent) } const iteratorNode = targetVForExpr.right context.report({ diff --git a/lib/rules/no-useless-mustaches.js b/lib/rules/no-useless-mustaches.js index c10889a23..89d8d5422 100644 --- a/lib/rules/no-useless-mustaches.js +++ b/lib/rules/no-useless-mustaches.js @@ -6,15 +6,10 @@ const utils = require('../utils') -/** - * @typedef {import('eslint').Rule.RuleContext} RuleContext - * @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer - */ - /** * Strip quotes string * @param {string} text - * @returns {string} + * @returns {string|null} */ function stripQuotesForHTML(text) { if ( @@ -76,13 +71,16 @@ module.exports = { if (!expression) { return } - let strValue, rawValue + /** @type {string} */ + let strValue + /** @type {string} */ + let rawValue if (expression.type === 'Literal') { if (typeof expression.value !== 'string') { return } strValue = expression.value - rawValue = expression.raw.slice(1, -1) + rawValue = sourceCode.getText(expression).slice(1, -1) } else if (expression.type === 'TemplateLiteral') { if (expression.expressions.length > 0) { return @@ -127,7 +125,6 @@ module.exports = { } context.report({ - // @ts-ignore node, messageId: 'unexpected', fix(fixer) { @@ -135,7 +132,6 @@ module.exports = { // cannot fix return null } - context.parserServices.getDocumentFragment() const text = stripQuotesForHTML(sourceCode.getText(expression)) if (text == null) { // unknowns diff --git a/lib/rules/no-useless-v-bind.js b/lib/rules/no-useless-v-bind.js index 2ffa71400..d64e95535 100644 --- a/lib/rules/no-useless-v-bind.js +++ b/lib/rules/no-useless-v-bind.js @@ -9,11 +9,6 @@ const utils = require('../utils') const DOUBLE_QUOTES_RE = /"/gu const SINGLE_QUOTES_RE = /'/gu -/** - * @typedef {import('eslint').Rule.RuleContext} RuleContext - * @typedef {import('vue-eslint-parser').AST.VDirective} VDirective - */ - module.exports = { meta: { docs: { @@ -52,20 +47,24 @@ module.exports = { * @param {VDirective} node the node to check */ function verify(node) { - if (!node.value || node.key.modifiers.length) { + const { value } = node + if (!value || node.key.modifiers.length) { return } - const { expression } = node.value + const { expression } = value if (!expression) { return } - let strValue, rawValue + /** @type {string} */ + let strValue + /** @type {string} */ + let rawValue if (expression.type === 'Literal') { if (typeof expression.value !== 'string') { return } strValue = expression.value - rawValue = expression.raw.slice(1, -1) + rawValue = sourceCode.getText(expression).slice(1, -1) } else if (expression.type === 'TemplateLiteral') { if (expression.expressions.length > 0) { return @@ -78,7 +77,7 @@ module.exports = { const tokenStore = context.parserServices.getTemplateBodyTokenStore() const hasComment = tokenStore - .getTokens(node.value, { includeComments: true }) + .getTokens(value, { includeComments: true }) .some((t) => t.type === 'Block' || t.type === 'Line') if (ignoreIncludesComment && hasComment) { return @@ -110,7 +109,6 @@ module.exports = { } context.report({ - // @ts-ignore node, messageId: 'unexpected', fix(fixer) { @@ -118,11 +116,11 @@ module.exports = { // cannot fix return null } - const text = sourceCode.getText(node.value) + const text = sourceCode.getText(value) const quoteChar = text[0] const shorthand = node.key.name.rawName === ':' - /** @type { [number, number] } */ + /** @type {Range} */ const keyDirectiveRange = [ node.key.name.range[0], node.key.name.range[1] + (shorthand ? 0 : 1) diff --git a/lib/rules/no-v-html.js b/lib/rules/no-v-html.js index 24044c644..33a1bfa30 100644 --- a/lib/rules/no-v-html.js +++ b/lib/rules/no-v-html.js @@ -20,8 +20,10 @@ module.exports = { fixable: null, schema: [] }, + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VDirective} node */ "VAttribute[directive=true][key.name.name='html']"(node) { context.report({ node, diff --git a/lib/rules/no-v-model-argument.js b/lib/rules/no-v-model-argument.js index 281aa9edf..615af912c 100644 --- a/lib/rules/no-v-model-argument.js +++ b/lib/rules/no-v-model-argument.js @@ -29,9 +29,10 @@ module.exports = { vModelRequireNoArgument: "'v-model' directives require no argument." } }, - + /** @param {RuleContext} context */ create(context) { return utils.defineTemplateBodyVisitor(context, { + /** @param {VDirective} node */ "VAttribute[directive=true][key.name.name='model']"(node) { const element = node.parent.parent diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js index 378a1138d..d9a8c3822 100644 --- a/lib/rules/no-watch-after-await.js +++ b/lib/rules/no-watch-after-await.js @@ -6,6 +6,9 @@ const { ReferenceTracker } = require('eslint-utils') const utils = require('../utils') +/** + * @param {CallExpression} node + */ function isMaybeUsedStopHandle(node) { const parent = node.parent if (parent) { @@ -47,11 +50,19 @@ module.exports = { forbidden: 'The `watch` after `await` expression are forbidden.' } }, + /** @param {RuleContext} context */ create(context) { const watchCallNodes = new Set() + /** @type {Map} */ const setupFunctions = new Map() - let scopeStack = { upper: null, functionNode: null } + /** + * @typedef {object} ScopeStack + * @property {ScopeStack} upper + * @property {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} functionNode + */ + /** @type {ScopeStack} */ + let scopeStack return Object.assign( { @@ -75,6 +86,7 @@ module.exports = { } }, utils.defineVueVisitor(context, { + /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */ ':function'(node) { scopeStack = { upper: scopeStack, functionNode: node } }, @@ -91,6 +103,7 @@ module.exports = { } setupFunctionData.afterAwait = true }, + /** @param {CallExpression} node */ CallExpression(node) { const setupFunctionData = setupFunctions.get(scopeStack.functionNode) if (!setupFunctionData || !setupFunctionData.afterAwait) { @@ -104,6 +117,7 @@ module.exports = { }) } }, + /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */ ':function:exit'(node) { scopeStack = scopeStack.upper diff --git a/lib/rules/one-component-per-file.js b/lib/rules/one-component-per-file.js index cc1ce17df..619942c1d 100644 --- a/lib/rules/one-component-per-file.js +++ b/lib/rules/one-component-per-file.js @@ -23,7 +23,9 @@ module.exports = { toManyComponents: 'There is more than one component in this file.' } }, + /** @param {RuleContext} context */ create(context) { + /** @type {ObjectExpression[]} */ const components = [] return Object.assign( diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js index afdb17828..fcff89ab2 100644 --- a/lib/rules/order-in-components.js +++ b/lib/rules/order-in-components.js @@ -7,6 +7,10 @@ const utils = require('../utils') const traverseNodes = require('vue-eslint-parser').AST.traverseNodes +/** + * @typedef {import('eslint-visitor-keys').VisitorKeys} VisitorKeys + */ + const defaultOrder = [ // Side Effects (triggers effects outside the component) 'el', @@ -71,6 +75,7 @@ const defaultOrder = [ 'renderError' ] +/** @type { { [key: string]: string[] } } */ const groups = { LIFECYCLE_HOOKS: [ 'beforeCreate', @@ -92,7 +97,11 @@ const groups = { ROUTER_GUARDS: ['beforeRouteEnter', 'beforeRouteUpdate', 'beforeRouteLeave'] } +/** + * @param {(string | string[])[]} order + */ function getOrderMap(order) { + /** @type {Map} */ const orderMap = new Map() order.forEach((property, i) => { @@ -106,6 +115,9 @@ function getOrderMap(order) { return orderMap } +/** + * @param {Token} node + */ function isComma(node) { return node.type === 'Punctuator' && node.value === ',' } @@ -114,15 +126,15 @@ const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**' /* es2016 */] const BITWISE_OPERATORS = ['&', '|', '^', '~', '<<', '>>', '>>>'] const COMPARISON_OPERATORS = ['==', '!=', '===', '!==', '>', '>=', '<', '<='] const RELATIONAL_OPERATORS = ['in', 'instanceof'] -const ALL_BINARY_OPERATORS = [].concat( - ARITHMETIC_OPERATORS, - BITWISE_OPERATORS, - COMPARISON_OPERATORS, - RELATIONAL_OPERATORS -) +const ALL_BINARY_OPERATORS = [ + ...ARITHMETIC_OPERATORS, + ...BITWISE_OPERATORS, + ...COMPARISON_OPERATORS, + ...RELATIONAL_OPERATORS +] const LOGICAL_OPERATORS = ['&&', '||', '??' /* es2020 */] -/* +/** * Result `true` if the node is sure that there are no side effects * * Currently known side effects types @@ -135,12 +147,13 @@ const LOGICAL_OPERATORS = ['&&', '||', '??' /* es2020 */] * node.type === 'UnaryExpression' && node.operator === 'delete' * * @param {ASTNode} node target node - * @param {Object} visitorKeys sourceCode.visitorKey - * @returns {Boolean} no side effects + * @param {VisitorKeys} visitorKeys sourceCode.visitorKey + * @returns {boolean} no side effects */ function isNotSideEffectsNode(node, visitorKeys) { let result = true - let skipNode = false + /** @type {ASTNode | null} */ + let skipNode = null traverseNodes(node, { visitorKeys, enterNode(node) { @@ -213,33 +226,63 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} + /** @type {(string|string[])[]} */ const order = options.order || defaultOrder - const extendedOrder = order.map((property) => groups[property] || property) + /** @type {(string|string[])[]} */ + const extendedOrder = order.map( + (property) => + (typeof property === 'string' && groups[property]) || property + ) const orderMap = getOrderMap(extendedOrder) const sourceCode = context.getSourceCode() - function checkOrder(propertiesNodes, orderMap) { + /** + * @param {string} name + */ + function getOrderPosition(name) { + const num = orderMap.get(name) + return num == null ? -1 : num + } + + /** + * @param {(Property | SpreadElement)[]} propertiesNodes + */ + function checkOrder(propertiesNodes) { const properties = propertiesNodes - .filter((property) => property.type === 'Property') - .map((property) => property.key) + .filter(utils.isProperty) + .map((property) => { + return { + node: property, + name: + utils.getStaticPropertyName(property) || + (property.key.type === 'Identifier' && property.key.name) || + '' + } + }) properties.forEach((property, i) => { + const orderPos = getOrderPosition(property.name) + if (orderPos < 0) { + return + } const propertiesAbove = properties.slice(0, i) const unorderedProperties = propertiesAbove - .filter((p) => orderMap.get(p.name) > orderMap.get(property.name)) + .filter( + (p) => getOrderPosition(p.name) > getOrderPosition(property.name) + ) .sort((p1, p2) => - orderMap.get(p1.name) > orderMap.get(p2.name) ? 1 : -1 + getOrderPosition(p1.name) > getOrderPosition(p2.name) ? 1 : -1 ) const firstUnorderedProperty = unorderedProperties[0] if (firstUnorderedProperty) { - const line = firstUnorderedProperty.loc.start.line + const line = firstUnorderedProperty.node.loc.start.line context.report({ - node: property, + node: property.node, message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`, data: { name: property.name, @@ -247,8 +290,8 @@ module.exports = { line }, fix(fixer) { - const propertyNode = property.parent - const firstUnorderedPropertyNode = firstUnorderedProperty.parent + const propertyNode = property.node + const firstUnorderedPropertyNode = firstUnorderedProperty.node const hasSideEffectsPossibility = propertiesNodes .slice( propertiesNodes.indexOf(firstUnorderedPropertyNode), @@ -259,7 +302,7 @@ module.exports = { !isNotSideEffectsNode(property, sourceCode.visitorKeys) ) if (hasSideEffectsPossibility) { - return undefined + return null } const afterComma = sourceCode.getTokenAfter(propertyNode) const hasAfterComma = isComma(afterComma) @@ -292,7 +335,7 @@ module.exports = { } return utils.executeOnVue(context, (obj) => { - checkOrder(obj.properties, orderMap) + checkOrder(obj.properties) }) } } diff --git a/lib/rules/padding-line-between-blocks.js b/lib/rules/padding-line-between-blocks.js index 0fb5ee8d5..b9215e2c9 100644 --- a/lib/rules/padding-line-between-blocks.js +++ b/lib/rules/padding-line-between-blocks.js @@ -30,7 +30,9 @@ function verifyForNever(context, prevBlock, nextBlock, betweenTokens) { return } const tokenOrNodes = [...betweenTokens, nextBlock] + /** @type {ASTNode | Token} */ let prev = prevBlock + /** @type {[ASTNode | Token, ASTNode | Token][]} */ const paddingLines = [] for (const tokenOrNode of tokenOrNodes) { const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line @@ -70,7 +72,9 @@ function verifyForNever(context, prevBlock, nextBlock, betweenTokens) { */ function verifyForAlways(context, prevBlock, nextBlock, betweenTokens) { const tokenOrNodes = [...betweenTokens, nextBlock] + /** @type {ASTNode | Token} */ let prev = prevBlock + /** @type {ASTNode | Token | undefined} */ let linebreak for (const tokenOrNode of tokenOrNodes) { const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line @@ -130,20 +134,30 @@ module.exports = { always: 'Expected blank line before this block.' } }, + /** @param {RuleContext} context */ create(context) { - const paddingType = PaddingTypes[context.options[0] || 'always'] - const documentFragment = - context.parserServices.getDocumentFragment && - context.parserServices.getDocumentFragment() + if (!context.parserServices.getDocumentFragment) { + return {} + } + /** @type {'always' | 'never'} */ + const option = context.options[0] || 'always' + const paddingType = PaddingTypes[option] + + const documentFragment = context.parserServices.getDocumentFragment() + /** @type {Token[]} */ let tokens + /** + * @returns {VElement[]} + */ function getTopLevelHTMLElements() { - if (documentFragment) { - return documentFragment.children.filter((e) => e.type === 'VElement') - } - return [] + return documentFragment.children.filter(utils.isVElement) } + /** + * @param {VElement} prev + * @param {VElement} next + */ function getTokenAndCommentsBetween(prev, next) { // When there is no