Skip to content

Commit 7b641c0

Browse files
committed
fix: report cross attribute duplicates
related to #2932 discussion
1 parent f0c52c0 commit 7b641c0

File tree

3 files changed

+109
-15
lines changed

3 files changed

+109
-15
lines changed

docs/rules/no-duplicate-class-names.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ This rule prevents the same class name from appearing multiple times within the
3838
<div :class="['foo foo', { 'bar bar baz': true }]"></div>
3939
<div :class="isActive ? 'foo foo' : 'bar'"></div>
4040
<div :class="'foo foo ' + 'bar'"></div>
41+
<div class="foo" :class="'foo'"></div>
4142
</template>
4243
```
4344

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ function* extractClassNodes(node, expression) {
2626
if (
2727
prop.type === 'Property' &&
2828
prop.key?.type === 'Literal' &&
29-
typeof prop.key.value === 'string' &&
30-
prop.key.value.includes(' ')
29+
typeof prop.key.value === 'string'
3130
) {
3231
yield { node: prop.key }
3332
}
@@ -66,6 +65,14 @@ function* extractClassNodes(node, expression) {
6665
}
6766
}
6867

68+
/**
69+
* @param {string} classList
70+
* @returns {string[]}
71+
*/
72+
function getClassNames(classList) {
73+
return classList.split(/\s+/).filter(Boolean)
74+
}
75+
6976
/**
7077
* @param {string} raw - raw class names string including quotes
7178
* @returns {string}
@@ -160,7 +167,7 @@ module.exports = {
160167

161168
if (typeof classList !== 'string') return
162169

163-
const classNames = classList.split(/\s+/).filter(Boolean)
170+
const classNames = getClassNames(classList)
164171
if (classNames.length <= 1) return
165172

166173
const seen = new Set()
@@ -199,8 +206,51 @@ module.exports = {
199206
"VAttribute[directive=true][key.argument.name='class'][value.type='VExpressionContainer']"(
200207
node
201208
) {
209+
const parent = node.parent
210+
const attrs = parent.attributes || []
211+
212+
const staticAttr = attrs.find(
213+
(attr) =>
214+
attr.key &&
215+
attr.key.name === 'class' &&
216+
attr.value &&
217+
attr.value.type === 'VLiteral'
218+
)
219+
220+
/** @type {Set<string> | null} */
221+
let staticClasses = null
222+
223+
if (
224+
staticAttr &&
225+
staticAttr.value &&
226+
staticAttr.value.type === 'VLiteral'
227+
) {
228+
staticClasses = new Set(getClassNames(String(staticAttr.value.value)))
229+
}
230+
202231
for (const { node: reportNode } of extractClassNodes(node)) {
203232
reportDuplicateClasses(reportNode)
233+
234+
if (staticClasses) {
235+
const classList =
236+
reportNode.value &&
237+
typeof reportNode.value === 'object' &&
238+
'raw' in reportNode.value
239+
? reportNode.value.raw
240+
: reportNode.value
241+
242+
if (typeof classList !== 'string') continue
243+
244+
const classNames = getClassNames(classList)
245+
const intersection = classNames.filter((n) => staticClasses.has(n))
246+
if (intersection.length > 0 && parent) {
247+
context.report({
248+
node: parent,
249+
messageId: 'duplicateClassName',
250+
data: { name: intersection.join(', ') }
251+
})
252+
}
253+
}
204254
}
205255
}
206256
})

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

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,10 @@ tester.run('no-duplicate-class-names', rule, {
4141
filename: 'no-duplicate-class-in-directive-object.vue',
4242
code: `<template><div :class="{ 'foo bar baz': true }"></div></template>`
4343
},
44-
{
45-
filename: 'duplicate-class-and-directive.vue',
46-
code: `<template><div class="foo" :class="'foo'"></div></template>`
47-
},
4844
{
4945
filename: 'duplicate-class-in-different-directive-object-keys.vue',
5046
code: `<template><div :class="{ 'foo': true, 'foo bar': true }"></div></template>`
5147
},
52-
{
53-
filename: 'duplicate-class-in-different-directive-array-items.vue',
54-
code: `<template><div :class="['foo', 'foo bar']"></div></template>`
55-
},
56-
{
57-
filename: 'duplicate-class-in-different-directive-mixed.vue',
58-
code: `<template><div :class="['foo', { 'foo bar': true }]"></div></template>`
59-
},
6048
{
6149
filename: 'class-conditional-expression.vue',
6250
code: `<template><div :class="isActive ? 'foo' : 'bar'"></div></template>`
@@ -228,6 +216,61 @@ tester.run('no-duplicate-class-names', rule, {
228216
type: 'VLiteral'
229217
}
230218
]
219+
},
220+
{
221+
filename: 'duplicate-class-in-different-attributes.vue',
222+
code: `<template><div class="foo" :class="'foo'"></div></template>`,
223+
output: null,
224+
errors: [
225+
{
226+
message: "Duplicate class name 'foo'.",
227+
type: 'VStartTag'
228+
}
229+
]
230+
},
231+
{
232+
filename: 'duplicate-class-different-attributes.vue',
233+
code: `<template><div class="foo" :class="'foo bar'"></div></template>`,
234+
output: null,
235+
errors: [
236+
{
237+
message: "Duplicate class name 'foo'.",
238+
type: 'VStartTag'
239+
}
240+
]
241+
},
242+
{
243+
filename: 'duplicate-class-different-attributes-multiple-duplicates.vue',
244+
code: `<template><div class="foo bar" :class="'foo bar'"></div></template>`,
245+
output: null,
246+
errors: [
247+
{
248+
message: "Duplicate class name 'foo, bar'.",
249+
type: 'VStartTag'
250+
}
251+
]
252+
},
253+
{
254+
filename: 'duplicate-class-different-attributes-array.vue',
255+
code: `<template><div class="foo" :class="['foo', 'bar']"></div></template>`,
256+
output: null,
257+
errors: [
258+
{
259+
message: "Duplicate class name 'foo'.",
260+
type: 'VStartTag'
261+
}
262+
]
263+
},
264+
{
265+
filename: 'duplicate-class-different-attributes-object.vue',
266+
code: `<template><div class="foo" :class="{ 'foo': true }"></div></template>`,
267+
output: null,
268+
errors: [
269+
{
270+
message: "Duplicate class name 'foo'.",
271+
type: 'VStartTag'
272+
}
273+
]
231274
}
232275
]
233276
})

0 commit comments

Comments
 (0)