@@ -10,15 +10,16 @@ const utils = require('../utils')
1010/**
1111 * @param {VDirective } node
1212 * @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 }> }
1415 */
15- function * extractClassNodes ( node , expression ) {
16+ function * extractClassNodes ( node , expression , unconditional = true ) {
1617 const nodeExpression = expression ?? node . value ?. expression
1718 if ( ! nodeExpression ) return
1819
1920 switch ( nodeExpression . type ) {
2021 case 'Literal' : {
21- yield { node : nodeExpression }
22+ yield { node : nodeExpression , unconditional }
2223 break
2324 }
2425 case 'ObjectExpression' : {
@@ -28,37 +29,36 @@ function* extractClassNodes(node, expression) {
2829 prop . key ?. type === 'Literal' &&
2930 typeof prop . key . value === 'string'
3031 ) {
31- yield { node : prop . key }
32+ yield { node : prop . key , unconditional : false }
3233 }
3334 }
3435 break
3536 }
3637 case 'ArrayExpression' : {
3738 for ( const element of nodeExpression . elements ) {
3839 if ( ! element || element . type === 'SpreadElement' ) continue
39-
40- yield * extractClassNodes ( node , element )
40+ yield * extractClassNodes ( node , element , unconditional )
4141 }
4242 break
4343 }
4444 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 )
4747 break
4848 }
4949 case 'TemplateLiteral' : {
5050 for ( const quasi of nodeExpression . quasis ) {
51- yield { node : quasi }
51+ yield { node : quasi , unconditional }
5252 }
5353 for ( const expr of nodeExpression . expressions ) {
54- yield * extractClassNodes ( node , expr )
54+ yield * extractClassNodes ( node , expr , unconditional )
5555 }
5656 break
5757 }
5858 case 'BinaryExpression' : {
5959 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 )
6262 }
6363 break
6464 }
@@ -138,6 +138,14 @@ function removeDuplicateClassNames(raw) {
138138 return quote + kept . join ( '' ) + quote
139139}
140140
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+
141149module . exports = {
142150 meta : {
143151 type : 'suggestion' ,
@@ -160,11 +168,7 @@ module.exports = {
160168 function reportDuplicateClasses ( node ) {
161169 if ( ! node ?. value ) return
162170
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 )
168172 if ( typeof classList !== 'string' ) return
169173
170174 const classNames = getClassNames ( classList )
@@ -193,6 +197,8 @@ module.exports = {
193197 return fixer . replaceText ( node , removeDuplicateClassNames ( raw ) )
194198 }
195199 } )
200+
201+ return duplicates
196202 }
197203
198204 return utils . defineTemplateBodyVisitor ( context , {
@@ -208,7 +214,6 @@ module.exports = {
208214 ) {
209215 const parent = node . parent
210216 const attrs = parent . attributes || [ ]
211-
212217 const staticAttr = attrs . find (
213218 ( attr ) =>
214219 attr . key &&
@@ -217,9 +222,9 @@ module.exports = {
217222 attr . value . type === 'VLiteral'
218223 )
219224
225+ // get static classes
220226 /** @type {Set<string> | null } */
221227 let staticClasses = null
222-
223228 if (
224229 staticAttr &&
225230 staticAttr . value &&
@@ -228,30 +233,65 @@ module.exports = {
228233 staticClasses = new Set ( getClassNames ( String ( staticAttr . value . value ) ) )
229234 }
230235
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 ( )
233240
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+ }
241248
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+ }
243263
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+ }
252279 }
253280 }
254281 }
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+ }
255295 }
256296 } )
257297 }
0 commit comments