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