From aa476d619434fba88984604c5fa2d3a530775926 Mon Sep 17 00:00:00 2001 From: ocavue Date: Tue, 15 Oct 2024 02:39:38 +1100 Subject: [PATCH 01/92] Fix Linter type import in index.d.ts (#2572) --- lib/index.d.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 48006c40a..2651c4d55 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,3 +1,5 @@ +import type { Linter } from 'eslint' + declare const vue: { meta: any configs: { @@ -19,19 +21,19 @@ declare const vue: { } rules: Record processors: { - ".vue": any + '.vue': any vue: any } environments: { /** * @deprecated */ - "setup-compiler-macros": { + 'setup-compiler-macros': { globals: { - defineProps: "readonly" - defineEmits: "readonly" - defineExpose: "readonly" - withDefaults: "readonly" + defineProps: 'readonly' + defineEmits: 'readonly' + defineExpose: 'readonly' + withDefaults: 'readonly' } } } From 7502cf8b9e06f0dfe0a805b5534b3766ba528954 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Mon, 21 Oct 2024 00:05:28 +0900 Subject: [PATCH 02/92] 9.29.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2bf46eae..626268933 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-vue", - "version": "9.29.0", + "version": "9.29.1", "description": "Official ESLint plugin for Vue.js", "main": "lib/index.js", "types": "lib/index.d.ts", From 207eb981df3bbab5d63846d7456d0925ab82394c Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 25 Oct 2024 10:53:55 +0200 Subject: [PATCH 03/92] Update development dependencies (#2582) --- eslint.config.js | 33 ++++++++++++++++++++------------ lib/rules/define-macros-order.js | 2 +- lib/rules/no-unused-refs.js | 2 +- lib/utils/indent-common.js | 16 ++++++++-------- lib/utils/indent-ts.js | 2 +- lib/utils/property-references.js | 2 +- lib/utils/selector.js | 2 +- package.json | 22 ++++++++++----------- 8 files changed, 45 insertions(+), 36 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 5ddca6175..ea76a9d73 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -42,6 +42,27 @@ module.exports = [ } }, + // turn off some rules from shared configs in all files + { + rules: { + 'eslint-plugin/require-meta-docs-recommended': 'off', // use `categories` instead + 'eslint-plugin/require-meta-schema-description': 'off', + + 'unicorn/filename-case': 'off', + 'unicorn/no-null': 'off', + 'unicorn/no-array-callback-reference': 'off', // doesn't work well with TypeScript's custom type guards + 'unicorn/no-useless-undefined': 'off', + 'unicorn/prefer-global-this': 'off', + 'unicorn/prefer-module': 'off', + 'unicorn/prefer-optional-catch-binding': 'off', // not supported by current ESLint parser version + 'unicorn/prefer-at': 'off', // turn off to prevent make breaking changes (ref: #2146) + 'unicorn/prefer-node-protocol': 'off', // turn off to prevent make breaking changes (ref: #2146) + 'unicorn/prefer-string-replace-all': 'off', // turn off to prevent make breaking changes (ref: #2146) + 'unicorn/prefer-top-level-await': 'off', // turn off to prevent make breaking changes (ref: #2146) + 'unicorn/prevent-abbreviations': 'off' + } + }, + { files: ['**/*.js'], languageOptions: { @@ -143,7 +164,6 @@ module.exports = [ 'error', { pattern: '^(enforce|require|disallow).*[^.]$' } ], - 'eslint-plugin/require-meta-docs-recommended': 'off', // use `categories` instead 'eslint-plugin/require-meta-fixable': [ 'error', { catchNoFixerButFixableProperty: true } @@ -184,17 +204,6 @@ module.exports = [ 'error', { checkArrowFunctions: false } ], - 'unicorn/filename-case': 'off', - 'unicorn/no-null': 'off', - 'unicorn/no-array-callback-reference': 'off', // doesn't work well with TypeScript's custom type guards - 'unicorn/no-useless-undefined': 'off', - 'unicorn/prefer-optional-catch-binding': 'off', // not supported by current ESLint parser version - 'unicorn/prefer-module': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/prefer-at': 'off', // turn off to prevent make breaking changes (ref: #2146) - 'unicorn/prefer-node-protocol': 'off', // turn off to prevent make breaking changes (ref: #2146) - 'unicorn/prefer-string-replace-all': 'off', // turn off to prevent make breaking changes (ref: #2146) - 'unicorn/prefer-top-level-await': 'off', // turn off to prevent make breaking changes (ref: #2146) 'internal/require-eslint-community': ['error'] } diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js index 920b5c294..55e6140cd 100644 --- a/lib/rules/define-macros-order.js +++ b/lib/rules/define-macros-order.js @@ -193,7 +193,7 @@ function create(context) { const targetStatementIndex = moveTargetNodes.indexOf(targetStatement) - if (targetStatementIndex >= 0) { + if (targetStatementIndex !== -1) { moveTargetNodes = moveTargetNodes.slice(0, targetStatementIndex) } reportNotOnTop(should.name, moveTargetNodes, targetStatement) diff --git a/lib/rules/no-unused-refs.js b/lib/rules/no-unused-refs.js index 79d82a39d..4896ce25a 100644 --- a/lib/rules/no-unused-refs.js +++ b/lib/rules/no-unused-refs.js @@ -128,7 +128,7 @@ module.exports = { } case 'CallExpression': { const argIndex = parent.arguments.indexOf(node) - if (argIndex > -1) { + if (argIndex !== -1) { // `foo($refs)` hasUnknown = true } diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js index bacf43e15..d2879b120 100644 --- a/lib/utils/indent-common.js +++ b/lib/utils/indent-common.js @@ -1531,8 +1531,13 @@ module.exports.defineVisitor = function create( const tokens = tokenStore.getTokensBetween(importToken, node.source) const fromIndex = tokens.map((t) => t.value).lastIndexOf('from') const { fromToken, beforeTokens, afterTokens } = - fromIndex >= 0 + fromIndex === -1 ? { + fromToken: null, + beforeTokens: [...tokens, tokenStore.getFirstToken(node.source)], + afterTokens: [] + } + : { fromToken: tokens[fromIndex], beforeTokens: tokens.slice(0, fromIndex), afterTokens: [ @@ -1540,11 +1545,6 @@ module.exports.defineVisitor = function create( tokenStore.getFirstToken(node.source) ] } - : { - fromToken: null, - beforeTokens: [...tokens, tokenStore.getFirstToken(node.source)], - afterTokens: [] - } /** @type {ImportSpecifier[]} */ const namedSpecifiers = [] @@ -1556,7 +1556,7 @@ module.exports.defineVisitor = function create( removeTokens.shift() for (const token of removeTokens) { const i = beforeTokens.indexOf(token) - if (i >= 0) { + if (i !== -1) { beforeTokens.splice(i, 1) } } @@ -1576,7 +1576,7 @@ module.exports.defineVisitor = function create( rightBrace ]) { const i = beforeTokens.indexOf(token) - if (i >= 0) { + if (i !== -1) { beforeTokens.splice(i, 1) } } diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js index f021e8a10..314858c9a 100644 --- a/lib/utils/indent-ts.js +++ b/lib/utils/indent-ts.js @@ -663,7 +663,7 @@ function defineVisitor({ const [, ...removeTokens] = tokenStore.getTokens(child) for (const token of removeTokens) { const i = afterTokens.indexOf(token) - if (i >= 0) { + if (i !== -1) { afterTokens.splice(i, 1) } } diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js index 57fe59975..99c73293b 100644 --- a/lib/utils/property-references.js +++ b/lib/utils/property-references.js @@ -343,7 +343,7 @@ function definePropertyReferenceExtractor( case 'CallExpression': { const argIndex = parent.arguments.indexOf(node) // `foo(arg)` - return !withInTemplate && argIndex > -1 + return !withInTemplate && argIndex !== -1 ? extractFromCall(parent, argIndex) : NEVER } diff --git a/lib/utils/selector.js b/lib/utils/selector.js index 7b1f15c3f..b5aa1adfc 100644 --- a/lib/utils/selector.js +++ b/lib/utils/selector.js @@ -543,7 +543,7 @@ function parseNth(pseudoNode) { .toLowerCase() const openParenIndex = argumentsText.indexOf('(') const closeParenIndex = argumentsText.lastIndexOf(')') - if (openParenIndex < 0 || closeParenIndex < 0) { + if (openParenIndex === -1 || closeParenIndex === -1) { throw new SelectorError( `Cannot parse An+B micro syntax (:nth-xxx() argument): ${argumentsText}.` ) diff --git a/package.json b/package.json index 626268933..28ba4bd78 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,9 @@ }, "devDependencies": { "@ota-meshi/site-kit-eslint-editor-vue": "^0.2.4", - "@stylistic/eslint-plugin": "^2.6.0", + "@stylistic/eslint-plugin": "^2.9.0", "@types/eslint": "^8.56.2", - "@types/eslint-visitor-keys": "^3.3.0", + "@types/eslint-visitor-keys": "^3.3.2", "@types/natural-compare": "^1.4.3", "@types/node": "^14.18.63", "@types/semver": "^7.5.8", @@ -78,24 +78,24 @@ "@typescript-eslint/types": "^7.18.0", "assert": "^2.1.0", "env-cmd": "^10.1.0", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-eslint-plugin": "~6.2.0", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-eslint-plugin": "~6.3.1", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsonc": "^2.16.0", "eslint-plugin-node-dependencies": "^0.12.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-unicorn": "^55.0.0", + "eslint-plugin-unicorn": "^56.0.0", "eslint-plugin-vue": "file:.", "espree": "^9.6.1", "events": "^3.3.0", - "markdownlint-cli": "^0.41.0", - "mocha": "^10.7.0", - "nyc": "^17.0.0", + "markdownlint-cli": "^0.42.0", + "mocha": "^10.7.3", + "nyc": "^17.1.0", "pathe": "^1.1.2", "prettier": "^3.3.3", - "typescript": "^5.5.4", - "vitepress": "^1.3.1" + "typescript": "^5.6.3", + "vitepress": "^1.4.1" } } From 86300c489f5f32a5a919cc5bc3bb3a43aad831dd Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Mon, 28 Oct 2024 18:01:11 +0800 Subject: [PATCH 04/92] fix(custom-event-name-casing): check defineEmits variable name in template (#2585) --- lib/rules/custom-event-name-casing.js | 9 ++++- tests/lib/rules/custom-event-name-casing.js | 45 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js index 09cc2fc36..5c33980ec 100644 --- a/lib/rules/custom-event-name-casing.js +++ b/lib/rules/custom-event-name-casing.js @@ -101,6 +101,7 @@ module.exports = { create(context) { /** @type {Map,emitReferenceIds:Set}>} */ const setupContexts = new Map() + let emitParamName = '' const options = context.options.length === 1 && typeof context.options[0] !== 'string' ? // For backward compatibility @@ -189,7 +190,11 @@ module.exports = { // cannot check return } - if (callee.type === 'Identifier' && callee.name === '$emit') { + + if ( + callee.type === 'Identifier' && + (callee.name === '$emit' || callee.name === emitParamName) + ) { verify(nameWithLoc) } } @@ -209,6 +214,8 @@ module.exports = { if (emitParam.type !== 'Identifier') { return } + emitParamName = emitParam.name + // const emit = defineEmits() const variable = findVariable( utils.getScope(context, emitParam), diff --git a/tests/lib/rules/custom-event-name-casing.js b/tests/lib/rules/custom-event-name-casing.js index 76f9156fd..6b6e84c80 100644 --- a/tests/lib/rules/custom-event-name-casing.js +++ b/tests/lib/rules/custom-event-name-casing.js @@ -329,6 +329,22 @@ tester.run('custom-event-name-casing', rule, { `, options: ['kebab-case'] + }, + // setup defineEmits + { + filename: 'test.vue', + code: ` + + + + `, + options: ['kebab-case'] } ], invalid: [ @@ -605,6 +621,35 @@ tester.run('custom-event-name-casing', rule, { line: 4 } ] + }, + { + // https://github.com/vuejs/eslint-plugin-vue/issues/2577 + filename: 'test.vue', + code: ` + + + + `, + errors: [ + { + message: "Custom event name 'foo-bar' must be camelCase.", + line: 4 + }, + { + message: "Custom event name 'foo-bar' must be camelCase.", + line: 8 + }, + { + message: "Custom event name 'foo-bar' must be camelCase.", + line: 9 + } + ] } ] }) From 9a56de8959dea8728aa3020324cd00990ae477be Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Mon, 28 Oct 2024 19:01:29 +0900 Subject: [PATCH 05/92] Fix false negatives and false positives in `vue/require-valid-default-prop` rule (#2586) --- lib/rules/require-valid-default-prop.js | 93 +++++++++++----- tests/lib/rules/require-valid-default-prop.js | 102 ++++++++++++++++++ 2 files changed, 168 insertions(+), 27 deletions(-) diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js index ca834d620..9ddef9216 100644 --- a/lib/rules/require-valid-default-prop.js +++ b/lib/rules/require-valid-default-prop.js @@ -230,7 +230,7 @@ module.exports = { } /** - * @param {*} node + * @param {Expression} node * @param {ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp} prop * @param {Iterable} expectedTypeNames */ @@ -249,17 +249,22 @@ module.exports = { }) } + /** + * @typedef {object} DefaultDefine + * @property {Expression} expression + * @property {'assignment'|'withDefaults'|'defaultProperty'} src + */ /** * @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props - * @param {(propName: string) => Expression[]} otherDefaultProvider + * @param {(propName: string) => Iterable} otherDefaultProvider */ function processPropDefs(props, otherDefaultProvider) { /** @type {PropDefaultFunctionContext[]} */ const propContexts = [] for (const prop of props) { let typeList - /** @type {Expression[]} */ - const defExprList = [] + /** @type {DefaultDefine[]} */ + const defaultList = [] if (prop.type === 'object') { if (prop.value.type === 'ObjectExpression') { const type = getPropertyNode(prop.value, 'type') @@ -268,9 +273,12 @@ module.exports = { typeList = getTypes(type.value) const def = getPropertyNode(prop.value, 'default') - if (!def) continue - - defExprList.push(def.value) + if (def) { + defaultList.push({ + src: 'defaultProperty', + expression: def.value + }) + } } else { typeList = getTypes(prop.value) } @@ -278,10 +286,10 @@ module.exports = { typeList = prop.types } if (prop.propName != null) { - defExprList.push(...otherDefaultProvider(prop.propName)) + defaultList.push(...otherDefaultProvider(prop.propName)) } - if (defExprList.length === 0) continue + if (defaultList.length === 0) continue const typeNames = new Set( typeList.filter((item) => NATIVE_TYPES.has(item)) @@ -289,8 +297,8 @@ module.exports = { // There is no native types detected if (typeNames.size === 0) continue - for (const defExpr of defExprList) { - const defType = getValueType(defExpr) + for (const defaultDef of defaultList) { + const defType = getValueType(defaultDef.expression) if (!defType) continue @@ -298,6 +306,11 @@ module.exports = { if (typeNames.has('Function')) { continue } + if (defaultDef.src === 'assignment') { + // Factory functions cannot be used in default definitions with initial value assignments. + report(defaultDef.expression, prop, typeNames) + continue + } if (defType.expression) { if (!defType.returnType || typeNames.has(defType.returnType)) { continue @@ -311,18 +324,23 @@ module.exports = { }) } } else { - if ( - typeNames.has(defType.type) && - !FUNCTION_VALUE_TYPES.has(defType.type) - ) { - continue + if (typeNames.has(defType.type)) { + if (defaultDef.src === 'assignment') { + continue + } + if (!FUNCTION_VALUE_TYPES.has(defType.type)) { + // For Array and Object, defaults must be defined in the factory function. + continue + } } report( - defExpr, + defaultDef.expression, prop, - [...typeNames].map((type) => - FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type - ) + defaultDef.src === 'assignment' + ? typeNames + : [...typeNames].map((type) => + FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type + ) ) } } @@ -425,12 +443,19 @@ module.exports = { utils.getWithDefaultsPropExpressions(node) const defaultsByAssignmentPatterns = utils.getDefaultPropExpressionsForPropsDestructure(node) - const propContexts = processPropDefs(props, (propName) => - [ - defaultsByWithDefaults[propName], - defaultsByAssignmentPatterns[propName]?.expression - ].filter(utils.isDef) - ) + const propContexts = processPropDefs(props, function* (propName) { + const withDefaults = defaultsByWithDefaults[propName] + if (withDefaults) { + yield { src: 'withDefaults', expression: withDefaults } + } + const assignmentPattern = defaultsByAssignmentPatterns[propName] + if (assignmentPattern) { + yield { + src: 'assignment', + expression: assignmentPattern.expression + } + } + }) scriptSetupPropsContexts.push({ node, props: propContexts }) }, /** @@ -450,7 +475,21 @@ module.exports = { } }, onDefinePropsExit() { - scriptSetupPropsContexts.pop() + const data = scriptSetupPropsContexts.pop() + if (!data) { + return + } + for (const { + prop, + types: typeNames, + default: defType + } of data.props) { + for (const returnType of defType.returnTypes) { + if (typeNames.has(returnType.type)) continue + + report(returnType.node, prop, typeNames) + } + } } }) ) diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js index 0f4fd1902..238460876 100644 --- a/tests/lib/rules/require-valid-default-prop.js +++ b/tests/lib/rules/require-valid-default-prop.js @@ -316,6 +316,21 @@ ruleTester.run('require-valid-default-prop', rule, { languageOptions: { parser: require('vue-eslint-parser') } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + } } ], @@ -1098,6 +1113,93 @@ ruleTester.run('require-valid-default-prop', rule, { line: 6 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: "Type of the default value for 'foo' prop must be a number.", + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: "Type of the default value for 'foo' prop must be a array.", + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: "Type of the default value for 'foo' prop must be a array.", + line: 7 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: "Type of the default value for 'foo' prop must be a array.", + line: 3 + } + ] } ] }) From 50bde65aa298cc26c3369077e5bb6c25399e4b8d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Mon, 28 Oct 2024 19:15:48 +0900 Subject: [PATCH 06/92] 9.30.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28ba4bd78..47a4381a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-vue", - "version": "9.29.1", + "version": "9.30.0", "description": "Official ESLint plugin for Vue.js", "main": "lib/index.js", "types": "lib/index.d.ts", From 4729a3b9286bd297db6f25c552cfc4f29eeab02a Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Mon, 11 Nov 2024 11:56:09 +0800 Subject: [PATCH 07/92] fix(define-macros-order): skip TSModuleDeclaration statements (#2593) --- lib/rules/define-macros-order.js | 1 + tests/lib/rules/define-macros-order.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js index 55e6140cd..0bdd5f212 100644 --- a/lib/rules/define-macros-order.js +++ b/lib/rules/define-macros-order.js @@ -53,6 +53,7 @@ function isUseStrictStatement(node) { function getTargetStatementPosition(scriptSetup, program) { const skipStatements = new Set([ 'ImportDeclaration', + 'TSModuleDeclaration', 'TSInterfaceDeclaration', 'TSTypeAliasDeclaration', 'DebuggerStatement', diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js index 72b359866..3def7cf1e 100644 --- a/tests/lib/rules/define-macros-order.js +++ b/tests/lib/rules/define-macros-order.js @@ -126,6 +126,8 @@ tester.run('define-macros-order', rule, { code: ` +``` + + + + + +```vue + +``` + + + ## :couple: Related Rules - [vue/multi-word-component-names](./multi-word-component-names.md) diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js index 90e53604f..56621b424 100644 --- a/lib/rules/no-reserved-component-names.js +++ b/lib/rules/no-reserved-component-names.js @@ -34,19 +34,6 @@ function isLowercase(word) { return /^[a-z]*$/.test(word) } -const RESERVED_NAMES_IN_HTML = new Set([ - ...htmlElements, - ...htmlElements.map(casing.capitalize) -]) -const RESERVED_NAMES_IN_OTHERS = new Set([ - ...deprecatedHtmlElements, - ...deprecatedHtmlElements.map(casing.capitalize), - ...kebabCaseElements, - ...kebabCaseElements.map(casing.pascalCase), - ...svgElements, - ...svgElements.filter(isLowercase).map(casing.capitalize) -]) - /** * @param {Expression | SpreadElement} node * @returns {node is (Literal | TemplateLiteral)} @@ -61,14 +48,14 @@ function canVerify(node) { } /** - * @param {string} name - * @returns {string} + * @template T + * @param {Set} set + * @param {Iterable} iterable */ -function getMessageId(name) { - if (RESERVED_NAMES_IN_HTML.has(name)) return 'reservedInHtml' - if (RESERVED_NAMES_IN_VUE.has(name)) return 'reservedInVue' - if (RESERVED_NAMES_IN_VUE3.has(name)) return 'reservedInVue3' - return 'reserved' +function addAll(set, iterable) { + for (const element of iterable) { + set.add(element) + } } module.exports = { @@ -90,6 +77,9 @@ module.exports = { }, disallowVue3BuiltInComponents: { type: 'boolean' + }, + htmlElementCaseSensitive: { + type: 'boolean' } }, additionalProperties: false @@ -109,6 +99,23 @@ module.exports = { options.disallowVueBuiltInComponents === true const disallowVue3BuiltInComponents = options.disallowVue3BuiltInComponents === true + const htmlElementCaseSensitive = options.htmlElementCaseSensitive === true + + const RESERVED_NAMES_IN_HTML = new Set(htmlElements) + const RESERVED_NAMES_IN_OTHERS = new Set([ + ...deprecatedHtmlElements, + ...kebabCaseElements, + ...svgElements + ]) + + if (!htmlElementCaseSensitive) { + addAll(RESERVED_NAMES_IN_HTML, htmlElements.map(casing.capitalize)) + addAll(RESERVED_NAMES_IN_OTHERS, [ + ...deprecatedHtmlElements.map(casing.capitalize), + ...kebabCaseElements.map(casing.pascalCase), + ...svgElements.filter(isLowercase).map(casing.capitalize) + ]) + } const reservedNames = new Set([ ...RESERVED_NAMES_IN_HTML, @@ -117,6 +124,17 @@ module.exports = { ...RESERVED_NAMES_IN_OTHERS ]) + /** + * @param {string} name + * @returns {string} + */ + function getMessageId(name) { + if (RESERVED_NAMES_IN_HTML.has(name)) return 'reservedInHtml' + if (RESERVED_NAMES_IN_VUE.has(name)) return 'reservedInVue' + if (RESERVED_NAMES_IN_VUE3.has(name)) return 'reservedInVue3' + return 'reserved' + } + /** * @param {Literal | TemplateLiteral} node */ diff --git a/tests/lib/rules/no-reserved-component-names.js b/tests/lib/rules/no-reserved-component-names.js index 41cddcfd5..231e1f98d 100644 --- a/tests/lib/rules/no-reserved-component-names.js +++ b/tests/lib/rules/no-reserved-component-names.js @@ -410,6 +410,16 @@ const invalidElements = [ 'xmp', 'Xmp' ] +const invalidLowerCaseElements = [] +const invalidUpperCaseElements = [] + +for (const element of invalidElements) { + if (element[0] === element[0].toLowerCase()) { + invalidLowerCaseElements.push(element) + } else { + invalidUpperCaseElements.push(element) + } +} const vue2BuiltInComponents = [ 'component', @@ -559,6 +569,16 @@ ruleTester.run('no-reserved-component-names', rule, { languageOptions, options: [{ disallowVueBuiltInComponents: true }] })), + ...invalidUpperCaseElements.map((name) => ({ + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + languageOptions, + options: [{ htmlElementCaseSensitive: true }] + })), { filename: 'test.vue', code: ``, @@ -701,6 +721,24 @@ ruleTester.run('no-reserved-component-names', rule, { } ] })), + ...invalidLowerCaseElements.map((name) => ({ + filename: `${name}.vue`, + code: ``, + languageOptions: { + parser: require('vue-eslint-parser'), + ...languageOptions + }, + options: [{ htmlElementCaseSensitive: true }], + errors: [ + { + messageId: RESERVED_NAMES_IN_HTML.has(name) + ? 'reservedInHtml' + : 'reserved', + data: { name }, + line: 1 + } + ] + })), ...vue2BuiltInComponents.map((name) => ({ filename: `${name}.vue`, code: ` From 54a99c59227749ed3937a0489202773bc107911a Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Mon, 11 Nov 2024 11:59:56 +0800 Subject: [PATCH 09/92] refactor: store the defineEmits variable name (#2592) --- lib/rules/no-unused-emit-declarations.js | 41 +++++++++--------------- lib/rules/require-explicit-emits.js | 36 +++++++-------------- 2 files changed, 26 insertions(+), 51 deletions(-) diff --git a/lib/rules/no-unused-emit-declarations.js b/lib/rules/no-unused-emit-declarations.js index 146438935..f13784d8e 100644 --- a/lib/rules/no-unused-emit-declarations.js +++ b/lib/rules/no-unused-emit-declarations.js @@ -56,18 +56,6 @@ function hasReferenceId(value, setupContext) { ) } -/** - * Check if the given name matches emitReferenceIds variable name - * @param {string} name - * @param {Set} emitReferenceIds - * @returns {boolean} - */ -function isEmitVariableName(name, emitReferenceIds) { - if (emitReferenceIds.size === 0) return false - const emitVariable = emitReferenceIds.values().next().value.name - return emitVariable === name -} - module.exports = { meta: { type: 'suggestion', @@ -91,6 +79,7 @@ module.exports = { /** @type {Map} */ const setupContexts = new Map() const programNode = context.getSourceCode().ast + let emitParamName = '' /** * @param {CallExpression} node @@ -204,14 +193,6 @@ module.exports = { const { contextReferenceIds, emitReferenceIds } = setupContext - // verify defineEmits variable in template - if ( - callee.type === 'Identifier' && - isEmitVariableName(callee.name, emitReferenceIds) - ) { - addEmitCall(node) - } - // verify setup(props,{emit}) {emit()} addEmitCallByReference(callee, emitReferenceIds, node) if (emit && emit.name === 'emit') { @@ -229,8 +210,11 @@ module.exports = { } } - // verify $emit() in template - if (callee.type === 'Identifier' && callee.name === '$emit') { + // verify $emit() and defineEmits variable in template + if ( + callee.type === 'Identifier' && + (callee.name === '$emit' || callee.name === emitParamName) + ) { addEmitCall(node) } } @@ -316,10 +300,15 @@ module.exports = { } const emitParam = node.parent.id - const variable = - emitParam.type === 'Identifier' - ? findVariable(utils.getScope(context, emitParam), emitParam) - : null + if (emitParam.type !== 'Identifier') { + return + } + + emitParamName = emitParam.name + const variable = findVariable( + utils.getScope(context, emitParam), + emitParam + ) if (!variable) { return } diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js index ba5c406b1..e912549e9 100644 --- a/lib/rules/require-explicit-emits.js +++ b/lib/rules/require-explicit-emits.js @@ -71,22 +71,6 @@ function getNameParamNode(node) { return null } -/** - * Check if the given name matches defineEmitsNode variable name - * @param {string} name - * @param {CallExpression | undefined} defineEmitsNode - * @returns {boolean} - */ -function isEmitVariableName(name, defineEmitsNode) { - const node = defineEmitsNode?.parent - - if (node?.type === 'VariableDeclarator' && node.id.type === 'Identifier') { - return name === node.id.name - } - - return false -} - module.exports = { meta: { type: 'suggestion', @@ -128,6 +112,7 @@ module.exports = { const vueEmitsDeclarations = new Map() /** @type {Map} */ const vuePropsDeclarations = new Map() + let emitParamName = '' /** * @typedef {object} VueTemplateDefineData @@ -271,11 +256,7 @@ module.exports = { // e.g. $emit() / emit() in template if ( callee.type === 'Identifier' && - (callee.name === '$emit' || - isEmitVariableName( - callee.name, - vueTemplateDefineData.defineEmits - )) + (callee.name === '$emit' || callee.name === emitParamName) ) { verifyEmit( vueTemplateDefineData.emits, @@ -308,10 +289,15 @@ module.exports = { } const emitParam = node.parent.id - const variable = - emitParam.type === 'Identifier' - ? findVariable(utils.getScope(context, emitParam), emitParam) - : null + if (emitParam.type !== 'Identifier') { + return + } + + emitParamName = emitParam.name + const variable = findVariable( + utils.getScope(context, emitParam), + emitParam + ) if (!variable) { return } From e13089e7feecdacf5f479ffc25491f685261d177 Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Mon, 11 Nov 2024 12:03:09 +0800 Subject: [PATCH 10/92] fix(require-explicit-slots): ignore attribute binding (#2591) --- lib/rules/require-explicit-slots.js | 55 ++++++++++++++++++----- tests/lib/rules/require-explicit-slots.js | 51 +++++++++++++++++++++ 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/lib/rules/require-explicit-slots.js b/lib/rules/require-explicit-slots.js index 0ae77b597..f87503bb7 100644 --- a/lib/rules/require-explicit-slots.js +++ b/lib/rules/require-explicit-slots.js @@ -35,6 +35,21 @@ function getSlotsName(node) { return null } +/** + * @param {VElement} node + * @return {VAttribute | VDirective | undefined} + */ +function getSlotNameNode(node) { + return node.startTag.attributes.find( + (node) => + (!node.directive && node.key.name === 'name') || + (node.directive && + node.key.name.name === 'bind' && + node.key.argument?.type === 'VIdentifier' && + node.key.argument?.name === 'name') + ) +} + module.exports = { meta: { type: 'problem', @@ -68,6 +83,19 @@ module.exports = { } const slotsDefined = new Set() + /** + * @param {VElement} node + * @param {string | undefined} slotName + */ + function reportMissingSlot(node, slotName) { + if (!slotsDefined.has(slotName)) { + context.report({ + node, + messageId: 'requireExplicitSlots' + }) + } + } + return utils.compositingVisitors( utils.defineScriptSetupVisitor(context, { onDefineSlotsEnter(node) { @@ -137,20 +165,27 @@ module.exports = { } }), utils.defineTemplateBodyVisitor(context, { + /** @param {VElement} node */ "VElement[name='slot']"(node) { - let slotName = 'default' - - const slotNameAttr = utils.getAttribute(node, 'name') + const nameNode = getSlotNameNode(node) - if (slotNameAttr?.value) { - slotName = slotNameAttr.value.value + // if no slot name is declared, default to 'default' + if (!nameNode) { + reportMissingSlot(node, 'default') + return } - if (!slotsDefined.has(slotName)) { - context.report({ - node, - messageId: 'requireExplicitSlots' - }) + if (nameNode.directive) { + const expression = nameNode.value?.expression + // ignore attribute binding except string literal + if (!expression || !utils.isStringLiteral(expression)) { + return + } + + const name = utils.getStringLiteralValue(expression) || undefined + reportMissingSlot(node, name) + } else { + reportMissingSlot(node, nameNode.value?.value) } } }) diff --git a/tests/lib/rules/require-explicit-slots.js b/tests/lib/rules/require-explicit-slots.js index ebbc28818..92d1a1334 100644 --- a/tests/lib/rules/require-explicit-slots.js +++ b/tests/lib/rules/require-explicit-slots.js @@ -160,6 +160,37 @@ tester.run('require-explicit-slots', rule, { parser: null } } + }, + // attribute binding + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [ @@ -291,6 +322,26 @@ tester.run('require-explicit-slots', rule, { } ] }, + { + // ignore attribute binding except string literal + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slots must be explicitly defined.' + } + ] + }, { filename: 'test.vue', code: ` From a5fb774ec93f2d0bf86383579d39a9adcf8b2137 Mon Sep 17 00:00:00 2001 From: Tomas Kudlac <41956970+Thomasan1999@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:03:21 +0100 Subject: [PATCH 11/92] feat: add prefer-use-template-ref rule (#2554) Co-authored-by: Flo Edelmann --- docs/rules/index.md | 1 + docs/rules/prefer-use-template-ref.md | 71 +++++ lib/index.js | 1 + lib/rules/prefer-use-template-ref.js | 89 ++++++ tests/lib/rules/prefer-use-template-ref.js | 323 +++++++++++++++++++++ 5 files changed, 485 insertions(+) create mode 100644 docs/rules/prefer-use-template-ref.md create mode 100644 lib/rules/prefer-use-template-ref.js create mode 100644 tests/lib/rules/prefer-use-template-ref.js diff --git a/docs/rules/index.md b/docs/rules/index.md index 1ba5978d2..4085ce237 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -270,6 +270,7 @@ For example: | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: | | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: | | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: | +| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref` for template refs | | :hammer: | | [vue/require-default-export](./require-default-export.md) | require components to be the default export | | :warning: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | :hammer: | | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: | diff --git a/docs/rules/prefer-use-template-ref.md b/docs/rules/prefer-use-template-ref.md new file mode 100644 index 000000000..6d7fde89a --- /dev/null +++ b/docs/rules/prefer-use-template-ref.md @@ -0,0 +1,71 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-use-template-ref +description: require using `useTemplateRef` instead of `ref` for template refs +--- + +# vue/prefer-use-template-ref + +> require using `useTemplateRef` instead of `ref` for template refs + +- :exclamation: _**This rule has not been released yet.**_ + +## :book: Rule Details + +Vue 3.5 introduced a new way of obtaining template refs via +the [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API. + +This rule enforces using the new `useTemplateRef` function instead of `ref` for template refs. + + + +```vue + + + +``` + + + +This rule skips `ref` template function refs as these should be used to allow custom implementation of storing `ref`. If you prefer +`useTemplateRef`, you have to change the value of the template `ref` to a string. + + + +```vue + + + +``` + + + +## :wrench: Options + +Nothing. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-use-template-ref.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-use-template-ref.js) diff --git a/lib/index.js b/lib/index.js index bb4abf40f..b09e247d5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -208,6 +208,7 @@ const plugin = { 'prefer-separate-static-class': require('./rules/prefer-separate-static-class'), 'prefer-template': require('./rules/prefer-template'), 'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'), + 'prefer-use-template-ref': require('./rules/prefer-use-template-ref'), 'prop-name-casing': require('./rules/prop-name-casing'), 'quote-props': require('./rules/quote-props'), 'require-component-is': require('./rules/require-component-is'), diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js new file mode 100644 index 000000000..8dcdccb38 --- /dev/null +++ b/lib/rules/prefer-use-template-ref.js @@ -0,0 +1,89 @@ +/** + * @author Thomasan1999 + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +/** @param expression {Expression | null} */ +function expressionIsRef(expression) { + // @ts-ignore + return expression?.callee?.name === 'ref' +} + +/** @type {import("eslint").Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'require using `useTemplateRef` instead of `ref` for template refs', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html' + }, + schema: [], + messages: { + preferUseTemplateRef: "Replace 'ref' with 'useTemplateRef'." + } + }, + /** @param {RuleContext} context */ + create(context) { + /** @type Set */ + const templateRefs = new Set() + + /** + * @typedef ScriptRef + * @type {{node: Expression, ref: string}} + */ + + /** + * @type ScriptRef[] */ + const scriptRefs = [] + + return utils.compositingVisitors( + utils.defineTemplateBodyVisitor( + context, + { + 'VAttribute[directive=false]'(node) { + if (node.key.name === 'ref' && node.value?.value) { + templateRefs.add(node.value.value) + } + } + }, + { + VariableDeclarator(declarator) { + if (!expressionIsRef(declarator.init)) { + return + } + + scriptRefs.push({ + // @ts-ignore + node: declarator.init, + // @ts-ignore + ref: declarator.id.name + }) + } + } + ), + { + 'Program:exit'() { + for (const templateRef of templateRefs) { + const scriptRef = scriptRefs.find( + (scriptRef) => scriptRef.ref === templateRef + ) + + if (!scriptRef) { + continue + } + + context.report({ + node: scriptRef.node, + messageId: 'preferUseTemplateRef' + }) + } + } + } + ) + } +} diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js new file mode 100644 index 000000000..49a2f0759 --- /dev/null +++ b/tests/lib/rules/prefer-use-template-ref.js @@ -0,0 +1,323 @@ +/** + * @author Thomasan1999 + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/prefer-use-template-ref') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-use-template-ref', rule, { + valid: [ + { + filename: 'single-use-template-ref.vue', + code: ` + + + ` + }, + { + filename: 'multiple-use-template-refs.vue', + code: ` + + + ` + }, + { + filename: 'use-template-ref-in-block.vue', + code: ` + + + ` + }, + { + filename: 'non-template-ref.vue', + code: ` + + + ` + }, + { + filename: 'counter.js', + code: ` + import { ref } from 'vue'; + const counter = ref(0); + const names = ref(new Set()); + function incrementCounter() { + counter.value++; + return counter.value; + } + function storeName(name) { + names.value.add(name) + } + ` + }, + { + filename: 'setup-function.vue', + code: ` + + + ` + }, + { + filename: 'options-api-no-refs.vue', + code: ` +