From 33c898d199e229b033beaa18fddee44004a23b59 Mon Sep 17 00:00:00 2001 From: ota Date: Wed, 10 Jun 2020 00:32:32 +0900 Subject: [PATCH] Chores: Add JSDoc type checking with TypeScript --- .eslintrc.js | 18 - .vscode/settings.json | 3 +- lib/processor.js | 24 +- lib/rules/array-bracket-spacing.js | 1 + lib/rules/arrow-spacing.js | 5 +- lib/rules/attribute-hyphenation.js | 13 +- lib/rules/attributes-order.js | 222 ++-- lib/rules/block-spacing.js | 10 +- lib/rules/brace-style.js | 10 +- lib/rules/camelcase.js | 5 +- lib/rules/comma-dangle.js | 5 +- lib/rules/comma-spacing.js | 12 +- lib/rules/comma-style.js | 19 +- lib/rules/comment-directive.js | 33 +- lib/rules/component-definition-name-casing.js | 21 +- .../component-name-in-template-casing.js | 18 +- lib/rules/component-tags-order.js | 74 +- lib/rules/custom-event-name-casing.js | 31 +- lib/rules/dot-location.js | 5 +- lib/rules/dot-notation.js | 5 +- lib/rules/eqeqeq.js | 5 +- lib/rules/func-call-spacing.js | 10 +- lib/rules/html-closing-bracket-newline.js | 7 +- lib/rules/html-closing-bracket-spacing.js | 60 +- lib/rules/html-comment-content-newline.js | 73 +- lib/rules/html-comment-content-spacing.js | 59 +- lib/rules/html-comment-indent.js | 20 +- lib/rules/html-end-tags.js | 2 +- lib/rules/html-indent.js | 1 + lib/rules/html-quotes.js | 3 +- lib/rules/html-self-closing.js | 61 +- lib/rules/jsx-uses-vars.js | 11 +- lib/rules/key-spacing.js | 10 +- lib/rules/keyword-spacing.js | 10 +- lib/rules/match-component-file-name.js | 24 +- lib/rules/max-attributes-per-line.js | 43 +- lib/rules/max-len.js | 90 +- .../multiline-html-element-content-newline.js | 70 +- lib/rules/mustache-interpolation-spacing.js | 3 +- lib/rules/name-property-casing.js | 26 +- lib/rules/no-arrow-functions-in-watch.js | 5 +- lib/rules/no-async-in-computed-properties.js | 43 +- lib/rules/no-bare-strings-in-template.js | 22 +- lib/rules/no-boolean-default.js | 59 +- lib/rules/no-confusing-v-for-v-if.js | 17 +- lib/rules/no-custom-modifiers-on-v-model.js | 1 + .../no-deprecated-data-object-declaration.js | 51 +- .../no-deprecated-dollar-listeners-api.js | 2 +- .../no-deprecated-dollar-scopedslots-api.js | 2 +- lib/rules/no-deprecated-events-api.js | 3 +- lib/rules/no-deprecated-filter.js | 2 +- .../no-deprecated-functional-template.js | 5 +- lib/rules/no-deprecated-html-element-is.js | 3 +- lib/rules/no-deprecated-inline-template.js | 3 +- lib/rules/no-deprecated-scope-attribute.js | 1 + lib/rules/no-deprecated-slot-attribute.js | 1 + .../no-deprecated-slot-scope-attribute.js | 1 + lib/rules/no-deprecated-v-bind-sync.js | 12 +- .../no-deprecated-v-on-native-modifier.js | 3 +- .../no-deprecated-v-on-number-modifiers.js | 5 +- .../no-deprecated-vue-config-keycodes.js | 3 +- lib/rules/no-dupe-keys.js | 8 +- lib/rules/no-duplicate-attr-inheritance.js | 10 +- lib/rules/no-duplicate-attributes.js | 19 +- lib/rules/no-empty-pattern.js | 5 +- lib/rules/no-extra-parens.js | 48 +- lib/rules/no-irregular-whitespace.js | 20 +- lib/rules/no-lifecycle-after-await.js | 29 +- lib/rules/no-multi-spaces.js | 8 +- lib/rules/no-multiple-slot-args.js | 8 +- lib/rules/no-multiple-template-root.js | 5 +- lib/rules/no-mutating-props.js | 71 +- lib/rules/no-parsing-error.js | 7 +- .../no-potential-component-option-typo.js | 54 +- lib/rules/no-ref-as-operand.js | 21 + lib/rules/no-reserved-component-names.js | 34 +- lib/rules/no-reserved-keys.js | 7 +- lib/rules/no-restricted-static-attribute.js | 4 +- lib/rules/no-restricted-syntax.js | 5 +- lib/rules/no-restricted-v-bind.js | 14 +- lib/rules/no-setup-props-destructure.js | 27 +- lib/rules/no-shared-component-data.js | 50 +- .../no-side-effects-in-computed-properties.js | 43 +- ...-spaces-around-equal-signs-in-attribute.js | 3 +- lib/rules/no-static-inline-styles.js | 11 +- lib/rules/no-template-key.js | 3 +- lib/rules/no-template-shadow.js | 27 +- lib/rules/no-template-target-blank.js | 22 +- lib/rules/no-textarea-mustache.js | 3 +- lib/rules/no-unregistered-components.js | 20 +- lib/rules/no-unsupported-features.js | 39 +- lib/rules/no-unused-components.js | 22 +- lib/rules/no-unused-properties.js | 93 +- lib/rules/no-unused-vars.js | 25 +- lib/rules/no-use-v-if-with-v-for.js | 16 +- lib/rules/no-useless-concat.js | 5 +- lib/rules/no-useless-mustaches.js | 16 +- lib/rules/no-useless-v-bind.js | 24 +- lib/rules/no-v-html.js | 2 + lib/rules/no-v-model-argument.js | 3 +- lib/rules/no-watch-after-await.js | 16 +- lib/rules/object-curly-newline.js | 1 + lib/rules/object-curly-spacing.js | 1 + lib/rules/object-property-newline.js | 1 + lib/rules/one-component-per-file.js | 2 + lib/rules/operator-linebreak.js | 5 +- lib/rules/order-in-components.js | 87 +- lib/rules/padding-line-between-blocks.js | 34 +- lib/rules/prefer-template.js | 5 +- lib/rules/prop-name-casing.js | 25 +- lib/rules/require-component-is.js | 3 +- lib/rules/require-default-prop.js | 10 +- lib/rules/require-direct-export.js | 28 +- lib/rules/require-explicit-emits.js | 110 +- lib/rules/require-name-property.js | 12 +- lib/rules/require-prop-type-constructor.js | 102 +- lib/rules/require-prop-types.js | 31 +- lib/rules/require-render-return.js | 23 +- lib/rules/require-slots-as-functions.js | 9 +- lib/rules/require-toggle-inside-transition.js | 5 +- lib/rules/require-v-for-key.js | 5 +- lib/rules/require-valid-default-prop.js | 71 +- lib/rules/return-in-computed-property.js | 13 +- lib/rules/return-in-emits-validator.js | 28 +- lib/rules/script-indent.js | 1 + ...singleline-html-element-content-newline.js | 60 +- lib/rules/sort-keys.js | 196 ++-- lib/rules/space-in-parens.js | 12 +- lib/rules/space-infix-ops.js | 10 +- lib/rules/space-unary-ops.js | 10 +- lib/rules/static-class-names-order.js | 14 +- .../syntaxes/dynamic-directive-arguments.js | 7 +- lib/rules/syntaxes/scope-attribute.js | 1 + lib/rules/syntaxes/slot-attribute.js | 11 +- lib/rules/syntaxes/slot-scope-attribute.js | 14 +- .../v-bind-prop-modifier-shorthand.js | 8 +- lib/rules/syntaxes/v-slot.js | 11 +- lib/rules/template-curly-spacing.js | 1 + lib/rules/this-in-template.js | 132 +-- lib/rules/use-v-on-exact.js | 54 +- lib/rules/v-bind-style.js | 3 +- lib/rules/v-on-function-call.js | 29 +- lib/rules/v-on-style.js | 3 +- lib/rules/v-slot-style.js | 11 +- lib/rules/valid-template-root.js | 3 +- lib/rules/valid-v-bind-sync.js | 5 +- lib/rules/valid-v-bind.js | 3 +- lib/rules/valid-v-cloak.js | 3 +- lib/rules/valid-v-else-if.js | 3 +- lib/rules/valid-v-else.js | 3 +- lib/rules/valid-v-for.js | 20 +- lib/rules/valid-v-html.js | 3 +- lib/rules/valid-v-if.js | 3 +- lib/rules/valid-v-model.js | 13 +- lib/rules/valid-v-on.js | 8 +- lib/rules/valid-v-once.js | 3 +- lib/rules/valid-v-pre.js | 3 +- lib/rules/valid-v-show.js | 3 +- lib/rules/valid-v-slot.js | 39 +- lib/rules/valid-v-text.js | 3 +- lib/utils/casing.js | 29 +- lib/utils/html-comments.js | 41 +- lib/utils/indent-common.js | 349 ++++--- lib/utils/index.js | 980 ++++++++++++------ lib/utils/keycode-to-key.js | 1 + package.json | 6 +- tests/lib/rules/attributes-order.js | 470 ++++----- tests/lib/utils/index.js | 16 +- tsconfig.json | 25 + typings/eslint-plugin-vue/global.d.ts | 175 ++++ .../eslint-plugin-vue/util-types/ast/ast.ts | 263 +++++ .../util-types/ast/es-ast.ts | 519 ++++++++++ .../eslint-plugin-vue/util-types/ast/index.ts | 5 + .../util-types/ast/jsx-ast.ts | 106 ++ .../util-types/ast/ts-ast.ts | 11 + .../eslint-plugin-vue/util-types/ast/v-ast.ts | 174 ++++ .../eslint-plugin-vue/util-types/errors.ts | 43 + .../util-types/node/index.ts | 3 + .../util-types/node/locations.ts | 13 + .../eslint-plugin-vue/util-types/node/node.ts | 11 + .../util-types/node/tokens.ts | 17 + .../util-types/parser-services.ts | 122 +++ typings/eslint-plugin-vue/util-types/utils.ts | 26 + typings/eslint-utils/index.d.ts | 72 ++ typings/eslint/index.d.ts | 417 ++++++++ typings/vue-eslint-parser/index.d.ts | 14 + 186 files changed, 5327 insertions(+), 2111 deletions(-) create mode 100644 tsconfig.json create mode 100644 typings/eslint-plugin-vue/global.d.ts create mode 100644 typings/eslint-plugin-vue/util-types/ast/ast.ts create mode 100644 typings/eslint-plugin-vue/util-types/ast/es-ast.ts create mode 100644 typings/eslint-plugin-vue/util-types/ast/index.ts create mode 100644 typings/eslint-plugin-vue/util-types/ast/jsx-ast.ts create mode 100644 typings/eslint-plugin-vue/util-types/ast/ts-ast.ts create mode 100644 typings/eslint-plugin-vue/util-types/ast/v-ast.ts create mode 100644 typings/eslint-plugin-vue/util-types/errors.ts create mode 100644 typings/eslint-plugin-vue/util-types/node/index.ts create mode 100644 typings/eslint-plugin-vue/util-types/node/locations.ts create mode 100644 typings/eslint-plugin-vue/util-types/node/node.ts create mode 100644 typings/eslint-plugin-vue/util-types/node/tokens.ts create mode 100644 typings/eslint-plugin-vue/util-types/parser-services.ts create mode 100644 typings/eslint-plugin-vue/util-types/utils.ts create mode 100644 typings/eslint-utils/index.d.ts create mode 100644 typings/eslint/index.d.ts create mode 100644 typings/vue-eslint-parser/index.d.ts 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/.vscode/settings.json b/.vscode/settings.json index 7bd96f8d9..62b19a13c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "javascript", "javascriptreact", { "language": "vue", "autoFix": true } - ] + ], + "typescript.tsdk": "node_modules/typescript/lib" } 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/array-bracket-spacing.js b/lib/rules/array-bracket-spacing.js index a08215e2b..6f671938b 100644 --- a/lib/rules/array-bracket-spacing.js +++ b/lib/rules/array-bracket-spacing.js @@ -7,6 +7,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule( + // @ts-ignore require('eslint/lib/rules/array-bracket-spacing'), { skipDynamicArguments: true } ) diff --git a/lib/rules/arrow-spacing.js b/lib/rules/arrow-spacing.js index 6586e8389..eda622a96 100644 --- a/lib/rules/arrow-spacing.js +++ b/lib/rules/arrow-spacing.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line -module.exports = wrapCoreRule(require('eslint/lib/rules/arrow-spacing')) +module.exports = wrapCoreRule( + // @ts-ignore + 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/block-spacing.js b/lib/rules/block-spacing.js index d12ad7da8..51f46aa37 100644 --- a/lib/rules/block-spacing.js +++ b/lib/rules/block-spacing.js @@ -6,6 +6,10 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/block-spacing'), { - skipDynamicArguments: true -}) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/block-spacing'), + { + skipDynamicArguments: true + } +) diff --git a/lib/rules/brace-style.js b/lib/rules/brace-style.js index 66680948d..6035315b9 100644 --- a/lib/rules/brace-style.js +++ b/lib/rules/brace-style.js @@ -6,6 +6,10 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/brace-style'), { - skipDynamicArguments: true -}) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/brace-style'), + { + skipDynamicArguments: true + } +) diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index c0ca04175..50cc5ad3e 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line -module.exports = wrapCoreRule(require('eslint/lib/rules/camelcase')) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/camelcase') +) diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js index cfd0a4185..a1374928c 100644 --- a/lib/rules/comma-dangle.js +++ b/lib/rules/comma-dangle.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line -module.exports = wrapCoreRule(require('eslint/lib/rules/comma-dangle')) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/comma-dangle') +) diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js index 2a68be8f1..519960a4c 100644 --- a/lib/rules/comma-spacing.js +++ b/lib/rules/comma-spacing.js @@ -6,7 +6,11 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/comma-spacing'), { - skipDynamicArguments: true, - skipDynamicArgumentsReport: true -}) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/comma-spacing'), + { + skipDynamicArguments: true, + skipDynamicArgumentsReport: true + } +) diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js index 40772dc37..2be1cbb5d 100644 --- a/lib/rules/comma-style.js +++ b/lib/rules/comma-style.js @@ -6,14 +6,19 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/comma-style'), { - create(_context, { coreHandlers }) { - return { - VSlotScopeExpression(node) { - if (coreHandlers.FunctionExpression) { - coreHandlers.FunctionExpression(node) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/comma-style'), + { + create(_context, { coreHandlers }) { + return { + VSlotScopeExpression(node) { + if (coreHandlers.FunctionExpression) { + // @ts-ignore + 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..0b47e51d0 100644 --- a/lib/rules/dot-location.js +++ b/lib/rules/dot-location.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line -module.exports = wrapCoreRule(require('eslint/lib/rules/dot-location')) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/dot-location') +) diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 68633b30d..fdca63838 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/dot-notation')) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/dot-notation') +) diff --git a/lib/rules/eqeqeq.js b/lib/rules/eqeqeq.js index 5e9d4d8b9..55f874a2e 100644 --- a/lib/rules/eqeqeq.js +++ b/lib/rules/eqeqeq.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line -module.exports = wrapCoreRule(require('eslint/lib/rules/eqeqeq')) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/eqeqeq') +) diff --git a/lib/rules/func-call-spacing.js b/lib/rules/func-call-spacing.js index da076f9cb..7fe081631 100644 --- a/lib/rules/func-call-spacing.js +++ b/lib/rules/func-call-spacing.js @@ -6,6 +6,10 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/func-call-spacing'), { - skipDynamicArguments: true -}) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/func-call-spacing'), + { + skipDynamicArguments: true + } +) 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/key-spacing.js b/lib/rules/key-spacing.js index 27e624517..7c64b6a9e 100644 --- a/lib/rules/key-spacing.js +++ b/lib/rules/key-spacing.js @@ -6,6 +6,10 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/key-spacing'), { - skipDynamicArguments: true -}) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/key-spacing'), + { + skipDynamicArguments: true + } +) diff --git a/lib/rules/keyword-spacing.js b/lib/rules/keyword-spacing.js index 0b0da6277..d8f98994e 100644 --- a/lib/rules/keyword-spacing.js +++ b/lib/rules/keyword-spacing.js @@ -6,6 +6,10 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/keyword-spacing'), { - skipDynamicArguments: true -}) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/keyword-spacing'), + { + skipDynamicArguments: true + } +) 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..d1369049a 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -82,13 +82,14 @@ 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 + // @ts-ignore line.replace(/\t/gu, (match, offset) => { const totalOffset = offset + extraCharacterCount const previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0 @@ -104,16 +105,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 +122,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 +146,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 +157,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 +173,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 +213,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 +229,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 +251,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 +267,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 +283,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 +298,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 +307,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 +316,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 +324,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 +365,7 @@ module.exports = { } } + /** @type {Range} */ let scriptLinesRange if (scriptTokens.length) { if (scriptComments.length) { @@ -458,7 +473,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 +485,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 +542,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..fcd2d92ad 100644 --- a/lib/rules/no-empty-pattern.js +++ b/lib/rules/no-empty-pattern.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line -module.exports = wrapCoreRule(require('eslint/lib/rules/no-empty-pattern')) +module.exports = wrapCoreRule( + // @ts-ignore + 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..e3a680599 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -7,17 +7,14 @@ const { isParenthesized } = require('eslint-utils') const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/no-extra-parens'), { - skipDynamicArguments: true, - 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 - */ +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/no-extra-parens'), + { + skipDynamicArguments: true, + create: createForVueSyntax + } +) /** * Check whether the given token is a left parenthesis. @@ -75,8 +72,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 +81,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 +120,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..da005f996 100644 --- a/lib/rules/no-restricted-syntax.js +++ b/lib/rules/no-restricted-syntax.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line -module.exports = wrapCoreRule(require('eslint/lib/rules/no-restricted-syntax')) +module.exports = wrapCoreRule( + // @ts-ignore + 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-concat.js b/lib/rules/no-useless-concat.js index 9038b9b93..b279d045c 100644 --- a/lib/rules/no-useless-concat.js +++ b/lib/rules/no-useless-concat.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/no-useless-concat')) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/no-useless-concat') +) 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/object-curly-newline.js b/lib/rules/object-curly-newline.js index 296abe570..7510865e3 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -7,6 +7,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule( + // @ts-ignore require('eslint/lib/rules/object-curly-newline'), { skipDynamicArguments: true } ) diff --git a/lib/rules/object-curly-spacing.js b/lib/rules/object-curly-spacing.js index 9830d2d43..e20fde40e 100644 --- a/lib/rules/object-curly-spacing.js +++ b/lib/rules/object-curly-spacing.js @@ -7,6 +7,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule( + // @ts-ignore require('eslint/lib/rules/object-curly-spacing'), { skipDynamicArguments: true } ) diff --git a/lib/rules/object-property-newline.js b/lib/rules/object-property-newline.js index d58aeb842..6aa1221f7 100644 --- a/lib/rules/object-property-newline.js +++ b/lib/rules/object-property-newline.js @@ -7,6 +7,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories module.exports = wrapCoreRule( + // @ts-ignore require('eslint/lib/rules/object-property-newline'), { skipDynamicArguments: true } ) 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/operator-linebreak.js b/lib/rules/operator-linebreak.js index 2546693d9..d39f02f03 100644 --- a/lib/rules/operator-linebreak.js +++ b/lib/rules/operator-linebreak.js @@ -6,4 +6,7 @@ const { wrapCoreRule } = require('../utils') // eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories -module.exports = wrapCoreRule(require('eslint/lib/rules/operator-linebreak')) +module.exports = wrapCoreRule( + // @ts-ignore + require('eslint/lib/rules/operator-linebreak') +) diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js index afdb17828..1a0c3a9c3 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', @@ -92,7 +96,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 +114,9 @@ function getOrderMap(order) { return orderMap } +/** + * @param {Token} node + */ function isComma(node) { return node.type === 'Punctuator' && node.value === ',' } @@ -114,15 +125,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 +146,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 +225,62 @@ module.exports = { } ] }, - + /** @param {RuleContext} context */ create(context) { const options = context.options[0] || {} const order = options.order || defaultOrder - const extendedOrder = order.map((property) => groups[property] || property) + /** @type {(string|string[])[]} */ + const extendedOrder = order.map( + // @ts-ignore + (property) => 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 +288,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 +300,7 @@ module.exports = { !isNotSideEffectsNode(property, sourceCode.visitorKeys) ) if (hasSideEffectsPossibility) { - return undefined + return null } const afterComma = sourceCode.getTokenAfter(propertyNode) const hasAfterComma = isComma(afterComma) @@ -292,7 +333,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