Skip to main content

fallow_extract/css_in_js/
tokens.rs

1//! CSS-in-JS design-token DEFINITION walker for the design-token blast-radius
2//! (CSS program Phase 3d).
3//!
4//! The zero-runtime CSS-in-JS libraries declare design tokens as a JS OBJECT
5//! passed to a library call, binding the token surface to an exported identifier
6//! that consumers read via member access (`import { vars } from './tokens';
7//! vars.color.primary`). This module is the DEFINITION half of the token
8//! blast-radius: it parses JS/TS with oxc, gates recognition on import-binding
9//! provenance (reusing the sibling `object::module_library`), and for each
10//! recognized token-definition call emits the access BINDING plus the flattened
11//! dotted LEAF token paths (with each leaf's source line). The CONSUMER half (who
12//! reads `vars.color.primary` across modules) is resolved in the analyze layer
13//! against the module graph; this walker only produces the defined-token side.
14//!
15//! Health-time-only, like the 3b/3c CSS-in-JS lifters: it runs over file SOURCE
16//! and persists nothing to the extraction cache (no `CACHE_VERSION` bump).
17//!
18//! # Recognized definition shapes
19//!
20//! Recognition is gated on the callee binding being imported from a recognized
21//! token library in THIS file (a local `defineVars` helper or an unrelated
22//! `createTheme` never fires):
23//!
24//! - StyleX `stylex.defineVars({...})` (namespace member call) or
25//!   `defineVars({...})` (named import). Binding = the assigned identifier and
26//!   every top-level key is one token, including conditional object values.
27//! - StyleX `unstable_defineVarsNested({...})`: binding = the assigned identifier;
28//!   namespace objects recurse while conditional objects and calls remain leaves.
29//! - vanilla-extract `createThemeContract({...})`: binding = the assigned
30//!   identifier (the contract IS the vars surface consumers read).
31//! - vanilla-extract `createTheme({...})` (1-arg): returns `[themeClass, vars]`;
32//!   binding = the SECOND array-destructure element (`vars`); `themeClass` is a
33//!   class string, not a token surface.
34//! - vanilla-extract `createGlobalTheme(selector, {...})` (2-arg): returns the
35//!   vars object; binding = the assigned identifier.
36//! - PandaCSS `defineTokens({...})`: binding = the assigned identifier; token
37//!   objects with a `value` field collapse to the token path (`colors.brand`),
38//!   matching `token('colors.brand')` consumers.
39//! - PandaCSS `defineConfig({ theme: { tokens, semanticTokens } })`: binding =
40//!   `pandaConfig`; only static token object literals are read.
41//!
42//! The two CONTRACT-IMPLEMENTATION forms are deliberately NOT definition sites
43//! here, because the contract they fill was already declared by
44//! `createThemeContract` (captured above) and that is the binding consumers read:
45//! - `createTheme(contract, {...})` (2-arg) returns a class string; tokens fill
46//!   the existing `contract`.
47//! - `createGlobalTheme(selector, contract, {...})` (3-arg) returns void.
48//!
49use std::path::Path;
50
51use oxc_ast::{
52    AstKind,
53    ast::{
54        Argument, ArrowFunctionExpression, AssignmentExpression, AssignmentTarget,
55        AssignmentTargetMaybeDefault, AssignmentTargetProperty, BindingPattern, BlockStatement,
56        CallExpression, ComputedMemberExpression, Declaration, Expression, Function,
57        IdentifierReference, ImportDeclarationSpecifier, NumericLiteral, ObjectExpression,
58        ObjectPropertyKind, Program, SimpleAssignmentTarget, Statement, StaticMemberExpression,
59        UnaryExpression, UnaryOperator, UpdateExpression, VariableDeclarationKind,
60        VariableDeclarator,
61    },
62};
63use oxc_ast_visit::{Visit, walk};
64use oxc_semantic::{ReferenceId, ScopeFlags, Scoping, SemanticBuilder, SymbolId};
65use oxc_span::{GetSpan, SourceType, Span};
66use rustc_hash::{FxHashMap, FxHashSet};
67
68use super::object::{Lib, module_library};
69use crate::js_text::JsText;
70
71const PANDA_CONFIG_BINDING: &str = "pandaConfig";
72
73/// A single defined design token: its dotted LEAF path relative to the access
74/// binding (`color.primary`, or flat `primaryColor` for StyleX), the 1-based
75/// source line of its key, and the static value when the literal is recoverable.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct CssInJsToken {
78    /// Dotted leaf path relative to the binding (e.g. `color.primary`).
79    pub path: String,
80    /// 1-based line of the token's key in the defining source.
81    pub def_line: u32,
82    /// Static token value for literal definitions. Dynamic expressions and
83    /// contract-only leaves have no value.
84    pub value: Option<String>,
85}
86
87/// A CSS-in-JS token-definition site: the exported access binding consumers read
88/// through (e.g. `vars`) and the flattened leaf tokens it defines.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct CssInJsTokenDef {
91    /// The identifier the token surface is bound to (`vars`), the receiver of
92    /// cross-module member access (`vars.color.primary`).
93    pub binding: String,
94    /// Which CSS-in-JS family defined the tokens.
95    pub origin: CssInJsTokenOrigin,
96    /// The flattened leaf tokens defined on `binding`.
97    pub tokens: Vec<CssInJsToken>,
98}
99
100/// The CSS-in-JS token system that produced a token definition.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum CssInJsTokenOrigin {
103    /// StyleX `defineVars`.
104    StyleX,
105    /// vanilla-extract `createTheme` family definitions.
106    VanillaExtract,
107    /// PandaCSS `defineTokens`.
108    Panda,
109    /// styled-components / Emotion theme object definitions.
110    Theme,
111}
112
113/// Walk a JS/TS source for CSS-in-JS design-token DEFINITIONS, returning each
114/// access binding and its flattened leaf token paths. Empty when the source has
115/// no recognized token-library import (provenance gate closed).
116#[must_use]
117pub fn css_in_js_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
118    let source_type = SourceType::from_path(path).unwrap_or_default();
119    crate::parse_bounded_or_default(source, source_type, |ret| {
120        let mut collector = TokenDefCollector::new(source);
121        collector.build_import_map(&ret.program);
122        if collector.imports.is_empty() {
123            return Vec::new();
124        }
125        let semantic_return = SemanticBuilder::new().build(&ret.program);
126        collector.build_const_object_map(&ret.program, semantic_return.semantic.scoping());
127        collector.collecting_mutations = true;
128        collector.visit_program(&ret.program);
129        collector.collecting_mutations = false;
130        collector.visit_program(&ret.program);
131        collector.defs
132    })
133}
134
135/// One located consumer of a CSS-in-JS token: the defined LEAF token path it
136/// reads (relative to the binding, e.g. `color.primary`) and the 1-based line of
137/// the member-access site.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct TokenConsumerHit {
140    /// The defined leaf token path consumed (`color.primary`), relative to the
141    /// access binding (the leading binding segment stripped).
142    pub token_path: String,
143    /// 1-based line of the member-access site in the consuming source.
144    pub line: u32,
145}
146
147/// Walk a JS/TS source for statically-authored theme object definitions used by
148/// styled-components and Emotion. A `theme` or `*Theme` variable with an object
149/// literal initializer becomes a token surface, with nested scalar leaves exposed
150/// as dotted paths.
151#[must_use]
152pub fn css_in_js_theme_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
153    let source_type = SourceType::from_path(path).unwrap_or_default();
154    crate::parse_bounded_or_default(source, source_type, |ret| {
155        let mut collector = ThemeDefCollector {
156            lines: LineCounter::new(source),
157            defs: Vec::new(),
158        };
159        collector.visit_program(&ret.program);
160        collector.defs
161    })
162}
163
164/// One attribution query to run against a single parsed consumer source. A scan
165/// runs any mix of queries against ONE parse of the source.
166pub enum ConsumerQuery<'a> {
167    /// Member-access reads `<alias>.a.b` of an imported token binding. A read is
168    /// a hit only when `a.b` is a defined leaf path, not an intermediate group.
169    MemberBinding {
170        /// The local identifier the token binding was imported under.
171        alias: &'a str,
172        /// The defined leaf token paths (`color.primary`).
173        leaf_paths: &'a FxHashSet<String>,
174    },
175    /// StyleX `createTheme(contract, ...)` applies the complete resolved
176    /// variable group, including partial and empty reset themes.
177    StyleXThemeGroup {
178        /// Local identifier of the resolved StyleX variable contract.
179        contract_alias: &'a str,
180        /// Every defined leaf in that contract group.
181        leaf_paths: &'a FxHashSet<String>,
182    },
183    /// PandaCSS `token('a.b')` calls through the given alias.
184    PandaTokenCall {
185        /// The local alias imported from Panda's generated token module.
186        alias: &'a str,
187        /// The defined leaf token paths (`colors.brand`).
188        leaf_paths: &'a FxHashSet<String>,
189    },
190    /// PandaCSS style-call object values naming token paths.
191    PandaStyleValues {
192        /// The local aliases for Panda style calls (`css`, `cva`).
193        aliases: &'a FxHashSet<String>,
194        /// The defined leaf token paths (`colors.brand`).
195        leaf_paths: &'a FxHashSet<String>,
196    },
197    /// styled-components / Emotion theme reads (`theme.colors.x`), including
198    /// `props.theme.colors.x`.
199    ThemeReads {
200        /// The defined leaf token paths (`colors.brand`).
201        leaf_paths: &'a FxHashSet<String>,
202    },
203}
204
205impl ConsumerQuery<'_> {
206    /// `true` for the queries that `BatchedBindingCollector` runs in one walk.
207    const fn is_batched(&self) -> bool {
208        matches!(
209            self,
210            Self::MemberBinding { .. } | Self::StyleXThemeGroup { .. }
211        )
212    }
213}
214
215/// Parse `source` once and run every query against the same AST, returning
216/// `(query_index, hit)` pairs so the caller can attribute each hit back to the
217/// definer that produced its query. A query with an empty alias or an empty
218/// leaf set contributes no hits and does not suppress the other queries.
219#[must_use]
220pub fn css_in_js_consumer_scan(
221    source: &str,
222    path: &Path,
223    queries: &[ConsumerQuery<'_>],
224) -> Vec<(usize, TokenConsumerHit)> {
225    if queries.is_empty() {
226        return Vec::new();
227    }
228    let source_type = SourceType::from_path(path).unwrap_or_default();
229    crate::parse_bounded_or_default(source, source_type, |ret| {
230        let mut out = Vec::new();
231        if queries.iter().any(ConsumerQuery::is_batched) {
232            let mut collector = BatchedBindingCollector {
233                lines: LineCounter::new(source),
234                queries,
235                namespaces: FxHashSet::default(),
236                theme_functions: FxHashSet::default(),
237                stylex_imports: FxHashMap::default(),
238                member_queries: FxHashMap::default(),
239                theme_queries: FxHashMap::default(),
240                root_reference_spans: FxHashSet::default(),
241                static_values: FxHashMap::default(),
242                static_alias_neighbors: FxHashMap::default(),
243                collecting_mutations: false,
244                hits: Vec::new(),
245            };
246            collector.build_import_map(&ret.program);
247            collector.build_query_indexes();
248            collector.build_static_value_map(&ret.program);
249            collector.build_root_reference_spans(&ret.program);
250            collector.collecting_mutations = true;
251            collector.visit_program(&ret.program);
252            collector.collecting_mutations = false;
253            collector.visit_program(&ret.program);
254            out.extend(collector.hits);
255        }
256        for (idx, query) in queries.iter().enumerate() {
257            if !query.is_batched() {
258                run_consumer_query(query, source, &ret.program, idx, &mut out);
259            }
260        }
261        out
262    })
263}
264
265struct BatchedBindingCollector<'a, 'q, 'v> {
266    lines: LineCounter<'a>,
267    queries: &'q [ConsumerQuery<'v>],
268    namespaces: FxHashSet<&'a str>,
269    theme_functions: FxHashSet<&'a str>,
270    stylex_imports: FxHashMap<&'a str, (Lib, &'a str)>,
271    member_queries: FxHashMap<&'v str, Vec<(usize, &'v FxHashSet<String>)>>,
272    theme_queries: FxHashMap<&'v str, Vec<(usize, &'v FxHashSet<String>)>>,
273    root_reference_spans: FxHashSet<Span>,
274    static_values: FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
275    static_alias_neighbors: FxHashMap<&'a str, FxHashSet<&'a str>>,
276    collecting_mutations: bool,
277    hits: Vec<(usize, TokenConsumerHit)>,
278}
279
280impl<'a> BatchedBindingCollector<'a, '_, '_> {
281    fn build_import_map(&mut self, program: &'a Program<'a>) {
282        for stmt in &program.body {
283            let Statement::ImportDeclaration(decl) = stmt else {
284                continue;
285            };
286            if decl.import_kind.is_type()
287                || module_library(&decl.source.value.text()) != Some(Lib::StyleX)
288            {
289                continue;
290            }
291            let Some(specifiers) = &decl.specifiers else {
292                continue;
293            };
294            for specifier in specifiers {
295                match specifier {
296                    ImportDeclarationSpecifier::ImportSpecifier(specifier)
297                        if !specifier.import_kind.is_type() =>
298                    {
299                        let local = specifier.local.name.as_str();
300                        let role = specifier.imported.name().as_str();
301                        self.stylex_imports.insert(local, (Lib::StyleX, role));
302                        if matches!(role, "createTheme" | "unstable_createThemeNested") {
303                            self.theme_functions.insert(local);
304                        }
305                    }
306                    ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
307                        let local = specifier.local.name.as_str();
308                        self.namespaces.insert(local);
309                        self.stylex_imports.insert(local, (Lib::StyleX, local));
310                    }
311                    ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
312                        let local = specifier.local.name.as_str();
313                        self.namespaces.insert(local);
314                        self.stylex_imports.insert(local, (Lib::StyleX, local));
315                    }
316                    ImportDeclarationSpecifier::ImportSpecifier(_) => {}
317                }
318            }
319        }
320    }
321
322    fn build_query_indexes(&mut self) {
323        for (idx, query) in self.queries.iter().enumerate() {
324            match query {
325                ConsumerQuery::MemberBinding { alias, leaf_paths } if !alias.is_empty() => {
326                    self.member_queries
327                        .entry(alias)
328                        .or_default()
329                        .push((idx, leaf_paths));
330                }
331                ConsumerQuery::StyleXThemeGroup {
332                    contract_alias,
333                    leaf_paths,
334                } if !contract_alias.is_empty() => {
335                    self.theme_queries
336                        .entry(contract_alias)
337                        .or_default()
338                        .push((idx, leaf_paths));
339                }
340                _ => {}
341            }
342        }
343    }
344
345    fn build_root_reference_spans(&mut self, program: &'a Program<'a>) {
346        let mut names: FxHashSet<&str> = self.member_queries.keys().copied().collect();
347        names.extend(self.theme_queries.keys().copied());
348        names.extend(self.namespaces.iter().copied());
349        names.extend(self.theme_functions.iter().copied());
350        names.extend(self.stylex_imports.keys().copied());
351        names.extend(self.static_values.keys().copied());
352
353        let semantic_return = SemanticBuilder::new().with_build_nodes(true).build(program);
354        let semantic = semantic_return.semantic;
355        let scoping = semantic.scoping();
356        let root_scope = scoping.root_scope_id();
357        for name in names {
358            if let Some(symbol_id) = scoping.get_binding(root_scope, oxc_str::Ident::from(name)) {
359                for reference in scoping.get_resolved_references(symbol_id) {
360                    if let AstKind::IdentifierReference(identifier) =
361                        semantic.nodes().kind(reference.node_id())
362                    {
363                        self.root_reference_spans.insert(identifier.span);
364                    }
365                }
366            }
367            if let Some(reference_ids) = scoping.root_unresolved_references().get(name) {
368                for reference_id in reference_ids {
369                    let reference = scoping.get_reference(*reference_id);
370                    if let AstKind::IdentifierReference(identifier) =
371                        semantic.nodes().kind(reference.node_id())
372                    {
373                        self.root_reference_spans.insert(identifier.span);
374                    }
375                }
376            }
377        }
378        for name in ["String", "Number", "Math", "Object", "Array"] {
379            if let Some(reference_ids) = scoping.root_unresolved_references().get(name) {
380                for reference_id in reference_ids {
381                    let reference = scoping.get_reference(*reference_id);
382                    if let AstKind::IdentifierReference(identifier) =
383                        semantic.nodes().kind(reference.node_id())
384                    {
385                        self.root_reference_spans.insert(identifier.span);
386                    }
387                }
388            }
389        }
390    }
391
392    fn build_static_value_map(&mut self, program: &'a Program<'a>) {
393        for stmt in &program.body {
394            let declaration = match stmt {
395                Statement::VariableDeclaration(declaration) => Some(&**declaration),
396                Statement::ExportDeclaration(export) => match &export.declaration {
397                    Declaration::VariableDeclaration(declaration) => Some(&**declaration),
398                    _ => None,
399                },
400                _ => None,
401            };
402            let Some(declaration) = declaration else {
403                continue;
404            };
405            if declaration.kind != VariableDeclarationKind::Const {
406                continue;
407            }
408            for declarator in &declaration.declarations {
409                let BindingPattern::BindingIdentifier(binding) = &declarator.id else {
410                    continue;
411                };
412                if let Some(init) = &declarator.init {
413                    self.static_values
414                        .insert(binding.name.as_str(), (declarator.span.start, init));
415                    if let Some(alias) = expression_root_binding(init) {
416                        self.static_alias_neighbors
417                            .entry(binding.name.as_str())
418                            .or_default()
419                            .insert(alias.name);
420                        self.static_alias_neighbors
421                            .entry(alias.name)
422                            .or_default()
423                            .insert(binding.name.as_str());
424                    }
425                }
426            }
427        }
428    }
429
430    fn is_theme_callee(&self, callee: &Expression<'a>) -> bool {
431        match callee {
432            Expression::Identifier(id) => {
433                self.root_reference_spans.contains(&id.span)
434                    && self.theme_functions.contains(id.name.as_str())
435            }
436            Expression::StaticMemberExpression(member) => {
437                let Expression::Identifier(object) = unwrap_transparent_expression(&member.object)
438                else {
439                    return false;
440                };
441                self.root_reference_spans.contains(&object.span)
442                    && self.namespaces.contains(object.name.as_str())
443                    && matches!(
444                        member.property.name.as_str(),
445                        "createTheme" | "unstable_createThemeNested"
446                    )
447            }
448            _ => false,
449        }
450    }
451
452    fn record_member(&mut self, chain: Option<(&str, Span, Vec<String>)>, span_start: u32) {
453        let Some((base, base_span, segments)) = chain else {
454            return;
455        };
456        if segments.is_empty() || !self.root_reference_spans.contains(&base_span) {
457            return;
458        }
459        let token_path = segments.join(".");
460        if let Some(queries) = self.member_queries.get(base) {
461            for &(idx, leaf_paths) in queries {
462                if !leaf_paths.contains(&token_path) {
463                    continue;
464                }
465                let line = self.lines.line_at(span_start);
466                self.hits.push((
467                    idx,
468                    TokenConsumerHit {
469                        token_path: token_path.clone(),
470                        line,
471                    },
472                ));
473            }
474        }
475    }
476
477    fn invalidate_static_value(&mut self, binding: Option<RootBinding<'_>>) {
478        if let Some(binding) = binding
479            && self.root_reference_spans.contains(&binding.span)
480        {
481            let mut pending = vec![binding.name];
482            let mut invalidated = FxHashSet::default();
483            while let Some(name) = pending.pop() {
484                if !invalidated.insert(name) {
485                    continue;
486                }
487                self.static_values.remove(name);
488                if let Some(neighbors) = self.static_alias_neighbors.get(name) {
489                    pending.extend(neighbors.iter().copied());
490                }
491            }
492        }
493    }
494
495    fn mutation_root_binding(
496        &self,
497        expression: &'a Expression<'a>,
498        before: u32,
499        visiting: &mut FxHashSet<&'a str>,
500    ) -> Option<RootBinding<'a>> {
501        if let Some(binding) = expression_root_binding(expression) {
502            return Some(binding);
503        }
504        let Expression::CallExpression(call) = unwrap_transparent_expression(expression) else {
505            return None;
506        };
507        let Expression::Identifier(callee) = unwrap_transparent_expression(&call.callee) else {
508            return None;
509        };
510        if !self.root_reference_spans.contains(&callee.span) {
511            return None;
512        }
513        let name = callee.name.as_str();
514        let &(declaration_start, value) = self.static_values.get(name)?;
515        let Expression::ArrowFunctionExpression(arrow) = unwrap_transparent_expression(value)
516        else {
517            return None;
518        };
519        if declaration_start >= before
520            || arrow.r#async
521            || arrow.params.rest.is_some()
522            || arrow.params.items.len() != call.arguments.len()
523            || !visiting.insert(name)
524        {
525            return None;
526        }
527        let Some(body) = stylex_arrow_expression_body(arrow) else {
528            visiting.remove(name);
529            return None;
530        };
531        let resolved = expression_root_binding(body)
532            .and_then(|body_root| {
533                arrow.params.items.iter().position(|parameter| {
534                    matches!(
535                        &parameter.pattern,
536                        BindingPattern::BindingIdentifier(binding)
537                            if binding.name.as_str() == body_root.name
538                    )
539                })
540            })
541            .and_then(|index| call.arguments.get(index))
542            .and_then(Argument::as_expression)
543            .and_then(|argument| self.mutation_root_binding(argument, before, visiting))
544            .or_else(|| self.mutation_root_binding(body, declaration_start, visiting));
545        visiting.remove(name);
546        resolved
547    }
548
549    fn binding_is_definitely_primitive(&self, binding: RootBinding<'_>, before: u32) -> bool {
550        let Some(&(declaration_start, value)) = self.static_values.get(binding.name) else {
551            return false;
552        };
553        declaration_start < before
554            && is_definitely_static_primitive(
555                value,
556                declaration_start,
557                &self.static_values,
558                &self.root_reference_spans,
559                &mut FxHashSet::default(),
560            )
561    }
562
563    fn record_theme_call(&mut self, call: &CallExpression<'a>) {
564        if call.arguments.len() != 2 || !self.is_theme_callee(&call.callee) {
565            return;
566        }
567        let Some(contract_expression) = call.arguments.first().and_then(Argument::as_expression)
568        else {
569            return;
570        };
571        let Some(contract) = self.resolve_theme_contract_alias(
572            contract_expression,
573            call.span.start,
574            &mut FxHashSet::default(),
575        ) else {
576            return;
577        };
578        let Some(overrides) = call.arguments.get(1).and_then(Argument::as_expression) else {
579            return;
580        };
581        if !is_static_stylex_theme_override_object(
582            overrides,
583            call.span.start,
584            &self.static_values,
585            &self.stylex_imports,
586            &self.root_reference_spans,
587            &mut FxHashSet::default(),
588        ) {
589            return;
590        }
591        let Some(queries) = self.theme_queries.get(contract) else {
592            return;
593        };
594        let line = self.lines.line_at(call.span().start);
595        for &(idx, leaf_paths) in queries {
596            self.hits.extend(
597                leaf_paths
598                    .iter()
599                    .cloned()
600                    .map(|token_path| (idx, TokenConsumerHit { token_path, line })),
601            );
602        }
603    }
604
605    fn resolve_theme_contract_alias(
606        &self,
607        expression: &'a Expression<'a>,
608        before: u32,
609        visiting: &mut FxHashSet<&'a str>,
610    ) -> Option<&'a str> {
611        let Expression::Identifier(identifier) = unwrap_transparent_expression(expression) else {
612            return None;
613        };
614        if !self.root_reference_spans.contains(&identifier.span) {
615            return None;
616        }
617        let name = identifier.name.as_str();
618        if self.theme_queries.contains_key(name) {
619            return Some(name);
620        }
621        let &(declaration_start, value) = self.static_values.get(name)?;
622        if declaration_start >= before || !visiting.insert(name) {
623            return None;
624        }
625        let resolved = self.resolve_theme_contract_alias(value, declaration_start, visiting);
626        visiting.remove(name);
627        resolved
628    }
629}
630
631impl<'a> Visit<'a> for BatchedBindingCollector<'a, '_, '_> {
632    fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
633        let mut chain = binding_access_object_chain(&member.object);
634        if let Some((_, _, segments)) = chain.as_mut() {
635            segments.push(member.property.name.to_string());
636        }
637        if !self.collecting_mutations {
638            self.record_member(chain, member.span().start);
639        }
640        walk::walk_static_member_expression(self, member);
641    }
642
643    fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
644        let mut chain = binding_access_object_chain(&member.object);
645        if let (Some((_, _, segments)), Some(key)) =
646            (chain.as_mut(), static_computed_key(&member.expression))
647        {
648            segments.push(key);
649        } else {
650            chain = None;
651        }
652        if !self.collecting_mutations {
653            self.record_member(chain, member.span().start);
654        }
655        walk::walk_computed_member_expression(self, member);
656    }
657
658    fn visit_variable_declarator(&mut self, declaration: &VariableDeclarator<'a>) {
659        if !self.collecting_mutations
660            && let (BindingPattern::BindingIdentifier(_), Some(Expression::CallExpression(call))) =
661                (&declaration.id, declaration.init.as_ref())
662        {
663            self.record_theme_call(call);
664        }
665        walk::walk_variable_declarator(self, declaration);
666    }
667
668    fn visit_assignment_expression(&mut self, assignment: &AssignmentExpression<'a>) {
669        if self.collecting_mutations {
670            for binding in assignment_target_root_bindings(&assignment.left) {
671                self.invalidate_static_value(Some(binding));
672            }
673            if let Some(receiver) = assignment_target_receiver_expression(&assignment.left) {
674                let binding = self.mutation_root_binding(
675                    receiver,
676                    assignment.span.start,
677                    &mut FxHashSet::default(),
678                );
679                self.invalidate_static_value(binding);
680            }
681        }
682        walk::walk_assignment_expression(self, assignment);
683    }
684
685    fn visit_update_expression(&mut self, update: &UpdateExpression<'a>) {
686        if self.collecting_mutations {
687            self.invalidate_static_value(simple_assignment_target_root_binding(&update.argument));
688        }
689        walk::walk_update_expression(self, update);
690    }
691
692    fn visit_unary_expression(&mut self, expression: &UnaryExpression<'a>) {
693        if self.collecting_mutations && expression.operator.is_delete() {
694            self.invalidate_static_value(expression_root_binding(&expression.argument));
695        }
696        walk::walk_unary_expression(self, expression);
697    }
698
699    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
700        if !self.collecting_mutations {
701            walk::walk_call_expression(self, call);
702            return;
703        }
704        let mut visiting = FxHashSet::default();
705        let is_stylex_helper = call.arguments.len() == 1
706            && is_root_stylex_static_call(
707                &call.callee,
708                &self.stylex_imports,
709                &self.root_reference_spans,
710            )
711            && call
712                .arguments
713                .first()
714                .and_then(Argument::as_expression)
715                .is_some_and(|argument| {
716                    is_static_stylex_theme_override(
717                        argument,
718                        call.span.start,
719                        &self.static_values,
720                        &self.stylex_imports,
721                        &self.root_reference_spans,
722                        &mut visiting,
723                    )
724                });
725        let is_pure = is_stylex_helper
726            || is_static_stylex_pure_call(
727                call,
728                call.span.start,
729                &self.static_values,
730                &self.stylex_imports,
731                &self.root_reference_spans,
732                &mut visiting,
733            );
734        if !self.is_theme_callee(&call.callee) && !is_pure {
735            if let Some(receiver) = call_receiver_expression(&call.callee) {
736                let binding = self.mutation_root_binding(
737                    receiver,
738                    call.span.start,
739                    &mut FxHashSet::default(),
740                );
741                self.invalidate_static_value(binding);
742            }
743            let possibly_mutated: Vec<RootBinding<'_>> = call
744                .arguments
745                .iter()
746                .filter_map(Argument::as_expression)
747                .filter_map(|argument| {
748                    self.mutation_root_binding(argument, call.span.start, &mut FxHashSet::default())
749                })
750                .collect();
751            for binding in possibly_mutated {
752                if !self.binding_is_definitely_primitive(binding, call.span.start) {
753                    self.invalidate_static_value(Some(binding));
754                }
755            }
756        }
757        walk::walk_call_expression(self, call);
758    }
759}
760
761/// Run one non-batched [`ConsumerQuery`] against an already-parsed `program`,
762/// and tag each resulting hit with `idx`. A query with an empty alias or an
763/// empty leaf set gives no hits.
764fn run_consumer_query<'a>(
765    query: &ConsumerQuery<'_>,
766    source: &'a str,
767    program: &Program<'a>,
768    idx: usize,
769    out: &mut Vec<(usize, TokenConsumerHit)>,
770) {
771    match query {
772        // `css_in_js_consumer_scan` runs these queries in `BatchedBindingCollector`.
773        ConsumerQuery::MemberBinding { .. } | ConsumerQuery::StyleXThemeGroup { .. } => {}
774        ConsumerQuery::PandaTokenCall { alias, leaf_paths } => {
775            if alias.is_empty() || leaf_paths.is_empty() {
776                return;
777            }
778            let mut collector = PandaTokenCallCollector {
779                lines: LineCounter::new(source),
780                alias,
781                leaf_paths,
782                hits: Vec::new(),
783            };
784            collector.visit_program(program);
785            out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
786        }
787        ConsumerQuery::PandaStyleValues {
788            aliases,
789            leaf_paths,
790        } => {
791            if aliases.is_empty() || leaf_paths.is_empty() {
792                return;
793            }
794            let mut collector = PandaStyleValueCollector {
795                lines: LineCounter::new(source),
796                aliases,
797                leaf_paths,
798                hits: Vec::new(),
799            };
800            collector.visit_program(program);
801            out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
802        }
803        ConsumerQuery::ThemeReads { leaf_paths } => {
804            if leaf_paths.is_empty() {
805                return;
806            }
807            let mut collector = ThemeConsumerCollector {
808                lines: LineCounter::new(source),
809                leaf_paths,
810                hits: Vec::new(),
811            };
812            collector.visit_program(program);
813            out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
814        }
815    }
816}
817
818struct PandaTokenCallCollector<'a, 'b> {
819    lines: LineCounter<'a>,
820    alias: &'b str,
821    leaf_paths: &'b FxHashSet<String>,
822    hits: Vec<TokenConsumerHit>,
823}
824
825impl<'a> Visit<'a> for PandaTokenCallCollector<'a, '_> {
826    fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
827        let Expression::Identifier(callee) = &call.callee else {
828            walk::walk_call_expression(self, call);
829            return;
830        };
831        if callee.name.as_str() == self.alias
832            && let Some(Argument::StringLiteral(lit)) = call.arguments.first()
833        {
834            let token_path = &*lit.value.text();
835            if self.leaf_paths.contains(token_path) {
836                let line = self.lines.line_at(call.span().start);
837                self.hits.push(TokenConsumerHit {
838                    token_path: token_path.to_owned(),
839                    line,
840                });
841            }
842        }
843        walk::walk_call_expression(self, call);
844    }
845}
846
847struct PandaStyleValueCollector<'a, 'b> {
848    lines: LineCounter<'a>,
849    aliases: &'b FxHashSet<String>,
850    leaf_paths: &'b FxHashSet<String>,
851    hits: Vec<TokenConsumerHit>,
852}
853
854impl<'a> PandaStyleValueCollector<'a, '_> {
855    fn record_object(&mut self, obj: &ObjectExpression<'a>) {
856        for prop in &obj.properties {
857            let ObjectPropertyKind::ObjectProperty(prop) = prop else {
858                continue;
859            };
860            self.record_expression(&prop.value);
861        }
862    }
863
864    fn record_expression(&mut self, expr: &Expression<'a>) {
865        match expr {
866            Expression::StringLiteral(lit) => {
867                let token_path = &*lit.value.text();
868                if self.leaf_paths.contains(token_path) {
869                    let line = self.lines.line_at(lit.span().start);
870                    self.hits.push(TokenConsumerHit {
871                        token_path: token_path.to_owned(),
872                        line,
873                    });
874                }
875            }
876            Expression::ObjectExpression(obj) => self.record_object(obj),
877            _ => {}
878        }
879    }
880}
881
882impl<'a> Visit<'a> for PandaStyleValueCollector<'a, '_> {
883    fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
884        let Expression::Identifier(callee) = &call.callee else {
885            walk::walk_call_expression(self, call);
886            return;
887        };
888        if self.aliases.contains(callee.name.as_str()) {
889            for arg in &call.arguments {
890                if let Argument::ObjectExpression(obj) = arg {
891                    self.record_object(obj);
892                }
893            }
894        }
895        walk::walk_call_expression(self, call);
896    }
897}
898
899struct ThemeDefCollector<'a> {
900    lines: LineCounter<'a>,
901    defs: Vec<CssInJsTokenDef>,
902}
903
904impl<'a> ThemeDefCollector<'a> {
905    fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
906        let BindingPattern::BindingIdentifier(binding) = &decl.id else {
907            return;
908        };
909        let binding_name = binding.name.as_str();
910        if !is_theme_binding_name(binding_name) {
911            return;
912        }
913        let Some(Expression::ObjectExpression(obj)) = &decl.init else {
914            return;
915        };
916        let mut tokens = Vec::new();
917        collect_token_leaves(
918            &mut self.lines,
919            obj,
920            "",
921            CssInJsTokenOrigin::Theme,
922            &mut tokens,
923        );
924        if tokens.is_empty() {
925            return;
926        }
927        self.defs.push(CssInJsTokenDef {
928            binding: binding_name.to_owned(),
929            origin: CssInJsTokenOrigin::Theme,
930            tokens,
931        });
932    }
933}
934
935impl<'a> Visit<'a> for ThemeDefCollector<'a> {
936    fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
937        self.process_declarator(decl);
938        walk::walk_variable_declarator(self, decl);
939    }
940}
941
942struct ThemeConsumerCollector<'a, 'b> {
943    lines: LineCounter<'a>,
944    leaf_paths: &'b FxHashSet<String>,
945    hits: Vec<TokenConsumerHit>,
946}
947
948impl<'a> ThemeConsumerCollector<'a, '_> {
949    fn record(&mut self, chain: Option<(&'a str, Vec<String>)>, span_start: u32) {
950        let Some((base, segments)) = chain else {
951            return;
952        };
953        let token_segments: &[String] = match base {
954            "theme" => &segments,
955            "props" if segments.first().is_some_and(|segment| segment == "theme") => &segments[1..],
956            _ => return,
957        };
958        if token_segments.is_empty() {
959            return;
960        }
961        let token_path = token_segments.join(".");
962        if self.leaf_paths.contains(&token_path) {
963            let line = self.lines.line_at(span_start);
964            self.hits.push(TokenConsumerHit { token_path, line });
965        }
966    }
967}
968
969impl<'a> Visit<'a> for ThemeConsumerCollector<'a, '_> {
970    fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
971        let mut chain = access_object_chain(&member.object);
972        if let Some((_, segments)) = chain.as_mut() {
973            segments.push(member.property.name.to_string());
974        }
975        self.record(chain, member.span().start);
976        walk::walk_static_member_expression(self, member);
977    }
978
979    fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
980        let mut chain = access_object_chain(&member.object);
981        if let (Some((_, segments)), Some(key)) =
982            (chain.as_mut(), static_computed_key(&member.expression))
983        {
984            segments.push(key);
985        } else {
986            chain = None;
987        }
988        self.record(chain, member.span().start);
989        walk::walk_computed_member_expression(self, member);
990    }
991}
992
993/// Reconstruct the `(base identifier, [segments])` chain of a member-access OBJECT
994/// expression, threading through both static (`a.b`) and string-literal-computed
995/// (`a['b']`) member access. `vars.color` -> `("vars", ["color"])`. Returns `None`
996/// if the chain is not rooted at a plain identifier (a call result, `this`, a
997/// non-literal computed key, etc.).
998fn access_object_chain<'a>(expr: &Expression<'a>) -> Option<(&'a str, Vec<String>)> {
999    match expr {
1000        Expression::Identifier(id) => Some((id.name.as_str(), Vec::new())),
1001        Expression::StaticMemberExpression(inner) => {
1002            let (base, mut segments) = access_object_chain(&inner.object)?;
1003            segments.push(inner.property.name.to_string());
1004            Some((base, segments))
1005        }
1006        Expression::ComputedMemberExpression(inner) => {
1007            let (base, mut segments) = access_object_chain(&inner.object)?;
1008            segments.push(static_computed_key(&inner.expression)?);
1009            Some((base, segments))
1010        }
1011        Expression::ParenthesizedExpression(expression) => {
1012            access_object_chain(&expression.expression)
1013        }
1014        Expression::TSAsExpression(expression) => access_object_chain(&expression.expression),
1015        Expression::TSSatisfiesExpression(expression) => {
1016            access_object_chain(&expression.expression)
1017        }
1018        Expression::TSNonNullExpression(expression) => access_object_chain(&expression.expression),
1019        Expression::TSTypeAssertion(expression) => access_object_chain(&expression.expression),
1020        _ => None,
1021    }
1022}
1023
1024fn binding_access_object_chain<'a>(expr: &Expression<'a>) -> Option<(&'a str, Span, Vec<String>)> {
1025    match expr {
1026        Expression::Identifier(id) => Some((id.name.as_str(), id.span, Vec::new())),
1027        Expression::StaticMemberExpression(inner) => {
1028            let (base, span, mut segments) = binding_access_object_chain(&inner.object)?;
1029            segments.push(inner.property.name.to_string());
1030            Some((base, span, segments))
1031        }
1032        Expression::ComputedMemberExpression(inner) => {
1033            let (base, span, mut segments) = binding_access_object_chain(&inner.object)?;
1034            segments.push(static_computed_key(&inner.expression)?);
1035            Some((base, span, segments))
1036        }
1037        Expression::ParenthesizedExpression(expression) => {
1038            binding_access_object_chain(&expression.expression)
1039        }
1040        Expression::TSAsExpression(expression) => {
1041            binding_access_object_chain(&expression.expression)
1042        }
1043        Expression::TSSatisfiesExpression(expression) => {
1044            binding_access_object_chain(&expression.expression)
1045        }
1046        Expression::TSNonNullExpression(expression) => {
1047            binding_access_object_chain(&expression.expression)
1048        }
1049        Expression::TSTypeAssertion(expression) => {
1050            binding_access_object_chain(&expression.expression)
1051        }
1052        _ => None,
1053    }
1054}
1055
1056fn unwrap_transparent_expression<'a, 'b: 'a>(mut expr: &'a Expression<'b>) -> &'a Expression<'b> {
1057    loop {
1058        expr = match expr {
1059            Expression::ParenthesizedExpression(expression) => &expression.expression,
1060            Expression::TSAsExpression(expression) => &expression.expression,
1061            Expression::TSSatisfiesExpression(expression) => &expression.expression,
1062            Expression::TSNonNullExpression(expression) => &expression.expression,
1063            Expression::TSTypeAssertion(expression) => &expression.expression,
1064            _ => return expr,
1065        };
1066    }
1067}
1068
1069fn call_receiver_root<'a, 'b: 'a>(callee: &'a Expression<'b>) -> Option<RootBinding<'a>> {
1070    match callee {
1071        Expression::StaticMemberExpression(member) => expression_root_binding(&member.object),
1072        Expression::ComputedMemberExpression(member) => expression_root_binding(&member.object),
1073        Expression::ParenthesizedExpression(expression) => {
1074            call_receiver_root(&expression.expression)
1075        }
1076        Expression::TSAsExpression(expression) => call_receiver_root(&expression.expression),
1077        Expression::TSSatisfiesExpression(expression) => call_receiver_root(&expression.expression),
1078        Expression::TSNonNullExpression(expression) => call_receiver_root(&expression.expression),
1079        Expression::TSTypeAssertion(expression) => call_receiver_root(&expression.expression),
1080        _ => None,
1081    }
1082}
1083
1084fn call_receiver_expression<'a, 'b: 'a>(callee: &'a Expression<'b>) -> Option<&'a Expression<'b>> {
1085    match callee {
1086        Expression::StaticMemberExpression(member) => Some(&member.object),
1087        Expression::ComputedMemberExpression(member) => Some(&member.object),
1088        Expression::ParenthesizedExpression(expression) => {
1089            call_receiver_expression(&expression.expression)
1090        }
1091        Expression::TSAsExpression(expression) => call_receiver_expression(&expression.expression),
1092        Expression::TSSatisfiesExpression(expression) => {
1093            call_receiver_expression(&expression.expression)
1094        }
1095        Expression::TSNonNullExpression(expression) => {
1096            call_receiver_expression(&expression.expression)
1097        }
1098        Expression::TSTypeAssertion(expression) => call_receiver_expression(&expression.expression),
1099        _ => None,
1100    }
1101}
1102
1103fn is_static_stylex_theme_override<'a>(
1104    expression: &'a Expression<'a>,
1105    before: u32,
1106    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1107    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
1108    root_reference_spans: &FxHashSet<Span>,
1109    visiting: &mut FxHashSet<&'a str>,
1110) -> bool {
1111    let expression = unwrap_transparent_expression(expression);
1112    if let Some(is_static) = is_static_stylex_composite_expression(
1113        expression,
1114        before,
1115        static_values,
1116        imports,
1117        root_reference_spans,
1118        visiting,
1119    ) {
1120        return is_static;
1121    }
1122    match expression {
1123        Expression::StringLiteral(_)
1124        | Expression::NumericLiteral(_)
1125        | Expression::BooleanLiteral(_)
1126        | Expression::NullLiteral(_)
1127        | Expression::BigIntLiteral(_) => true,
1128        Expression::ObjectExpression(object) => is_static_stylex_theme_object(
1129            object,
1130            before,
1131            static_values,
1132            imports,
1133            root_reference_spans,
1134            visiting,
1135        ),
1136        Expression::Identifier(identifier) => {
1137            if !root_reference_spans.contains(&identifier.span) {
1138                return false;
1139            }
1140            let name = identifier.name.as_str();
1141            let Some(&(declaration_start, value)) = static_values.get(name) else {
1142                return false;
1143            };
1144            if declaration_start >= before || !visiting.insert(name) {
1145                return false;
1146            }
1147            let is_static = is_static_stylex_theme_override(
1148                value,
1149                declaration_start,
1150                static_values,
1151                imports,
1152                root_reference_spans,
1153                visiting,
1154            );
1155            visiting.remove(name);
1156            is_static
1157        }
1158        Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) => {
1159            let mut member_visiting = visiting.clone();
1160            let resolver = StyleXStaticResolver {
1161                before,
1162                static_values,
1163                root_reference_spans,
1164            };
1165            resolve_static_stylex_member(expression, &resolver, &mut member_visiting).is_some_and(
1166                |resolved| {
1167                    is_static_stylex_theme_override(
1168                        resolved.value,
1169                        resolved.before,
1170                        static_values,
1171                        imports,
1172                        root_reference_spans,
1173                        &mut member_visiting,
1174                    )
1175                },
1176            )
1177        }
1178        Expression::CallExpression(call)
1179            if call.arguments.len() == 1
1180                && is_root_stylex_static_call(&call.callee, imports, root_reference_spans) =>
1181        {
1182            call.arguments.first().is_some_and(|argument| {
1183                argument.as_expression().is_some_and(|argument| {
1184                    is_static_stylex_theme_override(
1185                        argument,
1186                        before,
1187                        static_values,
1188                        imports,
1189                        root_reference_spans,
1190                        visiting,
1191                    )
1192                })
1193            })
1194        }
1195        Expression::CallExpression(call) => is_static_stylex_pure_call(
1196            call,
1197            before,
1198            static_values,
1199            imports,
1200            root_reference_spans,
1201            visiting,
1202        ),
1203        _ => false,
1204    }
1205}
1206
1207fn is_static_stylex_composite_expression<'a>(
1208    expression: &'a Expression<'a>,
1209    before: u32,
1210    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1211    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
1212    root_reference_spans: &FxHashSet<Span>,
1213    visiting: &mut FxHashSet<&'a str>,
1214) -> Option<bool> {
1215    let mut check = |expression| {
1216        is_static_stylex_theme_override(
1217            expression,
1218            before,
1219            static_values,
1220            imports,
1221            root_reference_spans,
1222            visiting,
1223        )
1224    };
1225    match expression {
1226        Expression::TemplateLiteral(template) => Some(template.expressions.iter().all(check)),
1227        Expression::TaggedTemplateExpression(tagged) => Some(
1228            is_root_string_raw_tag(&tagged.tag, static_values, root_reference_spans)
1229                && tagged.quasi.expressions.iter().all(check),
1230        ),
1231        Expression::UnaryExpression(unary) => Some(
1232            !unary.operator.is_delete()
1233                && (unary.operator != UnaryOperator::UnaryPlus
1234                    || !is_static_stylex_bigint_value(
1235                        &unary.argument,
1236                        before,
1237                        static_values,
1238                        root_reference_spans,
1239                        &mut FxHashSet::default(),
1240                    ))
1241                && check(&unary.argument),
1242        ),
1243        Expression::ConditionalExpression(conditional) => Some(
1244            check(&conditional.test)
1245                && check(&conditional.consequent)
1246                && check(&conditional.alternate),
1247        ),
1248        Expression::LogicalExpression(logical) => {
1249            Some(check(&logical.left) && check(&logical.right))
1250        }
1251        Expression::SequenceExpression(sequence) => Some(sequence.expressions.iter().all(check)),
1252        Expression::BinaryExpression(binary) => Some(
1253            !binary.operator.is_relational()
1254                && (!binary.operator.is_numeric_or_string_binary_operator()
1255                    || (!is_static_stylex_bigint_value(
1256                        &binary.left,
1257                        before,
1258                        static_values,
1259                        root_reference_spans,
1260                        &mut FxHashSet::default(),
1261                    ) && !is_static_stylex_bigint_value(
1262                        &binary.right,
1263                        before,
1264                        static_values,
1265                        root_reference_spans,
1266                        &mut FxHashSet::default(),
1267                    )))
1268                && check(&binary.left)
1269                && check(&binary.right),
1270        ),
1271        _ => None,
1272    }
1273}
1274
1275fn is_static_stylex_theme_object<'a>(
1276    object: &'a ObjectExpression<'a>,
1277    before: u32,
1278    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1279    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
1280    root_reference_spans: &FxHashSet<Span>,
1281    visiting: &mut FxHashSet<&'a str>,
1282) -> bool {
1283    object.properties.iter().all(|property| match property {
1284        ObjectPropertyKind::ObjectProperty(property) => {
1285            if property.computed {
1286                let Some(key) = property.key.as_expression() else {
1287                    return false;
1288                };
1289                if !is_static_stylex_computed_key(
1290                    key,
1291                    before,
1292                    static_values,
1293                    imports,
1294                    root_reference_spans,
1295                    visiting,
1296                ) {
1297                    return false;
1298                }
1299            }
1300            is_static_stylex_theme_override(
1301                &property.value,
1302                before,
1303                static_values,
1304                imports,
1305                root_reference_spans,
1306                visiting,
1307            )
1308        }
1309        ObjectPropertyKind::SpreadProperty(spread) => is_static_stylex_theme_override_object(
1310            &spread.argument,
1311            before,
1312            static_values,
1313            imports,
1314            root_reference_spans,
1315            visiting,
1316        ),
1317    })
1318}
1319
1320fn is_static_stylex_computed_key<'a>(
1321    expression: &'a Expression<'a>,
1322    before: u32,
1323    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1324    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
1325    root_reference_spans: &FxHashSet<Span>,
1326    visiting: &mut FxHashSet<&'a str>,
1327) -> bool {
1328    match unwrap_transparent_expression(expression) {
1329        Expression::StringLiteral(_) | Expression::NumericLiteral(_) => true,
1330        Expression::TemplateLiteral(template) => template.expressions.iter().all(|expression| {
1331            is_static_stylex_theme_override(
1332                expression,
1333                before,
1334                static_values,
1335                imports,
1336                root_reference_spans,
1337                visiting,
1338            )
1339        }),
1340        Expression::BinaryExpression(binary)
1341            if binary.operator.is_numeric_or_string_binary_operator() =>
1342        {
1343            is_static_stylex_theme_override(
1344                expression,
1345                before,
1346                static_values,
1347                imports,
1348                root_reference_spans,
1349                visiting,
1350            )
1351        }
1352        Expression::Identifier(identifier) => {
1353            if !root_reference_spans.contains(&identifier.span) {
1354                return false;
1355            }
1356            let name = identifier.name.as_str();
1357            let Some(&(declaration_start, value)) = static_values.get(name) else {
1358                return false;
1359            };
1360            if declaration_start >= before || !visiting.insert(name) {
1361                return false;
1362            }
1363            let is_static = is_static_stylex_computed_key(
1364                value,
1365                declaration_start,
1366                static_values,
1367                imports,
1368                root_reference_spans,
1369                visiting,
1370            );
1371            visiting.remove(name);
1372            is_static
1373        }
1374        Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) => {
1375            let mut member_visiting = visiting.clone();
1376            let resolver = StyleXStaticResolver {
1377                before,
1378                static_values,
1379                root_reference_spans,
1380            };
1381            resolve_static_stylex_member(expression, &resolver, &mut member_visiting).is_some_and(
1382                |resolved| {
1383                    is_static_stylex_computed_key(
1384                        resolved.value,
1385                        resolved.before,
1386                        static_values,
1387                        imports,
1388                        root_reference_spans,
1389                        &mut member_visiting,
1390                    )
1391                },
1392            )
1393        }
1394        _ => false,
1395    }
1396}
1397
1398struct StyleXStaticResolver<'maps, 'ast> {
1399    before: u32,
1400    static_values: &'maps FxHashMap<&'ast str, (u32, &'ast Expression<'ast>)>,
1401    root_reference_spans: &'maps FxHashSet<Span>,
1402}
1403
1404struct ResolvedStyleXStaticMember<'ast> {
1405    value: &'ast Expression<'ast>,
1406    before: u32,
1407}
1408
1409#[derive(Clone, Copy)]
1410struct ResolvedStyleXStaticObject<'ast> {
1411    object: &'ast ObjectExpression<'ast>,
1412    before: u32,
1413}
1414
1415fn resolve_static_stylex_member<'a>(
1416    expression: &'a Expression<'a>,
1417    resolver: &StyleXStaticResolver<'_, 'a>,
1418    visiting: &mut FxHashSet<&'a str>,
1419) -> Option<ResolvedStyleXStaticMember<'a>> {
1420    match unwrap_transparent_expression(expression) {
1421        Expression::StaticMemberExpression(member) => {
1422            let object = resolve_static_stylex_object(&member.object, resolver, visiting)?;
1423            resolve_static_stylex_object_property(
1424                object,
1425                member.property.name.as_str(),
1426                resolver,
1427                visiting,
1428            )
1429        }
1430        Expression::ComputedMemberExpression(member) => {
1431            let object = resolve_static_stylex_object(&member.object, resolver, visiting)?;
1432            let key = resolve_static_stylex_member_key(&member.expression, resolver, visiting)?;
1433            resolve_static_stylex_object_property(object, &key, resolver, visiting)
1434        }
1435        _ => None,
1436    }
1437}
1438
1439fn resolve_static_stylex_object<'a>(
1440    expression: &'a Expression<'a>,
1441    resolver: &StyleXStaticResolver<'_, 'a>,
1442    visiting: &mut FxHashSet<&'a str>,
1443) -> Option<ResolvedStyleXStaticObject<'a>> {
1444    match unwrap_transparent_expression(expression) {
1445        Expression::ObjectExpression(object) => Some(ResolvedStyleXStaticObject {
1446            object,
1447            before: resolver.before,
1448        }),
1449        Expression::Identifier(identifier) => {
1450            if !resolver.root_reference_spans.contains(&identifier.span) {
1451                return None;
1452            }
1453            let name = identifier.name.as_str();
1454            let &(declaration_start, value) = resolver.static_values.get(name)?;
1455            if declaration_start >= resolver.before || !visiting.insert(name) {
1456                return None;
1457            }
1458            let nested_resolver = StyleXStaticResolver {
1459                before: declaration_start,
1460                static_values: resolver.static_values,
1461                root_reference_spans: resolver.root_reference_spans,
1462            };
1463            resolve_static_stylex_object(value, &nested_resolver, visiting)
1464        }
1465        Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) => {
1466            let resolved = resolve_static_stylex_member(expression, resolver, visiting)?;
1467            let nested_resolver = StyleXStaticResolver {
1468                before: resolved.before,
1469                static_values: resolver.static_values,
1470                root_reference_spans: resolver.root_reference_spans,
1471            };
1472            resolve_static_stylex_object(resolved.value, &nested_resolver, visiting)
1473        }
1474        _ => None,
1475    }
1476}
1477
1478fn resolve_static_stylex_object_property<'a>(
1479    resolved_object: ResolvedStyleXStaticObject<'a>,
1480    wanted: &str,
1481    resolver: &StyleXStaticResolver<'_, 'a>,
1482    visiting: &mut FxHashSet<&'a str>,
1483) -> Option<ResolvedStyleXStaticMember<'a>> {
1484    for property in resolved_object.object.properties.iter().rev() {
1485        match property {
1486            ObjectPropertyKind::ObjectProperty(property) => {
1487                let key = if property.computed {
1488                    resolve_static_stylex_member_key(
1489                        property.key.as_expression()?,
1490                        resolver,
1491                        visiting,
1492                    )?
1493                } else {
1494                    property.key.static_name()?.text().into_owned()
1495                };
1496                if key == wanted {
1497                    return Some(ResolvedStyleXStaticMember {
1498                        value: &property.value,
1499                        before: resolved_object.before,
1500                    });
1501                }
1502            }
1503            ObjectPropertyKind::SpreadProperty(spread) => {
1504                let spread_resolver = StyleXStaticResolver {
1505                    before: resolved_object.before,
1506                    static_values: resolver.static_values,
1507                    root_reference_spans: resolver.root_reference_spans,
1508                };
1509                let spread_object =
1510                    resolve_static_stylex_object(&spread.argument, &spread_resolver, visiting)?;
1511                if let Some(value) =
1512                    resolve_static_stylex_object_property(spread_object, wanted, resolver, visiting)
1513                {
1514                    return Some(value);
1515                }
1516            }
1517        }
1518    }
1519    None
1520}
1521
1522fn resolve_static_stylex_member_key<'a>(
1523    expression: &'a Expression<'a>,
1524    resolver: &StyleXStaticResolver<'_, 'a>,
1525    visiting: &mut FxHashSet<&'a str>,
1526) -> Option<String> {
1527    if let Some(key) = static_computed_key(expression) {
1528        return Some(key);
1529    }
1530    let Expression::Identifier(identifier) = unwrap_transparent_expression(expression) else {
1531        return None;
1532    };
1533    if !resolver.root_reference_spans.contains(&identifier.span) {
1534        return None;
1535    }
1536    let name = identifier.name.as_str();
1537    let &(declaration_start, value) = resolver.static_values.get(name)?;
1538    if declaration_start >= resolver.before || !visiting.insert(name) {
1539        return None;
1540    }
1541    let nested_resolver = StyleXStaticResolver {
1542        before: declaration_start,
1543        static_values: resolver.static_values,
1544        root_reference_spans: resolver.root_reference_spans,
1545    };
1546    let key = resolve_static_stylex_member_key(value, &nested_resolver, visiting);
1547    visiting.remove(name);
1548    key
1549}
1550
1551fn is_static_stylex_theme_override_object<'a>(
1552    expression: &'a Expression<'a>,
1553    before: u32,
1554    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1555    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
1556    root_reference_spans: &FxHashSet<Span>,
1557    visiting: &mut FxHashSet<&'a str>,
1558) -> bool {
1559    match unwrap_transparent_expression(expression) {
1560        Expression::ObjectExpression(_) => is_static_stylex_theme_override(
1561            expression,
1562            before,
1563            static_values,
1564            imports,
1565            root_reference_spans,
1566            visiting,
1567        ),
1568        Expression::Identifier(identifier) => {
1569            if !root_reference_spans.contains(&identifier.span) {
1570                return false;
1571            }
1572            let name = identifier.name.as_str();
1573            let Some(&(declaration_start, value)) = static_values.get(name) else {
1574                return false;
1575            };
1576            if declaration_start >= before || !visiting.insert(name) {
1577                return false;
1578            }
1579            let is_static = is_static_stylex_theme_override_object(
1580                value,
1581                declaration_start,
1582                static_values,
1583                imports,
1584                root_reference_spans,
1585                visiting,
1586            );
1587            visiting.remove(name);
1588            is_static
1589        }
1590        Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) => {
1591            let mut member_visiting = visiting.clone();
1592            let resolver = StyleXStaticResolver {
1593                before,
1594                static_values,
1595                root_reference_spans,
1596            };
1597            resolve_static_stylex_member(expression, &resolver, &mut member_visiting).is_some_and(
1598                |resolved| {
1599                    is_static_stylex_theme_override_object(
1600                        resolved.value,
1601                        resolved.before,
1602                        static_values,
1603                        imports,
1604                        root_reference_spans,
1605                        &mut member_visiting,
1606                    )
1607                },
1608            )
1609        }
1610        _ => false,
1611    }
1612}
1613
1614fn is_root_string_raw_tag(
1615    tag: &Expression<'_>,
1616    static_values: &FxHashMap<&str, (u32, &Expression<'_>)>,
1617    root_reference_spans: &FxHashSet<Span>,
1618) -> bool {
1619    let Expression::StaticMemberExpression(member) = unwrap_transparent_expression(tag) else {
1620        return false;
1621    };
1622    let Expression::Identifier(object) = unwrap_transparent_expression(&member.object) else {
1623        return false;
1624    };
1625    object.name == "String"
1626        && member.property.name == "raw"
1627        && root_reference_spans.contains(&object.span)
1628        && !static_values.contains_key("String")
1629}
1630
1631fn is_static_stylex_pure_call<'a>(
1632    call: &'a CallExpression<'a>,
1633    before: u32,
1634    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1635    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
1636    root_reference_spans: &FxHashSet<Span>,
1637    visiting: &mut FxHashSet<&'a str>,
1638) -> bool {
1639    if is_static_stylex_local_arrow_call(
1640        call,
1641        before,
1642        static_values,
1643        imports,
1644        root_reference_spans,
1645        visiting,
1646    ) {
1647        return true;
1648    }
1649    let allow_array_arguments =
1650        is_root_object_from_entries_call(&call.callee, static_values, root_reference_spans);
1651    if allow_array_arguments {
1652        return is_static_stylex_entries_call(
1653            call,
1654            before,
1655            static_values,
1656            imports,
1657            root_reference_spans,
1658            visiting,
1659        );
1660    }
1661    if !is_root_scalar_pure_call(
1662        &call.callee,
1663        before,
1664        static_values,
1665        imports,
1666        root_reference_spans,
1667        visiting,
1668    ) || !is_static_stylex_scalar_call_shape(call, before, static_values, root_reference_spans)
1669    {
1670        return false;
1671    }
1672    if is_root_math_call(&call.callee, static_values, root_reference_spans)
1673        && call.arguments.iter().any(|argument| {
1674            argument.as_expression().is_none_or(|argument| {
1675                is_static_stylex_bigint_value(
1676                    argument,
1677                    before,
1678                    static_values,
1679                    root_reference_spans,
1680                    &mut FxHashSet::default(),
1681                )
1682            })
1683        })
1684    {
1685        return false;
1686    }
1687    call.arguments.iter().all(|argument| {
1688        argument.as_expression().is_some_and(|argument| {
1689            is_definitely_static_primitive(
1690                argument,
1691                before,
1692                static_values,
1693                root_reference_spans,
1694                visiting,
1695            ) && is_static_stylex_theme_override(
1696                argument,
1697                before,
1698                static_values,
1699                imports,
1700                root_reference_spans,
1701                visiting,
1702            )
1703        })
1704    })
1705}
1706
1707fn is_root_math_call(
1708    callee: &Expression<'_>,
1709    static_values: &FxHashMap<&str, (u32, &Expression<'_>)>,
1710    root_reference_spans: &FxHashSet<Span>,
1711) -> bool {
1712    let Expression::StaticMemberExpression(member) = unwrap_transparent_expression(callee) else {
1713        return false;
1714    };
1715    let Expression::Identifier(object) = unwrap_transparent_expression(&member.object) else {
1716        return false;
1717    };
1718    object.name == "Math"
1719        && root_reference_spans.contains(&object.span)
1720        && !static_values.contains_key("Math")
1721}
1722
1723fn is_definitely_static_primitive<'a>(
1724    expression: &'a Expression<'a>,
1725    before: u32,
1726    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1727    root_reference_spans: &FxHashSet<Span>,
1728    visiting: &mut FxHashSet<&'a str>,
1729) -> bool {
1730    match unwrap_transparent_expression(expression) {
1731        Expression::StringLiteral(_)
1732        | Expression::NumericLiteral(_)
1733        | Expression::BooleanLiteral(_)
1734        | Expression::NullLiteral(_)
1735        | Expression::BigIntLiteral(_) => true,
1736        Expression::TemplateLiteral(template) => template.expressions.iter().all(|expression| {
1737            is_definitely_static_primitive(
1738                expression,
1739                before,
1740                static_values,
1741                root_reference_spans,
1742                visiting,
1743            )
1744        }),
1745        Expression::UnaryExpression(unary) => {
1746            !unary.operator.is_delete()
1747                && is_definitely_static_primitive(
1748                    &unary.argument,
1749                    before,
1750                    static_values,
1751                    root_reference_spans,
1752                    visiting,
1753                )
1754        }
1755        Expression::BinaryExpression(binary) => is_static_stylex_primitive_pair(
1756            &binary.left,
1757            &binary.right,
1758            before,
1759            static_values,
1760            root_reference_spans,
1761            visiting,
1762        ),
1763        Expression::LogicalExpression(logical) => is_static_stylex_primitive_pair(
1764            &logical.left,
1765            &logical.right,
1766            before,
1767            static_values,
1768            root_reference_spans,
1769            visiting,
1770        ),
1771        Expression::ConditionalExpression(conditional) => is_static_stylex_primitive_pair(
1772            &conditional.consequent,
1773            &conditional.alternate,
1774            before,
1775            static_values,
1776            root_reference_spans,
1777            visiting,
1778        ),
1779        Expression::SequenceExpression(sequence) => {
1780            sequence.expressions.last().is_some_and(|expression| {
1781                is_definitely_static_primitive(
1782                    expression,
1783                    before,
1784                    static_values,
1785                    root_reference_spans,
1786                    visiting,
1787                )
1788            })
1789        }
1790        Expression::Identifier(identifier) => {
1791            if !root_reference_spans.contains(&identifier.span) {
1792                return false;
1793            }
1794            let name = identifier.name.as_str();
1795            let Some(&(declaration_start, value)) = static_values.get(name) else {
1796                return false;
1797            };
1798            if declaration_start >= before || !visiting.insert(name) {
1799                return false;
1800            }
1801            let is_primitive = is_definitely_static_primitive(
1802                value,
1803                declaration_start,
1804                static_values,
1805                root_reference_spans,
1806                visiting,
1807            );
1808            visiting.remove(name);
1809            is_primitive
1810        }
1811        _ => false,
1812    }
1813}
1814
1815fn is_static_stylex_primitive_pair<'a>(
1816    left: &'a Expression<'a>,
1817    right: &'a Expression<'a>,
1818    before: u32,
1819    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1820    root_reference_spans: &FxHashSet<Span>,
1821    visiting: &mut FxHashSet<&'a str>,
1822) -> bool {
1823    is_definitely_static_primitive(left, before, static_values, root_reference_spans, visiting)
1824        && is_definitely_static_primitive(
1825            right,
1826            before,
1827            static_values,
1828            root_reference_spans,
1829            visiting,
1830        )
1831}
1832
1833fn is_static_stylex_bigint_value<'a>(
1834    expression: &'a Expression<'a>,
1835    before: u32,
1836    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
1837    root_reference_spans: &FxHashSet<Span>,
1838    visiting: &mut FxHashSet<&'a str>,
1839) -> bool {
1840    let parameters = FxHashMap::default();
1841    let context = StyleXBigIntContext {
1842        before,
1843        static_values,
1844        root_reference_spans,
1845        parameters: &parameters,
1846    };
1847    is_static_stylex_bigint_value_with_context(expression, &context, visiting)
1848}
1849
1850#[derive(Clone, Copy)]
1851struct StyleXBigIntContext<'maps, 'ast> {
1852    before: u32,
1853    static_values: &'maps FxHashMap<&'ast str, (u32, &'ast Expression<'ast>)>,
1854    root_reference_spans: &'maps FxHashSet<Span>,
1855    parameters: &'maps FxHashMap<&'ast str, &'ast Expression<'ast>>,
1856}
1857
1858fn is_static_stylex_bigint_value_with_context<'a>(
1859    expression: &'a Expression<'a>,
1860    context: &StyleXBigIntContext<'_, 'a>,
1861    visiting: &mut FxHashSet<&'a str>,
1862) -> bool {
1863    match unwrap_transparent_expression(expression) {
1864        Expression::BigIntLiteral(_) => true,
1865        Expression::Identifier(identifier) => {
1866            let name = identifier.name.as_str();
1867            if let Some(value) = context.parameters.get(name) {
1868                return is_static_stylex_bigint_value_with_context(value, context, visiting);
1869            }
1870            if !context.root_reference_spans.contains(&identifier.span) {
1871                return false;
1872            }
1873            let Some(&(declaration_start, value)) = context.static_values.get(name) else {
1874                return false;
1875            };
1876            if declaration_start >= context.before || !visiting.insert(name) {
1877                return false;
1878            }
1879            let nested_context = StyleXBigIntContext {
1880                before: declaration_start,
1881                ..*context
1882            };
1883            let is_bigint =
1884                is_static_stylex_bigint_value_with_context(value, &nested_context, visiting);
1885            visiting.remove(name);
1886            is_bigint
1887        }
1888        Expression::UnaryExpression(unary)
1889            if matches!(
1890                unary.operator,
1891                UnaryOperator::UnaryNegation | UnaryOperator::BitwiseNot
1892            ) =>
1893        {
1894            is_static_stylex_bigint_value_with_context(&unary.argument, context, visiting)
1895        }
1896        Expression::BinaryExpression(binary)
1897            if binary.operator.is_numeric_or_string_binary_operator() =>
1898        {
1899            is_static_stylex_bigint_pair(&binary.left, &binary.right, context, visiting)
1900        }
1901        Expression::ConditionalExpression(conditional) => is_static_stylex_bigint_pair(
1902            &conditional.consequent,
1903            &conditional.alternate,
1904            context,
1905            visiting,
1906        ),
1907        Expression::LogicalExpression(logical) => {
1908            is_static_stylex_bigint_pair(&logical.left, &logical.right, context, visiting)
1909        }
1910        Expression::SequenceExpression(sequence) => {
1911            sequence.expressions.last().is_some_and(|expression| {
1912                is_static_stylex_bigint_value_with_context(expression, context, visiting)
1913            })
1914        }
1915        Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) => {
1916            let mut member_visiting = visiting.clone();
1917            let resolver = StyleXStaticResolver {
1918                before: context.before,
1919                static_values: context.static_values,
1920                root_reference_spans: context.root_reference_spans,
1921            };
1922            resolve_static_stylex_member(expression, &resolver, &mut member_visiting).is_some_and(
1923                |resolved| {
1924                    let nested_context = StyleXBigIntContext {
1925                        before: resolved.before,
1926                        ..*context
1927                    };
1928                    is_static_stylex_bigint_value_with_context(
1929                        resolved.value,
1930                        &nested_context,
1931                        &mut member_visiting,
1932                    )
1933                },
1934            )
1935        }
1936        Expression::CallExpression(call) => {
1937            is_static_stylex_bigint_arrow_call(call, context, visiting)
1938        }
1939        _ => false,
1940    }
1941}
1942
1943fn is_static_stylex_bigint_pair<'a>(
1944    left: &'a Expression<'a>,
1945    right: &'a Expression<'a>,
1946    context: &StyleXBigIntContext<'_, 'a>,
1947    visiting: &mut FxHashSet<&'a str>,
1948) -> bool {
1949    is_static_stylex_bigint_value_with_context(left, context, visiting)
1950        || is_static_stylex_bigint_value_with_context(right, context, visiting)
1951}
1952
1953fn is_static_stylex_bigint_arrow_call<'a>(
1954    call: &'a CallExpression<'a>,
1955    context: &StyleXBigIntContext<'_, 'a>,
1956    visiting: &mut FxHashSet<&'a str>,
1957) -> bool {
1958    let Expression::Identifier(callee) = unwrap_transparent_expression(&call.callee) else {
1959        return false;
1960    };
1961    if !context.root_reference_spans.contains(&callee.span) {
1962        return false;
1963    }
1964    let name = callee.name.as_str();
1965    let Some(&(declaration_start, value)) = context.static_values.get(name) else {
1966        return false;
1967    };
1968    let Expression::ArrowFunctionExpression(arrow) = unwrap_transparent_expression(value) else {
1969        return false;
1970    };
1971    if declaration_start >= context.before
1972        || arrow.r#async
1973        || arrow.params.rest.is_some()
1974        || arrow.params.items.len() != call.arguments.len()
1975        || !visiting.insert(name)
1976    {
1977        return false;
1978    }
1979    let Some(body) = stylex_arrow_expression_body(arrow) else {
1980        visiting.remove(name);
1981        return false;
1982    };
1983    let mut arrow_parameters = context.parameters.clone();
1984    for (parameter, argument) in arrow.params.items.iter().zip(&call.arguments) {
1985        let (BindingPattern::BindingIdentifier(binding), Some(argument)) =
1986            (&parameter.pattern, argument.as_expression())
1987        else {
1988            visiting.remove(name);
1989            return false;
1990        };
1991        arrow_parameters.insert(binding.name.as_str(), argument);
1992    }
1993    let nested_context = StyleXBigIntContext {
1994        before: context.before,
1995        parameters: &arrow_parameters,
1996        ..*context
1997    };
1998    let is_bigint = is_static_stylex_bigint_value_with_context(body, &nested_context, visiting);
1999    visiting.remove(name);
2000    is_bigint
2001}
2002
2003fn is_root_object_from_entries_call(
2004    callee: &Expression<'_>,
2005    static_values: &FxHashMap<&str, (u32, &Expression<'_>)>,
2006    root_reference_spans: &FxHashSet<Span>,
2007) -> bool {
2008    let Expression::StaticMemberExpression(member) = unwrap_transparent_expression(callee) else {
2009        return false;
2010    };
2011    let Expression::Identifier(object) = unwrap_transparent_expression(&member.object) else {
2012        return false;
2013    };
2014    object.name == "Object"
2015        && member.property.name == "fromEntries"
2016        && root_reference_spans.contains(&object.span)
2017        && !static_values.contains_key("Object")
2018}
2019
2020fn is_root_scalar_pure_call<'a>(
2021    callee: &'a Expression<'a>,
2022    before: u32,
2023    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2024    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
2025    root_reference_spans: &FxHashSet<Span>,
2026    visiting: &mut FxHashSet<&'a str>,
2027) -> bool {
2028    match unwrap_transparent_expression(callee) {
2029        Expression::Identifier(identifier) => {
2030            matches!(identifier.name.as_str(), "String" | "Number")
2031                && root_reference_spans.contains(&identifier.span)
2032                && !static_values.contains_key(identifier.name.as_str())
2033        }
2034        Expression::StaticMemberExpression(member) => {
2035            let method = member.property.name.as_str();
2036            match unwrap_transparent_expression(&member.object) {
2037                Expression::Identifier(object) if object.name == "Math" => {
2038                    root_reference_spans.contains(&object.span)
2039                        && !static_values.contains_key("Math")
2040                        && is_static_math_method(method)
2041                }
2042                object => match static_stylex_scalar_kind(
2043                    object,
2044                    before,
2045                    static_values,
2046                    imports,
2047                    root_reference_spans,
2048                    visiting,
2049                ) {
2050                    Some(StyleXScalarKind::String) => is_static_string_method(method),
2051                    Some(StyleXScalarKind::Number) => is_static_number_method(method),
2052                    Some(StyleXScalarKind::Other) | None => false,
2053                },
2054            }
2055        }
2056        _ => false,
2057    }
2058}
2059
2060#[derive(Clone, Copy)]
2061enum StyleXScalarKind {
2062    String,
2063    Number,
2064    Other,
2065}
2066
2067fn static_stylex_scalar_kind<'a>(
2068    expression: &'a Expression<'a>,
2069    before: u32,
2070    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2071    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
2072    root_reference_spans: &FxHashSet<Span>,
2073    visiting: &mut FxHashSet<&'a str>,
2074) -> Option<StyleXScalarKind> {
2075    match unwrap_transparent_expression(expression) {
2076        Expression::StringLiteral(_) => Some(StyleXScalarKind::String),
2077        Expression::NumericLiteral(_) => Some(StyleXScalarKind::Number),
2078        Expression::Identifier(identifier) => {
2079            if !root_reference_spans.contains(&identifier.span) {
2080                return None;
2081            }
2082            let name = identifier.name.as_str();
2083            let &(declaration_start, value) = static_values.get(name)?;
2084            if declaration_start >= before || !visiting.insert(name) {
2085                return None;
2086            }
2087            let kind = static_stylex_scalar_kind(
2088                value,
2089                declaration_start,
2090                static_values,
2091                imports,
2092                root_reference_spans,
2093                visiting,
2094            );
2095            visiting.remove(name);
2096            kind
2097        }
2098        Expression::CallExpression(call)
2099            if is_static_stylex_pure_call(
2100                call,
2101                before,
2102                static_values,
2103                imports,
2104                root_reference_spans,
2105                visiting,
2106            ) =>
2107        {
2108            static_stylex_scalar_call_result(&call.callee)
2109        }
2110        _ => None,
2111    }
2112}
2113
2114fn static_stylex_scalar_call_result(callee: &Expression<'_>) -> Option<StyleXScalarKind> {
2115    match unwrap_transparent_expression(callee) {
2116        Expression::Identifier(identifier) => match identifier.name.as_str() {
2117            "String" => Some(StyleXScalarKind::String),
2118            "Number" => Some(StyleXScalarKind::Number),
2119            _ => None,
2120        },
2121        Expression::StaticMemberExpression(member) => match member.property.name.as_str() {
2122            "at" | "charAt" | "concat" | "padEnd" | "padStart" | "replace" | "replaceAll"
2123            | "slice" | "substring" | "toExponential" | "toFixed" | "toLowerCase"
2124            | "toPrecision" | "toString" | "toUpperCase" | "trim" | "trimEnd" | "trimStart" => {
2125                Some(StyleXScalarKind::String)
2126            }
2127            "valueOf" => match unwrap_transparent_expression(&member.object) {
2128                Expression::StringLiteral(_) => Some(StyleXScalarKind::String),
2129                Expression::NumericLiteral(_) => Some(StyleXScalarKind::Number),
2130                _ => Some(StyleXScalarKind::Other),
2131            },
2132            method if is_static_math_method(method) => Some(StyleXScalarKind::Number),
2133            _ => Some(StyleXScalarKind::Other),
2134        },
2135        _ => None,
2136    }
2137}
2138
2139fn is_static_string_method(method: &str) -> bool {
2140    matches!(
2141        method,
2142        "at" | "charAt"
2143            | "charCodeAt"
2144            | "codePointAt"
2145            | "concat"
2146            | "endsWith"
2147            | "includes"
2148            | "indexOf"
2149            | "lastIndexOf"
2150            | "padEnd"
2151            | "padStart"
2152            | "replace"
2153            | "replaceAll"
2154            | "search"
2155            | "slice"
2156            | "startsWith"
2157            | "substring"
2158            | "toLowerCase"
2159            | "toString"
2160            | "toUpperCase"
2161            | "trim"
2162            | "trimEnd"
2163            | "trimStart"
2164            | "valueOf"
2165    )
2166}
2167
2168fn is_static_math_method(method: &str) -> bool {
2169    matches!(
2170        method,
2171        "abs"
2172            | "acos"
2173            | "acosh"
2174            | "asin"
2175            | "asinh"
2176            | "atan"
2177            | "atan2"
2178            | "atanh"
2179            | "cbrt"
2180            | "ceil"
2181            | "clz32"
2182            | "cos"
2183            | "cosh"
2184            | "exp"
2185            | "expm1"
2186            | "floor"
2187            | "fround"
2188            | "hypot"
2189            | "imul"
2190            | "log"
2191            | "log10"
2192            | "log1p"
2193            | "log2"
2194            | "max"
2195            | "min"
2196            | "pow"
2197            | "round"
2198            | "sign"
2199            | "sin"
2200            | "sinh"
2201            | "sqrt"
2202            | "tan"
2203            | "tanh"
2204            | "trunc"
2205    )
2206}
2207
2208fn is_static_stylex_entries_call<'a>(
2209    call: &'a CallExpression<'a>,
2210    before: u32,
2211    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2212    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
2213    root_reference_spans: &FxHashSet<Span>,
2214    visiting: &mut FxHashSet<&'a str>,
2215) -> bool {
2216    let Some(Expression::ArrayExpression(entries)) =
2217        call.arguments.first().and_then(Argument::as_expression)
2218    else {
2219        return false;
2220    };
2221    call.arguments.len() == 1
2222        && entries.elements.iter().all(|entry| {
2223            let Some(Expression::ArrayExpression(pair)) = entry.as_expression() else {
2224                return false;
2225            };
2226            let [key, value] = pair.elements.as_slice() else {
2227                return false;
2228            };
2229            let (Some(key), Some(value)) = (key.as_expression(), value.as_expression()) else {
2230                return false;
2231            };
2232            is_static_stylex_computed_key(
2233                key,
2234                before,
2235                static_values,
2236                imports,
2237                root_reference_spans,
2238                visiting,
2239            ) && is_static_stylex_theme_override(
2240                value,
2241                before,
2242                static_values,
2243                imports,
2244                root_reference_spans,
2245                visiting,
2246            )
2247        })
2248}
2249
2250fn is_static_number_method(method: &str) -> bool {
2251    matches!(
2252        method,
2253        "toExponential" | "toFixed" | "toPrecision" | "toString" | "valueOf"
2254    )
2255}
2256
2257fn is_static_stylex_scalar_call_shape<'a>(
2258    call: &'a CallExpression<'a>,
2259    before: u32,
2260    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2261    root_reference_spans: &FxHashSet<Span>,
2262) -> bool {
2263    let Expression::StaticMemberExpression(member) = unwrap_transparent_expression(&call.callee)
2264    else {
2265        return true;
2266    };
2267    match member.property.name.as_str() {
2268        "padStart" | "padEnd" => {
2269            static_padding_arguments_are_safe(call, before, static_values, root_reference_spans)
2270        }
2271        "at" | "charAt" | "charCodeAt" | "codePointAt" | "endsWith" | "includes" | "indexOf"
2272        | "lastIndexOf" | "slice" | "startsWith" | "substring" => call
2273            .arguments
2274            .iter()
2275            .filter_map(Argument::as_expression)
2276            .all(|argument| {
2277                !is_static_stylex_bigint_value(
2278                    argument,
2279                    before,
2280                    static_values,
2281                    root_reference_spans,
2282                    &mut FxHashSet::default(),
2283                )
2284            }),
2285        "toFixed" | "toExponential" => static_numeric_method_argument(
2286            call,
2287            0.0,
2288            100.0,
2289            before,
2290            static_values,
2291            root_reference_spans,
2292        ),
2293        "toPrecision" => static_numeric_method_argument(
2294            call,
2295            1.0,
2296            100.0,
2297            before,
2298            static_values,
2299            root_reference_spans,
2300        ),
2301        "toString"
2302            if static_stylex_scalar_call_result(&call.callee)
2303                .is_some_and(|kind| matches!(kind, StyleXScalarKind::String)) =>
2304        {
2305            static_numeric_method_argument(
2306                call,
2307                2.0,
2308                36.0,
2309                before,
2310                static_values,
2311                root_reference_spans,
2312            )
2313        }
2314        "valueOf" => call.arguments.is_empty(),
2315        _ => true,
2316    }
2317}
2318
2319fn static_numeric_method_argument<'a>(
2320    call: &'a CallExpression<'a>,
2321    min: f64,
2322    max: f64,
2323    before: u32,
2324    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2325    root_reference_spans: &FxHashSet<Span>,
2326) -> bool {
2327    let Some(argument) = call.arguments.first() else {
2328        return true;
2329    };
2330    if call.arguments.len() != 1 {
2331        return false;
2332    }
2333    let Some(argument) = argument.as_expression() else {
2334        return false;
2335    };
2336    static_stylex_numeric_value(
2337        argument,
2338        before,
2339        static_values,
2340        root_reference_spans,
2341        &mut FxHashSet::default(),
2342    )
2343    .is_some_and(|value| value.fract() == 0.0 && (min..=max).contains(&value))
2344}
2345
2346fn static_padding_arguments_are_safe<'a>(
2347    call: &'a CallExpression<'a>,
2348    before: u32,
2349    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2350    root_reference_spans: &FxHashSet<Span>,
2351) -> bool {
2352    const MAX_STATIC_PADDING: f64 = 1_000_000.0;
2353    let Some(target) = call.arguments.first() else {
2354        return true;
2355    };
2356    if call.arguments.len() > 2 {
2357        return false;
2358    }
2359    let Some(target) = target.as_expression() else {
2360        return false;
2361    };
2362    static_stylex_numeric_value(
2363        target,
2364        before,
2365        static_values,
2366        root_reference_spans,
2367        &mut FxHashSet::default(),
2368    )
2369    .is_some_and(|value| value.is_finite() && (0.0..=MAX_STATIC_PADDING).contains(&value))
2370}
2371
2372fn static_stylex_numeric_value<'a>(
2373    expression: &'a Expression<'a>,
2374    before: u32,
2375    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2376    root_reference_spans: &FxHashSet<Span>,
2377    visiting: &mut FxHashSet<&'a str>,
2378) -> Option<f64> {
2379    match unwrap_transparent_expression(expression) {
2380        Expression::NumericLiteral(value) => Some(value.value),
2381        Expression::Identifier(identifier) => {
2382            if !root_reference_spans.contains(&identifier.span) {
2383                return None;
2384            }
2385            let name = identifier.name.as_str();
2386            let &(declaration_start, value) = static_values.get(name)?;
2387            if declaration_start >= before || !visiting.insert(name) {
2388                return None;
2389            }
2390            let number = static_stylex_numeric_value(
2391                value,
2392                declaration_start,
2393                static_values,
2394                root_reference_spans,
2395                visiting,
2396            );
2397            visiting.remove(name);
2398            number
2399        }
2400        _ => None,
2401    }
2402}
2403
2404fn is_static_stylex_local_arrow_call<'a>(
2405    call: &'a CallExpression<'a>,
2406    before: u32,
2407    static_values: &FxHashMap<&'a str, (u32, &'a Expression<'a>)>,
2408    imports: &FxHashMap<&'a str, (Lib, &'a str)>,
2409    root_reference_spans: &FxHashSet<Span>,
2410    visiting: &mut FxHashSet<&'a str>,
2411) -> bool {
2412    let Expression::Identifier(callee) = unwrap_transparent_expression(&call.callee) else {
2413        return false;
2414    };
2415    if !root_reference_spans.contains(&callee.span) {
2416        return false;
2417    }
2418    let name = callee.name.as_str();
2419    let Some(&(declaration_start, value)) = static_values.get(name) else {
2420        return false;
2421    };
2422    let Expression::ArrowFunctionExpression(arrow) = unwrap_transparent_expression(value) else {
2423        return false;
2424    };
2425    if declaration_start >= before
2426        || arrow.r#async
2427        || arrow.params.rest.is_some()
2428        || arrow.params.items.len() != call.arguments.len()
2429        || !visiting.insert(name)
2430    {
2431        return false;
2432    }
2433    let mut parameters = FxHashMap::default();
2434    for (parameter, argument) in arrow.params.items.iter().zip(&call.arguments) {
2435        let (BindingPattern::BindingIdentifier(binding), Some(argument)) =
2436            (&parameter.pattern, argument.as_expression())
2437        else {
2438            visiting.remove(name);
2439            return false;
2440        };
2441        parameters.insert(binding.name.as_str(), argument);
2442    }
2443    let arguments_static = call.arguments.iter().all(|argument| {
2444        argument.as_expression().is_some_and(|argument| {
2445            is_static_stylex_theme_override(
2446                argument,
2447                before,
2448                static_values,
2449                imports,
2450                root_reference_spans,
2451                visiting,
2452            )
2453        })
2454    });
2455    let context = StyleXArrowStaticContext {
2456        before,
2457        static_values,
2458        imports,
2459        root_reference_spans,
2460        parameters,
2461    };
2462    let body_static = arguments_static
2463        && stylex_arrow_expression_body(arrow)
2464            .is_some_and(|body| is_static_stylex_arrow_body(body, &context, visiting));
2465    visiting.remove(name);
2466    body_static
2467}
2468
2469struct StyleXArrowStaticContext<'maps, 'ast> {
2470    before: u32,
2471    static_values: &'maps FxHashMap<&'ast str, (u32, &'ast Expression<'ast>)>,
2472    imports: &'maps FxHashMap<&'ast str, (Lib, &'ast str)>,
2473    root_reference_spans: &'maps FxHashSet<Span>,
2474    parameters: FxHashMap<&'ast str, &'ast Expression<'ast>>,
2475}
2476
2477fn stylex_arrow_expression_body<'a>(
2478    arrow: &'a ArrowFunctionExpression<'a>,
2479) -> Option<&'a Expression<'a>> {
2480    arrow.body.as_expression()
2481}
2482
2483fn resolve_stylex_arrow_parameter_member<'a>(
2484    expression: &'a Expression<'a>,
2485    context: &StyleXArrowStaticContext<'_, 'a>,
2486    visiting: &mut FxHashSet<&'a str>,
2487) -> Option<ResolvedStyleXStaticMember<'a>> {
2488    let (base, _, segments) = binding_access_object_chain(expression)?;
2489    let argument = context.parameters.get(base)?;
2490    let mut resolver = StyleXStaticResolver {
2491        before: context.before,
2492        static_values: context.static_values,
2493        root_reference_spans: context.root_reference_spans,
2494    };
2495    let mut object = resolve_static_stylex_object(argument, &resolver, visiting)?;
2496    let mut segments = segments.into_iter().peekable();
2497    while let Some(segment) = segments.next() {
2498        let member = resolve_static_stylex_object_property(object, &segment, &resolver, visiting)?;
2499        if segments.peek().is_none() {
2500            return Some(member);
2501        }
2502        resolver = StyleXStaticResolver {
2503            before: member.before,
2504            ..resolver
2505        };
2506        object = resolve_static_stylex_object(member.value, &resolver, visiting)?;
2507    }
2508    None
2509}
2510
2511fn is_static_stylex_arrow_body<'a>(
2512    expression: &'a Expression<'a>,
2513    context: &StyleXArrowStaticContext<'_, 'a>,
2514    visiting: &mut FxHashSet<&'a str>,
2515) -> bool {
2516    let expression = unwrap_transparent_expression(expression);
2517    match expression {
2518        Expression::Identifier(identifier)
2519            if context.parameters.contains_key(identifier.name.as_str()) =>
2520        {
2521            true
2522        }
2523        Expression::TemplateLiteral(template) => template
2524            .expressions
2525            .iter()
2526            .all(|expression| is_static_stylex_arrow_body(expression, context, visiting)),
2527        Expression::UnaryExpression(unary) => {
2528            let bigint_context = StyleXBigIntContext {
2529                before: context.before,
2530                static_values: context.static_values,
2531                root_reference_spans: context.root_reference_spans,
2532                parameters: &context.parameters,
2533            };
2534            !unary.operator.is_delete()
2535                && (unary.operator != UnaryOperator::UnaryPlus
2536                    || !is_static_stylex_bigint_value_with_context(
2537                        &unary.argument,
2538                        &bigint_context,
2539                        &mut visiting.clone(),
2540                    ))
2541                && is_static_stylex_arrow_body(&unary.argument, context, visiting)
2542        }
2543        Expression::BinaryExpression(binary) => {
2544            let bigint_context = StyleXBigIntContext {
2545                before: context.before,
2546                static_values: context.static_values,
2547                root_reference_spans: context.root_reference_spans,
2548                parameters: &context.parameters,
2549            };
2550            !binary.operator.is_relational()
2551                && (!binary.operator.is_numeric_or_string_binary_operator()
2552                    || !is_static_stylex_bigint_pair(
2553                        &binary.left,
2554                        &binary.right,
2555                        &bigint_context,
2556                        &mut visiting.clone(),
2557                    ))
2558                && is_static_stylex_arrow_body(&binary.left, context, visiting)
2559                && is_static_stylex_arrow_body(&binary.right, context, visiting)
2560        }
2561        Expression::LogicalExpression(logical) => {
2562            is_static_stylex_arrow_body(&logical.left, context, visiting)
2563                && is_static_stylex_arrow_body(&logical.right, context, visiting)
2564        }
2565        Expression::ConditionalExpression(conditional) => [
2566            &conditional.test,
2567            &conditional.consequent,
2568            &conditional.alternate,
2569        ]
2570        .into_iter()
2571        .all(|expression| is_static_stylex_arrow_body(expression, context, visiting)),
2572        Expression::SequenceExpression(sequence) => sequence
2573            .expressions
2574            .iter()
2575            .all(|expression| is_static_stylex_arrow_body(expression, context, visiting)),
2576        Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) => {
2577            if let Some(resolved) =
2578                resolve_stylex_arrow_parameter_member(expression, context, visiting)
2579            {
2580                return is_static_stylex_theme_override(
2581                    resolved.value,
2582                    resolved.before,
2583                    context.static_values,
2584                    context.imports,
2585                    context.root_reference_spans,
2586                    visiting,
2587                );
2588            }
2589            is_static_stylex_theme_override(
2590                expression,
2591                context.before,
2592                context.static_values,
2593                context.imports,
2594                context.root_reference_spans,
2595                visiting,
2596            )
2597        }
2598        _ => is_static_stylex_theme_override(
2599            expression,
2600            context.before,
2601            context.static_values,
2602            context.imports,
2603            context.root_reference_spans,
2604            visiting,
2605        ),
2606    }
2607}
2608
2609fn is_root_stylex_static_call(
2610    callee: &Expression<'_>,
2611    imports: &FxHashMap<&str, (Lib, &str)>,
2612    root_reference_spans: &FxHashSet<Span>,
2613) -> bool {
2614    match unwrap_transparent_expression(callee) {
2615        Expression::Identifier(identifier) => {
2616            root_reference_spans.contains(&identifier.span)
2617                && imports
2618                    .get(identifier.name.as_str())
2619                    .is_some_and(|(lib, role)| {
2620                        *lib == Lib::StyleX
2621                            && matches!(*role, "unstable_conditional" | "keyframes" | "positionTry")
2622                    })
2623        }
2624        Expression::StaticMemberExpression(member) => match &member.object {
2625            Expression::Identifier(object) => {
2626                root_reference_spans.contains(&object.span)
2627                    && imports
2628                        .get(object.name.as_str())
2629                        .is_some_and(|(lib, role)| {
2630                            *lib == Lib::StyleX
2631                                && (if *role == "types" {
2632                                    is_stylex_type_helper(member.property.name.as_str())
2633                                } else {
2634                                    matches!(
2635                                        member.property.name.as_str(),
2636                                        "unstable_conditional" | "keyframes" | "positionTry"
2637                                    )
2638                                })
2639                        })
2640            }
2641            Expression::StaticMemberExpression(namespace) => {
2642                let Expression::Identifier(object) = &namespace.object else {
2643                    return false;
2644                };
2645                root_reference_spans.contains(&object.span)
2646                    && imports
2647                        .get(object.name.as_str())
2648                        .is_some_and(|(lib, _)| *lib == Lib::StyleX)
2649                    && namespace.property.name.as_str() == "types"
2650                    && is_stylex_type_helper(member.property.name.as_str())
2651            }
2652            _ => false,
2653        },
2654        _ => false,
2655    }
2656}
2657
2658fn is_stylex_type_helper(name: &str) -> bool {
2659    matches!(
2660        name,
2661        "angle"
2662            | "color"
2663            | "image"
2664            | "integer"
2665            | "length"
2666            | "lengthPercentage"
2667            | "number"
2668            | "percentage"
2669            | "resolution"
2670            | "time"
2671            | "transformFunction"
2672            | "transformList"
2673            | "url"
2674    )
2675}
2676
2677/// The value of a static string or numeric computed-member key, or `None` for a
2678/// dynamic key that cannot be resolved without executing code.
2679fn static_computed_key(expr: &Expression<'_>) -> Option<String> {
2680    match unwrap_transparent_expression(expr) {
2681        Expression::StringLiteral(lit) => Some(lit.value.text().into_owned()),
2682        Expression::NumericLiteral(lit) => Some(format_numeric_token(lit)),
2683        Expression::TemplateLiteral(template) if template.expressions.is_empty() => template
2684            .quasis
2685            .first()
2686            .map(|quasi| quasi.value.raw.to_string()),
2687        _ => None,
2688    }
2689}
2690
2691#[derive(Clone, Copy)]
2692struct RootBinding<'a> {
2693    name: &'a str,
2694    span: Span,
2695    reference_id: Option<ReferenceId>,
2696}
2697
2698fn root_binding<'a>(identifier: &'a IdentifierReference<'_>) -> RootBinding<'a> {
2699    RootBinding {
2700        name: identifier.name.as_str(),
2701        span: identifier.span,
2702        reference_id: identifier.reference_id.get(),
2703    }
2704}
2705
2706fn expression_root_binding<'a, 'b: 'a>(expr: &'a Expression<'b>) -> Option<RootBinding<'a>> {
2707    match expr {
2708        Expression::Identifier(id) => Some(root_binding(id)),
2709        Expression::StaticMemberExpression(member) => expression_root_binding(&member.object),
2710        Expression::ComputedMemberExpression(member) => expression_root_binding(&member.object),
2711        Expression::ParenthesizedExpression(expression) => {
2712            expression_root_binding(&expression.expression)
2713        }
2714        Expression::TSAsExpression(expression) => expression_root_binding(&expression.expression),
2715        Expression::TSSatisfiesExpression(expression) => {
2716            expression_root_binding(&expression.expression)
2717        }
2718        Expression::TSNonNullExpression(expression) => {
2719            expression_root_binding(&expression.expression)
2720        }
2721        Expression::TSTypeAssertion(expression) => expression_root_binding(&expression.expression),
2722        _ => None,
2723    }
2724}
2725
2726fn assignment_target_root_bindings<'a, 'b: 'a>(
2727    target: &'a AssignmentTarget<'b>,
2728) -> Vec<RootBinding<'a>> {
2729    let mut bindings = Vec::new();
2730    collect_assignment_target_root_bindings(target, &mut bindings);
2731    bindings
2732}
2733
2734fn assignment_target_receiver_expression<'a, 'b: 'a>(
2735    target: &'a AssignmentTarget<'b>,
2736) -> Option<&'a Expression<'b>> {
2737    match target {
2738        AssignmentTarget::StaticMemberExpression(member) => Some(&member.object),
2739        AssignmentTarget::ComputedMemberExpression(member) => Some(&member.object),
2740        AssignmentTarget::TSAsExpression(expression) => {
2741            call_receiver_expression(&expression.expression)
2742        }
2743        AssignmentTarget::TSSatisfiesExpression(expression) => {
2744            call_receiver_expression(&expression.expression)
2745        }
2746        AssignmentTarget::TSNonNullExpression(expression) => {
2747            call_receiver_expression(&expression.expression)
2748        }
2749        AssignmentTarget::TSTypeAssertion(expression) => {
2750            call_receiver_expression(&expression.expression)
2751        }
2752        _ => None,
2753    }
2754}
2755
2756fn collect_assignment_target_root_bindings<'a, 'b: 'a>(
2757    target: &'a AssignmentTarget<'b>,
2758    bindings: &mut Vec<RootBinding<'a>>,
2759) {
2760    match target {
2761        AssignmentTarget::AssignmentTargetIdentifier(id) => bindings.push(root_binding(id)),
2762        AssignmentTarget::StaticMemberExpression(member) => {
2763            bindings.extend(expression_root_binding(&member.object));
2764        }
2765        AssignmentTarget::ComputedMemberExpression(member) => {
2766            bindings.extend(expression_root_binding(&member.object));
2767        }
2768        AssignmentTarget::TSAsExpression(expression) => {
2769            bindings.extend(expression_root_binding(&expression.expression));
2770        }
2771        AssignmentTarget::TSSatisfiesExpression(expression) => {
2772            bindings.extend(expression_root_binding(&expression.expression));
2773        }
2774        AssignmentTarget::TSNonNullExpression(expression) => {
2775            bindings.extend(expression_root_binding(&expression.expression));
2776        }
2777        AssignmentTarget::TSTypeAssertion(expression) => {
2778            bindings.extend(expression_root_binding(&expression.expression));
2779        }
2780        AssignmentTarget::ArrayAssignmentTarget(array) => {
2781            for element in array.elements.iter().flatten() {
2782                collect_assignment_target_maybe_default_root_bindings(element, bindings);
2783            }
2784            if let Some(rest) = &array.rest {
2785                collect_assignment_target_root_bindings(&rest.target, bindings);
2786            }
2787        }
2788        AssignmentTarget::ObjectAssignmentTarget(object) => {
2789            for property in &object.properties {
2790                match property {
2791                    AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(property) => {
2792                        bindings.push(root_binding(&property.binding));
2793                    }
2794                    AssignmentTargetProperty::AssignmentTargetPropertyProperty(property) => {
2795                        collect_assignment_target_maybe_default_root_bindings(
2796                            &property.binding,
2797                            bindings,
2798                        );
2799                    }
2800                }
2801            }
2802            if let Some(rest) = &object.rest {
2803                collect_assignment_target_root_bindings(&rest.target, bindings);
2804            }
2805        }
2806        AssignmentTarget::PrivateFieldExpression(_) => {}
2807    }
2808}
2809
2810fn collect_assignment_target_maybe_default_root_bindings<'a, 'b: 'a>(
2811    target: &'a AssignmentTargetMaybeDefault<'b>,
2812    bindings: &mut Vec<RootBinding<'a>>,
2813) {
2814    if let AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(default) = target {
2815        collect_assignment_target_root_bindings(&default.binding, bindings);
2816    } else if let Some(target) = target.as_assignment_target() {
2817        collect_assignment_target_root_bindings(target, bindings);
2818    }
2819}
2820
2821fn simple_assignment_target_root_binding<'a, 'b: 'a>(
2822    target: &'a SimpleAssignmentTarget<'b>,
2823) -> Option<RootBinding<'a>> {
2824    match target {
2825        SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => Some(root_binding(id)),
2826        SimpleAssignmentTarget::StaticMemberExpression(member) => {
2827            expression_root_binding(&member.object)
2828        }
2829        SimpleAssignmentTarget::ComputedMemberExpression(member) => {
2830            expression_root_binding(&member.object)
2831        }
2832        SimpleAssignmentTarget::TSAsExpression(expression) => {
2833            expression_root_binding(&expression.expression)
2834        }
2835        SimpleAssignmentTarget::TSSatisfiesExpression(expression) => {
2836            expression_root_binding(&expression.expression)
2837        }
2838        SimpleAssignmentTarget::TSNonNullExpression(expression) => {
2839            expression_root_binding(&expression.expression)
2840        }
2841        SimpleAssignmentTarget::TSTypeAssertion(expression) => {
2842            expression_root_binding(&expression.expression)
2843        }
2844        SimpleAssignmentTarget::PrivateFieldExpression(_) => None,
2845    }
2846}
2847
2848/// Where the access binding comes from for a recognized token-definition call.
2849#[derive(Clone, Copy)]
2850enum BindingSource {
2851    /// The assigned identifier (`const vars = ...`).
2852    LhsIdent,
2853    /// An element of an array-destructure (`const [_, vars] = ...`).
2854    TupleElement(usize),
2855}
2856
2857/// A recognized token-definition call: where the binding comes from and which
2858/// argument carries the token object.
2859#[derive(Clone, Copy)]
2860struct Recognized {
2861    binding_source: BindingSource,
2862    tokens_arg: usize,
2863    origin: CssInJsTokenOrigin,
2864    stylex_shape: Option<StyleXTokenShape>,
2865}
2866
2867#[derive(Clone, Copy)]
2868enum StyleXTokenShape {
2869    Flat,
2870    Nested,
2871}
2872
2873/// Collects token-definition sites, gated on import provenance.
2874struct TokenDefCollector<'a> {
2875    lines: LineCounter<'a>,
2876    /// local-binding name -> (library, canonical role). Mirrors the
2877    /// `css_in_js_object` provenance map but for token-definition roles.
2878    imports: FxHashMap<&'a str, (Lib, &'a str)>,
2879    /// Top-level immutable object literals available to macro calls in source
2880    /// order. Mutable or non-object bindings are deliberately absent.
2881    const_objects: FxHashMap<SymbolId, (u32, &'a ObjectExpression<'a>)>,
2882    /// Every resolved reference to a top-level immutable object literal.
2883    const_object_references: FxHashMap<ReferenceId, SymbolId>,
2884    /// Top-level constant condition names used by computed StyleX condition keys.
2885    const_strings: FxHashMap<ReferenceId, (u32, oxc_str::JSStr<'a>)>,
2886    nested_depth: u32,
2887    collecting_mutations: bool,
2888    defs: Vec<CssInJsTokenDef>,
2889}
2890
2891impl<'a> TokenDefCollector<'a> {
2892    fn new(source: &'a str) -> Self {
2893        Self {
2894            lines: LineCounter::new(source),
2895            imports: FxHashMap::default(),
2896            const_objects: FxHashMap::default(),
2897            const_object_references: FxHashMap::default(),
2898            const_strings: FxHashMap::default(),
2899            nested_depth: 0,
2900            collecting_mutations: false,
2901            defs: Vec::new(),
2902        }
2903    }
2904
2905    /// Map each import binding from a recognized token library to its library +
2906    /// canonical role. Named imports dispatch on the imported (canonical) name so
2907    /// `import { createTheme as ct }` still fires; default / namespace bindings
2908    /// (`import * as stylex`) carry the local name for member-call recognition.
2909    fn build_import_map(&mut self, program: &Program<'a>) {
2910        for stmt in &program.body {
2911            let Statement::ImportDeclaration(decl) = stmt else {
2912                continue;
2913            };
2914            if decl.import_kind.is_type() {
2915                continue;
2916            }
2917            let Some(lib) = module_library(&decl.source.value.text()) else {
2918                continue;
2919            };
2920            let Some(specifiers) = &decl.specifiers else {
2921                continue;
2922            };
2923            for specifier in specifiers {
2924                let (local, role) = match specifier {
2925                    ImportDeclarationSpecifier::ImportSpecifier(s) if !s.import_kind.is_type() => {
2926                        (s.local.name.as_str(), s.imported.name().as_str())
2927                    }
2928                    ImportDeclarationSpecifier::ImportSpecifier(_) => continue,
2929                    ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
2930                        (s.local.name.as_str(), s.local.name.as_str())
2931                    }
2932                    ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
2933                        (s.local.name.as_str(), s.local.name.as_str())
2934                    }
2935                };
2936                self.imports.insert(local, (lib, role));
2937            }
2938        }
2939    }
2940
2941    fn build_const_object_map(&mut self, program: &'a Program<'a>, scoping: &Scoping) {
2942        for stmt in &program.body {
2943            let declaration = match stmt {
2944                Statement::VariableDeclaration(declaration) => Some(&**declaration),
2945                Statement::ExportDeclaration(export) => match &export.declaration {
2946                    Declaration::VariableDeclaration(declaration) => Some(&**declaration),
2947                    _ => None,
2948                },
2949                _ => None,
2950            };
2951            let Some(declaration) = declaration else {
2952                continue;
2953            };
2954            if declaration.kind != VariableDeclarationKind::Const {
2955                continue;
2956            }
2957            for declarator in &declaration.declarations {
2958                let BindingPattern::BindingIdentifier(binding) = &declarator.id else {
2959                    continue;
2960                };
2961                let Some(symbol_id) = binding.symbol_id.get() else {
2962                    continue;
2963                };
2964                let declaration_start = declarator.span.start;
2965                match declarator.init.as_ref().map(unwrap_transparent_expression) {
2966                    Some(Expression::ObjectExpression(obj)) => {
2967                        self.const_objects
2968                            .insert(symbol_id, (declaration_start, obj));
2969                        self.const_object_references.extend(
2970                            scoping
2971                                .get_resolved_reference_ids(symbol_id)
2972                                .iter()
2973                                .copied()
2974                                .map(|reference_id| (reference_id, symbol_id)),
2975                        );
2976                    }
2977                    Some(Expression::StringLiteral(value)) => {
2978                        self.const_strings.extend(
2979                            scoping
2980                                .get_resolved_reference_ids(symbol_id)
2981                                .iter()
2982                                .copied()
2983                                .map(|reference_id| {
2984                                    (reference_id, (declaration_start, value.value))
2985                                }),
2986                        );
2987                    }
2988                    _ => {}
2989                }
2990            }
2991        }
2992    }
2993
2994    /// Resolve a call's callee to `(library, role)` if its binding is a recognized
2995    /// token-library import. Handles both a named/aliased import callee
2996    /// (`defineVars(...)`) and a namespace member call (`stylex.defineVars(...)`).
2997    fn callee_role(&self, callee: &Expression<'a>) -> Option<(Lib, &'a str)> {
2998        match callee {
2999            Expression::Identifier(id) => self.imports.get(id.name.as_str()).copied(),
3000            Expression::StaticMemberExpression(member) => {
3001                let Expression::Identifier(obj) = &member.object else {
3002                    return None;
3003                };
3004                let (lib, _) = *self.imports.get(obj.name.as_str())?;
3005                // Member-call role is the accessed property (`stylex.defineVars`).
3006                Some((lib, member.property.name.as_str()))
3007            }
3008            _ => None,
3009        }
3010    }
3011
3012    /// Dispatch `(library, role, arg_count)` to a recognized token-definition
3013    /// form, or `None` (unrecognized, or a contract-implementation form whose
3014    /// contract is the canonical definition).
3015    fn recognize(lib: Lib, role: &str, arg_count: usize) -> Option<Recognized> {
3016        let single = |tokens_arg, origin, stylex_shape| {
3017            Some(Recognized {
3018                binding_source: BindingSource::LhsIdent,
3019                tokens_arg,
3020                origin,
3021                stylex_shape,
3022            })
3023        };
3024        match (lib, role) {
3025            // `defineVars(obj)` / `createThemeContract(obj)`: binding = the assigned
3026            // identifier, token object = arg 0.
3027            (Lib::StyleX, "defineVars") if arg_count >= 1 => {
3028                single(0, CssInJsTokenOrigin::StyleX, Some(StyleXTokenShape::Flat))
3029            }
3030            (Lib::StyleX, "unstable_defineVarsNested") if arg_count >= 1 => single(
3031                0,
3032                CssInJsTokenOrigin::StyleX,
3033                Some(StyleXTokenShape::Nested),
3034            ),
3035            (Lib::VanillaExtract, "createThemeContract") if arg_count >= 1 => {
3036                single(0, CssInJsTokenOrigin::VanillaExtract, None)
3037            }
3038            // 1-arg createTheme returns [themeClass, vars]; tokens on the second
3039            // destructure element. The 2-arg (contract, tokens) form fills an
3040            // existing contract and is skipped (createThemeContract is canonical).
3041            (Lib::VanillaExtract, "createTheme") if arg_count == 1 => Some(Recognized {
3042                binding_source: BindingSource::TupleElement(1),
3043                tokens_arg: 0,
3044                origin: CssInJsTokenOrigin::VanillaExtract,
3045                stylex_shape: None,
3046            }),
3047            // 2-arg createGlobalTheme(selector, tokens) returns the vars object;
3048            // the 3-arg (selector, contract, tokens) form returns void (contract
3049            // canonical), so only the 2-arg form is a definition site here.
3050            (Lib::VanillaExtract, "createGlobalTheme") if arg_count == 2 => {
3051                single(1, CssInJsTokenOrigin::VanillaExtract, None)
3052            }
3053            (Lib::Panda, "defineTokens") if arg_count >= 1 => {
3054                single(0, CssInJsTokenOrigin::Panda, None)
3055            }
3056            _ => None,
3057        }
3058    }
3059
3060    /// Extract the access binding name from a declarator's binding pattern for the
3061    /// recognized binding source.
3062    fn binding_name(decl: &VariableDeclarator<'a>, source: BindingSource) -> Option<&'a str> {
3063        match source {
3064            BindingSource::LhsIdent => match &decl.id {
3065                BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
3066                _ => None,
3067            },
3068            BindingSource::TupleElement(index) => {
3069                let BindingPattern::ArrayPattern(arr) = &decl.id else {
3070                    return None;
3071                };
3072                let element = arr.elements.get(index)?.as_ref()?;
3073                match element {
3074                    BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
3075                    _ => None,
3076                }
3077            }
3078        }
3079    }
3080
3081    fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
3082        let Some(Expression::CallExpression(call)) = &decl.init else {
3083            return;
3084        };
3085        if self.process_panda_config_call(call) {
3086            return;
3087        }
3088        let Some((lib, role)) = self.callee_role(&call.callee) else {
3089            return;
3090        };
3091        let Some(recognized) = Self::recognize(lib, role, call.arguments.len()) else {
3092            return;
3093        };
3094        let Some(binding) = Self::binding_name(decl, recognized.binding_source) else {
3095            return;
3096        };
3097        let Some(obj) = self.resolve_object_argument(call, recognized.tokens_arg) else {
3098            return;
3099        };
3100        let mut tokens = Vec::new();
3101        let complete = if let Some(shape) = recognized.stylex_shape {
3102            let context = StyleXTokenContext {
3103                imports: &self.imports,
3104                const_strings: &self.const_strings,
3105                before: call.span.start,
3106            };
3107            collect_stylex_token_object(&mut self.lines, obj, "", shape, &context, &mut tokens)
3108        } else {
3109            collect_token_leaves(&mut self.lines, obj, "", recognized.origin, &mut tokens);
3110            true
3111        };
3112        if !complete {
3113            return;
3114        }
3115        if tokens.is_empty() {
3116            return;
3117        }
3118        self.defs.push(CssInJsTokenDef {
3119            binding: binding.to_owned(),
3120            origin: recognized.origin,
3121            tokens,
3122        });
3123    }
3124
3125    fn resolve_object_argument(
3126        &self,
3127        call: &'a oxc_ast::ast::CallExpression<'a>,
3128        index: usize,
3129    ) -> Option<&'a ObjectExpression<'a>> {
3130        let expr = call.arguments.get(index)?.as_expression()?;
3131        match unwrap_transparent_expression(expr) {
3132            Expression::ObjectExpression(obj) => Some(obj),
3133            Expression::Identifier(id) => {
3134                let reference_id = id.reference_id.get()?;
3135                let symbol_id = self.const_object_references.get(&reference_id)?;
3136                let (declaration_start, object) = self.const_objects.get(symbol_id)?;
3137                (*declaration_start < call.span.start).then_some(*object)
3138            }
3139            _ => None,
3140        }
3141    }
3142
3143    fn invalidate_const_object(&mut self, binding: Option<RootBinding<'_>>) {
3144        let Some(reference_id) = binding.and_then(|binding| binding.reference_id) else {
3145            return;
3146        };
3147        let Some(symbol_id) = self.const_object_references.get(&reference_id) else {
3148            return;
3149        };
3150        self.const_objects.remove(symbol_id);
3151    }
3152
3153    fn process_panda_config_call(&mut self, call: &oxc_ast::ast::CallExpression<'a>) -> bool {
3154        let Some((Lib::Panda, "defineConfig")) = self.callee_role(&call.callee) else {
3155            return false;
3156        };
3157        let Some(Argument::ObjectExpression(obj)) = call.arguments.first() else {
3158            return true;
3159        };
3160        let mut tokens = Vec::new();
3161        collect_panda_config_token_leaves(&mut self.lines, obj, &mut tokens);
3162        if !tokens.is_empty() {
3163            self.defs.push(CssInJsTokenDef {
3164                binding: PANDA_CONFIG_BINDING.to_string(),
3165                origin: CssInJsTokenOrigin::Panda,
3166                tokens,
3167            });
3168        }
3169        true
3170    }
3171}
3172
3173struct StyleXTokenContext<'maps, 'ast> {
3174    imports: &'maps FxHashMap<&'ast str, (Lib, &'ast str)>,
3175    const_strings: &'maps FxHashMap<ReferenceId, (u32, oxc_str::JSStr<'ast>)>,
3176    before: u32,
3177}
3178
3179fn collect_stylex_token_object(
3180    lines: &mut LineCounter<'_>,
3181    obj: &ObjectExpression<'_>,
3182    prefix: &str,
3183    shape: StyleXTokenShape,
3184    context: &StyleXTokenContext<'_, '_>,
3185    out: &mut Vec<CssInJsToken>,
3186) -> bool {
3187    for prop in &obj.properties {
3188        let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3189            return false;
3190        };
3191        let Some(key) = prop.key.static_name() else {
3192            return false;
3193        };
3194        let path = if prefix.is_empty() {
3195            key.text().into_owned()
3196        } else {
3197            format!("{prefix}.{}", key.text())
3198        };
3199        let line = lines.line_at(prop.key.span().start);
3200        match shape {
3201            StyleXTokenShape::Flat => out.push(CssInJsToken {
3202                path,
3203                def_line: line,
3204                value: stylex_static_token_value(&prop.value, context.imports),
3205            }),
3206            StyleXTokenShape::Nested => match unwrap_transparent_expression(&prop.value) {
3207                Expression::ObjectExpression(nested)
3208                    if !is_stylex_conditional_object(
3209                        nested,
3210                        context.const_strings,
3211                        context.before,
3212                    ) =>
3213                {
3214                    if !collect_stylex_token_object(lines, nested, &path, shape, context, out) {
3215                        return false;
3216                    }
3217                }
3218                value => out.push(CssInJsToken {
3219                    path,
3220                    def_line: line,
3221                    value: stylex_static_token_value(value, context.imports),
3222                }),
3223            },
3224        }
3225    }
3226    true
3227}
3228
3229fn is_stylex_conditional_object(
3230    obj: &ObjectExpression<'_>,
3231    const_strings: &FxHashMap<ReferenceId, (u32, oxc_str::JSStr<'_>)>,
3232    before: u32,
3233) -> bool {
3234    let mut has_default = false;
3235    for prop in &obj.properties {
3236        let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3237            return false;
3238        };
3239        let key = if prop.computed {
3240            match prop.key.as_expression() {
3241                Some(Expression::Identifier(id)) => {
3242                    let Some(reference_id) = id.reference_id.get() else {
3243                        return false;
3244                    };
3245                    let Some((declaration_start, condition)) = const_strings.get(&reference_id)
3246                    else {
3247                        return false;
3248                    };
3249                    if *declaration_start >= before {
3250                        return false;
3251                    }
3252                    condition.text().into_owned()
3253                }
3254                Some(expression) => {
3255                    let Some(condition) = static_computed_key(expression) else {
3256                        return false;
3257                    };
3258                    condition
3259                }
3260                None => return false,
3261            }
3262        } else {
3263            let Some(key) = prop.key.static_name() else {
3264                return false;
3265            };
3266            key.text().into_owned()
3267        };
3268        if key == "default" {
3269            has_default = true;
3270        } else if !key.starts_with('@') {
3271            return false;
3272        }
3273    }
3274    has_default
3275}
3276
3277fn stylex_static_token_value(
3278    value: &Expression<'_>,
3279    imports: &FxHashMap<&str, (Lib, &str)>,
3280) -> Option<String> {
3281    match unwrap_transparent_expression(value) {
3282        Expression::ObjectExpression(obj) => obj.properties.iter().find_map(|prop| {
3283            let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3284                return None;
3285            };
3286            (prop.key.static_name().is_some_and(|name| name == "default"))
3287                .then(|| stylex_static_token_value(&prop.value, imports))
3288                .flatten()
3289        }),
3290        Expression::CallExpression(call) if is_stylex_static_value_call(&call.callee, imports) => {
3291            call.arguments
3292                .first()
3293                .and_then(Argument::as_expression)
3294                .and_then(|value| stylex_static_token_value(value, imports))
3295        }
3296        _ => static_token_value(value),
3297    }
3298}
3299
3300fn is_stylex_static_value_call(
3301    callee: &Expression<'_>,
3302    imports: &FxHashMap<&str, (Lib, &str)>,
3303) -> bool {
3304    match callee {
3305        Expression::Identifier(id) => imports
3306            .get(id.name.as_str())
3307            .is_some_and(|(lib, role)| *lib == Lib::StyleX && *role == "unstable_conditional"),
3308        Expression::StaticMemberExpression(member) => match &member.object {
3309            Expression::Identifier(object) => {
3310                imports
3311                    .get(object.name.as_str())
3312                    .is_some_and(|(lib, role)| {
3313                        *lib == Lib::StyleX
3314                            && (*role == "types"
3315                                || member.property.name.as_str() == "unstable_conditional")
3316                    })
3317            }
3318            Expression::StaticMemberExpression(namespace) => {
3319                let Expression::Identifier(object) = &namespace.object else {
3320                    return false;
3321                };
3322                imports.get(object.name.as_str()).is_some_and(|(lib, _)| {
3323                    *lib == Lib::StyleX && namespace.property.name.as_str() == "types"
3324                })
3325            }
3326            _ => false,
3327        },
3328        _ => false,
3329    }
3330}
3331
3332/// Flatten an object literal into dotted LEAF paths. An inline-object value
3333/// recurses (an intermediate token GROUP, not a token); a value-producing
3334/// expression (string / number / `null` contract leaf / call like
3335/// `px(2 * grid)` / template / member access like `colors.red['500']`) is a LEAF
3336/// token. A BARE IDENTIFIER value (`palette: tailwindPalette`) is SKIPPED: it
3337/// references something whose structure is invisible here, most often an imported
3338/// token GROUP (recording it as a leaf would invent a phantom token and wrongly
3339/// credit every `vars.palette.<x>` access to it). Spreads and computed keys are
3340/// skipped because they cannot be resolved statically.
3341fn collect_token_leaves(
3342    lines: &mut LineCounter<'_>,
3343    obj: &ObjectExpression<'_>,
3344    prefix: &str,
3345    origin: CssInJsTokenOrigin,
3346    out: &mut Vec<CssInJsToken>,
3347) {
3348    for prop in &obj.properties {
3349        let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3350            continue;
3351        };
3352        let Some(key) = prop.key.static_name() else {
3353            continue;
3354        };
3355        let path = if prefix.is_empty() {
3356            key.text().into_owned()
3357        } else {
3358            format!("{prefix}.{}", key.text())
3359        };
3360        match &prop.value {
3361            Expression::ObjectExpression(nested)
3362                if origin == CssInJsTokenOrigin::Panda
3363                    && !prefix.is_empty()
3364                    && object_has_static_key(nested, "value") =>
3365            {
3366                out.push(CssInJsToken {
3367                    path,
3368                    def_line: lines.line_at(prop.key.span().start),
3369                    value: object_static_property_value(nested, "value"),
3370                });
3371            }
3372            Expression::ObjectExpression(nested) => {
3373                collect_token_leaves(lines, nested, &path, origin, out);
3374            }
3375            // A bare identifier is an unresolvable reference, usually an imported
3376            // token group; do not record it as a leaf.
3377            Expression::Identifier(_) => {}
3378            _ => out.push(CssInJsToken {
3379                value: static_token_value(&prop.value),
3380                path,
3381                def_line: lines.line_at(prop.key.span().start),
3382            }),
3383        }
3384    }
3385}
3386
3387fn object_static_property_value(obj: &ObjectExpression<'_>, wanted: &str) -> Option<String> {
3388    obj.properties.iter().find_map(|prop| {
3389        let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3390            return None;
3391        };
3392        (prop.key.static_name().is_some_and(|name| name == wanted))
3393            .then(|| static_token_value(&prop.value))
3394            .flatten()
3395    })
3396}
3397
3398fn static_token_value(value: &Expression<'_>) -> Option<String> {
3399    match value {
3400        Expression::StringLiteral(lit) => {
3401            Some(lit.value.text().trim().to_owned()).filter(|text| !text.is_empty())
3402        }
3403        Expression::NumericLiteral(num) => Some(format_numeric_token(num)),
3404        Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
3405            if let Expression::NumericLiteral(num) = &unary.argument {
3406                Some(format!("-{}", format_numeric_token(num)))
3407            } else {
3408                None
3409            }
3410        }
3411        _ => None,
3412    }
3413}
3414
3415fn format_numeric_token(num: &NumericLiteral<'_>) -> String {
3416    if num.value.fract() == 0.0 {
3417        format!("{:.0}", num.value)
3418    } else {
3419        num.value.to_string()
3420    }
3421}
3422
3423fn is_theme_binding_name(name: &str) -> bool {
3424    let lower = name.to_ascii_lowercase();
3425    lower == "theme" || lower.ends_with("theme")
3426}
3427
3428fn object_has_static_key(obj: &ObjectExpression<'_>, wanted: &str) -> bool {
3429    obj.properties.iter().any(|prop| {
3430        let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3431            return false;
3432        };
3433        prop.key.static_name().is_some_and(|key| key == wanted)
3434    })
3435}
3436
3437fn object_static_property_object<'a>(
3438    obj: &'a ObjectExpression<'a>,
3439    wanted: &str,
3440) -> Option<&'a ObjectExpression<'a>> {
3441    obj.properties.iter().find_map(|prop| {
3442        let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3443            return None;
3444        };
3445        if prop.key.static_name().is_some_and(|name| name == wanted)
3446            && let Expression::ObjectExpression(value) = &prop.value
3447        {
3448            Some(&**value)
3449        } else {
3450            None
3451        }
3452    })
3453}
3454
3455fn collect_panda_config_token_leaves(
3456    lines: &mut LineCounter<'_>,
3457    obj: &ObjectExpression<'_>,
3458    out: &mut Vec<CssInJsToken>,
3459) {
3460    let Some(theme) = object_static_property_object(obj, "theme") else {
3461        return;
3462    };
3463    for key in ["tokens", "semanticTokens"] {
3464        if let Some(tokens) = object_static_property_object(theme, key) {
3465            collect_token_leaves(lines, tokens, "", CssInJsTokenOrigin::Panda, out);
3466        }
3467    }
3468}
3469
3470impl<'a> Visit<'a> for TokenDefCollector<'a> {
3471    fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
3472        if !self.collecting_mutations && self.nested_depth == 0 {
3473            self.process_declarator(decl);
3474        }
3475        walk::walk_variable_declarator(self, decl);
3476    }
3477
3478    fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
3479        self.nested_depth = self.nested_depth.saturating_add(1);
3480        walk::walk_function(self, function, flags);
3481        self.nested_depth = self.nested_depth.saturating_sub(1);
3482    }
3483
3484    fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
3485        self.nested_depth = self.nested_depth.saturating_add(1);
3486        walk::walk_arrow_function_expression(self, function);
3487        self.nested_depth = self.nested_depth.saturating_sub(1);
3488    }
3489
3490    fn visit_block_statement(&mut self, block: &BlockStatement<'a>) {
3491        self.nested_depth = self.nested_depth.saturating_add(1);
3492        walk::walk_block_statement(self, block);
3493        self.nested_depth = self.nested_depth.saturating_sub(1);
3494    }
3495
3496    fn visit_assignment_expression(&mut self, assignment: &AssignmentExpression<'a>) {
3497        if self.collecting_mutations {
3498            for binding in assignment_target_root_bindings(&assignment.left) {
3499                self.invalidate_const_object(Some(binding));
3500            }
3501        }
3502        walk::walk_assignment_expression(self, assignment);
3503    }
3504
3505    fn visit_update_expression(&mut self, update: &UpdateExpression<'a>) {
3506        if self.collecting_mutations {
3507            self.invalidate_const_object(simple_assignment_target_root_binding(&update.argument));
3508        }
3509        walk::walk_update_expression(self, update);
3510    }
3511
3512    fn visit_unary_expression(&mut self, expression: &UnaryExpression<'a>) {
3513        if self.collecting_mutations && expression.operator.is_delete() {
3514            self.invalidate_const_object(expression_root_binding(&expression.argument));
3515        }
3516        walk::walk_unary_expression(self, expression);
3517    }
3518
3519    fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
3520        if self.collecting_mutations && self.callee_role(&call.callee).is_none() {
3521            if let Some(receiver) = call_receiver_root(&call.callee) {
3522                self.invalidate_const_object(Some(receiver));
3523            }
3524            let possibly_mutated: Vec<RootBinding<'_>> = call
3525                .arguments
3526                .iter()
3527                .filter_map(Argument::as_expression)
3528                .filter_map(expression_root_binding)
3529                .collect();
3530            for binding in possibly_mutated {
3531                self.invalidate_const_object(Some(binding));
3532            }
3533        }
3534        walk::walk_call_expression(self, call);
3535    }
3536
3537    fn visit_export_default_declaration(
3538        &mut self,
3539        decl: &oxc_ast::ast::ExportDefaultDeclaration<'a>,
3540    ) {
3541        if !self.collecting_mutations
3542            && let Some(Expression::CallExpression(call)) = decl.declaration.as_expression()
3543        {
3544            self.process_panda_config_call(call);
3545        }
3546        walk::walk_export_default_declaration(self, decl);
3547    }
3548}
3549
3550/// Count `\n` bytes in `s` as a saturating `u32`.
3551fn count_newlines_u32(s: &str) -> u32 {
3552    u32::try_from(s.bytes().filter(|&b| b == b'\n').count()).unwrap_or(u32::MAX)
3553}
3554
3555/// Incremental 1-based line-number counter over a fixed `source` (issue #1843
3556/// follow-up). The old free `line_at` counted the newlines in `source[..offset]`
3557/// from the start on every call, so a token file with M definitions cost
3558/// O(M * source-len). Definitions and consumer hits are visited in source order,
3559/// so this advances a cursor by only the newline delta since the previous query
3560/// (`source[last_offset..offset]`), making a whole walk O(source-len). A
3561/// non-monotonic query rewinds by the reverse delta, and an out-of-range or
3562/// non-char-boundary offset clamps to line 1 exactly as the previous `line_at`
3563/// did (matching `css::line_at_offset`), so the result is byte-identical to a
3564/// from-scratch count regardless of query order. Deliberately a plain cursor,
3565/// mirroring the `MAX_BINDING_PATH_DEPTH` bounded-work companions.
3566struct LineCounter<'a> {
3567    source: &'a str,
3568    /// Byte offset of the last query whose line was computed. Always a valid
3569    /// char boundary (only ever assigned a boundary-checked `end`).
3570    last_offset: usize,
3571    /// `1 + count_newlines(&source[..last_offset])`, the invariant maintained
3572    /// across queries.
3573    last_line: u32,
3574}
3575
3576impl<'a> LineCounter<'a> {
3577    fn new(source: &'a str) -> Self {
3578        Self {
3579            source,
3580            last_offset: 0,
3581            last_line: 1,
3582        }
3583    }
3584
3585    /// 1-based line number of `offset`, byte-identical to the previous
3586    /// `line_at(source, offset)`.
3587    fn line_at(&mut self, offset: u32) -> u32 {
3588        let end = (offset as usize).min(self.source.len());
3589        // Preserve the previous `.get(..end)` contract: a non-char-boundary
3590        // offset clamps to line 1 rather than panicking on the slice below.
3591        if !self.source.is_char_boundary(end) {
3592            return 1;
3593        }
3594        if end >= self.last_offset {
3595            let delta = count_newlines_u32(&self.source[self.last_offset..end]);
3596            self.last_line = self.last_line.saturating_add(delta);
3597        } else {
3598            let delta = count_newlines_u32(&self.source[end..self.last_offset]);
3599            self.last_line = self.last_line.saturating_sub(delta);
3600        }
3601        self.last_offset = end;
3602        self.last_line
3603    }
3604}
3605
3606#[cfg(all(test, not(miri)))]
3607mod tests {
3608    use super::*;
3609
3610    fn defs(source: &str) -> Vec<CssInJsTokenDef> {
3611        css_in_js_token_defs(source, Path::new("tokens.ts"))
3612    }
3613
3614    fn paths(defs: &[CssInJsTokenDef], binding: &str) -> Vec<String> {
3615        defs.iter()
3616            .find(|d| d.binding == binding)
3617            .map(|d| d.tokens.iter().map(|t| t.path.clone()).collect())
3618            .unwrap_or_default()
3619    }
3620
3621    fn token_values(defs: &[CssInJsTokenDef], binding: &str) -> Vec<(String, Option<String>)> {
3622        defs.iter()
3623            .find(|d| d.binding == binding)
3624            .map(|d| {
3625                d.tokens
3626                    .iter()
3627                    .map(|t| (t.path.clone(), t.value.clone()))
3628                    .collect()
3629            })
3630            .unwrap_or_default()
3631    }
3632
3633    fn theme_defs(source: &str) -> Vec<CssInJsTokenDef> {
3634        css_in_js_theme_token_defs(source, Path::new("theme.ts"))
3635    }
3636
3637    #[test]
3638    fn incremental_def_lines_match_source_order() {
3639        // Issue #1843 follow-up (FIX B): the incremental LineCounter must yield
3640        // the same def_line as a from-scratch newline count for every token,
3641        // across MULTIPLE definitions on multiple lines (the source-order cursor
3642        // must advance correctly between separate `defineVars` calls, not just
3643        // within one).
3644        let src = "import { defineVars } from '@stylexjs/stylex';\n\
3645export const colors = defineVars({\n\
3646primary: '#000',\n\
3647secondary: '#fff',\n\
3648});\n\
3649export const space = defineVars({\n\
3650sm: '4px',\n\
3651lg: '16px',\n\
3652});\n";
3653        let d = defs(src);
3654        let line_of = |binding: &str, path: &str| {
3655            d.iter()
3656                .find(|def| def.binding == binding)
3657                .and_then(|def| def.tokens.iter().find(|t| t.path == path))
3658                .unwrap_or_else(|| panic!("token {binding}.{path} present"))
3659                .def_line
3660        };
3661        assert_eq!(line_of("colors", "primary"), 3);
3662        assert_eq!(line_of("colors", "secondary"), 4);
3663        assert_eq!(line_of("space", "sm"), 7);
3664        assert_eq!(line_of("space", "lg"), 8);
3665    }
3666
3667    #[test]
3668    fn stylex_define_vars_flat_namespace_call() {
3669        let d = defs(
3670            r"
3671import * as stylex from '@stylexjs/stylex';
3672export const vars = stylex.defineVars({ primaryColor: '#3b82f6', spacingSm: '4px' });
3673",
3674        );
3675        assert_eq!(paths(&d, "vars"), vec!["primaryColor", "spacingSm"]);
3676        assert_eq!(
3677            token_values(&d, "vars"),
3678            vec![
3679                ("primaryColor".to_string(), Some("#3b82f6".to_string())),
3680                ("spacingSm".to_string(), Some("4px".to_string())),
3681            ]
3682        );
3683    }
3684
3685    #[test]
3686    fn stylex_define_vars_conditional_value_is_one_flat_token() {
3687        let d = defs(
3688            r"
3689import { defineVars } from '@stylexjs/stylex';
3690const DARK = '@media (prefers-color-scheme: dark)';
3691export const vars = defineVars({ color: { default: '#000', [DARK]: '#fff' } });
3692",
3693        );
3694        assert_eq!(paths(&d, "vars"), vec!["color"]);
3695        assert_eq!(
3696            token_values(&d, "vars"),
3697            vec![("color".to_string(), Some("#000".to_string()))]
3698        );
3699    }
3700
3701    #[test]
3702    fn stylex_nested_vars_recurse_and_stop_at_conditional_leaves() {
3703        let d = defs(
3704            r"
3705import * as stylex from 'stylex';
3706import { unstable_conditional as cond } from 'stylex';
3707const DARK = '@media (prefers-color-scheme: dark)';
3708export const vars = stylex.unstable_defineVarsNested({
3709  surface: {
3710    bg: { default: '#fff', [DARK]: '#111' },
3711    text: cond({ default: '#000', [DARK]: '#eee' }),
3712  },
3713  typed: stylex.types.color('red'),
3714});
3715",
3716        );
3717        assert_eq!(
3718            paths(&d, "vars"),
3719            vec!["surface.bg", "surface.text", "typed"]
3720        );
3721        assert_eq!(
3722            token_values(&d, "vars"),
3723            vec![
3724                ("surface.bg".to_string(), Some("#fff".to_string())),
3725                ("surface.text".to_string(), Some("#000".to_string())),
3726                ("typed".to_string(), Some("red".to_string())),
3727            ]
3728        );
3729    }
3730
3731    #[test]
3732    fn stylex_nested_default_token_namespace_is_not_a_conditional_leaf() {
3733        let d = defs(
3734            r"
3735import * as stylex from '@stylexjs/stylex';
3736export const vars = stylex.unstable_defineVarsNested({
3737  button: {
3738    primary: {
3739      background: {
3740        default: stylex.unstable_conditional({ default: 'blue' }),
3741        hovered: stylex.unstable_conditional({ default: 'navy' }),
3742      },
3743    },
3744  },
3745});
3746",
3747        );
3748        assert_eq!(
3749            paths(&d, "vars"),
3750            vec![
3751                "button.primary.background.default",
3752                "button.primary.background.hovered",
3753            ]
3754        );
3755    }
3756
3757    #[test]
3758    fn stylex_nested_static_computed_conditions_remain_one_leaf() {
3759        let d = defs(
3760            r"
3761import * as stylex from '@stylexjs/stylex';
3762export const vars = stylex.unstable_defineVarsNested({
3763  surface: {
3764    color: {
3765      ['default']: '#fff',
3766      [`@media (prefers-color-scheme: dark)`]: '#111',
3767    },
3768  },
3769});
3770",
3771        );
3772        assert_eq!(paths(&d, "vars"), vec!["surface.color"]);
3773    }
3774
3775    #[test]
3776    fn stylex_define_vars_resolves_local_const_object() {
3777        let d = defs(
3778            r"
3779import { defineVars as define } from '@stylexjs/stylex';
3780const values = { foreground: '#111', background: '#fff' };
3781export const vars = define(values);
3782",
3783        );
3784        assert_eq!(paths(&d, "vars"), vec!["foreground", "background"]);
3785    }
3786
3787    #[test]
3788    fn stylex_define_vars_resolves_transparent_typescript_wrappers() {
3789        let d = defs(
3790            r"
3791import { defineVars } from '@stylexjs/stylex';
3792const values = { foreground: '#111' } as const;
3793export const vars = defineVars(values);
3794export const more = defineVars(
3795  { background: '#fff' } satisfies Record<string, string>,
3796);
3797",
3798        );
3799        assert_eq!(paths(&d, "vars"), vec!["foreground"]);
3800        assert_eq!(paths(&d, "more"), vec!["background"]);
3801    }
3802
3803    #[test]
3804    fn stylex_define_vars_abstains_after_local_const_object_mutation() {
3805        let d = defs(
3806            r"
3807import { defineVars } from '@stylexjs/stylex';
3808const values = { foreground: '#111' };
3809values.foreground = getColor();
3810export const vars = defineVars(values);
3811",
3812        );
3813        assert!(d.is_empty(), "mutated token objects must abstain: {d:?}");
3814    }
3815
3816    #[test]
3817    fn stylex_define_vars_abstains_when_local_object_is_mutated_after_definition() {
3818        let d = defs(
3819            r"
3820import { defineVars } from '@stylexjs/stylex';
3821const values = { foreground: '#111' };
3822export const vars = defineVars(values);
3823values.foreground = getColor();
3824",
3825        );
3826        assert!(
3827            d.is_empty(),
3828            "later mutation must invalidate the definition: {d:?}"
3829        );
3830    }
3831
3832    #[test]
3833    fn stylex_define_vars_abstains_after_delete_or_receiver_call() {
3834        for mutation in ["delete values.foreground;", "values.mutate();"] {
3835            let source = format!(
3836                "import {{ defineVars }} from '@stylexjs/stylex';\nconst values = {{ foreground: '#111' }};\n{mutation}\nexport const vars = defineVars(values);"
3837            );
3838            let d = defs(&source);
3839            assert!(d.is_empty(), "mutated token objects must abstain: {source}");
3840        }
3841    }
3842
3843    #[test]
3844    fn stylex_define_vars_requires_declared_unmutated_root_object() {
3845        for source in [
3846            r"
3847import { defineVars } from '@stylexjs/stylex';
3848export const vars = defineVars(values);
3849const values = { foreground: '#111' };
3850",
3851            r"
3852import { defineVars } from '@stylexjs/stylex';
3853const values = { foreground: '#111' };
3854({ foreground: values.foreground } = next);
3855export const vars = defineVars(values);
3856",
3857            r"
3858import { defineVars } from '@stylexjs/stylex';
3859const values = { foreground: '#111' };
3860[values.foreground] = next;
3861export const vars = defineVars(values);
3862",
3863        ] {
3864            let d = defs(source);
3865            assert!(d.is_empty(), "invalid static object must abstain: {source}");
3866        }
3867    }
3868
3869    #[test]
3870    fn stylex_define_vars_ignores_shadowed_mutation() {
3871        let d = defs(
3872            r"
3873import { defineVars } from '@stylexjs/stylex';
3874const values = { foreground: '#111' };
3875function mutate(values) { values.foreground = '#fff'; }
3876export const vars = defineVars(values);
3877",
3878        );
3879        assert_eq!(paths(&d, "vars"), vec!["foreground"]);
3880    }
3881
3882    #[test]
3883    fn stylex_nested_condition_must_be_declared_before_use() {
3884        let d = defs(
3885            r"
3886import { unstable_defineVarsNested } from '@stylexjs/stylex';
3887export const vars = unstable_defineVarsNested({
3888  surface: { color: { default: '#fff', [DARK]: '#111' } },
3889});
3890const DARK = '@media (prefers-color-scheme: dark)';
3891",
3892        );
3893        assert!(d.is_empty(), "TDZ condition must abstain: {d:?}");
3894    }
3895
3896    #[test]
3897    fn stylex_nested_scope_shadow_does_not_define_tokens() {
3898        let d = defs(
3899            r"
3900import * as stylex from '@stylexjs/stylex';
3901export const makeVars = (stylex) => {
3902  const vars = stylex.defineVars({ foreground: '#111' });
3903  return vars;
3904};
3905",
3906        );
3907        assert!(d.is_empty(), "nested shadowed calls must abstain: {d:?}");
3908    }
3909
3910    #[test]
3911    fn stylex_arbitrary_value_call_does_not_invent_comparable_value() {
3912        let d = defs(
3913            r"
3914import { defineVars } from '@stylexjs/stylex';
3915const dynamic = value => value;
3916export const vars = defineVars({ color: dynamic({ default: '#111' }) });
3917",
3918        );
3919        assert_eq!(paths(&d, "vars"), vec!["color"]);
3920        assert_eq!(token_values(&d, "vars"), vec![("color".to_string(), None)]);
3921    }
3922
3923    #[test]
3924    fn panda_define_tokens_collapses_value_objects() {
3925        let d = defs(
3926            r"
3927import { defineTokens } from '@pandacss/dev';
3928export const tokens = defineTokens({
3929  colors: {
3930    brand: { value: '#f05a28' },
3931    accent: { value: '{colors.brand}' },
3932  },
3933  spacing: { card: { value: '1rem' } },
3934});
3935",
3936        );
3937        assert_eq!(
3938            paths(&d, "tokens"),
3939            vec!["colors.brand", "colors.accent", "spacing.card"]
3940        );
3941        assert_eq!(
3942            token_values(&d, "tokens"),
3943            vec![
3944                ("colors.brand".to_string(), Some("#f05a28".to_string())),
3945                (
3946                    "colors.accent".to_string(),
3947                    Some("{colors.brand}".to_string())
3948                ),
3949                ("spacing.card".to_string(), Some("1rem".to_string())),
3950            ]
3951        );
3952        assert_eq!(
3953            d.iter().find(|d| d.binding == "tokens").unwrap().origin,
3954            CssInJsTokenOrigin::Panda
3955        );
3956    }
3957
3958    #[test]
3959    fn panda_define_config_extracts_tokens_and_semantic_tokens() {
3960        let d = defs(
3961            r"
3962import { defineConfig } from '@pandacss/dev';
3963
3964export default defineConfig({
3965  theme: {
3966    tokens: {
3967      colors: {
3968        brand: { value: '#f05a28' },
3969      },
3970    },
3971    semanticTokens: {
3972      colors: {
3973        surface: { value: { base: '{colors.brand}', _dark: '#111111' } },
3974      },
3975    },
3976    recipes: {
3977      card: { base: { color: 'colors.brand' } },
3978    },
3979  },
3980});
3981",
3982        );
3983        assert_eq!(
3984            paths(&d, "pandaConfig"),
3985            vec!["colors.brand", "colors.surface"]
3986        );
3987        assert_eq!(
3988            token_values(&d, "pandaConfig"),
3989            vec![
3990                ("colors.brand".to_string(), Some("#f05a28".to_string())),
3991                ("colors.surface".to_string(), None),
3992            ]
3993        );
3994        assert_eq!(
3995            d.iter()
3996                .find(|d| d.binding == "pandaConfig")
3997                .unwrap()
3998                .origin,
3999            CssInJsTokenOrigin::Panda
4000        );
4001    }
4002
4003    #[test]
4004    fn theme_object_definitions_flatten_static_leaves() {
4005        let d = theme_defs(
4006            r"
4007export const appTheme = {
4008  colors: { brand: '#f05a28', accent: '#111' },
4009  space: { card: '1rem' },
4010  dynamic: palette,
4011};
4012",
4013        );
4014        assert_eq!(
4015            paths(&d, "appTheme"),
4016            vec!["colors.brand", "colors.accent", "space.card"]
4017        );
4018        assert_eq!(
4019            token_values(&d, "appTheme"),
4020            vec![
4021                ("colors.brand".to_string(), Some("#f05a28".to_string())),
4022                ("colors.accent".to_string(), Some("#111".to_string())),
4023                ("space.card".to_string(), Some("1rem".to_string())),
4024            ]
4025        );
4026        assert_eq!(
4027            d.iter().find(|d| d.binding == "appTheme").unwrap().origin,
4028            CssInJsTokenOrigin::Theme
4029        );
4030    }
4031
4032    #[test]
4033    fn theme_consumers_credit_props_and_destructured_theme_reads() {
4034        let leaves = ["colors.brand", "space.card"]
4035            .into_iter()
4036            .map(str::to_owned)
4037            .collect();
4038        let hits = scan_one(
4039            r"
4040import styled from 'styled-components';
4041export const Card = styled.div`
4042  color: ${({ theme }) => theme.colors.brand};
4043  margin: ${props => props.theme.space.card};
4044`;
4045",
4046            Path::new("card.tsx"),
4047            ConsumerQuery::ThemeReads {
4048                leaf_paths: &leaves,
4049            },
4050        );
4051        let mut token_paths: Vec<String> = hits.into_iter().map(|hit| hit.token_path).collect();
4052        token_paths.sort();
4053        assert_eq!(token_paths, vec!["colors.brand", "space.card"]);
4054    }
4055
4056    #[test]
4057    fn ve_create_theme_tuple_destructure_binds_element_one() {
4058        let d = defs(
4059            r"
4060import { createTheme } from '@vanilla-extract/css';
4061export const [themeClass, vars] = createTheme({
4062  color: { brand: 'red', accent: 'blue' },
4063  space: { small: '4px' },
4064});
4065",
4066        );
4067        // Token paths bind to `vars` (element 1), NOT `themeClass`.
4068        assert_eq!(
4069            paths(&d, "vars"),
4070            vec!["color.brand", "color.accent", "space.small"]
4071        );
4072        assert!(paths(&d, "themeClass").is_empty());
4073    }
4074
4075    #[test]
4076    fn ve_create_theme_contract_null_leaves() {
4077        let d = defs(
4078            r"
4079import { createThemeContract } from '@vanilla-extract/css';
4080export const vars = createThemeContract({ color: { brand: null, accent: null } });
4081",
4082        );
4083        // `null` contract leaves are tokens (the contract declares the shape).
4084        assert_eq!(paths(&d, "vars"), vec!["color.brand", "color.accent"]);
4085    }
4086
4087    #[test]
4088    fn ve_create_global_theme_two_arg_binds_lhs_tokens_in_second_arg() {
4089        let d = defs(
4090            r"
4091import { createGlobalTheme } from '@vanilla-extract/css';
4092export const vars = createGlobalTheme(':root', { color: { brand: 'red' } });
4093",
4094        );
4095        assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
4096    }
4097
4098    #[test]
4099    fn ve_create_theme_two_arg_contract_impl_is_not_a_definition_site() {
4100        // The 2-arg form fills an existing contract (declared by
4101        // createThemeContract elsewhere); it must NOT introduce a binding.
4102        let d = defs(
4103            r"
4104import { createTheme } from '@vanilla-extract/css';
4105export const themeClass = createTheme(vars, { color: { brand: 'red' } });
4106",
4107        );
4108        assert!(
4109            d.is_empty(),
4110            "2-arg createTheme must not define tokens, got {d:?}"
4111        );
4112    }
4113
4114    #[test]
4115    fn ve_create_global_theme_three_arg_contract_impl_is_not_a_definition_site() {
4116        let d = defs(
4117            r"
4118import { createGlobalTheme } from '@vanilla-extract/css';
4119createGlobalTheme(':root', vars, { color: { brand: 'red' } });
4120",
4121        );
4122        assert!(
4123            d.is_empty(),
4124            "3-arg createGlobalTheme must not define tokens, got {d:?}"
4125        );
4126    }
4127
4128    #[test]
4129    fn aliased_named_import_still_fires() {
4130        let d = defs(
4131            r"
4132import { createThemeContract as ct } from '@vanilla-extract/css';
4133export const vars = ct({ color: { brand: null } });
4134",
4135        );
4136        assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
4137    }
4138
4139    #[test]
4140    fn local_helper_not_from_library_does_not_fire() {
4141        // A local `defineVars` shadowing the StyleX name must not be recognized.
4142        let d = defs(
4143            r"
4144function defineVars(o) { return o; }
4145export const vars = defineVars({ color: { primary: '#000' } });
4146",
4147        );
4148        assert!(d.is_empty(), "local defineVars must not fire, got {d:?}");
4149    }
4150
4151    #[test]
4152    fn unrelated_create_theme_import_does_not_fire() {
4153        let d = defs(
4154            r"
4155import { createTheme } from '@mui/material/styles';
4156export const theme = createTheme({ palette: { primary: { main: '#000' } } });
4157",
4158        );
4159        assert!(d.is_empty(), "non-VE createTheme must not fire, got {d:?}");
4160    }
4161
4162    #[test]
4163    fn type_only_import_does_not_fire() {
4164        let d = defs(
4165            r"
4166import type { defineVars } from '@stylexjs/stylex';
4167export const vars = defineVars({ color: { primary: '#000' } });
4168",
4169        );
4170        assert!(
4171            d.is_empty(),
4172            "type-only import must not gate recognition, got {d:?}"
4173        );
4174    }
4175
4176    #[test]
4177    fn token_def_lines_are_per_leaf() {
4178        let src = "import { unstable_defineVarsNested } from '@stylexjs/stylex';\nexport const vars = unstable_defineVarsNested({\n  color: {\n    primary: '#000',\n    secondary: '#fff',\n  },\n});\n";
4179        let d = defs(src);
4180        let def = d.iter().find(|d| d.binding == "vars").unwrap();
4181        let primary = def
4182            .tokens
4183            .iter()
4184            .find(|t| t.path == "color.primary")
4185            .unwrap();
4186        let secondary = def
4187            .tokens
4188            .iter()
4189            .find(|t| t.path == "color.secondary")
4190            .unwrap();
4191        assert_eq!(primary.def_line, 4);
4192        assert_eq!(secondary.def_line, 5);
4193    }
4194
4195    #[test]
4196    fn stylex_spread_or_dynamic_key_abstains_for_whole_definition() {
4197        let d = defs(
4198            r"
4199import { defineVars } from '@stylexjs/stylex';
4200const base = { a: '1' };
4201export const vars = defineVars({ ...base, ['x' + 'y']: '2', real: '#000' });
4202",
4203        );
4204        assert!(d.is_empty(), "partial StyleX shapes must abstain: {d:?}");
4205    }
4206
4207    #[test]
4208    fn stylex_nested_dynamic_condition_key_abstains_for_whole_definition() {
4209        let d = defs(
4210            r"
4211import * as stylex from '@stylexjs/stylex';
4212export const vars = stylex.unstable_defineVarsNested({
4213  surface: { default: '#fff', [getCondition()]: '#111' },
4214});
4215",
4216        );
4217        assert!(
4218            d.is_empty(),
4219            "dynamic StyleX conditions must abstain: {d:?}"
4220        );
4221    }
4222
4223    #[test]
4224    fn identifier_valued_key_is_not_a_leaf_but_call_and_member_values_are() {
4225        // `palette: tailwindPalette` (bare identifier, an imported group) must NOT
4226        // become a phantom `palette` leaf; `radius: px(2)` (call) and
4227        // `red: colors.red['500']` (member access) are real scalar leaves.
4228        let d = defs(
4229            r"
4230import { createGlobalTheme } from '@vanilla-extract/css';
4231export const vars = createGlobalTheme(':root', {
4232  palette: tailwindPalette,
4233  radius: px(2),
4234  red: colors.red['500'],
4235});
4236",
4237        );
4238        let p = paths(&d, "vars");
4239        assert!(
4240            !p.contains(&"palette".to_string()),
4241            "identifier-valued key must not be a leaf: {p:?}"
4242        );
4243        assert!(
4244            p.contains(&"radius".to_string()),
4245            "call-valued key is a leaf: {p:?}"
4246        );
4247        assert!(
4248            p.contains(&"red".to_string()),
4249            "member-valued key is a leaf: {p:?}"
4250        );
4251    }
4252
4253    #[test]
4254    fn no_css_in_js_import_returns_empty() {
4255        let d = defs("export const vars = { color: { primary: '#000' } };");
4256        assert!(d.is_empty());
4257    }
4258
4259    fn leaves(paths: &[&str]) -> FxHashSet<String> {
4260        paths.iter().map(|s| (*s).to_string()).collect()
4261    }
4262
4263    /// Run one query and drop the query index from each hit.
4264    fn scan_one(source: &str, path: &Path, query: ConsumerQuery<'_>) -> Vec<TokenConsumerHit> {
4265        css_in_js_consumer_scan(source, path, &[query])
4266            .into_iter()
4267            .map(|(_, hit)| hit)
4268            .collect()
4269    }
4270
4271    fn consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
4272        let leaf_paths = leaves(paths);
4273        scan_one(
4274            source,
4275            Path::new("card.ts"),
4276            ConsumerQuery::MemberBinding {
4277                alias,
4278                leaf_paths: &leaf_paths,
4279            },
4280        )
4281    }
4282
4283    fn panda_consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
4284        let leaf_paths = leaves(paths);
4285        scan_one(
4286            source,
4287            Path::new("card.ts"),
4288            ConsumerQuery::PandaTokenCall {
4289                alias,
4290                leaf_paths: &leaf_paths,
4291            },
4292        )
4293    }
4294
4295    fn panda_style_consumers(
4296        source: &str,
4297        aliases: &[&str],
4298        paths: &[&str],
4299    ) -> Vec<TokenConsumerHit> {
4300        let aliases = aliases.iter().map(|s| (*s).to_string()).collect();
4301        let leaf_paths = leaves(paths);
4302        scan_one(
4303            source,
4304            Path::new("card.ts"),
4305            ConsumerQuery::PandaStyleValues {
4306                aliases: &aliases,
4307                leaf_paths: &leaf_paths,
4308            },
4309        )
4310    }
4311
4312    #[test]
4313    fn consumer_matches_deepest_leaf_not_intermediate_group() {
4314        // `vars.color.primary` is the leaf; `vars.color` (an intermediate group)
4315        // must NOT be counted, so exactly one hit per access site.
4316        let hits = consumers(
4317            "const a = vars.color.primary;",
4318            "vars",
4319            &["color.primary", "color.secondary"],
4320        );
4321        assert_eq!(hits.len(), 1);
4322        assert_eq!(hits[0].token_path, "color.primary");
4323        assert_eq!(hits[0].line, 1);
4324    }
4325
4326    #[test]
4327    fn consumer_aliased_receiver() {
4328        // The caller passes the local alias; member access on it is matched.
4329        let hits = consumers("const a = v.color.primary;", "v", &["color.primary"]);
4330        assert_eq!(hits.len(), 1);
4331        assert_eq!(hits[0].token_path, "color.primary");
4332    }
4333
4334    #[test]
4335    fn consumer_multiple_sites_distinct_lines() {
4336        let src = "const a = vars.color.primary;\nconst b = vars.space.sm;\nconst c = vars.color.primary;";
4337        let hits = consumers(src, "vars", &["color.primary", "space.sm"]);
4338        assert_eq!(hits.len(), 3);
4339        let lines: Vec<u32> = hits.iter().map(|h| h.line).collect();
4340        assert_eq!(lines, vec![1, 2, 3]);
4341    }
4342
4343    #[test]
4344    fn consumer_in_style_object_value_position() {
4345        // The dominant real shape: a token read inside a style-call object value.
4346        let hits = consumers(
4347            "export const s = stylex.create({ root: { color: vars.color.primary } });",
4348            "vars",
4349            &["color.primary"],
4350        );
4351        assert_eq!(hits.len(), 1);
4352        assert_eq!(hits[0].token_path, "color.primary");
4353    }
4354
4355    #[test]
4356    fn panda_token_call_consumer_matches_string_literal() {
4357        let hits = panda_consumers(
4358            "export const c = css({ color: token('colors.brand') });",
4359            "token",
4360            &["colors.brand", "colors.accent"],
4361        );
4362        assert_eq!(hits.len(), 1);
4363        assert_eq!(hits[0].token_path, "colors.brand");
4364    }
4365
4366    #[test]
4367    fn panda_style_value_consumer_matches_known_token_string() {
4368        let hits = panda_style_consumers(
4369            "export const c = css({ color: 'colors.brand', _hover: { bg: 'colors.accent' } });",
4370            &["css"],
4371            &["colors.brand", "colors.accent", "colors.unused"],
4372        );
4373        let paths: Vec<_> = hits.iter().map(|hit| hit.token_path.as_str()).collect();
4374        assert_eq!(paths, vec!["colors.brand", "colors.accent"]);
4375    }
4376
4377    #[test]
4378    fn panda_style_value_consumer_ignores_unimported_alias() {
4379        let hits = panda_style_consumers(
4380            "export const c = notPanda({ color: 'colors.brand' });",
4381            &["css"],
4382            &["colors.brand"],
4383        );
4384        assert!(hits.is_empty());
4385    }
4386
4387    #[test]
4388    fn consumer_flat_stylex_depth_one() {
4389        let hits = consumers("const a = vars.primaryColor;", "vars", &["primaryColor"]);
4390        assert_eq!(hits.len(), 1);
4391        assert_eq!(hits[0].token_path, "primaryColor");
4392    }
4393
4394    #[test]
4395    fn stylex_theme_call_consumes_complete_group_once() {
4396        let leaf_paths = leaves(&["surface.bg", "surface.text"]);
4397        let queries = [ConsumerQuery::StyleXThemeGroup {
4398            contract_alias: "tokens",
4399            leaf_paths: &leaf_paths,
4400        }];
4401        let hits = css_in_js_consumer_scan(
4402            "import { createTheme as theme } from 'stylex';\nconst reset = theme(tokens, {});",
4403            Path::new("theme.ts"),
4404            &queries,
4405        );
4406        let mut paths: Vec<_> = hits
4407            .into_iter()
4408            .map(|(_, hit)| (hit.token_path, hit.line))
4409            .collect();
4410        paths.sort();
4411        assert_eq!(
4412            paths,
4413            vec![
4414                ("surface.bg".to_string(), 2),
4415                ("surface.text".to_string(), 2),
4416            ]
4417        );
4418    }
4419
4420    #[test]
4421    fn stylex_theme_call_abstains_for_type_only_or_unknown_contract() {
4422        let leaf_paths = leaves(&["surface.bg"]);
4423        let queries = [ConsumerQuery::StyleXThemeGroup {
4424            contract_alias: "tokens",
4425            leaf_paths: &leaf_paths,
4426        }];
4427        for source in [
4428            "import { type createTheme } from '@stylexjs/stylex'; createTheme(tokens, {});",
4429            "import * as stylex from '@stylexjs/stylex'; stylex.createTheme(other, {});",
4430            "const createTheme = (a, b) => b; createTheme(tokens, {});",
4431            "import { createTheme } from 'stylex'; const run = (createTheme) => createTheme(tokens, {});",
4432            "import * as stylex from 'stylex'; const run = (stylex) => stylex.createTheme(tokens, {});",
4433            "import { createTheme } from 'stylex'; const run = (tokens) => createTheme(tokens, {});",
4434        ] {
4435            assert!(
4436                css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4437                "must abstain: {source}"
4438            );
4439        }
4440    }
4441
4442    #[test]
4443    fn stylex_theme_call_requires_bound_exact_static_shape() {
4444        let leaf_paths = leaves(&["surface.bg"]);
4445        let queries = [ConsumerQuery::StyleXThemeGroup {
4446            contract_alias: "tokens",
4447            leaf_paths: &leaf_paths,
4448        }];
4449        for source in [
4450            "import { createTheme } from 'stylex'; createTheme(tokens, {});",
4451            "import { createTheme } from 'stylex'; const theme = createTheme(tokens);",
4452            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, {}, {});",
4453            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, '#fff');",
4454            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, getOverrides());",
4455            "import { createTheme } from 'stylex'; const { theme } = createTheme(tokens, {});",
4456        ] {
4457            assert!(
4458                css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4459                "must abstain: {source}"
4460            );
4461        }
4462    }
4463
4464    #[test]
4465    fn stylex_theme_call_accepts_unmutated_local_static_override_and_wrapped_contract() {
4466        let leaf_paths = leaves(&["surface.bg"]);
4467        let queries = [ConsumerQuery::StyleXThemeGroup {
4468            contract_alias: "tokens",
4469            leaf_paths: &leaf_paths,
4470        }];
4471        let source = r"
4472import { createTheme, unstable_conditional } from 'stylex';
4473const overrides = {
4474  surface: unstable_conditional({ default: '#fff', '@media (prefers-color-scheme: dark)': '#111' }),
4475} as const;
4476const theme = createTheme(tokens as typeof tokens, overrides);
4477";
4478        let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4479        assert_eq!(hits.len(), 1);
4480        assert_eq!(hits[0].1.token_path, "surface.bg");
4481    }
4482
4483    #[test]
4484    fn stylex_theme_call_accepts_official_static_expression_shapes() {
4485        let leaf_paths = leaves(&["surface.bg"]);
4486        let queries = [ConsumerQuery::StyleXThemeGroup {
4487            contract_alias: "tokens",
4488            leaf_paths: &leaf_paths,
4489        }];
4490        let source = r"
4491import { createTheme, types } from 'stylex';
4492const DARK = '@media (prefers-color-scheme: dark)';
4493const name = 'light';
4494const RADIUS = 4;
4495const theme = createTheme(tokens, {
4496  [DARK]: `${name}green`,
4497  radius: RADIUS * 2,
4498  typed: types.length({ default: RADIUS * 2 }),
4499});
4500";
4501        let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4502        assert_eq!(hits.len(), 1);
4503        assert_eq!(hits[0].1.token_path, "surface.bg");
4504    }
4505
4506    #[test]
4507    fn stylex_theme_call_accepts_generic_evaluator_shapes() {
4508        let leaf_paths = leaves(&["surface.bg"]);
4509        let queries = [ConsumerQuery::StyleXThemeGroup {
4510            contract_alias: "tokens",
4511            leaf_paths: &leaf_paths,
4512        }];
4513        let source = r"
4514import { createTheme } from 'stylex';
4515const palette = { green: 'green' };
4516const base = { color: 'red' };
4517const alias = base;
4518const FLAG = true;
4519const RADIUS = '4';
4520const choose = value => value ? 'red' : 'blue';
4521const pick = value => value.color;
4522consume(RADIUS);
4523const theme = createTheme(tokens, {
4524  ...alias,
4525  member: palette.green,
4526  conditional: FLAG ? 'red' : 'blue',
4527  logical: FLAG && 'red',
4528  sequence: (0, 'red'),
4529  raw: String.raw`red-${2}`,
4530  math: Math.max(1, 2),
4531  stringMethod: 'red'.toUpperCase(),
4532  entries: Object.fromEntries([['color', 'red']]),
4533  arrow: choose(FLAG),
4534  arrowMember: pick({ color: 'red' }),
4535  coercedRadius: +RADIUS,
4536  bigintEquality: (1n === 1n) ? 8 : 4,
4537});
4538";
4539        let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4540        assert_eq!(hits.len(), 1);
4541        assert_eq!(hits[0].1.token_path, "surface.bg");
4542    }
4543
4544    #[test]
4545    fn stylex_theme_call_resolves_static_contract_aliases_and_scalar_receivers() {
4546        let leaf_paths = leaves(&["surface.bg"]);
4547        let queries = [ConsumerQuery::StyleXThemeGroup {
4548            contract_alias: "tokens",
4549            leaf_paths: &leaf_paths,
4550        }];
4551        let source = r"
4552import { createTheme } from 'stylex';
4553const alias = tokens;
4554const secondAlias = alias;
4555const color = 'red';
4556const digits = 2;
4557const theme = createTheme(secondAlias, {
4558  color: color.toUpperCase().toLowerCase(),
4559  radius: (1).toFixed(digits),
4560});
4561color.toUpperCase();
4562const second = createTheme(alias, { color });
4563";
4564        let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4565        assert_eq!(hits.len(), 2);
4566    }
4567
4568    #[test]
4569    fn stylex_pure_call_arguments_remain_static_across_theme_calls() {
4570        let leaf_paths = leaves(&["surface.bg"]);
4571        let queries = [ConsumerQuery::StyleXThemeGroup {
4572            contract_alias: "tokens",
4573            leaf_paths: &leaf_paths,
4574        }];
4575        let source = r"
4576import { createTheme } from 'stylex';
4577const RADIUS = 4;
4578const first = createTheme(tokens, { radius: Math.max(RADIUS, 2) });
4579const second = createTheme(tokens, { radius: RADIUS * 2 });
4580";
4581        let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4582        assert_eq!(hits.len(), 2);
4583    }
4584
4585    #[test]
4586    fn stylex_theme_call_rejects_transitive_tdz_cycles_and_dynamic_helpers() {
4587        let leaf_paths = leaves(&["surface.bg"]);
4588        let queries = [ConsumerQuery::StyleXThemeGroup {
4589            contract_alias: "tokens",
4590            leaf_paths: &leaf_paths,
4591        }];
4592        for source in [
4593            "import { createTheme } from 'stylex'; const palette = { green: later }; const later = 'red'; const theme = createTheme(tokens, { color: palette.green });",
4594            "import { createTheme } from 'stylex'; const palette = { green: palette.green }; const theme = createTheme(tokens, { color: palette.green });",
4595            "import { createTheme } from 'stylex'; const choose = value => getColor(value); const theme = createTheme(tokens, { color: choose(true) });",
4596            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Object.assign({}, { color: 'red' }) });",
4597            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Object.fromEntries() });",
4598            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Object.fromEntries([['color', ['red']]]) });",
4599            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Math.notAFunction(1) });",
4600            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x'.repeat(-1) });",
4601            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: (1).toFixed(1000) });",
4602            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { radius: 1n + 1 });",
4603            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { radius: +1n });",
4604            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { radius: Math.max(1n, 2n) });",
4605            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x' in 1 });",
4606            "import { createTheme } from 'stylex'; const base = { color: 'red' }; const alias = base; alias.color = getColor(); const theme = createTheme(tokens, base);",
4607            "import { createTheme } from 'stylex'; const big = () => 1n; const theme = createTheme(tokens, { radius: Math.max(big(), 2) });",
4608            "import { createTheme } from 'stylex'; const big = () => LATER; const LATER = 1n; const theme = createTheme(tokens, { radius: Math.max(big(), 2) });",
4609            "import { createTheme } from 'stylex'; const base = { color: 'red' }; const get = () => base; get().color = getColor(); const theme = createTheme(tokens, base);",
4610            "import { createTheme } from 'stylex'; const base = { color: 'red' }; const get = () => base; mutate(get()); const theme = createTheme(tokens, base);",
4611            "import { createTheme } from 'stylex'; const base = { nested: { color: 'red' } }; const member = value => value.nested; member(base).color = getColor(); const theme = createTheme(tokens, base);",
4612            "import { createTheme } from 'stylex'; const base = { nested: { color: 'red' } }; const member = value => value.nested; mutate(member(base)); const theme = createTheme(tokens, base);",
4613            "import { createTheme } from 'stylex'; const base = { color: 'red' }; const theme = createTheme(tokens, base); base.color = getColor();",
4614            "import { createTheme } from 'stylex'; const choose = value => { return value; }; const theme = createTheme(tokens, { color: choose('red') });",
4615            "import { createTheme } from 'stylex'; const choose = value => { return value; mutate(); }; const theme = createTheme(tokens, { color: choose('red') });",
4616            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'a'.localeCompare('b', 'not_a_locale') });",
4617            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x'.padStart(1 / 0, 'a') });",
4618            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'abc'.charAt(1n) });",
4619            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'abc'.includes('a', 1n) });",
4620            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String(getFlag() ? 'a' : 'b') });",
4621            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String((mutate(), 'red')) });",
4622            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String(+1n) });",
4623            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x'.concat(1n + 1n) });",
4624            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String('x' in 1) });",
4625            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Math.max(+1n, 2) });",
4626            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { [1n + 1]: 'red' });",
4627            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { [1n / 0n]: 'red' });",
4628            "import { createTheme } from 'stylex'; const alias = tokens; alias = other; const theme = createTheme(alias, { color: 'red' });",
4629        ] {
4630            assert!(
4631                css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4632                "must abstain: {source}"
4633            );
4634        }
4635    }
4636
4637    #[test]
4638    fn stylex_theme_call_rejects_noncompiler_static_shapes() {
4639        let leaf_paths = leaves(&["surface.bg"]);
4640        let queries = [ConsumerQuery::StyleXThemeGroup {
4641            contract_alias: "tokens",
4642            leaf_paths: &leaf_paths,
4643        }];
4644        for source in [
4645            "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { value: [] });",
4646            "import stylex, { createTheme } from 'stylex'; const theme = createTheme(tokens, { value: stylex.types.notAType({}) });",
4647            "import stylex, { createTheme } from 'stylex'; const theme = createTheme(tokens, { value: stylex.keyframes() });",
4648            "import { createTheme } from 'stylex'; const overrides = {}; const run = (overrides) => { const theme = createTheme(tokens, overrides); };",
4649            "import { createTheme } from 'stylex'; const overrides = {}; ({ value: overrides.value } = next); const theme = createTheme(tokens, overrides);",
4650        ] {
4651            assert!(
4652                css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4653                "must abstain: {source}"
4654            );
4655        }
4656    }
4657
4658    #[test]
4659    fn consumer_other_binding_not_matched() {
4660        // A same-named member access on a DIFFERENT binding must not be a hit.
4661        let hits = consumers("const a = other.color.primary;", "vars", &["color.primary"]);
4662        assert!(hits.is_empty());
4663    }
4664
4665    #[test]
4666    fn consumer_shadowed_token_alias_is_not_matched() {
4667        let hits = consumers(
4668            "const read = (vars) => vars.color.primary;",
4669            "vars",
4670            &["color.primary"],
4671        );
4672        assert!(hits.is_empty());
4673    }
4674
4675    #[test]
4676    fn consumer_lexical_shadowing_respects_loops_catch_and_tdz() {
4677        let source = r"
4678import { vars } from './tokens.stylex';
4679for (const vars of groups) { use(vars.color.primary); }
4680try { work(); } catch (vars) { use(vars.color.primary); }
4681{ use(vars.color.primary); const vars = fallback; }
4682use((vars as typeof vars).color.primary);
4683";
4684        let hits = consumers(source, "vars", &["color.primary"]);
4685        assert_eq!(hits.len(), 1);
4686        assert_eq!(hits[0].line, 6);
4687    }
4688
4689    #[test]
4690    fn stylex_theme_callee_loop_shadow_does_not_escape_loop_scope() {
4691        let leaf_paths = leaves(&["surface.bg"]);
4692        let queries = [ConsumerQuery::StyleXThemeGroup {
4693            contract_alias: "tokens",
4694            leaf_paths: &leaf_paths,
4695        }];
4696        let source = r"
4697import { createTheme } from 'stylex';
4698for (const createTheme of factories) {
4699  const bad = createTheme(tokens, {});
4700}
4701const good = createTheme(tokens, {});
4702";
4703        let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4704        assert_eq!(hits.len(), 1);
4705        assert_eq!(hits[0].1.line, 6);
4706    }
4707
4708    #[test]
4709    fn consumer_deeper_access_past_leaf_matches_leaf_subexpression_once() {
4710        // `vars.color.primary.toString()` reads the leaf `color.primary`; the outer
4711        // `.toString` chain is not a leaf, the inner `vars.color.primary` is.
4712        let hits = consumers(
4713            "const a = vars.color.primary.toString();",
4714            "vars",
4715            &["color.primary"],
4716        );
4717        assert_eq!(hits.len(), 1);
4718        assert_eq!(hits[0].token_path, "color.primary");
4719    }
4720
4721    #[test]
4722    fn consumer_undefined_path_not_matched() {
4723        let hits = consumers("const a = vars.color.tertiary;", "vars", &["color.primary"]);
4724        assert!(hits.is_empty());
4725    }
4726
4727    #[test]
4728    fn consumer_bracket_notation_hyphenated_key() {
4729        // Hyphenated / digit-leading token keys are not valid JS identifiers, so
4730        // they are consumed via bracket notation; the leaf path keeps the raw key.
4731        let hits = consumers(
4732            "const a = vars.color['gray-100'];\nconst b = vars.borderRadius['0x'];",
4733            "vars",
4734            &["color.gray-100", "borderRadius.0x"],
4735        );
4736        let paths: Vec<&str> = hits.iter().map(|h| h.token_path.as_str()).collect();
4737        assert!(paths.contains(&"color.gray-100"));
4738        assert!(paths.contains(&"borderRadius.0x"));
4739        assert_eq!(hits.len(), 2);
4740    }
4741
4742    #[test]
4743    fn consumer_mixed_dot_and_bracket_chain() {
4744        // `vars['color'].primary` and `vars.color['primary']` both reconstruct the
4745        // same `color.primary` leaf.
4746        let hits = consumers(
4747            "const a = vars['color'].primary;\nconst b = vars.color['primary'];",
4748            "vars",
4749            &["color.primary"],
4750        );
4751        assert_eq!(hits.len(), 2);
4752        assert!(hits.iter().all(|h| h.token_path == "color.primary"));
4753    }
4754
4755    #[test]
4756    fn consumer_numeric_computed_key_is_matched() {
4757        let hits = consumers("const a = vars.color.gray[50];", "vars", &["color.gray.50"]);
4758        assert_eq!(hits.len(), 1);
4759        assert_eq!(hits[0].token_path, "color.gray.50");
4760    }
4761
4762    #[test]
4763    fn consumer_non_literal_computed_key_not_matched() {
4764        // A dynamic computed key cannot be resolved statically (lower-bound miss).
4765        let hits = consumers(
4766            "const k = 'primary'; const a = vars.color[k];",
4767            "vars",
4768            &["color.primary"],
4769        );
4770        assert!(hits.is_empty());
4771    }
4772
4773    #[test]
4774    fn consumer_empty_inputs_short_circuit() {
4775        assert!(consumers("const a = vars.color.primary;", "", &["color.primary"]).is_empty());
4776        assert!(consumers("const a = vars.color.primary;", "vars", &[]).is_empty());
4777    }
4778
4779    #[test]
4780    fn consumer_scan_matches_individual_calls() {
4781        // One source exercising all four query kinds; the scan must return exactly
4782        // the union of the four individual functions' hits, each tagged with the
4783        // index of the query that produced it.
4784        let source = "const a = vars.color.primary;\nconst b = css({ color: token('colors.brand'), background: 'colors.accent' });\nconst c = theme.space.card;";
4785        let path = Path::new("card.tsx");
4786
4787        let member_leaves = leaves(&["color.primary"]);
4788        let panda_call_leaves = leaves(&["colors.brand"]);
4789        let panda_style_aliases = leaves(&["css"]);
4790        let panda_style_leaves = leaves(&["colors.accent"]);
4791        let theme_leaves = leaves(&["space.card"]);
4792
4793        let queries = [
4794            ConsumerQuery::MemberBinding {
4795                alias: "vars",
4796                leaf_paths: &member_leaves,
4797            },
4798            ConsumerQuery::PandaTokenCall {
4799                alias: "token",
4800                leaf_paths: &panda_call_leaves,
4801            },
4802            ConsumerQuery::PandaStyleValues {
4803                aliases: &panda_style_aliases,
4804                leaf_paths: &panda_style_leaves,
4805            },
4806            ConsumerQuery::ThemeReads {
4807                leaf_paths: &theme_leaves,
4808            },
4809        ];
4810        let scanned = css_in_js_consumer_scan(source, path, &queries);
4811
4812        let individual: Vec<(usize, TokenConsumerHit)> = scan_one(
4813            source,
4814            path,
4815            ConsumerQuery::MemberBinding {
4816                alias: "vars",
4817                leaf_paths: &member_leaves,
4818            },
4819        )
4820        .into_iter()
4821        .map(|hit| (0, hit))
4822        .chain(
4823            scan_one(
4824                source,
4825                path,
4826                ConsumerQuery::PandaTokenCall {
4827                    alias: "token",
4828                    leaf_paths: &panda_call_leaves,
4829                },
4830            )
4831            .into_iter()
4832            .map(|hit| (1, hit)),
4833        )
4834        .chain(
4835            scan_one(
4836                source,
4837                path,
4838                ConsumerQuery::PandaStyleValues {
4839                    aliases: &panda_style_aliases,
4840                    leaf_paths: &panda_style_leaves,
4841                },
4842            )
4843            .into_iter()
4844            .map(|hit| (2, hit)),
4845        )
4846        .chain(
4847            scan_one(
4848                source,
4849                path,
4850                ConsumerQuery::ThemeReads {
4851                    leaf_paths: &theme_leaves,
4852                },
4853            )
4854            .into_iter()
4855            .map(|hit| (3, hit)),
4856        )
4857        .collect();
4858
4859        assert_eq!(scanned, individual);
4860        assert_eq!(scanned.len(), 4);
4861        assert_eq!(
4862            scanned[0],
4863            (
4864                0,
4865                TokenConsumerHit {
4866                    token_path: "color.primary".to_string(),
4867                    line: 1,
4868                }
4869            )
4870        );
4871        assert_eq!(
4872            scanned[3],
4873            (
4874                3,
4875                TokenConsumerHit {
4876                    token_path: "space.card".to_string(),
4877                    line: 3,
4878                }
4879            )
4880        );
4881    }
4882
4883    #[test]
4884    fn consumer_scan_empty_query_is_isolated() {
4885        // An empty-alias query short-circuits to no hits WITHOUT suppressing the
4886        // valid query that follows it.
4887        let source = "const a = vars.color.primary;";
4888        let path = Path::new("card.ts");
4889        let empty_leaves = leaves(&["color.primary"]);
4890        let valid_leaves = leaves(&["color.primary"]);
4891        let queries = [
4892            ConsumerQuery::MemberBinding {
4893                alias: "",
4894                leaf_paths: &empty_leaves,
4895            },
4896            ConsumerQuery::MemberBinding {
4897                alias: "vars",
4898                leaf_paths: &valid_leaves,
4899            },
4900        ];
4901        let scanned = css_in_js_consumer_scan(source, path, &queries);
4902        assert_eq!(scanned.len(), 1);
4903        assert_eq!(scanned[0].0, 1);
4904        assert_eq!(scanned[0].1.token_path, "color.primary");
4905    }
4906
4907    #[test]
4908    fn consumer_scan_two_member_queries_same_source() {
4909        // Two definers imported under different aliases with an overlapping leaf
4910        // path; each read attributes to the alias (query index) it used.
4911        let source = "const a = brand.color.primary;\nconst b = accent.color.primary;";
4912        let path = Path::new("card.ts");
4913        let brand_leaves = leaves(&["color.primary"]);
4914        let accent_leaves = leaves(&["color.primary"]);
4915        let queries = [
4916            ConsumerQuery::MemberBinding {
4917                alias: "brand",
4918                leaf_paths: &brand_leaves,
4919            },
4920            ConsumerQuery::MemberBinding {
4921                alias: "accent",
4922                leaf_paths: &accent_leaves,
4923            },
4924        ];
4925        let scanned = css_in_js_consumer_scan(source, path, &queries);
4926        assert_eq!(scanned.len(), 2);
4927        assert!(scanned.contains(&(
4928            0,
4929            TokenConsumerHit {
4930                token_path: "color.primary".to_string(),
4931                line: 1,
4932            }
4933        )));
4934        assert!(scanned.contains(&(
4935            1,
4936            TokenConsumerHit {
4937                token_path: "color.primary".to_string(),
4938                line: 2,
4939            }
4940        )));
4941    }
4942}