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