From a353a33e4c7f2fab4e99b3670092eece88443a1f Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Fri, 17 Oct 2025 04:09:12 -0500 Subject: [PATCH 01/32] 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 27/32] 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 28/32] 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 `