@@ -20,12 +20,19 @@ import { visitorKeys } from "./css-visitor-keys.js";
2020//-----------------------------------------------------------------------------
2121
2222/**
23- * @import { CssNode, CssNodePlain, CssLocationRange, Comment, Lexer, StyleSheetPlain } from "@eslint/css-tree"
23+ * @import { CssNode, CssNodePlain, CssLocationRange, Comment, Lexer, StyleSheetPlain, DeclarationPlain, AtrulePlain, FunctionNodePlain, ValuePlain, Raw } from "@eslint/css-tree"
2424 * @import { SourceRange, FileProblem, DirectiveType, RulesConfig } from "@eslint/core"
2525 * @import { CSSSyntaxElement } from "../types.js"
2626 * @import { CSSLanguageOptions } from "./css-language.js"
2727 */
2828
29+ /**
30+ * @typedef {Object } CustomPropertyUses
31+ * @property {Array<DeclarationPlain> } declarations Declaration nodes where the custom property value is declared.
32+ * @property {Array<AtrulePlain> } definitions Atrule nodes where the custom property is defined using `@property`.
33+ * @property {Array<FunctionNodePlain> } references Function nodes (`var()`) where the custom property is used.
34+ */
35+
2936//-----------------------------------------------------------------------------
3037// Helpers
3138//-----------------------------------------------------------------------------
@@ -86,6 +93,18 @@ export class CSSSourceCode extends TextSourceCodeBase {
8693 */
8794 #inlineConfigComments;
8895
96+ /**
97+ * Map of custom property names to their uses.
98+ * @type {Map<string, CustomPropertyUses>|undefined }
99+ */
100+ #customProperties;
101+
102+ /**
103+ * Map of declarations to the var() functions they contain.
104+ * @type {WeakMap<CssNodePlain, Array<FunctionNodePlain>> }
105+ */
106+ #declarationVariables = new WeakMap ( ) ;
107+
89108 /**
90109 * The AST of the source code.
91110 * @type {StyleSheetPlain }
@@ -254,6 +273,22 @@ export class CSSSourceCode extends TextSourceCodeBase {
254273 return this . #parents. get ( node ) ;
255274 }
256275
276+ /**
277+ * Ensures the custom properties map entry exists for a given name.
278+ * @param {string } name The custom property name.
279+ * @returns {CustomPropertyUses } The uses object.
280+ */
281+ #ensureCustomProperty( name ) {
282+ if ( ! this . #customProperties. has ( name ) ) {
283+ this . #customProperties. set ( name , {
284+ declarations : [ ] ,
285+ definitions : [ ] ,
286+ references : [ ] ,
287+ } ) ;
288+ }
289+ return this . #customProperties. get ( name ) ;
290+ }
291+
257292 /**
258293 * Traverse the source code and return the steps that were taken.
259294 * @returns {Iterable<CSSTraversalStep> } The steps that were taken while traversing the source code.
@@ -266,13 +301,70 @@ export class CSSSourceCode extends TextSourceCodeBase {
266301
267302 /** @type {Array<CSSTraversalStep> } */
268303 const steps = ( this . #steps = [ ] ) ;
304+ this . #customProperties = new Map ( ) ;
269305
270306 // Note: We can't use `walk` from `css-tree` because it uses `CssNode` instead of `CssNodePlain`
271307
308+ /**
309+ * Stack of declaration nodes currently being visited.
310+ * Used to track which var() functions belong to which declaration.
311+ * @type {Array<CssNodePlain> }
312+ */
313+ const declStack = [ ] ;
314+
272315 const visit = ( node , parent ) => {
273316 // first set the parent
274317 this . #parents. set ( node , parent ) ;
275318
319+ // Track custom property declarations
320+ if ( node . type === "Declaration" && node . property . startsWith ( "--" ) ) {
321+ this . #ensureCustomProperty( node . property ) . declarations . push (
322+ node ,
323+ ) ;
324+ }
325+
326+ // Track @property definitions
327+ if ( node . type === "Atrule" && node . name === "property" ) {
328+ const identNode = node . prelude ?. children ?. [ 0 ] ;
329+ if (
330+ identNode ?. type === "Identifier" &&
331+ identNode . name . startsWith ( "--" )
332+ ) {
333+ this . #ensureCustomProperty( identNode . name ) . definitions . push (
334+ node ,
335+ ) ;
336+ }
337+ }
338+
339+ // Track var() references
340+ if ( node . type === "Function" && node . name . toLowerCase ( ) === "var" ) {
341+ const identNode = node . children ?. [ 0 ] ;
342+ if (
343+ identNode ?. type === "Identifier" &&
344+ identNode . name . startsWith ( "--" )
345+ ) {
346+ this . #ensureCustomProperty( identNode . name ) . references . push (
347+ node ,
348+ ) ;
349+ }
350+
351+ // Associate this var() with the current declaration
352+ if ( declStack . length > 0 ) {
353+ const currentDecl = declStack . at ( - 1 ) ;
354+ const vars = this . #declarationVariables. get ( currentDecl ) ;
355+ if ( vars ) {
356+ vars . push ( node ) ;
357+ }
358+ }
359+ }
360+
361+ // Track declaration context for getDeclarationVariables
362+ const isDeclaration = node . type === "Declaration" ;
363+ if ( isDeclaration ) {
364+ declStack . push ( node ) ;
365+ this . #declarationVariables. set ( node , [ ] ) ;
366+ }
367+
276368 // then add the step
277369 steps . push (
278370 new CSSTraversalStep ( {
@@ -297,6 +389,11 @@ export class CSSSourceCode extends TextSourceCodeBase {
297389 }
298390 }
299391
392+ // Pop declaration context
393+ if ( isDeclaration ) {
394+ declStack . pop ( ) ;
395+ }
396+
300397 // then add the exit step
301398 steps . push (
302399 new CSSTraversalStep ( {
@@ -311,4 +408,223 @@ export class CSSSourceCode extends TextSourceCodeBase {
311408
312409 return steps ;
313410 }
411+
412+ /**
413+ * Returns an array of `var()` function nodes used in the given declaration's value.
414+ * @param {DeclarationPlain } declaration The declaration node to inspect.
415+ * @returns {Array<FunctionNodePlain> } The `var()` function nodes found in the declaration.
416+ */
417+ getDeclarationVariables ( declaration ) {
418+ // Ensure traversal has happened
419+ if ( ! this . #customProperties) {
420+ this . traverse ( ) ;
421+ }
422+
423+ return this . #declarationVariables. get ( declaration ) ?? [ ] ;
424+ }
425+
426+ /**
427+ * Returns the closest computed value for a `var()` function node or a
428+ * custom property name.
429+ *
430+ * When called with a `var()` function node, the resolution order is:
431+ * 1. If the current rule block has one or more custom property declarations
432+ * for the variable, return the value of the last one.
433+ * 2. If the `var()` has a fallback value, return the fallback.
434+ * 3. If a previous rule had a custom property declaration, return the last value.
435+ * 4. If there's a `@property` with an `initial-value`, return the initial value.
436+ * 5. Otherwise, return `undefined`.
437+ *
438+ * When called with a custom property name string, returns the last declared
439+ * value for that property, or the `@property` initial-value if no
440+ * declarations exist.
441+ * @param {FunctionNodePlain|string } funcOrName The `var()` function node or custom property name.
442+ * @returns {ValuePlain|Raw|undefined } The closest value node, or `undefined`.
443+ */
444+ getClosestVariableValue ( funcOrName ) {
445+ // Ensure traversal has happened
446+ if ( ! this . #customProperties) {
447+ this . traverse ( ) ;
448+ }
449+
450+ // When called with a string name, return the last declaration value
451+ if ( typeof funcOrName === "string" ) {
452+ const uses = this . #customProperties. get ( funcOrName ) ;
453+
454+ if ( uses && uses . declarations . length > 0 ) {
455+ return uses . declarations . at ( - 1 ) . value ;
456+ }
457+
458+ // Fall back to @property initial-value
459+ if ( uses ) {
460+ for ( const definition of uses . definitions ) {
461+ const block = definition . block ;
462+ if ( block ?. children ) {
463+ for ( const child of block . children ) {
464+ if (
465+ child . type === "Declaration" &&
466+ child . property === "initial-value"
467+ ) {
468+ return child . value ;
469+ }
470+ }
471+ }
472+ }
473+ }
474+
475+ return undefined ;
476+ }
477+
478+ const func = funcOrName ;
479+ const identNode = func . children ?. [ 0 ] ;
480+ if ( ! identNode || identNode . type !== "Identifier" ) {
481+ return undefined ;
482+ }
483+
484+ const varName = identNode . name ;
485+ const uses = this . #customProperties. get ( varName ) ;
486+
487+ // Find the enclosing Rule node for this var() function
488+ let ruleBlock = null ;
489+ let ancestor = this . #parents. get ( func ) ;
490+ while ( ancestor ) {
491+ if ( ancestor . type === "Rule" ) {
492+ ruleBlock = ancestor ;
493+ break ;
494+ }
495+ ancestor = this . #parents. get ( ancestor ) ;
496+ }
497+
498+ // Step 1: Check current rule block for declarations
499+ if ( ruleBlock && uses ) {
500+ const blockDeclarations = uses . declarations . filter ( decl => {
501+ let parent = this . #parents. get ( decl ) ;
502+ while ( parent ) {
503+ if ( parent === ruleBlock ) {
504+ return true ;
505+ }
506+ if ( parent . type === "Rule" ) {
507+ return false ;
508+ }
509+ parent = this . #parents. get ( parent ) ;
510+ }
511+ return false ;
512+ } ) ;
513+
514+ if ( blockDeclarations . length > 0 ) {
515+ return blockDeclarations . at ( - 1 ) . value ;
516+ }
517+ }
518+
519+ // Step 2: Check fallback value
520+ if ( func . children . length >= 3 ) {
521+ const fallback = func . children [ 2 ] ;
522+ if ( fallback ) {
523+ return /** @type {Raw } */ ( fallback ) ;
524+ }
525+ }
526+
527+ // Step 3: Check declarations in previous rules
528+ if ( uses ) {
529+ const funcOffset = func . loc ?. start ?. offset ?? Infinity ;
530+ const previousDeclarations = uses . declarations . filter ( decl => {
531+ // Must not be in the same rule block (already checked)
532+ let parent = this . #parents. get ( decl ) ;
533+ while ( parent ) {
534+ if ( parent === ruleBlock ) {
535+ return false ;
536+ }
537+ if ( parent . type === "Rule" ) {
538+ break ;
539+ }
540+ parent = this . #parents. get ( parent ) ;
541+ }
542+ return ( decl . loc ?. start ?. offset ?? 0 ) < funcOffset ;
543+ } ) ;
544+
545+ if ( previousDeclarations . length > 0 ) {
546+ return previousDeclarations . at ( - 1 ) . value ;
547+ }
548+ }
549+
550+ // Step 4: Check @property initial-value
551+ if ( uses ) {
552+ for ( const definition of uses . definitions ) {
553+ const block = definition . block ;
554+ if ( block ?. children ) {
555+ for ( const child of block . children ) {
556+ if (
557+ child . type === "Declaration" &&
558+ child . property === "initial-value"
559+ ) {
560+ return child . value ;
561+ }
562+ }
563+ }
564+ }
565+ }
566+
567+ // Step 5: Return undefined
568+ return undefined ;
569+ }
570+
571+ /**
572+ * Returns all possible values for a `var()` function node.
573+ *
574+ * The returned array is composed of:
575+ * 1. If there's a `@property` with an `initial-value`, that value comes first.
576+ * 2. The values from custom property declarations throughout the file, in source order.
577+ * 3. The fallback value (if present) comes last.
578+ * @param {FunctionNodePlain } func The `var()` function node.
579+ * @returns {Array<ValuePlain|Raw> } Array of value nodes.
580+ */
581+ getVariableValues ( func ) {
582+ // Ensure traversal has happened
583+ if ( ! this . #customProperties) {
584+ this . traverse ( ) ;
585+ }
586+
587+ const identNode = func . children ?. [ 0 ] ;
588+ if ( ! identNode || identNode . type !== "Identifier" ) {
589+ return [ ] ;
590+ }
591+
592+ const varName = identNode . name ;
593+ const uses = this . #customProperties. get ( varName ) ;
594+
595+ /** @type {Array<ValuePlain|Raw> } */
596+ const values = [ ] ;
597+
598+ if ( uses ) {
599+ // Step 1: @property initial-value first
600+ for ( const definition of uses . definitions ) {
601+ const block = definition . block ;
602+ if ( block ?. children ) {
603+ for ( const child of block . children ) {
604+ if (
605+ child . type === "Declaration" &&
606+ child . property === "initial-value"
607+ ) {
608+ values . push ( child . value ) ;
609+ }
610+ }
611+ }
612+ }
613+
614+ // Step 2: All declarations in source order
615+ for ( const decl of uses . declarations ) {
616+ values . push ( decl . value ) ;
617+ }
618+ }
619+
620+ // Step 3: Fallback value last
621+ if ( func . children . length >= 3 ) {
622+ const fallback = func . children [ 2 ] ;
623+ if ( fallback ) {
624+ values . push ( /** @type {Raw } */ ( fallback ) ) ;
625+ }
626+ }
627+
628+ return values ;
629+ }
314630}
0 commit comments