Skip to content

Commit 4149a17

Browse files
committed
fix: improve non-intersecting conditions and combining parents
1 parent 083aafe commit 4149a17

File tree

2 files changed

+111
-18
lines changed

2 files changed

+111
-18
lines changed

lib/rules/no-duplicate-class-names.js

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,21 @@ const utils = require('../utils')
1111
* @param {VDirective} node
1212
* @param {Expression} [expression]
1313
* @param {boolean} [unconditional=true] whether the expression is unconditional
14-
* @return {IterableIterator<{ node: Literal | TemplateElement, unconditional: boolean }>}
14+
* @param {Expression} [parentExpr] parent expression for context
15+
* @return {IterableIterator<{ node: Literal | TemplateElement, unconditional: boolean, parentExpr?: Expression }>}
1516
*/
16-
function* extractClassNodes(node, expression, unconditional = true) {
17+
function* extractClassNodes(
18+
node,
19+
expression,
20+
unconditional = true,
21+
parentExpr
22+
) {
1723
const nodeExpression = expression ?? node.value?.expression
1824
if (!nodeExpression) return
1925

2026
switch (nodeExpression.type) {
2127
case 'Literal': {
22-
yield { node: nodeExpression, unconditional }
28+
yield { node: nodeExpression, unconditional, parentExpr }
2329
break
2430
}
2531
case 'ObjectExpression': {
@@ -29,42 +35,76 @@ function* extractClassNodes(node, expression, unconditional = true) {
2935
prop.key?.type === 'Literal' &&
3036
typeof prop.key.value === 'string'
3137
) {
32-
yield { node: prop.key, unconditional: false }
38+
yield {
39+
node: prop.key,
40+
unconditional: false,
41+
parentExpr: nodeExpression
42+
}
3343
}
3444
}
3545
break
3646
}
3747
case 'ArrayExpression': {
3848
for (const element of nodeExpression.elements) {
3949
if (!element || element.type === 'SpreadElement') continue
40-
yield* extractClassNodes(node, element, unconditional)
50+
yield* extractClassNodes(node, element, unconditional, nodeExpression)
4151
}
4252
break
4353
}
4454
case 'ConditionalExpression': {
45-
yield* extractClassNodes(node, nodeExpression.consequent, false)
46-
yield* extractClassNodes(node, nodeExpression.alternate, false)
55+
yield* extractClassNodes(
56+
node,
57+
nodeExpression.consequent,
58+
false,
59+
nodeExpression
60+
)
61+
yield* extractClassNodes(
62+
node,
63+
nodeExpression.alternate,
64+
false,
65+
nodeExpression
66+
)
4767
break
4868
}
4969
case 'TemplateLiteral': {
5070
for (const quasi of nodeExpression.quasis) {
51-
yield { node: quasi, unconditional }
71+
yield { node: quasi, unconditional, parentExpr: nodeExpression }
5272
}
5373
for (const expr of nodeExpression.expressions) {
54-
yield* extractClassNodes(node, expr, unconditional)
74+
yield* extractClassNodes(node, expr, unconditional, nodeExpression)
5575
}
5676
break
5777
}
5878
case 'BinaryExpression': {
5979
if (nodeExpression.operator === '+') {
60-
yield* extractClassNodes(node, nodeExpression.left, unconditional)
61-
yield* extractClassNodes(node, nodeExpression.right, unconditional)
80+
yield* extractClassNodes(
81+
node,
82+
nodeExpression.left,
83+
unconditional,
84+
nodeExpression
85+
)
86+
yield* extractClassNodes(
87+
node,
88+
nodeExpression.right,
89+
unconditional,
90+
nodeExpression
91+
)
6292
}
6393
break
6494
}
6595
case 'LogicalExpression': {
66-
yield* extractClassNodes(node, nodeExpression.left, unconditional)
67-
yield* extractClassNodes(node, nodeExpression.right, unconditional)
96+
yield* extractClassNodes(
97+
node,
98+
nodeExpression.left,
99+
unconditional,
100+
nodeExpression
101+
)
102+
yield* extractClassNodes(
103+
node,
104+
nodeExpression.right,
105+
false,
106+
nodeExpression
107+
)
68108
break
69109
}
70110
}
@@ -250,11 +290,15 @@ module.exports = {
250290
/** @type {Map<string, ASTNode>} */
251291
const seen = new Map()
252292

253-
/** @type {Map<string, {node: ASTNode, unconditional: boolean}>} */
293+
/** @type {Map<string, {node: ASTNode, unconditional: boolean, parentExpr?: Expression}>} */
254294
const collected = new Map()
255295

256296
const classNodes = extractClassNodes(node)
257-
for (const { node: reportNode, unconditional } of classNodes) {
297+
for (const {
298+
node: reportNode,
299+
unconditional,
300+
parentExpr
301+
} of classNodes) {
258302
// report fixable duplicates and collect reported class names
259303
const reportedClasses = reportDuplicateClasses(reportNode)
260304
if (reportedClasses) {
@@ -272,14 +316,21 @@ module.exports = {
272316
if (reported.has(className)) continue
273317
const existing = collected.get(className)
274318
if (existing) {
275-
// only add duplicate if at least one is unconditional
276-
if (existing.unconditional || unconditional) {
319+
// only add duplicate if at least one is unconditional, or share the same combining parent
320+
const isSameParent =
321+
parentExpr &&
322+
existing.parentExpr === parentExpr &&
323+
(parentExpr.type === 'BinaryExpression' ||
324+
parentExpr.type === 'TemplateLiteral')
325+
326+
if (existing.unconditional || unconditional || isSameParent) {
277327
duplicatesInExpression.add(className)
278328
}
279329
} else {
280330
collected.set(className, {
281331
node: reportNode.parent,
282-
unconditional
332+
unconditional,
333+
parentExpr
283334
})
284335
}
285336
// track unconditional duplicates separately for reporting

tests/lib/rules/no-duplicate-class-names.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ tester.run('no-duplicate-class-names', rule, {
5656
{
5757
filename: 'class-object-duplicate-value.vue',
5858
code: `<div :class="{ 'foo bar': isActive, 'foo': isAnotherActive }"></div>`
59+
},
60+
{
61+
filename: 'class-non-intersecting-conditions.vue',
62+
code: `<template><div :class="[isActive1 && { 'foo': isActive2, 'bar': isActive3 }, isActive4 && 'bar']"></div></template>`
63+
},
64+
{
65+
filename: 'class-multiple-logical-non-intersecting.vue',
66+
code: `<template><div :class="[isActive1 && 'foo', isActive2 && 'foo']"></div></template>`
67+
},
68+
{
69+
filename: 'class-binary-in-logical-non-intersecting.vue',
70+
code: `<template><div :class="[isActive1 && ('foo' + ' bar'), isActive2 && 'foo']"></div></template>`
5971
}
6072
],
6173
invalid: [
@@ -562,6 +574,36 @@ tester.run('no-duplicate-class-names', rule, {
562574
endColumn: 63
563575
}
564576
]
577+
},
578+
{
579+
filename: 'duplicate-class-binary-in-logical-expression.vue',
580+
code: `<template><div :class="isActive && 'bar' + 'bar'"></div></template>`,
581+
output: null,
582+
errors: [
583+
{
584+
message: "Duplicate class name 'bar'.",
585+
type: 'BinaryExpression',
586+
line: 1,
587+
column: 36,
588+
endLine: 1,
589+
endColumn: 49
590+
}
591+
]
592+
},
593+
{
594+
filename: 'duplicate-class-template-literal-in-logical-expression.vue',
595+
code: '<template><div :class="condition && `foo ${bar} foo`"></div></template>',
596+
output: null,
597+
errors: [
598+
{
599+
message: "Duplicate class name 'foo'.",
600+
type: 'TemplateLiteral',
601+
line: 1,
602+
column: 37,
603+
endLine: 1,
604+
endColumn: 53
605+
}
606+
]
565607
}
566608
]
567609
})

0 commit comments

Comments
 (0)