Skip to content

Implement variable scoping and shadowing support#14

Draft
nstepien wants to merge 14 commits intomainfrom
claude/add-scoping-tests-dg2FY
Draft

Implement variable scoping and shadowing support#14
nstepien wants to merge 14 commits intomainfrom
claude/add-scoping-tests-dg2FY

Conversation

@nstepien
Copy link
Copy Markdown
Owner

@nstepien nstepien commented Mar 7, 2026

Summary

This PR adds proper variable scoping and shadowing support to the CSS-in-JS plugin. Previously, the plugin used a flat map of identifiers, which didn't account for JavaScript's lexical scoping rules. Now it correctly resolves variables based on their scope chain, allowing inner scopes to shadow outer scope variables.

Key Changes

  • Scope Chain Implementation: Introduced a Scope interface that maintains a chain of scopes with parent references, enabling proper lexical scope resolution

    • Root scope tracks module-level declarations
    • New scopes are created for block statements (functions, blocks, etc.)
    • Scope chain is walked when resolving identifier values
  • Variable Resolution: Updated resolveValue() to walk up the scope chain instead of using a flat identifier map

    • Checks the current scope first, then parent scopes in order
    • Correctly resolves shadowed variables to their innermost declaration
  • Scope Tracking: Added visitor hooks for BlockStatement entry/exit to manage the current scope during AST traversal

  • Declaration Scope Association: Each CSS declaration now stores a reference to its scope, enabling correct variable resolution at code generation time

  • Export Handling: Modified export recording to only apply at module-level (root scope), preventing function-local declarations from being incorrectly exported

Implementation Details

  • The scope chain is maintained during AST traversal via currentScope variable
  • Block statements create new child scopes that are properly cleaned up on exit
  • Variable shadowing is naturally supported through the scope chain lookup mechanism
  • Imported identifiers are checked after the scope chain, maintaining proper precedence
  • Comprehensive test fixture (scoping.input.ts) covers 10 different scoping scenarios including nested functions, arrow functions, block scopes, and variable shadowing

Testing

Added extensive test coverage with a new fixture file containing 10 test cases covering:

  • Function scope shadowing
  • Nested function scopes (3 levels deep)
  • Arrow function scopes
  • Block scopes (if/for/plain blocks)
  • Import shadowing
  • CSS class name interpolations across scopes
  • var declarations (function-scoped)
  • Multiple simultaneous variable shadows
  • Sequential blocks with same variable names
  • Deeply nested mixed scope types

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB

claude added 2 commits March 7, 2026 04:43
The plugin was using a flat Map for local identifiers, causing inner
scope shadows (functions, blocks, arrows) to overwrite module-level
values. This led to incorrect CSS interpolation resolution.

Replace the flat map with a scope chain that tracks identifiers per
BlockStatement. Each CSS declaration captures its scope, and resolution
walks up the chain to find the nearest binding.

Add comprehensive scoping tests covering function shadows, nested
functions (3 levels), arrow functions, block scopes, import shadowing,
CSS class interpolation across scopes, var declarations, multiple
simultaneous shadows, and sequential blocks.

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
Scope handling has been implemented via BlockStatement-based scope
chain tracking.

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 7, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6540deb-c718-469e-ae1f-14b569924691

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/add-scoping-tests-dg2FY

Comment @coderabbitai help to get the list of available commands and usage tips.

claude and others added 12 commits March 7, 2026 13:02
Instead of importing from @oxc-project/types (not a direct dependency),
extract the type from rolldown's VisitorObject type.

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
…roject/types

The package is hoisted by npm as a transitive dependency of rolldown.

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
Track scopes for ForStatement, ForInStatement, ForOfStatement,
SwitchStatement, CatchClause, FunctionDeclaration, FunctionExpression,
and ArrowFunctionExpression in addition to BlockStatement.

This prevents incorrect resolution of shadowed variables in these
contexts (e.g. for-loop variables shadowing outer const declarations).

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
Major scope tracking improvements:

- Record function/arrow parameters in scope as unknown values so they
  correctly shadow outer variables (prevents incorrect resolution of
  css`` interpolations that reference parameters)
- Record catch clause parameters in scope for proper shadowing
- Record all VariableDeclarators regardless of init type (let x;,
  const x = fn()) so they shadow outer variables
- Merge parent scope with child BlockStatement to avoid redundant
  double scoping (function body, for body, catch body)
- Add StaticBlock scope to prevent class static block variables from
  leaking to module scope
- Change Scope.identifiers to Map<string, string | undefined> where
  undefined means "declared but value unknown at build time"

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
… Set

Use a Set<object> of body nodes instead of a boolean flag + boolean[]
stack. Parent nodes (functions, for-statements, catch) add their body
BlockStatement to the set, and the BlockStatement handler checks it
to skip redundant scope creation.

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
- Use Set<BlockStatement> instead of Set<object> with proper type import
- Check node.body.type === 'BlockStatement' consistently in all handlers
  (functions, arrow functions) instead of mixing !== null and !expression
- Refactor VariableDeclarator to switch pattern with early returns,
  falling through to shadow-only recording as the default

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
In strict mode (ESM), function and class declaration names create
bindings in the containing scope. Record these names so they properly
shadow outer variables:

- FunctionDeclaration: name recorded in parent scope (before pushScope)
- FunctionExpression: name recorded in function scope (after pushScope),
  since it's only visible inside the function body for recursion
- ClassDeclaration: name recorded in parent scope

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
Add recursive recordBindingPattern function that extracts all binding
identifiers from ObjectPattern, ArrayPattern, AssignmentPattern, and
RestElement nodes. This ensures destructuring declarations like
const [color] = arr, const { color } = obj, and destructured function
parameters properly shadow outer variables.

Also use recordBindingPattern for catch clause params (handling
destructured catch like catch ({ message })).

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
…scoping test

Much easier to read and understand the expected output at a glance,
and less brittle than matching individual patterns with regexps.

https://claude.ai/code/session_01Dbp3WX1HM1choWg55p9VhB
@nstepien nstepien self-assigned this Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants