From 4149a17e17281157da78fd47b57bebd9c60ff90f Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Wed, 26 Nov 2025 15:28:33 -0500 Subject: [PATCH 1/4] fix: improve non-intersecting conditions and combining parents --- lib/rules/no-duplicate-class-names.js | 87 ++++++++++++++++----- tests/lib/rules/no-duplicate-class-names.js | 42 ++++++++++ 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/lib/rules/no-duplicate-class-names.js b/lib/rules/no-duplicate-class-names.js index 0c1b33fc1..3a7737a04 100644 --- a/lib/rules/no-duplicate-class-names.js +++ b/lib/rules/no-duplicate-class-names.js @@ -11,15 +11,21 @@ const utils = require('../utils') * @param {VDirective} node * @param {Expression} [expression] * @param {boolean} [unconditional=true] whether the expression is unconditional - * @return {IterableIterator<{ node: Literal | TemplateElement, unconditional: boolean }>} + * @param {Expression} [parentExpr] parent expression for context + * @return {IterableIterator<{ node: Literal | TemplateElement, unconditional: boolean, parentExpr?: Expression }>} */ -function* extractClassNodes(node, expression, unconditional = true) { +function* extractClassNodes( + node, + expression, + unconditional = true, + parentExpr +) { const nodeExpression = expression ?? node.value?.expression if (!nodeExpression) return switch (nodeExpression.type) { case 'Literal': { - yield { node: nodeExpression, unconditional } + yield { node: nodeExpression, unconditional, parentExpr } break } case 'ObjectExpression': { @@ -29,7 +35,11 @@ function* extractClassNodes(node, expression, unconditional = true) { prop.key?.type === 'Literal' && typeof prop.key.value === 'string' ) { - yield { node: prop.key, unconditional: false } + yield { + node: prop.key, + unconditional: false, + parentExpr: nodeExpression + } } } break @@ -37,34 +47,64 @@ function* extractClassNodes(node, expression, unconditional = true) { case 'ArrayExpression': { for (const element of nodeExpression.elements) { if (!element || element.type === 'SpreadElement') continue - yield* extractClassNodes(node, element, unconditional) + yield* extractClassNodes(node, element, unconditional, nodeExpression) } break } case 'ConditionalExpression': { - yield* extractClassNodes(node, nodeExpression.consequent, false) - yield* extractClassNodes(node, nodeExpression.alternate, false) + yield* extractClassNodes( + node, + nodeExpression.consequent, + false, + nodeExpression + ) + yield* extractClassNodes( + node, + nodeExpression.alternate, + false, + nodeExpression + ) break } case 'TemplateLiteral': { for (const quasi of nodeExpression.quasis) { - yield { node: quasi, unconditional } + yield { node: quasi, unconditional, parentExpr: nodeExpression } } for (const expr of nodeExpression.expressions) { - yield* extractClassNodes(node, expr, unconditional) + yield* extractClassNodes(node, expr, unconditional, nodeExpression) } break } case 'BinaryExpression': { if (nodeExpression.operator === '+') { - yield* extractClassNodes(node, nodeExpression.left, unconditional) - yield* extractClassNodes(node, nodeExpression.right, unconditional) + yield* extractClassNodes( + node, + nodeExpression.left, + unconditional, + nodeExpression + ) + yield* extractClassNodes( + node, + nodeExpression.right, + unconditional, + nodeExpression + ) } break } case 'LogicalExpression': { - yield* extractClassNodes(node, nodeExpression.left, unconditional) - yield* extractClassNodes(node, nodeExpression.right, unconditional) + yield* extractClassNodes( + node, + nodeExpression.left, + unconditional, + nodeExpression + ) + yield* extractClassNodes( + node, + nodeExpression.right, + false, + nodeExpression + ) break } } @@ -250,11 +290,15 @@ module.exports = { /** @type {Map} */ const seen = new Map() - /** @type {Map} */ + /** @type {Map} */ const collected = new Map() const classNodes = extractClassNodes(node) - for (const { node: reportNode, unconditional } of classNodes) { + for (const { + node: reportNode, + unconditional, + parentExpr + } of classNodes) { // report fixable duplicates and collect reported class names const reportedClasses = reportDuplicateClasses(reportNode) if (reportedClasses) { @@ -272,14 +316,21 @@ module.exports = { if (reported.has(className)) continue const existing = collected.get(className) if (existing) { - // only add duplicate if at least one is unconditional - if (existing.unconditional || unconditional) { + // only add duplicate if at least one is unconditional, or share the same combining parent + const isSameParent = + parentExpr && + existing.parentExpr === parentExpr && + (parentExpr.type === 'BinaryExpression' || + parentExpr.type === 'TemplateLiteral') + + if (existing.unconditional || unconditional || isSameParent) { duplicatesInExpression.add(className) } } else { collected.set(className, { node: reportNode.parent, - unconditional + unconditional, + parentExpr }) } // track unconditional duplicates separately for reporting diff --git a/tests/lib/rules/no-duplicate-class-names.js b/tests/lib/rules/no-duplicate-class-names.js index 2384350cc..a9a52aa1b 100644 --- a/tests/lib/rules/no-duplicate-class-names.js +++ b/tests/lib/rules/no-duplicate-class-names.js @@ -56,6 +56,18 @@ tester.run('no-duplicate-class-names', rule, { { filename: 'class-object-duplicate-value.vue', code: `
` + }, + { + filename: 'class-non-intersecting-conditions.vue', + code: `` + }, + { + filename: 'class-multiple-logical-non-intersecting.vue', + code: `` + }, + { + filename: 'class-binary-in-logical-non-intersecting.vue', + code: `` } ], invalid: [ @@ -562,6 +574,36 @@ tester.run('no-duplicate-class-names', rule, { endColumn: 63 } ] + }, + { + filename: 'duplicate-class-binary-in-logical-expression.vue', + code: ``, + output: null, + errors: [ + { + message: "Duplicate class name 'bar'.", + type: 'BinaryExpression', + line: 1, + column: 36, + endLine: 1, + endColumn: 49 + } + ] + }, + { + filename: 'duplicate-class-template-literal-in-logical-expression.vue', + code: '', + output: null, + errors: [ + { + message: "Duplicate class name 'foo'.", + type: 'TemplateLiteral', + line: 1, + column: 37, + endLine: 1, + endColumn: 53 + } + ] } ] }) From dba440c6e2dc02eeacc14b75f5e567cc63cb70a0 Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Wed, 26 Nov 2025 15:38:15 -0500 Subject: [PATCH 2/4] dosc: add changeset --- .changeset/major-planes-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/major-planes-fly.md diff --git a/.changeset/major-planes-fly.md b/.changeset/major-planes-fly.md new file mode 100644 index 000000000..aa4930be0 --- /dev/null +++ b/.changeset/major-planes-fly.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-vue': patch +--- + +Fixed false positives in non-intersecting conditions in `vue/no-duplicate-class-names` and correctly detect duplicates in combining expressions From 5d96b7e0db85818fbc3a6f0619ab85e99e622e87 Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Wed, 26 Nov 2025 15:43:53 -0500 Subject: [PATCH 3/4] test: add space + update variable name for consistency --- tests/lib/rules/no-duplicate-class-names.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/lib/rules/no-duplicate-class-names.js b/tests/lib/rules/no-duplicate-class-names.js index a9a52aa1b..4734c0037 100644 --- a/tests/lib/rules/no-duplicate-class-names.js +++ b/tests/lib/rules/no-duplicate-class-names.js @@ -577,7 +577,7 @@ tester.run('no-duplicate-class-names', rule, { }, { filename: 'duplicate-class-binary-in-logical-expression.vue', - code: ``, + code: ``, output: null, errors: [ { @@ -586,13 +586,13 @@ tester.run('no-duplicate-class-names', rule, { line: 1, column: 36, endLine: 1, - endColumn: 49 + endColumn: 50 } ] }, { filename: 'duplicate-class-template-literal-in-logical-expression.vue', - code: '', + code: '', output: null, errors: [ { From 791d65e5bbd5088d8a83e4429e4ed86c25d6931d Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Wed, 26 Nov 2025 15:52:51 -0500 Subject: [PATCH 4/4] test: fix columns number --- tests/lib/rules/no-duplicate-class-names.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/rules/no-duplicate-class-names.js b/tests/lib/rules/no-duplicate-class-names.js index 4734c0037..94845ec3e 100644 --- a/tests/lib/rules/no-duplicate-class-names.js +++ b/tests/lib/rules/no-duplicate-class-names.js @@ -599,9 +599,9 @@ tester.run('no-duplicate-class-names', rule, { message: "Duplicate class name 'foo'.", type: 'TemplateLiteral', line: 1, - column: 37, + column: 36, endLine: 1, - endColumn: 53 + endColumn: 52 } ] }