Skip to content

feat: track CSS custom properties on CSSSourceCode#411

Open
Copilot wants to merge 5 commits intomainfrom
copilot/track-css-variables-sourcecode
Open

feat: track CSS custom properties on CSSSourceCode#411
Copilot wants to merge 5 commits intomainfrom
copilot/track-css-variables-sourcecode

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 27, 2026

Implements the CSS variable tracking RFC, centralizing custom property tracking on CSSSourceCode so rules don't duplicate this logic. Also fixes variable hoisting — var() references before their declaration now resolve correctly.

CSSSourceCode — new API

Three new public methods backed by private #customProperties Map and #declarationVariables WeakMap, populated during traverse():

  • getDeclarationVariables(declaration) — returns var() function nodes used in a declaration's value, including nested var() in fallback positions (e.g., var(--a, var(--b)))
  • getClosestVariableValue(func) — returns the closest value for a var() node. Resolution priority per RFC: same-block declaration → fallback → other-block declarations (hoisted) → @property initial-value → undefined
  • getVariableValues(func) — returns all known values: @property initial-value, then declarations in source order, then fallback

Nested var() calls in fallback positions are handled by #extractNestedVarRefs, which re-parses Raw fallback text nodes (the CSS parser represents fallbacks as raw text) and tracks any nested var() references with correctly adjusted source locations.

// Rule usage example
const closestValue = sourceCode.getClosestVariableValue(varFuncNode);
const allValues = sourceCode.getVariableValues(varFuncNode);
const varsInDecl = sourceCode.getDeclarationVariables(declNode);

no-invalid-properties — refactored to use new API

  • Uses getClosestVariableValue() for primary resolution instead of maintaining its own variable map
  • Populates internal vars map incrementally via rule visitor methods; hoisting is handled by getClosestVariableValue() which has access to all declarations collected during CSSSourceCode.traverse()
  • Falls back to direct chain resolution when closest value contains unresolvable nested var() refs
  • 3 test cases move from invalid → valid: when a var() has a valid fallback, the fallback is now preferred over an invalid value from another selector block

Tests

  • 21 tests covering all three CSSSourceCode methods (same-block, fallback priority, hoisting, @property, multiple declarations, cross-declaration isolation, nested var() in fallback positions including deep nesting)
  • 2 hoisting tests for no-invalid-properties

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@github-project-automation github-project-automation bot moved this to Needs Triage in Triage Mar 27, 2026
Copilot AI linked an issue Mar 27, 2026 that may be closed by this pull request
1 task
@eslint-github-bot
Copy link
Copy Markdown

Hi @copilot!, thanks for the Pull Request

The pull request title isn't properly formatted. We ask that you update the pull request title to match this format, as we use it to generate changelogs and automate releases.

  • The commit message tag wasn't recognized. Did you mean "docs", "fix", or "feat"?
  • There should be a space following the initial tag and colon, for example 'feat: Message'.
  • The first letter of the tag should be in lowercase

To Fix: You can fix this problem by clicking 'Edit' next to the pull request title at the top of this page.

Read more about contributing to ESLint here

1 similar comment
@eslint-github-bot
Copy link
Copy Markdown

Hi @copilot!, thanks for the Pull Request

The pull request title isn't properly formatted. We ask that you update the pull request title to match this format, as we use it to generate changelogs and automate releases.

  • The commit message tag wasn't recognized. Did you mean "docs", "fix", or "feat"?
  • There should be a space following the initial tag and colon, for example 'feat: Message'.
  • The first letter of the tag should be in lowercase

To Fix: You can fix this problem by clicking 'Edit' next to the pull request title at the top of this page.

Read more about contributing to ESLint here

…no-invalid-properties

Implements the RFC for CSS custom property tracking on CSSSourceCode:
- Add #customProperties map and #declarationVariables WeakMap
- Populate during traverse() to track declarations, @Property definitions, and var() references
- Add getDeclarationVariables(declaration) method
- Add getClosestVariableValue(func) method
- Add getVariableValues(func) method

Refactor no-invalid-properties rule to use the new API:
- Use getClosestVariableValue() for primary variable resolution
- Pre-populate vars map from AST to support variable hoisting (fixes #199)
- Fallback values take priority over other-block declarations per RFC

Add comprehensive tests for all new CSSSourceCode methods.

Agent-Logs-Url: https://github.com/eslint/css/sessions/e41fbbf4-0081-469f-8248-1e854213b08c

Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com>
@eslint-github-bot
Copy link
Copy Markdown

Hi @copilot!, thanks for the Pull Request

The pull request title isn't properly formatted. We ask that you update the pull request title to match this format, as we use it to generate changelogs and automate releases.

  • The commit message tag wasn't recognized. Did you mean "docs", "fix", or "feat"?
  • There should be a space following the initial tag and colon, for example 'feat: Message'.
  • The first letter of the tag should be in lowercase

To Fix: You can fix this problem by clicking 'Edit' next to the pull request title at the top of this page.

Read more about contributing to ESLint here

2 similar comments
@eslint-github-bot
Copy link
Copy Markdown

Hi @copilot!, thanks for the Pull Request

The pull request title isn't properly formatted. We ask that you update the pull request title to match this format, as we use it to generate changelogs and automate releases.

  • The commit message tag wasn't recognized. Did you mean "docs", "fix", or "feat"?
  • There should be a space following the initial tag and colon, for example 'feat: Message'.
  • The first letter of the tag should be in lowercase

To Fix: You can fix this problem by clicking 'Edit' next to the pull request title at the top of this page.

Read more about contributing to ESLint here

@eslint-github-bot
Copy link
Copy Markdown

Hi @copilot!, thanks for the Pull Request

The pull request title isn't properly formatted. We ask that you update the pull request title to match this format, as we use it to generate changelogs and automate releases.

  • The commit message tag wasn't recognized. Did you mean "docs", "fix", or "feat"?
  • There should be a space following the initial tag and colon, for example 'feat: Message'.
  • The first letter of the tag should be in lowercase

To Fix: You can fix this problem by clicking 'Edit' next to the pull request title at the top of this page.

Read more about contributing to ESLint here

Copilot AI changed the title [WIP] Add tracking for CSS custom properties in SourceCode feat: track CSS custom properties on CSSSourceCode Mar 27, 2026
Copilot AI requested a review from nzakas March 27, 2026 21:07
* @param {Function} visitor The visitor function to call for each node.
* @returns {void}
*/
function walkAST(node, visitor) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot the rule itself is doing a depth-first traversal of the AST. Doing it again is inefficient. Remove the extra traversal and use the rule methods to do any traversing that is necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the walkAST function and StyleSheet() visitor. The vars map is now populated incrementally in "Rule > Block Declaration:exit" as the rule traverses. Hoisting is handled by sourceCode.getClosestVariableValue() which has access to all declarations collected during CSSSourceCode.traverse(). Commit: 63d930c

…le visitor methods instead

Agent-Logs-Url: https://github.com/eslint/css/sessions/fa674c27-bfb3-46b4-8c86-407a2fef55c5

Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com>
Copilot AI requested a review from nzakas March 30, 2026 20:31
Copy link
Copy Markdown
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Would like a second review before merging.

@nzakas nzakas marked this pull request as ready for review April 1, 2026 21:08
@nzakas
Copy link
Copy Markdown
Member

nzakas commented Apr 1, 2026

@copilot run ESLint and prettier and fix any problems so the CI passes.

@nzakas nzakas moved this from Needs Triage to Second Review Needed in Triage Apr 1, 2026
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot run ESLint and prettier and fix any problems so the CI passes.

Fixed in 735ebbe. Ran both ESLint (clean) and Prettier (3 files had formatting issues in css-source-code.js, no-invalid-properties.js, and css-source-code.test.js). All 1021 tests still pass.

Copilot AI requested a review from nzakas April 1, 2026 21:12
@lumirlumir lumirlumir added the accepted There is consensus among the team that this change meets the criteria for inclusion label Apr 2, 2026
Comment on lines +937 to +951
/**
* Helper to create a CSSSourceCode from CSS text and trigger traversal.
* @param {string} css The CSS text.
* @returns {import("../../src/languages/css-source-code.js").CSSSourceCode} The source code instance.
*/
function createSourceCode(css) {
const sourceCode = new CSSSourceCode({
text: css,
ast: toPlainObject(parse(css, { positions: true })),
});

// trigger traversal to populate custom properties data
sourceCode.traverse();
return sourceCode;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be clearer to move this helper to the top level, since the same test helper is repeated three times across the newly added describe test blocks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved both createSourceCode and findVarFunc to the top level, shared across all three describe blocks. Commit: 051fea2

}

// Track var() references
if (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also handle nested var() calls in fallback positions, such as var(--a, var(--b))?

It seems that no-invalid-properties already has custom logic for resolving nested fallback var() references. If this RFC is meant to centralize that behavior in CSSSourceCode, I think this case need to be covered as well. Otherwise, rules may still need their own fallback parsing logic.

Just for reference, according to MDN, the following syntax is valid:

https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/var#syntax

var(--custom-prop, var(--default-value));
var(--custom-prop, var(--default-value, red));

When I tested it against a nested var() function, it failed:

		it("should return nested var() function nodes", () => {
			const css = "a { color: var(--my-color-1, var(--my-color-2)); }";
			const sourceCode = createSourceCode(css);
			const decl = sourceCode.ast.children[0].block.children[0];
			const vars = sourceCode.getDeclarationVariables(decl);

			console.log(decl.value.children[0]);
			console.log(vars);

			assert.strictEqual(vars.length, 2);
			assert.strictEqual(vars[0].children[0].name, "--my-color-1");
			assert.strictEqual(vars[1].children[0].name, "--my-color-2");
		});

The stdout is logged as follows:

{
  type: 'Function',
  loc: {
    source: '<unknown>',
    start: { offset: 11, line: 1, column: 12 },
    end: { offset: 47, line: 1, column: 48 }
  },
  name: 'var',
  children: [
    { type: 'Identifier', loc: [Object], name: '--my-color-1' },
    { type: 'Operator', loc: [Object], value: ',' },
    { type: 'Raw', loc: [Object], value: ' var(--my-color-2)' }
  ]
}
[
  {
    type: 'Function',
    loc: { source: '<unknown>', start: [Object], end: [Object] },
    name: 'var',
    children: [ [Object], [Object], [Object] ]
  }
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added #extractNestedVarRefs private method that re-parses Raw fallback text using the CSS parser and tracks any nested var() references found. It handles arbitrary nesting depth by recursing. Location info is adjusted to map back to the original source positions via adjustLoc/adjustAllLocs helpers. Two new tests cover single-level and 3-level deep nesting. Commit: 051fea2

@nzakas
Copy link
Copy Markdown
Member

nzakas commented Apr 8, 2026

@copilot Address @lumirlumir's feedback.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 8, 2026

@copilot Address @lumirlumir's feedback.

Both items addressed in 051fea2 — test helpers moved to top level, and nested var() in fallback positions now tracked via #extractNestedVarRefs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accepted There is consensus among the team that this change meets the criteria for inclusion feature

Projects

Status: Second Review Needed

Development

Successfully merging this pull request may close these issues.

Change Request: Track variables on SourceCode

3 participants