@@ -233,11 +233,18 @@ module.exports = {
233233 staticClasses = new Set ( getClassNames ( String ( staticAttr . value . value ) ) )
234234 }
235235
236+ /** @type {Set<string> } */
236237 const reported = new Set ( )
238+
239+ /** @type {Set<string> } */
237240 const duplicatesInExpression = new Set ( )
241+
238242 /** @type {Map<string, ASTNode> } */
239243 const seen = new Map ( )
240244
245+ /** @type {Map<string, {node: ASTNode, unconditional: boolean}> } */
246+ const collected = new Map ( )
247+
241248 const classNodes = extractClassNodes ( node )
242249 for ( const { node : reportNode , unconditional } of classNodes ) {
243250 // report fixable duplicates and collect reported class names
@@ -246,44 +253,53 @@ module.exports = {
246253 for ( const classes of reportedClasses ) reported . add ( classes )
247254 }
248255
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- }
256+ // collect all class names and check for cross nodes duplicates
257+ const classList = getRawValue ( reportNode )
258+ if ( typeof classList !== 'string' ) continue
259+ const classNames = getClassNames ( classList )
260+
261+ for ( const className of classNames ) {
262+ // skip if already reported by reportDuplicateClasses
263+ if ( reported . has ( className ) ) continue
264+ const existing = collected . get ( className )
265+ if ( existing ) {
266+ // only add duplicate if at least one is unconditional
267+ if ( existing . unconditional || unconditional ) {
268+ duplicatesInExpression . add ( className )
269+ }
270+ } else {
271+ collected . set ( className , {
272+ node : reportNode . parent ,
273+ unconditional
274+ } )
275+ }
276+ // track unconditional duplicates separately for reporting
277+ if ( unconditional ) {
278+ if ( seen . has ( className ) ) {
279+ duplicatesInExpression . add ( className )
280+ } else {
281+ seen . set ( className , reportNode . parent )
260282 }
261283 }
262284 }
263285
264- // report duplicates between static and dynamic class attributes
286+ // report cross attribute duplicates
265287 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- }
288+ const intersection = classNames . filter ( ( n ) => staticClasses . has ( n ) )
289+ if ( intersection . length > 0 && parent ) {
290+ context . report ( {
291+ node : parent ,
292+ messageId : 'duplicateClassName' ,
293+ data : { name : intersection . join ( ', ' ) }
294+ } )
279295 }
280296 }
281297 }
282298
283- // report duplicates between dynamic class nodes excluding already reported
284- for ( const r of reported ) duplicatesInExpression . delete ( r )
299+ // report cross node duplicates
285300 for ( const className of duplicatesInExpression ) {
286- const reportNode = seen . get ( className )
301+ const reportNode =
302+ seen . get ( className ) || collected . get ( className ) ?. node
287303 if ( reportNode ) {
288304 context . report ( {
289305 node : reportNode ,
0 commit comments