From 553abe61c4d7a8964fb154069ea6a82d14b2b3b6 Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Tue, 23 Sep 2025 16:42:03 +0800 Subject: [PATCH 01/36] test: check referenced imported type (#2936) --- tests/fixtures/typescript/src/test01.ts | 7 +++++ tests/lib/rules/no-dupe-keys.js | 41 +++++++++++++++++++++++++ tests/lib/rules/prop-name-casing.js | 37 ++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/tests/fixtures/typescript/src/test01.ts b/tests/fixtures/typescript/src/test01.ts index 917643c67..3d2e059e5 100644 --- a/tests/fixtures/typescript/src/test01.ts +++ b/tests/fixtures/typescript/src/test01.ts @@ -18,6 +18,13 @@ export type Props2 = { h?: string[] i?: readonly string[] } +export type Props3 = { + snake_case: string + 'kebab-case': number + camelCase: boolean + PascalCase?: string + foo: number +} export type Slots1 = { default(props: { msg: string }): any diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js index 794dbd2ec..e4ca1e8ec 100644 --- a/tests/lib/rules/no-dupe-keys.js +++ b/tests/lib/rules/no-dupe-keys.js @@ -6,6 +6,9 @@ const rule = require('../../../lib/rules/no-dupe-keys') const RuleTester = require('../../eslint-compat').RuleTester +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const ruleTester = new RuleTester({ languageOptions: { @@ -511,6 +514,20 @@ ruleTester.run('no-dupe-keys', rule, { `, languageOptions: { parser: require('vue-eslint-parser') } + }, + { + code: ` + + `, + ...getTypeScriptFixtureTestOptions() } ], @@ -1245,6 +1262,30 @@ ruleTester.run('no-dupe-keys', rule, { endColumn: 24 } ] + }, + { + code: ` + + `, + errors: [ + { + message: + "Duplicate key 'foo'. May cause name collision in script or template tag.", + line: 7, + column: 13, + endLine: 9, + endColumn: 9 + } + ], + ...getTypeScriptFixtureTestOptions() } ] }) diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js index a83893a5a..b50ff1b02 100644 --- a/tests/lib/rules/prop-name-casing.js +++ b/tests/lib/rules/prop-name-casing.js @@ -7,6 +7,9 @@ const semver = require('semver') const rule = require('../../../lib/rules/prop-name-casing') const RuleTester = require('../../eslint-compat').RuleTester +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const languageOptions = { ecmaVersion: 2018, @@ -376,6 +379,16 @@ ruleTester.run('prop-name-casing', rule, { { ignoreProps: ['ignored_prop', '/^ignored-pattern-/'] } ], languageOptions + }, + { + code: ` + + `, + ...getTypeScriptFixtureTestOptions() } ], @@ -751,6 +764,30 @@ ruleTester.run('prop-name-casing', rule, { line: 3 } ] + }, + { + code: ` + + `, + errors: [ + { + message: 'Prop "snake_case" is not in camelCase.', + line: 5 + }, + { + message: 'Prop "kebab-case" is not in camelCase.', + line: 5 + }, + { + message: 'Prop "PascalCase" is not in camelCase.', + line: 5 + } + ], + ...getTypeScriptFixtureTestOptions() } ] }) From f9dcbdab3e8518b1cec15e9e89b35abe871bb2c7 Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Mon, 13 Oct 2025 19:22:43 +0800 Subject: [PATCH 02/36] docs: clarify regex string (#2942) --- docs/rules/no-bare-strings-in-template.md | 2 +- docs/rules/no-deprecated-slot-attribute.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rules/no-bare-strings-in-template.md b/docs/rules/no-bare-strings-in-template.md index 23a23c116..f51da16b8 100644 --- a/docs/rules/no-bare-strings-in-template.md +++ b/docs/rules/no-bare-strings-in-template.md @@ -72,7 +72,7 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind] } ``` -- `allowlist` ... An array of allowed strings or regular expression patterns (e.g. `/\d+/` to allow numbers). +- `allowlist` ... An array of allowed strings or regular expression patterns (e.g. `"/\d+/"` to allow numbers). - `attributes` ... An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name. - `directives` ... An array of directive names to check literal value. diff --git a/docs/rules/no-deprecated-slot-attribute.md b/docs/rules/no-deprecated-slot-attribute.md index 34f941ec4..6d29161f6 100644 --- a/docs/rules/no-deprecated-slot-attribute.md +++ b/docs/rules/no-deprecated-slot-attribute.md @@ -49,8 +49,8 @@ This rule reports deprecated `slot` attribute in Vue.js v2.6.0+. } ``` -- `"ignore"` (`string[]`) An array of tags or regular expression patterns (e.g. `/^custom-/`) that ignore these rules. This option will check both kebab-case and PascalCase versions of the given tag names. Default is empty. -- `"ignoreParents"` (`string[]`) An array of tags or regular expression patterns (e.g. `/^custom-/`) for parents that ignore these rules. This option is especially useful for [Web-Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components). Default is empty. +- `"ignore"` (`string[]`) An array of tags or regular expression patterns (e.g. `"/^custom-/"`) that ignore these rules. This option will check both kebab-case and PascalCase versions of the given tag names. Default is empty. +- `"ignoreParents"` (`string[]`) An array of tags or regular expression patterns (e.g. `"/^custom-/"`) for parents that ignore these rules. This option is especially useful for [Web-Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components). Default is empty. ### `"ignore": ["my-component"]` From 5e509d0b72ec18967b471a9b73e8b313ad8b8382 Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Wed, 15 Oct 2025 20:07:23 +0800 Subject: [PATCH 03/36] fix(no-negated-v-if-condition): swap elements (#2941) --- .changeset/tired-emus-matter.md | 5 + lib/rules/no-negated-v-if-condition.js | 148 +++++++++-------- tests/lib/rules/no-negated-v-if-condition.js | 166 ++++++++++++++++--- 3 files changed, 229 insertions(+), 90 deletions(-) create mode 100644 .changeset/tired-emus-matter.md diff --git a/.changeset/tired-emus-matter.md b/.changeset/tired-emus-matter.md new file mode 100644 index 000000000..52f45dd2b --- /dev/null +++ b/.changeset/tired-emus-matter.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-vue': patch +--- + +Fixed `no-negated-v-if-condition` rule to swap entire elements diff --git a/lib/rules/no-negated-v-if-condition.js b/lib/rules/no-negated-v-if-condition.js index 73ee877c0..e0fec2546 100644 --- a/lib/rules/no-negated-v-if-condition.js +++ b/lib/rules/no-negated-v-if-condition.js @@ -53,6 +53,21 @@ function isDirectlyFollowedByElse(element) { return nextElement ? utils.hasDirective(nextElement, 'else') : false } +/** + * @param {VElement} element + */ +function getDirective(element) { + return /** @type {VIfDirective|undefined} */ ( + element.startTag.attributes.find( + (attr) => + attr.directive && + attr.key.name && + attr.key.name.name && + ['if', 'else-if', 'else'].includes(attr.key.name.name) + ) + ) +} + module.exports = { meta: { type: 'suggestion', @@ -73,9 +88,37 @@ module.exports = { /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode() - const templateTokens = - sourceCode.parserServices.getTemplateBodyTokenStore && - sourceCode.parserServices.getTemplateBodyTokenStore() + + const processedPairs = new Set() + + /** + * @param {Expression} expression + * @returns {string} + */ + function getConvertedCondition(expression) { + if ( + expression.type === 'UnaryExpression' && + expression.operator === '!' + ) { + return sourceCode.text.slice( + expression.range[0] + 1, + expression.range[1] + ) + } + + if (expression.type === 'BinaryExpression') { + const left = sourceCode.getText(expression.left) + const right = sourceCode.getText(expression.right) + + if (expression.operator === '!=') { + return `${left} == ${right}` + } else if (expression.operator === '!==') { + return `${left} === ${right}` + } + } + + return sourceCode.getText(expression) + } /** * @param {VIfDirective} node @@ -100,6 +143,12 @@ module.exports = { return } + const pairKey = `${element.range[0]}-${elseElement.range[0]}` + if (processedPairs.has(pairKey)) { + return + } + processedPairs.add(pairKey) + context.report({ node: expression, messageId: 'negatedCondition', @@ -107,87 +156,54 @@ module.exports = { { messageId: 'fixNegatedCondition', *fix(fixer) { - yield* convertNegatedCondition(fixer, expression) - yield* swapElementContents(fixer, element, elseElement) + yield* swapElements(fixer, element, elseElement, expression) } } ] }) } - /** - * @param {RuleFixer} fixer - * @param {Expression} expression - */ - function* convertNegatedCondition(fixer, expression) { - if ( - expression.type === 'UnaryExpression' && - expression.operator === '!' - ) { - const token = templateTokens.getFirstToken(expression) - if (token?.type === 'Punctuator' && token.value === '!') { - yield fixer.remove(token) - } - return - } - - if (expression.type === 'BinaryExpression') { - const operatorToken = templateTokens.getTokenAfter( - expression.left, - (token) => - token?.type === 'Punctuator' && token.value === expression.operator - ) - - if (!operatorToken) return - - if (expression.operator === '!=') { - yield fixer.replaceText(operatorToken, '==') - } else if (expression.operator === '!==') { - yield fixer.replaceText(operatorToken, '===') - } - } - } - - /** - * @param {VElement} element - * @returns {string} - */ - function getElementContent(element) { - if (element.children.length === 0 || !element.endTag) { - return '' - } - - const contentStart = element.startTag.range[1] - const contentEnd = element.endTag.range[0] - - return sourceCode.text.slice(contentStart, contentEnd) - } - /** * @param {RuleFixer} fixer * @param {VElement} ifElement * @param {VElement} elseElement + * @param {Expression} expression */ - function* swapElementContents(fixer, ifElement, elseElement) { - if (!ifElement.endTag || !elseElement.endTag) { - return - } + function* swapElements(fixer, ifElement, elseElement, expression) { + const convertedCondition = getConvertedCondition(expression) - const ifContent = getElementContent(ifElement) - const elseContent = getElementContent(elseElement) + const ifDir = getDirective(ifElement) + const elseDir = getDirective(elseElement) - if (ifContent === elseContent) { + if (!ifDir || !elseDir) { return } - yield fixer.replaceTextRange( - [ifElement.startTag.range[1], ifElement.endTag.range[0]], - elseContent + const ifDirectiveName = ifDir.key.name.name + + const ifText = sourceCode.text.slice( + ifElement.range[0], + ifElement.range[1] ) - yield fixer.replaceTextRange( - [elseElement.startTag.range[1], elseElement.endTag.range[0]], - ifContent + const elseText = sourceCode.text.slice( + elseElement.range[0], + elseElement.range[1] ) + + const newIfDirective = `v-${ifDirectiveName}="${convertedCondition}"` + const newIfText = + elseText.slice(0, elseDir.range[0] - elseElement.range[0]) + + newIfDirective + + elseText.slice(elseDir.range[1] - elseElement.range[0]) + + const newElseDirective = 'v-else' + const newElseText = + ifText.slice(0, ifDir.range[0] - ifElement.range[0]) + + newElseDirective + + ifText.slice(ifDir.range[1] - ifElement.range[0]) + + yield fixer.replaceTextRange(ifElement.range, newIfText) + yield fixer.replaceTextRange(elseElement.range, newElseText) } return utils.defineTemplateBodyVisitor(context, { diff --git a/tests/lib/rules/no-negated-v-if-condition.js b/tests/lib/rules/no-negated-v-if-condition.js index ee60562ea..fc0e02d0d 100644 --- a/tests/lib/rules/no-negated-v-if-condition.js +++ b/tests/lib/rules/no-negated-v-if-condition.js @@ -149,8 +149,8 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ @@ -165,8 +165,8 @@ tester.run('no-negated-v-if-condition', rule, { messageId: 'fixNegatedCondition', output: ` ` } @@ -178,8 +178,8 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ @@ -194,8 +194,8 @@ tester.run('no-negated-v-if-condition', rule, { messageId: 'fixNegatedCondition', output: ` ` } @@ -265,26 +265,26 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ { messageId: 'negatedCondition', line: 4, - column: 25, + column: 26, endLine: 4, - endColumn: 29, + endColumn: 30, suggestions: [ { messageId: 'fixNegatedCondition', output: ` ` } @@ -296,26 +296,144 @@ tester.run('no-negated-v-if-condition', rule, { filename: 'test.vue', code: ` `, errors: [ { messageId: 'negatedCondition', line: 4, - column: 25, + column: 26, endLine: 4, - endColumn: 27, + endColumn: 28, suggestions: [ { messageId: 'fixNegatedCondition', output: ` + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20, + endLine: 3, + endColumn: 30, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'negatedCondition', + line: 3, + column: 20, + endLine: 3, + endColumn: 26, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + + ` + } + ] + }, + { + messageId: 'negatedCondition', + line: 4, + column: 23, + endLine: 4, + endColumn: 29, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + + ` + } + ] + }, + { + messageId: 'negatedCondition', + line: 8, + column: 23, + endLine: 8, + endColumn: 30, + suggestions: [ + { + messageId: 'fixNegatedCondition', + output: ` + ` } From 36ee5d8cc2f70f5c79e88d17d25efd46ee3c1cd0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:15:57 +0200 Subject: [PATCH 04/36] Version Packages (#2943) --- .changeset/tired-emus-matter.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/tired-emus-matter.md diff --git a/.changeset/tired-emus-matter.md b/.changeset/tired-emus-matter.md deleted file mode 100644 index 52f45dd2b..000000000 --- a/.changeset/tired-emus-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'eslint-plugin-vue': patch ---- - -Fixed `no-negated-v-if-condition` rule to swap entire elements diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec26c26e..b4caed28b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # eslint-plugin-vue +## 10.5.1 + +### Patch Changes + +- Fixed [`vue/no-negated-v-if-condition`](https://eslint.vuejs.org/rules/no-negated-v-if-condition.html) rule to swap entire elements ([#2941](https://github.com/vuejs/eslint-plugin-vue/pull/2941)) + ## 10.5.0 ### Minor Changes diff --git a/package.json b/package.json index d49f7f557..68f0b1419 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-vue", - "version": "10.5.0", + "version": "10.5.1", "description": "Official ESLint plugin for Vue.js", "main": "lib/index.js", "types": "lib/index.d.ts", From a353a33e4c7f2fab4e99b3670092eece88443a1f Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Fri, 17 Oct 2025 04:09:12 -0500 Subject: [PATCH 05/36] feat: add `vue/no-duplicate-class-names` rule (#2934) --- .changeset/ten-lines-fail.md | 5 + docs/rules/index.md | 2 + docs/rules/no-duplicate-class-names.md | 57 ++ lib/index.js | 1 + lib/rules/no-duplicate-class-names.js | 329 ++++++++++++ tests/lib/rules/no-duplicate-class-names.js | 567 ++++++++++++++++++++ 6 files changed, 961 insertions(+) create mode 100644 .changeset/ten-lines-fail.md create mode 100644 docs/rules/no-duplicate-class-names.md create mode 100644 lib/rules/no-duplicate-class-names.js create mode 100644 tests/lib/rules/no-duplicate-class-names.js diff --git a/.changeset/ten-lines-fail.md b/.changeset/ten-lines-fail.md new file mode 100644 index 000000000..7ba93232a --- /dev/null +++ b/.changeset/ten-lines-fail.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-vue': minor +--- + +Added new [`vue/no-duplicate-class-names`](https://eslint.vuejs.org/rules/no-duplicate-class-names.html) rule diff --git a/docs/rules/index.md b/docs/rules/index.md index 26542db38..3209c157d 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -234,6 +234,7 @@ For example: | [vue/no-bare-strings-in-template] | disallow the use of bare strings in ``, options: ['always'], errors: [ - errorMessage({ + { messageId: 'missingOpeningSpace', line: 4 - }), - errorMessage({ + }, + { messageId: 'missingClosingSpace', line: 4 - }) + } ] }, { @@ -125,14 +113,14 @@ tester.run('space-in-parens', rule, { > `, errors: [ - errorMessage({ + { messageId: 'rejectedOpeningSpace', line: 4 - }), - errorMessage({ + }, + { messageId: 'rejectedClosingSpace', line: 4 - }) + } ] }, { @@ -150,14 +138,14 @@ tester.run('space-in-parens', rule, { `, options: ['always'], errors: [ - errorMessage({ + { messageId: 'missingOpeningSpace', line: 4 - }), - errorMessage({ + }, + { messageId: 'missingClosingSpace', line: 4 - }) + } ] }, { @@ -174,14 +162,14 @@ tester.run('space-in-parens', rule, { > `, errors: [ - errorMessage({ + { messageId: 'rejectedOpeningSpace', line: 4 - }), - errorMessage({ + }, + { messageId: 'rejectedClosingSpace', line: 4 - }) + } ] }, { @@ -199,14 +187,14 @@ tester.run('space-in-parens', rule, { `, options: ['always'], errors: [ - errorMessage({ + { messageId: 'missingOpeningSpace', line: 4 - }), - errorMessage({ + }, + { messageId: 'missingClosingSpace', line: 4 - }) + } ] }, @@ -225,14 +213,14 @@ tester.run('space-in-parens', rule, { } `, errors: [ - errorMessage({ + { messageId: 'rejectedOpeningSpace', line: 4 - }), - errorMessage({ + }, + { messageId: 'rejectedClosingSpace', line: 4 - }) + } ] } ] diff --git a/tests/lib/rules/space-infix-ops.js b/tests/lib/rules/space-infix-ops.js index 0325884a7..d05e25170 100644 --- a/tests/lib/rules/space-infix-ops.js +++ b/tests/lib/rules/space-infix-ops.js @@ -3,17 +3,14 @@ */ 'use strict' -const { RuleTester, ESLint } = require('../../eslint-compat') -const semver = require('semver') +const { RuleTester } = require('../../eslint-compat') const rule = require('../../../lib/rules/space-infix-ops') const tester = new RuleTester({ languageOptions: { parser: require('vue-eslint-parser'), ecmaVersion: 2015 } }) -const message = semver.lt(ESLint.version, '5.10.0') - ? () => 'Infix operators must be spaced.' - : (operator) => `Operator '${operator}' must be spaced.` +const message = (operator) => `Operator '${operator}' must be spaced.` tester.run('space-infix-ops', rule, { valid: [ From 16d17a4b4aa20215e487bab0f601f5a5ce12b9b4 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 5 Dec 2025 10:30:29 +0800 Subject: [PATCH 31/36] test: rewrite some files in `.ts` (#2968) --- eslint.config.mjs | 1 + .../lib/configs/{eslintrc.js => eslintrc.ts} | 6 +- tests/lib/configs/{flat.js => flat.ts} | 103 +++++++++--------- .../rules/{block-order.js => block-order.ts} | 40 ++----- ...ment-directive.js => comment-directive.ts} | 25 +++-- tsconfig.json | 4 + 6 files changed, 83 insertions(+), 96 deletions(-) rename tests/lib/configs/{eslintrc.js => eslintrc.ts} (83%) rename tests/lib/configs/{flat.js => flat.ts} (65%) rename tests/lib/rules/{block-order.js => block-order.ts} (94%) rename tests/lib/rules/{comment-directive.js => comment-directive.ts} (97%) diff --git a/eslint.config.mjs b/eslint.config.mjs index f5c8aee09..4857aaeb7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -275,6 +275,7 @@ export default typegen([ '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-object-type': 'off', '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/triple-slash-reference': 'off', '@typescript-eslint/unified-signatures': 'off', '@typescript-eslint/ban-ts-comment': [ diff --git a/tests/lib/configs/eslintrc.js b/tests/lib/configs/eslintrc.ts similarity index 83% rename from tests/lib/configs/eslintrc.js rename to tests/lib/configs/eslintrc.ts index a9febaeeb..85d25022c 100644 --- a/tests/lib/configs/eslintrc.js +++ b/tests/lib/configs/eslintrc.ts @@ -1,7 +1,5 @@ -'use strict' - -const { ESLint } = require('../../eslint-compat') -const plugin = require('../../../lib/index') +import { ESLint } from '../../eslint-compat' +import plugin from '../../../lib' describe('eslintrc configs', () => { for (const name of Object.keys(plugin.configs)) { diff --git a/tests/lib/configs/flat.js b/tests/lib/configs/flat.ts similarity index 65% rename from tests/lib/configs/flat.js rename to tests/lib/configs/flat.ts index 51449953a..ed9e282b1 100644 --- a/tests/lib/configs/flat.js +++ b/tests/lib/configs/flat.ts @@ -3,14 +3,13 @@ * @author 唯然 */ -'use strict' +import { Linter } from 'eslint' +import plugin from '../../../lib' +import { strict as assert } from 'assert' +import { FlatESLint } from '../../eslint-compat' -const plugin = require('../../../lib/index') -const { strict: assert } = require('assert') // node v14 does not support 'assert/strict' -const { FlatESLint } = require('../../eslint-compat') - -function mergeConfig(configs) { - let config = { rules: {}, plugins: {} } +function mergeConfig(configs: Linter.FlatConfig[]): Linter.FlatConfig { + let config: Linter.FlatConfig = { rules: {}, plugins: {} } for (const item of configs) { config = { ...config, @@ -37,16 +36,16 @@ describe('flat configs', () => { const forVue = mergeConfig( base.filter((config) => config.files?.includes('*.vue') || !config.files) ) - assert.strictEqual(forVue.plugins.vue, plugin) + assert.strictEqual(forVue.plugins!.vue, plugin) assert.strictEqual(forVue.processor, 'vue/vue') - assert.strictEqual(forVue.rules['vue/comment-directive'], 'error') + assert.strictEqual(forVue.rules!['vue/comment-directive'], 'error') const forOtherThanVue = mergeConfig( base.filter((config) => !config.files?.includes('*.vue')) ) - assert.strictEqual(forOtherThanVue.plugins.vue, plugin) + assert.strictEqual(forOtherThanVue.plugins!.vue, plugin) assert.strictEqual( - forOtherThanVue.rules['vue/comment-directive'], + forOtherThanVue.rules!['vue/comment-directive'], undefined ) }) @@ -61,20 +60,20 @@ describe('flat configs', () => { (config) => config.files?.includes('*.vue') || !config.files ) ) - assert.strictEqual(forVue.plugins.vue, plugin) - assert.strictEqual(forVue.rules['vue/comment-directive'], 'error') - assert.strictEqual(forVue.rules['vue/multi-word-component-names'], 'error') + assert.strictEqual(forVue.plugins!.vue, plugin) + assert.strictEqual(forVue.rules!['vue/comment-directive'], 'error') + assert.strictEqual(forVue.rules!['vue/multi-word-component-names'], 'error') const forOtherThanVue = mergeConfig( essential.filter((config) => !config.files?.includes('*.vue')) ) - assert.strictEqual(forOtherThanVue.plugins.vue, plugin) + assert.strictEqual(forOtherThanVue.plugins!.vue, plugin) assert.strictEqual( - forOtherThanVue.rules['vue/comment-directive'], + forOtherThanVue.rules!['vue/comment-directive'], undefined ) assert.strictEqual( - forOtherThanVue.rules['vue/multi-word-component-names'], + forOtherThanVue.rules!['vue/multi-word-component-names'], 'error' ) }) @@ -89,20 +88,20 @@ describe('flat configs', () => { (config) => config.files?.includes('*.vue') || !config.files ) ) - assert.strictEqual(forVue.plugins.vue, plugin) - assert.strictEqual(forVue.rules['vue/comment-directive'], 'error') - assert.strictEqual(forVue.rules['vue/multi-word-component-names'], 'error') + assert.strictEqual(forVue.plugins!.vue, plugin) + assert.strictEqual(forVue.rules!['vue/comment-directive'], 'error') + assert.strictEqual(forVue.rules!['vue/multi-word-component-names'], 'error') const forOtherThanVue = mergeConfig( stronglyRecommended.filter((config) => !config.files?.includes('*.vue')) ) - assert.strictEqual(forOtherThanVue.plugins.vue, plugin) + assert.strictEqual(forOtherThanVue.plugins!.vue, plugin) assert.strictEqual( - forOtherThanVue.rules['vue/comment-directive'], + forOtherThanVue.rules!['vue/comment-directive'], undefined ) assert.strictEqual( - forOtherThanVue.rules['vue/multi-word-component-names'], + forOtherThanVue.rules!['vue/multi-word-component-names'], 'error' ) }) @@ -117,24 +116,24 @@ describe('flat configs', () => { (config) => config.files?.includes('*.vue') || !config.files ) ) - assert.strictEqual(forVue.plugins.vue, plugin) - assert.strictEqual(forVue.rules['vue/comment-directive'], 'error') - assert.strictEqual(forVue.rules['vue/multi-word-component-names'], 'error') - assert.strictEqual(forVue.rules['vue/attributes-order'], 'warn') + assert.strictEqual(forVue.plugins!.vue, plugin) + assert.strictEqual(forVue.rules!['vue/comment-directive'], 'error') + assert.strictEqual(forVue.rules!['vue/multi-word-component-names'], 'error') + assert.strictEqual(forVue.rules!['vue/attributes-order'], 'warn') const forOtherThanVue = mergeConfig( recommended.filter((config) => !config.files?.includes('*.vue')) ) - assert.strictEqual(forOtherThanVue.plugins.vue, plugin) + assert.strictEqual(forOtherThanVue.plugins!.vue, plugin) assert.strictEqual( - forOtherThanVue.rules['vue/comment-directive'], + forOtherThanVue.rules!['vue/comment-directive'], undefined ) assert.strictEqual( - forOtherThanVue.rules['vue/multi-word-component-names'], + forOtherThanVue.rules!['vue/multi-word-component-names'], 'error' ) - assert.strictEqual(forOtherThanVue.rules['vue/attributes-order'], 'warn') + assert.strictEqual(forOtherThanVue.rules!['vue/attributes-order'], 'warn') }) it('should export vue2-essential config', () => { @@ -147,20 +146,20 @@ describe('flat configs', () => { (config) => config.files?.includes('*.vue') || !config.files ) ) - assert.strictEqual(forVue.plugins.vue, plugin) - assert.strictEqual(forVue.rules['vue/comment-directive'], 'error') - assert.strictEqual(forVue.rules['vue/multi-word-component-names'], 'error') + assert.strictEqual(forVue.plugins!.vue, plugin) + assert.strictEqual(forVue.rules!['vue/comment-directive'], 'error') + assert.strictEqual(forVue.rules!['vue/multi-word-component-names'], 'error') const forOtherThanVue = mergeConfig( essential.filter((config) => !config.files?.includes('*.vue')) ) - assert.strictEqual(forOtherThanVue.plugins.vue, plugin) + assert.strictEqual(forOtherThanVue.plugins!.vue, plugin) assert.strictEqual( - forOtherThanVue.rules['vue/comment-directive'], + forOtherThanVue.rules!['vue/comment-directive'], undefined ) assert.strictEqual( - forOtherThanVue.rules['vue/multi-word-component-names'], + forOtherThanVue.rules!['vue/multi-word-component-names'], 'error' ) }) @@ -175,20 +174,20 @@ describe('flat configs', () => { (config) => config.files?.includes('*.vue') || !config.files ) ) - assert.strictEqual(forVue.plugins.vue, plugin) - assert.strictEqual(forVue.rules['vue/comment-directive'], 'error') - assert.strictEqual(forVue.rules['vue/multi-word-component-names'], 'error') + assert.strictEqual(forVue.plugins!.vue, plugin) + assert.strictEqual(forVue.rules!['vue/comment-directive'], 'error') + assert.strictEqual(forVue.rules!['vue/multi-word-component-names'], 'error') const forOtherThanVue = mergeConfig( stronglyRecommended.filter((config) => !config.files?.includes('*.vue')) ) - assert.strictEqual(forOtherThanVue.plugins.vue, plugin) + assert.strictEqual(forOtherThanVue.plugins!.vue, plugin) assert.strictEqual( - forOtherThanVue.rules['vue/comment-directive'], + forOtherThanVue.rules!['vue/comment-directive'], undefined ) assert.strictEqual( - forOtherThanVue.rules['vue/multi-word-component-names'], + forOtherThanVue.rules!['vue/multi-word-component-names'], 'error' ) }) @@ -203,24 +202,24 @@ describe('flat configs', () => { (config) => config.files?.includes('*.vue') || !config.files ) ) - assert.strictEqual(forVue.plugins.vue, plugin) - assert.strictEqual(forVue.rules['vue/comment-directive'], 'error') - assert.strictEqual(forVue.rules['vue/multi-word-component-names'], 'error') - assert.strictEqual(forVue.rules['vue/attributes-order'], 'warn') + assert.strictEqual(forVue.plugins!.vue, plugin) + assert.strictEqual(forVue.rules!['vue/comment-directive'], 'error') + assert.strictEqual(forVue.rules!['vue/multi-word-component-names'], 'error') + assert.strictEqual(forVue.rules!['vue/attributes-order'], 'warn') const forOtherThanVue = mergeConfig( recommended.filter((config) => !config.files?.includes('*.vue')) ) - assert.strictEqual(forOtherThanVue.plugins.vue, plugin) + assert.strictEqual(forOtherThanVue.plugins!.vue, plugin) assert.strictEqual( - forOtherThanVue.rules['vue/comment-directive'], + forOtherThanVue.rules!['vue/comment-directive'], undefined ) assert.strictEqual( - forOtherThanVue.rules['vue/multi-word-component-names'], + forOtherThanVue.rules!['vue/multi-word-component-names'], 'error' ) - assert.strictEqual(forOtherThanVue.rules['vue/attributes-order'], 'warn') + assert.strictEqual(forOtherThanVue.rules!['vue/attributes-order'], 'warn') }) it('should work the suppress comments with base config', async () => { @@ -281,7 +280,7 @@ describe('flat configs', () => { const result = await eslint.lintText(code, { filePath: 'MyComponent.vue' }) assert.deepStrictEqual( - result[0].messages.map((message) => message.ruleId), + result[0].messages.map((message: Linter.LintMessage) => message.ruleId), [ 'vue/no-parsing-error', 'vue/max-attributes-per-line', diff --git a/tests/lib/rules/block-order.js b/tests/lib/rules/block-order.ts similarity index 94% rename from tests/lib/rules/block-order.js rename to tests/lib/rules/block-order.ts index 4eb25e2bd..659522cb6 100644 --- a/tests/lib/rules/block-order.js +++ b/tests/lib/rules/block-order.ts @@ -1,12 +1,13 @@ /** * @author Yosuke Ota */ -'use strict' - -const rule = require('../../../lib/rules/block-order') -const RuleTester = require('../../eslint-compat').RuleTester -const assert = require('assert') -const { ESLint } = require('../../eslint-compat') +import { Rule } from '../../../node_modules/@types/eslint' +import assert from 'assert' +import parserVue from 'vue-eslint-parser' +import rule from '../../../lib/rules/block-order' +import { ESLint, RuleTester } from '../../eslint-compat' +import pluginVue from '../../../lib' +import processor from '../../../lib/processor' // Initialize linter. const eslint = new ESLint({ @@ -14,26 +15,26 @@ const eslint = new ESLint({ overrideConfig: { files: ['**/*.vue'], languageOptions: { - parser: require('vue-eslint-parser'), + parser: parserVue, ecmaVersion: 2015 }, - plugins: { vue: require('../../../lib/index') }, + plugins: { vue: pluginVue }, rules: { 'vue/comment-directive': 'error', 'vue/block-order': 'error' }, - processor: require('../../../lib/processor') + processor }, fix: true }) const tester = new RuleTester({ languageOptions: { - parser: require('vue-eslint-parser') + parser: parserVue } }) -tester.run('block-order', rule, { +tester.run('block-order', rule as unknown as Rule.RuleModule, { valid: [ // default '', @@ -71,44 +72,36 @@ tester.run('block-order', rule, { // order { code: '', - output: null, options: [{ order: ['script', 'template', 'style'] }] }, { code: '', - output: null, options: [{ order: ['template', 'script', 'style'] }] }, { code: '', - output: null, options: [{ order: ['style', 'template', 'script'] }] }, { code: '', - output: null, options: [{ order: ['template', 'docs', 'script', 'style'] }] }, { code: '', - output: null, options: [{ order: ['template', 'script', 'style'] }] }, { code: '
text

', - output: null, options: [{ order: ['docs', 'script', 'template', 'style'] }] }, { code: '', - output: null, options: [ { order: ['script[setup]', 'script:not([setup])', 'template', 'style'] } ] }, { code: '', - output: null, options: [ { order: [['script[setup]', 'script:not([setup])', 'template'], 'style'] @@ -117,24 +110,20 @@ tester.run('block-order', rule, { }, { code: '', - output: null, options: [{ order: ['script', 'template', 'style'] }] }, { code: '', - output: null, options: [{ order: [['script', 'template'], 'style'] }] }, { code: '', - output: null, options: [ { order: ['script:not([setup])', 'script[setup]', 'template', 'style'] } ] }, { code: '', - output: null, options: [ { order: [['script:not([setup])', 'script[setup]', 'template'], 'style'] @@ -143,7 +132,6 @@ tester.run('block-order', rule, { }, { code: '', - output: null, options: [ { order: [ @@ -158,7 +146,6 @@ tester.run('block-order', rule, { }, { code: '', - output: null, options: [ { order: [ @@ -175,17 +162,14 @@ tester.run('block-order', rule, { }, { code: '', - output: null, options: [{ order: [['docs', 'script', 'template'], 'style'] }] }, { code: '', - output: null, options: [{ order: ['i18n[locale=en]', 'i18n[locale=ja]'] }] }, { code: '', - output: null, options: [{ order: ['style:not([scoped])', 'style[scoped]'] }] }, diff --git a/tests/lib/rules/comment-directive.js b/tests/lib/rules/comment-directive.ts similarity index 97% rename from tests/lib/rules/comment-directive.js rename to tests/lib/rules/comment-directive.ts index b9acb1092..2a7efdab6 100644 --- a/tests/lib/rules/comment-directive.js +++ b/tests/lib/rules/comment-directive.ts @@ -3,10 +3,11 @@ * @author Toru Nagashima */ -'use strict' - -const assert = require('assert') -const { ESLint } = require('../../eslint-compat') +import assert from 'assert' +import parserVue from 'vue-eslint-parser' +import { ESLint } from '../../eslint-compat' +import pluginVue from '../../../lib' +import processor from '../../../lib/processor' // Initialize linter. const eslint = new ESLint({ @@ -14,21 +15,21 @@ const eslint = new ESLint({ overrideConfig: { files: ['*.*'], languageOptions: { - parser: require('vue-eslint-parser'), + parser: parserVue, ecmaVersion: 2015 }, - plugins: { vue: require('../../../lib/index') }, + plugins: { vue: pluginVue }, rules: { 'no-unused-vars': 'error', 'vue/comment-directive': 'error', 'vue/no-parsing-error': 'error', 'vue/no-duplicate-attributes': 'error' }, - processor: require('../../../lib/processor') + processor } }) -async function lintMessages(code) { +async function lintMessages(code: string) { const result = await eslint.lintText(code, { filePath: 'test.vue' }) return result[0].messages } @@ -357,10 +358,10 @@ describe('comment-directive', () => { overrideConfig: { files: ['**/*.vue'], languageOptions: { - parser: require('vue-eslint-parser'), + parser: parserVue, ecmaVersion: 2015 }, - plugins: { vue: require('../../../lib/index') }, + plugins: { vue: pluginVue }, rules: { 'no-unused-vars': 'error', 'vue/comment-directive': [ @@ -370,11 +371,11 @@ describe('comment-directive', () => { 'vue/no-parsing-error': 'error', 'vue/no-duplicate-attributes': 'error' }, - processor: require('../../../lib/processor') + processor } }) - async function lintMessages(code) { + async function lintMessages(code: string) { const result = await eslint.lintText(code, { filePath: 'test.vue' }) return result[0].messages } diff --git a/tsconfig.json b/tsconfig.json index b488a6b93..2008bef78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,9 @@ "module": "node16", "moduleResolution": "Node16", "lib": ["es2020"], + "types": [ + "vitest/globals" + ], "allowJs": true, "checkJs": true, "noEmit": true, @@ -25,6 +28,7 @@ }, "include": [ "lib/**/*", + "tests/**/*.ts", "typings/eslint-plugin-vue/global.d.ts", "docs/.vitepress/**/*.ts", "docs/.vitepress/**/*.mts" From d617fbbd61383df5ac271deb1dfa4e589181582c Mon Sep 17 00:00:00 2001 From: rzzf Date: Fri, 5 Dec 2025 10:44:25 +0800 Subject: [PATCH 32/36] feat(vue/no-negated-v-if-condition): upgrade rule suggestion to autofix (#2984) Co-authored-by: Flo Edelmann --- .changeset/rich-zebras-type.md | 5 + docs/rules/index.md | 2 +- docs/rules/no-negated-v-if-condition.md | 4 +- lib/rules/no-negated-v-if-condition.js | 16 +- tests/lib/rules/no-negated-v-if-condition.js | 198 ++++++------------- 5 files changed, 73 insertions(+), 152 deletions(-) create mode 100644 .changeset/rich-zebras-type.md diff --git a/.changeset/rich-zebras-type.md b/.changeset/rich-zebras-type.md new file mode 100644 index 000000000..62c2438ae --- /dev/null +++ b/.changeset/rich-zebras-type.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-vue": minor +--- + +Changed `vue/no-negated-v-if-condition` suggestion to autofix diff --git a/docs/rules/index.md b/docs/rules/index.md index 3209c157d..5d8deaf00 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -238,7 +238,7 @@ For example: | [vue/no-empty-component-block] | disallow the `