@@ -131,12 +131,16 @@ export default {
131
131
const vars = new Map ( ) ;
132
132
133
133
/**
134
- * We need to track this as a stack because we can have nested
135
- * rules that use the `var()` function, and we need to
136
- * ensure that we validate the innermost rule first.
137
- * @type {Array<Map<string,FunctionNodePlain>> }
134
+ * @type {Array<{
135
+ * valueParts: string[],
136
+ * functionPartsStack: string[][],
137
+ * valueSegmentLocs: Map<string,CssLocationRange>,
138
+ * skipValidation: boolean,
139
+ * hadVarSubstitution: boolean,
140
+ * resolvedCache: Map<string,string>
141
+ * }>}
138
142
*/
139
- const replacements = [ ] ;
143
+ const declStack = [ ] ;
140
144
141
145
const [ { allowUnknownVariables } ] = context . options ;
142
146
@@ -245,26 +249,153 @@ export default {
245
249
return null ;
246
250
}
247
251
252
+ /**
253
+ * Process a var function node and add its resolved value to the value list
254
+ * @param {Object } varNode The var() function node
255
+ * @param {string[] } valueList Array to collect processed values
256
+ * @param {Map<string,CssLocationRange> } valueSegmentLocs Map of rebuilt value segments to their locations
257
+ * @param {Map<string, string> } resolvedCache Cache for resolved variable values to prevent redundant lookups
258
+ * @returns {boolean } Whether processing was successful
259
+ */
260
+ function processVarFunction (
261
+ varNode ,
262
+ valueList ,
263
+ valueSegmentLocs ,
264
+ resolvedCache ,
265
+ ) {
266
+ const varValue = vars . get ( varNode . children [ 0 ] . name ) ;
267
+
268
+ if ( varValue ) {
269
+ const resolvedValue = resolveVariable (
270
+ varNode . children [ 0 ] . name ,
271
+ resolvedCache ,
272
+ ) ;
273
+ if ( resolvedValue ) {
274
+ valueList . push ( resolvedValue ) ;
275
+ valueSegmentLocs . set ( resolvedValue , varNode . loc ) ;
276
+ return true ;
277
+ }
278
+ }
279
+
280
+ // If the variable is not found and doesn't have a fallback value, report it
281
+ if ( varNode . children . length === 1 ) {
282
+ if ( ! allowUnknownVariables ) {
283
+ context . report ( {
284
+ loc : varNode . children [ 0 ] . loc ,
285
+ messageId : "unknownVar" ,
286
+ data : { var : varNode . children [ 0 ] . name } ,
287
+ } ) ;
288
+ return false ;
289
+ }
290
+ return true ;
291
+ }
292
+
293
+ // Handle fallback values
294
+ if ( varNode . children [ 2 ] . type !== "Raw" ) {
295
+ return true ;
296
+ }
297
+
298
+ const fallbackValue = varNode . children [ 2 ] . value . trim ( ) ;
299
+ const resolvedFallbackValue = resolveFallback (
300
+ fallbackValue ,
301
+ resolvedCache ,
302
+ ) ;
303
+ if ( resolvedFallbackValue ) {
304
+ valueList . push ( resolvedFallbackValue ) ;
305
+ valueSegmentLocs . set ( resolvedFallbackValue , varNode . loc ) ;
306
+ return true ;
307
+ }
308
+
309
+ // No valid fallback found
310
+ if ( ! allowUnknownVariables ) {
311
+ context . report ( {
312
+ loc : varNode . children [ 0 ] . loc ,
313
+ messageId : "unknownVar" ,
314
+ data : { var : varNode . children [ 0 ] . name } ,
315
+ } ) ;
316
+ return false ;
317
+ }
318
+
319
+ return true ;
320
+ }
321
+
248
322
return {
249
323
"Rule > Block > Declaration" ( ) {
250
- replacements . push ( new Map ( ) ) ;
324
+ declStack . push ( {
325
+ valueParts : [ ] ,
326
+ functionPartsStack : [ ] ,
327
+ valueSegmentLocs : new Map ( ) ,
328
+ skipValidation : false ,
329
+ hadVarSubstitution : false ,
330
+ /**
331
+ * Cache for resolved variable values within this single declaration.
332
+ * Prevents re-resolving the same variable and re-walking long `var()` chains.
333
+ */
334
+ resolvedCache : new Map ( ) ,
335
+ } ) ;
251
336
} ,
252
337
253
- "Function[name=/^var$/i]" ( node ) {
254
- const map = replacements . at ( - 1 ) ;
255
- if ( ! map ) {
338
+ "Rule > Block > Declaration > Value > *:not(Function)" ( node ) {
339
+ const state = declStack . at ( - 1 ) ;
340
+ const text = sourceCode . getText ( node ) . trim ( ) ;
341
+ state . valueParts . push ( text ) ;
342
+ state . valueSegmentLocs . set ( text , node . loc ) ;
343
+ } ,
344
+
345
+ Function ( ) {
346
+ declStack . at ( - 1 ) . functionPartsStack . push ( [ ] ) ;
347
+ } ,
348
+
349
+ "Function > *:not(Function)" ( node ) {
350
+ const state = declStack . at ( - 1 ) ;
351
+ const parts = state . functionPartsStack . at ( - 1 ) ;
352
+ const text = sourceCode . getText ( node ) . trim ( ) ;
353
+ parts . push ( text ) ;
354
+ state . valueSegmentLocs . set ( text , node . loc ) ;
355
+ } ,
356
+
357
+ "Function:exit" ( node ) {
358
+ const state = declStack . at ( - 1 ) ;
359
+ if ( state . skipValidation ) {
256
360
return ;
257
361
}
258
362
259
- /*
260
- * Store the custom property name and the function node
261
- * so can use these to validate the value later.
262
- */
263
- const name = node . children [ 0 ] . name ;
264
- map . set ( name , node ) ;
363
+ const parts = state . functionPartsStack . pop ( ) ;
364
+ let result ;
365
+ if ( node . name . toLowerCase ( ) === "var" ) {
366
+ const resolvedParts = [ ] ;
367
+ const success = processVarFunction (
368
+ node ,
369
+ resolvedParts ,
370
+ state . valueSegmentLocs ,
371
+ state . resolvedCache ,
372
+ ) ;
373
+
374
+ if ( ! success ) {
375
+ state . skipValidation = true ;
376
+ return ;
377
+ }
378
+
379
+ if ( resolvedParts . length === 0 ) {
380
+ return ;
381
+ }
382
+
383
+ state . hadVarSubstitution = true ;
384
+ result = resolvedParts [ 0 ] ;
385
+ } else {
386
+ result = `${ node . name } (${ parts . join ( " " ) } )` ;
387
+ }
388
+
389
+ const parentParts = state . functionPartsStack . at ( - 1 ) ;
390
+ if ( parentParts ) {
391
+ parentParts . push ( result ) ;
392
+ } else {
393
+ state . valueParts . push ( result ) ;
394
+ }
265
395
} ,
266
396
267
397
"Rule > Block > Declaration:exit" ( node ) {
398
+ const state = declStack . pop ( ) ;
268
399
if ( node . property . startsWith ( "--" ) ) {
269
400
// store the custom property name and value to validate later
270
401
vars . set ( node . property , node . value ) ;
@@ -273,106 +404,13 @@ export default {
273
404
return ;
274
405
}
275
406
276
- const varsFound = replacements . pop ( ) ;
407
+ if ( state . skipValidation ) {
408
+ return ;
409
+ }
277
410
278
- /** @type {Map<string,CssLocationRange> } */
279
- const valuesWithVarLocs = new Map ( ) ;
280
- const usingVars = varsFound ?. size > 0 ;
281
411
let value = node . value ;
282
-
283
- if ( usingVars ) {
284
- const valueList = [ ] ;
285
- /**
286
- * Cache for resolved variable values within this single declaration.
287
- * Prevents re-resolving the same variable and re-walking long `var()` chains.
288
- * @type {Map<string,string> }
289
- */
290
- const resolvedCache = new Map ( ) ;
291
- const valueNodes = node . value . children ;
292
-
293
- // When `var()` is used, we store all the values to `valueList` with the replacement of `var()` with there values or fallback values
294
- for ( const child of valueNodes ) {
295
- // If value is a function starts with `var()`
296
- if (
297
- child . type === "Function" &&
298
- child . name . toLowerCase ( ) === "var"
299
- ) {
300
- const varValue = vars . get ( child . children [ 0 ] . name ) ;
301
-
302
- // If the variable is found, use its value, otherwise check for fallback values
303
- if ( varValue ) {
304
- const resolvedValue = resolveVariable (
305
- child . children [ 0 ] . name ,
306
- resolvedCache ,
307
- ) ;
308
- if ( resolvedValue !== null ) {
309
- valueList . push ( resolvedValue ) ;
310
- valuesWithVarLocs . set (
311
- resolvedValue ,
312
- child . loc ,
313
- ) ;
314
- } else {
315
- if ( ! allowUnknownVariables ) {
316
- context . report ( {
317
- loc : child . children [ 0 ] . loc ,
318
- messageId : "unknownVar" ,
319
- data : {
320
- var : child . children [ 0 ] . name ,
321
- } ,
322
- } ) ;
323
- return ;
324
- }
325
- }
326
- } else {
327
- // If the variable is not found and doesn't have a fallback value, report it
328
- if ( child . children . length === 1 ) {
329
- if ( ! allowUnknownVariables ) {
330
- context . report ( {
331
- loc : child . children [ 0 ] . loc ,
332
- messageId : "unknownVar" ,
333
- data : {
334
- var : child . children [ 0 ] . name ,
335
- } ,
336
- } ) ;
337
-
338
- return ;
339
- }
340
- } else {
341
- // If it has a fallback value, use that
342
- if ( child . children [ 2 ] . type === "Raw" ) {
343
- const raw =
344
- child . children [ 2 ] . value . trim ( ) ;
345
- const resolvedFallbackValue =
346
- resolveFallback ( raw , resolvedCache ) ;
347
- if ( resolvedFallbackValue !== null ) {
348
- valueList . push (
349
- resolvedFallbackValue ,
350
- ) ;
351
- valuesWithVarLocs . set (
352
- resolvedFallbackValue ,
353
- child . loc ,
354
- ) ;
355
- } else if ( ! allowUnknownVariables ) {
356
- context . report ( {
357
- loc : child . children [ 0 ] . loc ,
358
- messageId : "unknownVar" ,
359
- data : {
360
- var : child . children [ 0 ] . name ,
361
- } ,
362
- } ) ;
363
- return ;
364
- }
365
- }
366
- }
367
- }
368
- } else {
369
- // If the child is not a `var()` function, just add its text to the `valueList`
370
- const valueText = sourceCode . getText ( child ) . trim ( ) ;
371
- valueList . push ( valueText ) ;
372
- valuesWithVarLocs . set ( valueText , child . loc ) ;
373
- }
374
- }
375
-
412
+ if ( state . hadVarSubstitution ) {
413
+ const valueList = state . valueParts ;
376
414
value =
377
415
valueList . length > 0
378
416
? valueList . join ( " " )
@@ -385,7 +423,7 @@ export default {
385
423
// validation failure
386
424
if ( isSyntaxMatchError ( error ) ) {
387
425
const errorValue =
388
- usingVars &&
426
+ state . hadVarSubstitution &&
389
427
value . slice (
390
428
error . mismatchOffset ,
391
429
error . mismatchOffset + error . mismatchLength ,
@@ -398,8 +436,8 @@ export default {
398
436
* If so, use that location; otherwise, use the error's
399
437
* reported location.
400
438
*/
401
- loc : usingVars
402
- ? ( valuesWithVarLocs . get ( errorValue ) ??
439
+ loc : state . hadVarSubstitution
440
+ ? ( state . valueSegmentLocs . get ( errorValue ) ??
403
441
node . value . loc )
404
442
: error . loc ,
405
443
messageId : "invalidPropertyValue" ,
@@ -411,12 +449,8 @@ export default {
411
449
* only include the part that caused the error.
412
450
* Otherwise, use the full value from the error.
413
451
*/
414
- value : usingVars
415
- ? value . slice (
416
- error . mismatchOffset ,
417
- error . mismatchOffset +
418
- error . mismatchLength ,
419
- )
452
+ value : state . hadVarSubstitution
453
+ ? errorValue
420
454
: error . css ,
421
455
expected : error . syntax ,
422
456
} ,
0 commit comments