@@ -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
0 commit comments