@@ -10,15 +10,16 @@ const utils = require('../utils')
10
10
/**
11
11
* @param {VDirective } node
12
12
* @param {Expression } [expression]
13
- * @return {IterableIterator<{ node: Literal | TemplateElement }> }
13
+ * @param {boolean } [unconditional=true] whether the expression is unconditional
14
+ * @return {IterableIterator<{ node: Literal | TemplateElement, unconditional: boolean }> }
14
15
*/
15
- function * extractClassNodes ( node , expression ) {
16
+ function * extractClassNodes ( node , expression , unconditional = true ) {
16
17
const nodeExpression = expression ?? node . value ?. expression
17
18
if ( ! nodeExpression ) return
18
19
19
20
switch ( nodeExpression . type ) {
20
21
case 'Literal' : {
21
- yield { node : nodeExpression }
22
+ yield { node : nodeExpression , unconditional }
22
23
break
23
24
}
24
25
case 'ObjectExpression' : {
@@ -28,37 +29,36 @@ function* extractClassNodes(node, expression) {
28
29
prop . key ?. type === 'Literal' &&
29
30
typeof prop . key . value === 'string'
30
31
) {
31
- yield { node : prop . key }
32
+ yield { node : prop . key , unconditional : false }
32
33
}
33
34
}
34
35
break
35
36
}
36
37
case 'ArrayExpression' : {
37
38
for ( const element of nodeExpression . elements ) {
38
39
if ( ! element || element . type === 'SpreadElement' ) continue
39
-
40
- yield * extractClassNodes ( node , element )
40
+ yield * extractClassNodes ( node , element , unconditional )
41
41
}
42
42
break
43
43
}
44
44
case 'ConditionalExpression' : {
45
- yield * extractClassNodes ( node , nodeExpression . consequent )
46
- yield * extractClassNodes ( node , nodeExpression . alternate )
45
+ yield * extractClassNodes ( node , nodeExpression . consequent , false )
46
+ yield * extractClassNodes ( node , nodeExpression . alternate , false )
47
47
break
48
48
}
49
49
case 'TemplateLiteral' : {
50
50
for ( const quasi of nodeExpression . quasis ) {
51
- yield { node : quasi }
51
+ yield { node : quasi , unconditional }
52
52
}
53
53
for ( const expr of nodeExpression . expressions ) {
54
- yield * extractClassNodes ( node , expr )
54
+ yield * extractClassNodes ( node , expr , unconditional )
55
55
}
56
56
break
57
57
}
58
58
case 'BinaryExpression' : {
59
59
if ( nodeExpression . operator === '+' ) {
60
- yield * extractClassNodes ( node , nodeExpression . left )
61
- yield * extractClassNodes ( node , nodeExpression . right )
60
+ yield * extractClassNodes ( node , nodeExpression . left , unconditional )
61
+ yield * extractClassNodes ( node , nodeExpression . right , unconditional )
62
62
}
63
63
break
64
64
}
@@ -138,6 +138,14 @@ function removeDuplicateClassNames(raw) {
138
138
return quote + kept . join ( '' ) + quote
139
139
}
140
140
141
+ /** @param {VLiteral | Literal | TemplateElement | null } node */
142
+ function getRawValue ( node ) {
143
+ if ( ! node ?. value ) return null
144
+ return typeof node . value === 'object' && 'raw' in node . value
145
+ ? node . value . raw
146
+ : node . value
147
+ }
148
+
141
149
module . exports = {
142
150
meta : {
143
151
type : 'suggestion' ,
@@ -160,11 +168,7 @@ module.exports = {
160
168
function reportDuplicateClasses ( node ) {
161
169
if ( ! node ?. value ) return
162
170
163
- const classList =
164
- typeof node . value === 'object' && 'raw' in node . value
165
- ? node . value . raw
166
- : node . value
167
-
171
+ const classList = getRawValue ( node )
168
172
if ( typeof classList !== 'string' ) return
169
173
170
174
const classNames = getClassNames ( classList )
@@ -193,6 +197,8 @@ module.exports = {
193
197
return fixer . replaceText ( node , removeDuplicateClassNames ( raw ) )
194
198
}
195
199
} )
200
+
201
+ return duplicates
196
202
}
197
203
198
204
return utils . defineTemplateBodyVisitor ( context , {
@@ -208,7 +214,6 @@ module.exports = {
208
214
) {
209
215
const parent = node . parent
210
216
const attrs = parent . attributes || [ ]
211
-
212
217
const staticAttr = attrs . find (
213
218
( attr ) =>
214
219
attr . key &&
@@ -217,9 +222,9 @@ module.exports = {
217
222
attr . value . type === 'VLiteral'
218
223
)
219
224
225
+ // get static classes
220
226
/** @type {Set<string> | null } */
221
227
let staticClasses = null
222
-
223
228
if (
224
229
staticAttr &&
225
230
staticAttr . value &&
@@ -228,30 +233,65 @@ module.exports = {
228
233
staticClasses = new Set ( getClassNames ( String ( staticAttr . value . value ) ) )
229
234
}
230
235
231
- for ( const { node : reportNode } of extractClassNodes ( node ) ) {
232
- reportDuplicateClasses ( reportNode )
236
+ const reported = new Set ( )
237
+ const duplicatesInExpression = new Set ( )
238
+ /** @type {Map<string, ASTNode> } */
239
+ const seen = new Map ( )
233
240
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
+ const classNodes = extractClassNodes ( node )
242
+ for ( const { node : reportNode , unconditional } of classNodes ) {
243
+ // report fixable duplicates and collect reported class names
244
+ const reportedClasses = reportDuplicateClasses ( reportNode )
245
+ if ( reportedClasses ) {
246
+ for ( const classes of reportedClasses ) reported . add ( classes )
247
+ }
241
248
242
- if ( typeof classList !== 'string' ) continue
249
+ // collect duplicates within the expression nodes
250
+ if ( unconditional ) {
251
+ const classList = getRawValue ( reportNode )
252
+ if ( typeof classList === 'string' ) {
253
+ const classNames = getClassNames ( classList )
254
+ for ( const className of classNames ) {
255
+ if ( seen . has ( className ) ) {
256
+ duplicatesInExpression . add ( className )
257
+ } else {
258
+ seen . set ( className , reportNode . parent )
259
+ }
260
+ }
261
+ }
262
+ }
243
263
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
- } )
264
+ // report duplicates between static and dynamic class attributes
265
+ if ( staticClasses ) {
266
+ const classList = getRawValue ( reportNode )
267
+ if ( typeof classList === 'string' ) {
268
+ const classNames = getClassNames ( classList )
269
+ const intersection = classNames . filter ( ( n ) =>
270
+ staticClasses . has ( n )
271
+ )
272
+ if ( intersection . length > 0 && parent ) {
273
+ context . report ( {
274
+ node : parent ,
275
+ messageId : 'duplicateClassName' ,
276
+ data : { name : intersection . join ( ', ' ) }
277
+ } )
278
+ }
252
279
}
253
280
}
254
281
}
282
+
283
+ // report duplicates between dynamic class nodes excluding already reported
284
+ for ( const r of reported ) duplicatesInExpression . delete ( r )
285
+ for ( const className of duplicatesInExpression ) {
286
+ const reportNode = seen . get ( className )
287
+ if ( reportNode ) {
288
+ context . report ( {
289
+ node : reportNode ,
290
+ messageId : 'duplicateClassName' ,
291
+ data : { name : className }
292
+ } )
293
+ }
294
+ }
255
295
}
256
296
} )
257
297
}
0 commit comments