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