@@ -25,6 +25,27 @@ import { isSyntaxMatchError, isSyntaxReferenceError } from "../util.js";
2525// Helpers
2626//-----------------------------------------------------------------------------
2727
28+ /**
29+ * Regex to match var() functional notation with optional fallback.
30+ */
31+ const varFunctionPattern = / v a r \( \s * ( - - [ ^ , \s ) ] + ) \s * (?: , \s * ( .+ ) ) ? \) / iu;
32+
33+ /**
34+ * Parses a var() function text and extracts the custom property name and fallback.
35+ * @param {string } text
36+ * @returns {{ name: string, fallbackText: string | null } | null }
37+ */
38+ function parseVarFunction ( text ) {
39+ const match = text . match ( varFunctionPattern ) ;
40+ if ( ! match ) {
41+ return null ;
42+ }
43+ return {
44+ name : match [ 1 ] . trim ( ) ,
45+ fallbackText : match [ 2 ] ?. trim ( ) ,
46+ } ;
47+ }
48+
2849/**
2950 * Extracts the list of fallback value or variable name used in a `var()` that is used as fallback function.
3051 * For example, for `var(--my-color, var(--fallback-color, red));` it will return `["--fallback-color", "red"]`.
@@ -36,31 +57,26 @@ function getVarFallbackList(value) {
3657 let currentValue = value ;
3758
3859 while ( true ) {
39- const match = currentValue . match (
40- / v a r \( \s * ( - - [ ^ , \s ) ] + ) \s * (?: , \s * ( .+ ) ) ? \) / iu,
41- ) ;
60+ const parsed = parseVarFunction ( currentValue ) ;
4261
43- if ( ! match ) {
62+ if ( ! parsed ) {
4463 break ;
4564 }
4665
47- const prop = match [ 1 ] . trim ( ) ;
48- const fallback = match [ 2 ] ?. trim ( ) ;
49-
50- list . push ( prop ) ;
66+ list . push ( parsed . name ) ;
5167
52- if ( ! fallback ) {
68+ if ( ! parsed . fallbackText ) {
5369 break ;
5470 }
5571
5672 // If fallback is not another var(), we're done
57- if ( ! fallback . toLowerCase ( ) . includes ( "var(" ) ) {
58- list . push ( fallback ) ;
73+ if ( ! parsed . fallbackText . toLowerCase ( ) . includes ( "var(" ) ) {
74+ list . push ( parsed . fallbackText ) ;
5975 break ;
6076 }
6177
6278 // Continue parsing from fallback
63- currentValue = fallback ;
79+ currentValue = parsed . fallbackText ;
6480 }
6581
6682 return list ;
@@ -124,6 +140,111 @@ export default {
124140
125141 const [ { allowUnknownVariables } ] = context . options ;
126142
143+ /**
144+ * Iteratively resolves CSS variable references until a value is found.
145+ * @param {string } variableName The variable name to resolve
146+ * @param {Map<string, string> } cache Cache for memoization within a single resolution scope
147+ * @param {Set<string> } [seen] Set of already seen variables to detect cycles
148+ * @returns {string|null } The resolved value or null if not found
149+ */
150+ function resolveVariable ( variableName , cache , seen = new Set ( ) ) {
151+ /** @type {Array<string> } */
152+ const fallbackStack = [ ] ;
153+ let currentVarName = variableName ;
154+
155+ /*
156+ * Resolves a CSS variable by following its reference chain.
157+ *
158+ * Phase 1: Follow var() references
159+ * - Use `seen` to detect cycles
160+ * - Use `cache` for memoization
161+ * - If value is concrete: cache and return
162+ * - If value is another var(--next, <fallback>):
163+ * push fallback to stack and continue with --next
164+ * - If variable unknown: proceed to Phase 2
165+ *
166+ * Phase 2: Try fallback values (if Phase 1 failed)
167+ * - Process fallbacks in reverse order (LIFO)
168+ * - Resolve each via resolveFallback()
169+ * - Return first successful resolution
170+ */
171+ while ( true ) {
172+ if ( seen . has ( currentVarName ) ) {
173+ break ;
174+ }
175+ seen . add ( currentVarName ) ;
176+
177+ if ( cache . has ( currentVarName ) ) {
178+ return cache . get ( currentVarName ) ;
179+ }
180+
181+ const valueNode = vars . get ( currentVarName ) ;
182+ if ( ! valueNode ) {
183+ break ;
184+ }
185+
186+ const valueText = sourceCode . getText ( valueNode ) . trim ( ) ;
187+ const parsed = parseVarFunction ( valueText ) ;
188+
189+ if ( ! parsed ) {
190+ cache . set ( currentVarName , valueText ) ;
191+ return valueText ;
192+ }
193+
194+ if ( parsed . fallbackText ) {
195+ fallbackStack . push ( parsed . fallbackText ) ;
196+ }
197+ currentVarName = parsed . name ;
198+ }
199+
200+ while ( fallbackStack . length > 0 ) {
201+ const fallbackText = fallbackStack . pop ( ) ;
202+ // eslint-disable-next-line no-use-before-define -- resolveFallback and resolveVariable are mutually recursive
203+ const resolvedFallback = resolveFallback (
204+ fallbackText ,
205+ cache ,
206+ seen ,
207+ ) ;
208+ if ( resolvedFallback !== null ) {
209+ return resolvedFallback ;
210+ }
211+ }
212+
213+ return null ;
214+ }
215+
216+ /**
217+ * Resolves a fallback text which can contain nested var() calls.
218+ * Returns the first resolvable value or null if none resolve.
219+ * @param {string } rawFallbackText
220+ * @param {Map<string, string> } cache Cache for memoization within a single resolution scope
221+ * @param {Set<string> } [seen] Set of already seen variables to detect cycles
222+ * @returns {string | null }
223+ */
224+ function resolveFallback ( rawFallbackText , cache , seen = new Set ( ) ) {
225+ const fallbackVarList = getVarFallbackList ( rawFallbackText ) ;
226+ if ( fallbackVarList . length === 0 ) {
227+ return rawFallbackText ;
228+ }
229+
230+ for ( const fallbackCandidate of fallbackVarList ) {
231+ if ( fallbackCandidate . startsWith ( "--" ) ) {
232+ const resolved = resolveVariable (
233+ fallbackCandidate ,
234+ cache ,
235+ seen ,
236+ ) ;
237+ if ( resolved !== null ) {
238+ return resolved ;
239+ }
240+ continue ;
241+ }
242+ return fallbackCandidate . trim ( ) ;
243+ }
244+
245+ return null ;
246+ }
247+
127248 return {
128249 "Rule > Block > Declaration" ( ) {
129250 replacements . push ( new Map ( ) ) ;
@@ -161,6 +282,12 @@ export default {
161282
162283 if ( usingVars ) {
163284 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 ( ) ;
164291 const valueNodes = node . value . children ;
165292
166293 // When `var()` is used, we store all the values to `valueList` with the replacement of `var()` with there values or fallback values
@@ -174,12 +301,28 @@ export default {
174301
175302 // If the variable is found, use its value, otherwise check for fallback values
176303 if ( varValue ) {
177- const varValueText = sourceCode
178- . getText ( varValue )
179- . trim ( ) ;
180-
181- valueList . push ( varValueText ) ;
182- valuesWithVarLocs . set ( varValueText , child . loc ) ;
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+ }
183326 } else {
184327 // If the variable is not found and doesn't have a fallback value, report it
185328 if ( child . children . length === 1 ) {
@@ -197,81 +340,27 @@ export default {
197340 } else {
198341 // If it has a fallback value, use that
199342 if ( child . children [ 2 ] . type === "Raw" ) {
200- const fallbackVarList =
201- getVarFallbackList (
202- child . children [ 2 ] . value . trim ( ) ,
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 ,
203350 ) ;
204- if ( fallbackVarList . length > 0 ) {
205- let gotFallbackVarValue = false ;
206-
207- for ( const fallbackVar of fallbackVarList ) {
208- if (
209- fallbackVar . startsWith ( "--" )
210- ) {
211- const fallbackVarValue =
212- vars . get ( fallbackVar ) ;
213-
214- if ( ! fallbackVarValue ) {
215- continue ; // Try the next fallback
216- }
217-
218- valueList . push (
219- sourceCode
220- . getText (
221- fallbackVarValue ,
222- )
223- . trim ( ) ,
224- ) ;
225- valuesWithVarLocs . set (
226- sourceCode
227- . getText (
228- fallbackVarValue ,
229- )
230- . trim ( ) ,
231- child . loc ,
232- ) ;
233- gotFallbackVarValue = true ;
234- break ; // Stop after finding the first valid variable
235- } else {
236- const fallbackValue =
237- fallbackVar . trim ( ) ;
238- valueList . push (
239- fallbackValue ,
240- ) ;
241- valuesWithVarLocs . set (
242- fallbackValue ,
243- child . loc ,
244- ) ;
245- gotFallbackVarValue = true ;
246- break ; // Stop after finding the first non-variable fallback
247- }
248- }
249-
250- // If none of the fallback value is defined then report an error
251- if (
252- ! allowUnknownVariables &&
253- ! gotFallbackVarValue
254- ) {
255- context . report ( {
256- loc : child . children [ 0 ] . loc ,
257- messageId : "unknownVar" ,
258- data : {
259- var : child . children [ 0 ]
260- . name ,
261- } ,
262- } ) ;
263-
264- return ;
265- }
266- } else {
267- // if it has a fallback value, use that
268- const fallbackValue =
269- child . children [ 2 ] . value . trim ( ) ;
270- valueList . push ( fallbackValue ) ;
271351 valuesWithVarLocs . set (
272- fallbackValue ,
352+ resolvedFallbackValue ,
273353 child . loc ,
274354 ) ;
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 ;
275364 }
276365 }
277366 }
0 commit comments