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().with_build_nodes(true).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::ExportDeclaration(export) => match &export.declaration {
400 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 arrow.body.as_expression()
2484}
2485
2486fn resolve_stylex_arrow_parameter_member<'a>(
2487 expression: &'a Expression<'a>,
2488 context: &StyleXArrowStaticContext<'_, 'a>,
2489 visiting: &mut FxHashSet<&'a str>,
2490) -> Option<ResolvedStyleXStaticMember<'a>> {
2491 let (base, _, segments) = binding_access_object_chain(expression)?;
2492 let argument = context.parameters.get(base)?;
2493 let mut resolver = StyleXStaticResolver {
2494 before: context.before,
2495 static_values: context.static_values,
2496 root_reference_spans: context.root_reference_spans,
2497 };
2498 let mut object = resolve_static_stylex_object(argument, &resolver, visiting)?;
2499 let mut segments = segments.into_iter().peekable();
2500 while let Some(segment) = segments.next() {
2501 let member = resolve_static_stylex_object_property(object, &segment, &resolver, visiting)?;
2502 if segments.peek().is_none() {
2503 return Some(member);
2504 }
2505 resolver = StyleXStaticResolver {
2506 before: member.before,
2507 ..resolver
2508 };
2509 object = resolve_static_stylex_object(member.value, &resolver, visiting)?;
2510 }
2511 None
2512}
2513
2514fn is_static_stylex_arrow_body<'a>(
2515 expression: &'a Expression<'a>,
2516 context: &StyleXArrowStaticContext<'_, 'a>,
2517 visiting: &mut FxHashSet<&'a str>,
2518) -> bool {
2519 let expression = unwrap_transparent_expression(expression);
2520 match expression {
2521 Expression::Identifier(identifier)
2522 if context.parameters.contains_key(identifier.name.as_str()) =>
2523 {
2524 true
2525 }
2526 Expression::TemplateLiteral(template) => template
2527 .expressions
2528 .iter()
2529 .all(|expression| is_static_stylex_arrow_body(expression, context, visiting)),
2530 Expression::UnaryExpression(unary) => {
2531 let bigint_context = StyleXBigIntContext {
2532 before: context.before,
2533 static_values: context.static_values,
2534 root_reference_spans: context.root_reference_spans,
2535 parameters: &context.parameters,
2536 };
2537 !unary.operator.is_delete()
2538 && (unary.operator != UnaryOperator::UnaryPlus
2539 || !is_static_stylex_bigint_value_with_context(
2540 &unary.argument,
2541 &bigint_context,
2542 &mut visiting.clone(),
2543 ))
2544 && is_static_stylex_arrow_body(&unary.argument, context, visiting)
2545 }
2546 Expression::BinaryExpression(binary) => {
2547 let bigint_context = StyleXBigIntContext {
2548 before: context.before,
2549 static_values: context.static_values,
2550 root_reference_spans: context.root_reference_spans,
2551 parameters: &context.parameters,
2552 };
2553 !binary.operator.is_relational()
2554 && (!binary.operator.is_numeric_or_string_binary_operator()
2555 || !is_static_stylex_bigint_pair(
2556 &binary.left,
2557 &binary.right,
2558 &bigint_context,
2559 &mut visiting.clone(),
2560 ))
2561 && is_static_stylex_arrow_body(&binary.left, context, visiting)
2562 && is_static_stylex_arrow_body(&binary.right, context, visiting)
2563 }
2564 Expression::LogicalExpression(logical) => {
2565 is_static_stylex_arrow_body(&logical.left, context, visiting)
2566 && is_static_stylex_arrow_body(&logical.right, context, visiting)
2567 }
2568 Expression::ConditionalExpression(conditional) => [
2569 &conditional.test,
2570 &conditional.consequent,
2571 &conditional.alternate,
2572 ]
2573 .into_iter()
2574 .all(|expression| is_static_stylex_arrow_body(expression, context, visiting)),
2575 Expression::SequenceExpression(sequence) => sequence
2576 .expressions
2577 .iter()
2578 .all(|expression| is_static_stylex_arrow_body(expression, context, visiting)),
2579 Expression::StaticMemberExpression(_) | Expression::ComputedMemberExpression(_) => {
2580 if let Some(resolved) =
2581 resolve_stylex_arrow_parameter_member(expression, context, visiting)
2582 {
2583 return is_static_stylex_theme_override(
2584 resolved.value,
2585 resolved.before,
2586 context.static_values,
2587 context.imports,
2588 context.root_reference_spans,
2589 visiting,
2590 );
2591 }
2592 is_static_stylex_theme_override(
2593 expression,
2594 context.before,
2595 context.static_values,
2596 context.imports,
2597 context.root_reference_spans,
2598 visiting,
2599 )
2600 }
2601 _ => is_static_stylex_theme_override(
2602 expression,
2603 context.before,
2604 context.static_values,
2605 context.imports,
2606 context.root_reference_spans,
2607 visiting,
2608 ),
2609 }
2610}
2611
2612fn is_root_stylex_static_call(
2613 callee: &Expression<'_>,
2614 imports: &FxHashMap<&str, (Lib, &str)>,
2615 root_reference_spans: &FxHashSet<Span>,
2616) -> bool {
2617 match unwrap_transparent_expression(callee) {
2618 Expression::Identifier(identifier) => {
2619 root_reference_spans.contains(&identifier.span)
2620 && imports
2621 .get(identifier.name.as_str())
2622 .is_some_and(|(lib, role)| {
2623 *lib == Lib::StyleX
2624 && matches!(*role, "unstable_conditional" | "keyframes" | "positionTry")
2625 })
2626 }
2627 Expression::StaticMemberExpression(member) => match &member.object {
2628 Expression::Identifier(object) => {
2629 root_reference_spans.contains(&object.span)
2630 && imports
2631 .get(object.name.as_str())
2632 .is_some_and(|(lib, role)| {
2633 *lib == Lib::StyleX
2634 && (if *role == "types" {
2635 is_stylex_type_helper(member.property.name.as_str())
2636 } else {
2637 matches!(
2638 member.property.name.as_str(),
2639 "unstable_conditional" | "keyframes" | "positionTry"
2640 )
2641 })
2642 })
2643 }
2644 Expression::StaticMemberExpression(namespace) => {
2645 let Expression::Identifier(object) = &namespace.object else {
2646 return false;
2647 };
2648 root_reference_spans.contains(&object.span)
2649 && imports
2650 .get(object.name.as_str())
2651 .is_some_and(|(lib, _)| *lib == Lib::StyleX)
2652 && namespace.property.name.as_str() == "types"
2653 && is_stylex_type_helper(member.property.name.as_str())
2654 }
2655 _ => false,
2656 },
2657 _ => false,
2658 }
2659}
2660
2661fn is_stylex_type_helper(name: &str) -> bool {
2662 matches!(
2663 name,
2664 "angle"
2665 | "color"
2666 | "image"
2667 | "integer"
2668 | "length"
2669 | "lengthPercentage"
2670 | "number"
2671 | "percentage"
2672 | "resolution"
2673 | "time"
2674 | "transformFunction"
2675 | "transformList"
2676 | "url"
2677 )
2678}
2679
2680fn static_computed_key(expr: &Expression<'_>) -> Option<String> {
2683 match unwrap_transparent_expression(expr) {
2684 Expression::StringLiteral(lit) => Some(lit.value.to_string()),
2685 Expression::NumericLiteral(lit) => Some(format_numeric_token(lit)),
2686 Expression::TemplateLiteral(template) if template.expressions.is_empty() => template
2687 .quasis
2688 .first()
2689 .map(|quasi| quasi.value.raw.to_string()),
2690 _ => None,
2691 }
2692}
2693
2694#[derive(Clone, Copy)]
2695struct RootBinding<'a> {
2696 name: &'a str,
2697 span: Span,
2698 reference_id: Option<ReferenceId>,
2699}
2700
2701fn root_binding<'a>(identifier: &'a IdentifierReference<'_>) -> RootBinding<'a> {
2702 RootBinding {
2703 name: identifier.name.as_str(),
2704 span: identifier.span,
2705 reference_id: identifier.reference_id.get(),
2706 }
2707}
2708
2709fn expression_root_binding<'a, 'b: 'a>(expr: &'a Expression<'b>) -> Option<RootBinding<'a>> {
2710 match expr {
2711 Expression::Identifier(id) => Some(root_binding(id)),
2712 Expression::StaticMemberExpression(member) => expression_root_binding(&member.object),
2713 Expression::ComputedMemberExpression(member) => expression_root_binding(&member.object),
2714 Expression::ParenthesizedExpression(expression) => {
2715 expression_root_binding(&expression.expression)
2716 }
2717 Expression::TSAsExpression(expression) => expression_root_binding(&expression.expression),
2718 Expression::TSSatisfiesExpression(expression) => {
2719 expression_root_binding(&expression.expression)
2720 }
2721 Expression::TSNonNullExpression(expression) => {
2722 expression_root_binding(&expression.expression)
2723 }
2724 Expression::TSTypeAssertion(expression) => expression_root_binding(&expression.expression),
2725 _ => None,
2726 }
2727}
2728
2729fn assignment_target_root_bindings<'a, 'b: 'a>(
2730 target: &'a AssignmentTarget<'b>,
2731) -> Vec<RootBinding<'a>> {
2732 let mut bindings = Vec::new();
2733 collect_assignment_target_root_bindings(target, &mut bindings);
2734 bindings
2735}
2736
2737fn assignment_target_receiver_expression<'a, 'b: 'a>(
2738 target: &'a AssignmentTarget<'b>,
2739) -> Option<&'a Expression<'b>> {
2740 match target {
2741 AssignmentTarget::StaticMemberExpression(member) => Some(&member.object),
2742 AssignmentTarget::ComputedMemberExpression(member) => Some(&member.object),
2743 AssignmentTarget::TSAsExpression(expression) => {
2744 call_receiver_expression(&expression.expression)
2745 }
2746 AssignmentTarget::TSSatisfiesExpression(expression) => {
2747 call_receiver_expression(&expression.expression)
2748 }
2749 AssignmentTarget::TSNonNullExpression(expression) => {
2750 call_receiver_expression(&expression.expression)
2751 }
2752 AssignmentTarget::TSTypeAssertion(expression) => {
2753 call_receiver_expression(&expression.expression)
2754 }
2755 _ => None,
2756 }
2757}
2758
2759fn collect_assignment_target_root_bindings<'a, 'b: 'a>(
2760 target: &'a AssignmentTarget<'b>,
2761 bindings: &mut Vec<RootBinding<'a>>,
2762) {
2763 match target {
2764 AssignmentTarget::AssignmentTargetIdentifier(id) => bindings.push(root_binding(id)),
2765 AssignmentTarget::StaticMemberExpression(member) => {
2766 bindings.extend(expression_root_binding(&member.object));
2767 }
2768 AssignmentTarget::ComputedMemberExpression(member) => {
2769 bindings.extend(expression_root_binding(&member.object));
2770 }
2771 AssignmentTarget::TSAsExpression(expression) => {
2772 bindings.extend(expression_root_binding(&expression.expression));
2773 }
2774 AssignmentTarget::TSSatisfiesExpression(expression) => {
2775 bindings.extend(expression_root_binding(&expression.expression));
2776 }
2777 AssignmentTarget::TSNonNullExpression(expression) => {
2778 bindings.extend(expression_root_binding(&expression.expression));
2779 }
2780 AssignmentTarget::TSTypeAssertion(expression) => {
2781 bindings.extend(expression_root_binding(&expression.expression));
2782 }
2783 AssignmentTarget::ArrayAssignmentTarget(array) => {
2784 for element in array.elements.iter().flatten() {
2785 collect_assignment_target_maybe_default_root_bindings(element, bindings);
2786 }
2787 if let Some(rest) = &array.rest {
2788 collect_assignment_target_root_bindings(&rest.target, bindings);
2789 }
2790 }
2791 AssignmentTarget::ObjectAssignmentTarget(object) => {
2792 for property in &object.properties {
2793 match property {
2794 AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(property) => {
2795 bindings.push(root_binding(&property.binding));
2796 }
2797 AssignmentTargetProperty::AssignmentTargetPropertyProperty(property) => {
2798 collect_assignment_target_maybe_default_root_bindings(
2799 &property.binding,
2800 bindings,
2801 );
2802 }
2803 }
2804 }
2805 if let Some(rest) = &object.rest {
2806 collect_assignment_target_root_bindings(&rest.target, bindings);
2807 }
2808 }
2809 AssignmentTarget::PrivateFieldExpression(_) => {}
2810 }
2811}
2812
2813fn collect_assignment_target_maybe_default_root_bindings<'a, 'b: 'a>(
2814 target: &'a AssignmentTargetMaybeDefault<'b>,
2815 bindings: &mut Vec<RootBinding<'a>>,
2816) {
2817 if let AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(default) = target {
2818 collect_assignment_target_root_bindings(&default.binding, bindings);
2819 } else if let Some(target) = target.as_assignment_target() {
2820 collect_assignment_target_root_bindings(target, bindings);
2821 }
2822}
2823
2824fn simple_assignment_target_root_binding<'a, 'b: 'a>(
2825 target: &'a SimpleAssignmentTarget<'b>,
2826) -> Option<RootBinding<'a>> {
2827 match target {
2828 SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => Some(root_binding(id)),
2829 SimpleAssignmentTarget::StaticMemberExpression(member) => {
2830 expression_root_binding(&member.object)
2831 }
2832 SimpleAssignmentTarget::ComputedMemberExpression(member) => {
2833 expression_root_binding(&member.object)
2834 }
2835 SimpleAssignmentTarget::TSAsExpression(expression) => {
2836 expression_root_binding(&expression.expression)
2837 }
2838 SimpleAssignmentTarget::TSSatisfiesExpression(expression) => {
2839 expression_root_binding(&expression.expression)
2840 }
2841 SimpleAssignmentTarget::TSNonNullExpression(expression) => {
2842 expression_root_binding(&expression.expression)
2843 }
2844 SimpleAssignmentTarget::TSTypeAssertion(expression) => {
2845 expression_root_binding(&expression.expression)
2846 }
2847 SimpleAssignmentTarget::PrivateFieldExpression(_) => None,
2848 }
2849}
2850
2851#[derive(Clone, Copy)]
2853enum BindingSource {
2854 LhsIdent,
2856 TupleElement(usize),
2858}
2859
2860#[derive(Clone, Copy)]
2863struct Recognized {
2864 binding_source: BindingSource,
2865 tokens_arg: usize,
2866 origin: CssInJsTokenOrigin,
2867 stylex_shape: Option<StyleXTokenShape>,
2868}
2869
2870#[derive(Clone, Copy)]
2871enum StyleXTokenShape {
2872 Flat,
2873 Nested,
2874}
2875
2876struct TokenDefCollector<'a> {
2878 lines: LineCounter<'a>,
2879 imports: FxHashMap<&'a str, (Lib, &'a str)>,
2882 const_objects: FxHashMap<SymbolId, (u32, &'a ObjectExpression<'a>)>,
2885 const_object_references: FxHashMap<ReferenceId, SymbolId>,
2887 const_strings: FxHashMap<ReferenceId, (u32, &'a str)>,
2889 nested_depth: u32,
2890 collecting_mutations: bool,
2891 defs: Vec<CssInJsTokenDef>,
2892}
2893
2894impl<'a> TokenDefCollector<'a> {
2895 fn new(source: &'a str) -> Self {
2896 Self {
2897 lines: LineCounter::new(source),
2898 imports: FxHashMap::default(),
2899 const_objects: FxHashMap::default(),
2900 const_object_references: FxHashMap::default(),
2901 const_strings: FxHashMap::default(),
2902 nested_depth: 0,
2903 collecting_mutations: false,
2904 defs: Vec::new(),
2905 }
2906 }
2907
2908 fn build_import_map(&mut self, program: &Program<'a>) {
2913 for stmt in &program.body {
2914 let Statement::ImportDeclaration(decl) = stmt else {
2915 continue;
2916 };
2917 if decl.import_kind.is_type() {
2918 continue;
2919 }
2920 let Some(lib) = module_library(decl.source.value.as_str()) else {
2921 continue;
2922 };
2923 let Some(specifiers) = &decl.specifiers else {
2924 continue;
2925 };
2926 for specifier in specifiers {
2927 let (local, role) = match specifier {
2928 ImportDeclarationSpecifier::ImportSpecifier(s) if !s.import_kind.is_type() => {
2929 (s.local.name.as_str(), s.imported.name().as_str())
2930 }
2931 ImportDeclarationSpecifier::ImportSpecifier(_) => continue,
2932 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
2933 (s.local.name.as_str(), s.local.name.as_str())
2934 }
2935 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
2936 (s.local.name.as_str(), s.local.name.as_str())
2937 }
2938 };
2939 self.imports.insert(local, (lib, role));
2940 }
2941 }
2942 }
2943
2944 fn build_const_object_map(&mut self, program: &'a Program<'a>, scoping: &Scoping) {
2945 for stmt in &program.body {
2946 let declaration = match stmt {
2947 Statement::VariableDeclaration(declaration) => Some(&**declaration),
2948 Statement::ExportDeclaration(export) => match &export.declaration {
2949 Declaration::VariableDeclaration(declaration) => Some(&**declaration),
2950 _ => None,
2951 },
2952 _ => None,
2953 };
2954 let Some(declaration) = declaration else {
2955 continue;
2956 };
2957 if declaration.kind != VariableDeclarationKind::Const {
2958 continue;
2959 }
2960 for declarator in &declaration.declarations {
2961 let BindingPattern::BindingIdentifier(binding) = &declarator.id else {
2962 continue;
2963 };
2964 let Some(symbol_id) = binding.symbol_id.get() else {
2965 continue;
2966 };
2967 let declaration_start = declarator.span.start;
2968 match declarator.init.as_ref().map(unwrap_transparent_expression) {
2969 Some(Expression::ObjectExpression(obj)) => {
2970 self.const_objects
2971 .insert(symbol_id, (declaration_start, obj));
2972 self.const_object_references.extend(
2973 scoping
2974 .get_resolved_reference_ids(symbol_id)
2975 .iter()
2976 .copied()
2977 .map(|reference_id| (reference_id, symbol_id)),
2978 );
2979 }
2980 Some(Expression::StringLiteral(value)) => {
2981 self.const_strings.extend(
2982 scoping
2983 .get_resolved_reference_ids(symbol_id)
2984 .iter()
2985 .copied()
2986 .map(|reference_id| {
2987 (reference_id, (declaration_start, value.value.as_str()))
2988 }),
2989 );
2990 }
2991 _ => {}
2992 }
2993 }
2994 }
2995 }
2996
2997 fn callee_role(&self, callee: &Expression<'a>) -> Option<(Lib, &'a str)> {
3001 match callee {
3002 Expression::Identifier(id) => self.imports.get(id.name.as_str()).copied(),
3003 Expression::StaticMemberExpression(member) => {
3004 let Expression::Identifier(obj) = &member.object else {
3005 return None;
3006 };
3007 let (lib, _) = *self.imports.get(obj.name.as_str())?;
3008 Some((lib, member.property.name.as_str()))
3010 }
3011 _ => None,
3012 }
3013 }
3014
3015 fn recognize(lib: Lib, role: &str, arg_count: usize) -> Option<Recognized> {
3019 let single = |tokens_arg, origin, stylex_shape| {
3020 Some(Recognized {
3021 binding_source: BindingSource::LhsIdent,
3022 tokens_arg,
3023 origin,
3024 stylex_shape,
3025 })
3026 };
3027 match (lib, role) {
3028 (Lib::StyleX, "defineVars") if arg_count >= 1 => {
3031 single(0, CssInJsTokenOrigin::StyleX, Some(StyleXTokenShape::Flat))
3032 }
3033 (Lib::StyleX, "unstable_defineVarsNested") if arg_count >= 1 => single(
3034 0,
3035 CssInJsTokenOrigin::StyleX,
3036 Some(StyleXTokenShape::Nested),
3037 ),
3038 (Lib::VanillaExtract, "createThemeContract") if arg_count >= 1 => {
3039 single(0, CssInJsTokenOrigin::VanillaExtract, None)
3040 }
3041 (Lib::VanillaExtract, "createTheme") if arg_count == 1 => Some(Recognized {
3045 binding_source: BindingSource::TupleElement(1),
3046 tokens_arg: 0,
3047 origin: CssInJsTokenOrigin::VanillaExtract,
3048 stylex_shape: None,
3049 }),
3050 (Lib::VanillaExtract, "createGlobalTheme") if arg_count == 2 => {
3054 single(1, CssInJsTokenOrigin::VanillaExtract, None)
3055 }
3056 (Lib::Panda, "defineTokens") if arg_count >= 1 => {
3057 single(0, CssInJsTokenOrigin::Panda, None)
3058 }
3059 _ => None,
3060 }
3061 }
3062
3063 fn binding_name(decl: &VariableDeclarator<'a>, source: BindingSource) -> Option<&'a str> {
3066 match source {
3067 BindingSource::LhsIdent => match &decl.id {
3068 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
3069 _ => None,
3070 },
3071 BindingSource::TupleElement(index) => {
3072 let BindingPattern::ArrayPattern(arr) = &decl.id else {
3073 return None;
3074 };
3075 let element = arr.elements.get(index)?.as_ref()?;
3076 match element {
3077 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
3078 _ => None,
3079 }
3080 }
3081 }
3082 }
3083
3084 fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
3085 let Some(Expression::CallExpression(call)) = &decl.init else {
3086 return;
3087 };
3088 if self.process_panda_config_call(call) {
3089 return;
3090 }
3091 let Some((lib, role)) = self.callee_role(&call.callee) else {
3092 return;
3093 };
3094 let Some(recognized) = Self::recognize(lib, role, call.arguments.len()) else {
3095 return;
3096 };
3097 let Some(binding) = Self::binding_name(decl, recognized.binding_source) else {
3098 return;
3099 };
3100 let Some(obj) = self.resolve_object_argument(call, recognized.tokens_arg) else {
3101 return;
3102 };
3103 let mut tokens = Vec::new();
3104 let complete = if let Some(shape) = recognized.stylex_shape {
3105 let context = StyleXTokenContext {
3106 imports: &self.imports,
3107 const_strings: &self.const_strings,
3108 before: call.span.start,
3109 };
3110 collect_stylex_token_object(&mut self.lines, obj, "", shape, &context, &mut tokens)
3111 } else {
3112 collect_token_leaves(&mut self.lines, obj, "", recognized.origin, &mut tokens);
3113 true
3114 };
3115 if !complete {
3116 return;
3117 }
3118 if tokens.is_empty() {
3119 return;
3120 }
3121 self.defs.push(CssInJsTokenDef {
3122 binding: binding.to_owned(),
3123 origin: recognized.origin,
3124 tokens,
3125 });
3126 }
3127
3128 fn resolve_object_argument(
3129 &self,
3130 call: &'a oxc_ast::ast::CallExpression<'a>,
3131 index: usize,
3132 ) -> Option<&'a ObjectExpression<'a>> {
3133 let expr = call.arguments.get(index)?.as_expression()?;
3134 match unwrap_transparent_expression(expr) {
3135 Expression::ObjectExpression(obj) => Some(obj),
3136 Expression::Identifier(id) => {
3137 let reference_id = id.reference_id.get()?;
3138 let symbol_id = self.const_object_references.get(&reference_id)?;
3139 let (declaration_start, object) = self.const_objects.get(symbol_id)?;
3140 (*declaration_start < call.span.start).then_some(*object)
3141 }
3142 _ => None,
3143 }
3144 }
3145
3146 fn invalidate_const_object(&mut self, binding: Option<RootBinding<'_>>) {
3147 let Some(reference_id) = binding.and_then(|binding| binding.reference_id) else {
3148 return;
3149 };
3150 let Some(symbol_id) = self.const_object_references.get(&reference_id) else {
3151 return;
3152 };
3153 self.const_objects.remove(symbol_id);
3154 }
3155
3156 fn process_panda_config_call(&mut self, call: &oxc_ast::ast::CallExpression<'a>) -> bool {
3157 let Some((Lib::Panda, "defineConfig")) = self.callee_role(&call.callee) else {
3158 return false;
3159 };
3160 let Some(Argument::ObjectExpression(obj)) = call.arguments.first() else {
3161 return true;
3162 };
3163 let mut tokens = Vec::new();
3164 collect_panda_config_token_leaves(&mut self.lines, obj, &mut tokens);
3165 if !tokens.is_empty() {
3166 self.defs.push(CssInJsTokenDef {
3167 binding: PANDA_CONFIG_BINDING.to_string(),
3168 origin: CssInJsTokenOrigin::Panda,
3169 tokens,
3170 });
3171 }
3172 true
3173 }
3174}
3175
3176struct StyleXTokenContext<'maps, 'ast> {
3177 imports: &'maps FxHashMap<&'ast str, (Lib, &'ast str)>,
3178 const_strings: &'maps FxHashMap<ReferenceId, (u32, &'ast str)>,
3179 before: u32,
3180}
3181
3182fn collect_stylex_token_object(
3183 lines: &mut LineCounter<'_>,
3184 obj: &ObjectExpression<'_>,
3185 prefix: &str,
3186 shape: StyleXTokenShape,
3187 context: &StyleXTokenContext<'_, '_>,
3188 out: &mut Vec<CssInJsToken>,
3189) -> bool {
3190 for prop in &obj.properties {
3191 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3192 return false;
3193 };
3194 let Some(key) = prop.key.static_name() else {
3195 return false;
3196 };
3197 let path = if prefix.is_empty() {
3198 key.to_string()
3199 } else {
3200 format!("{prefix}.{key}")
3201 };
3202 let line = lines.line_at(prop.key.span().start);
3203 match shape {
3204 StyleXTokenShape::Flat => out.push(CssInJsToken {
3205 path,
3206 def_line: line,
3207 value: stylex_static_token_value(&prop.value, context.imports),
3208 }),
3209 StyleXTokenShape::Nested => match unwrap_transparent_expression(&prop.value) {
3210 Expression::ObjectExpression(nested)
3211 if !is_stylex_conditional_object(
3212 nested,
3213 context.const_strings,
3214 context.before,
3215 ) =>
3216 {
3217 if !collect_stylex_token_object(lines, nested, &path, shape, context, out) {
3218 return false;
3219 }
3220 }
3221 value => out.push(CssInJsToken {
3222 path,
3223 def_line: line,
3224 value: stylex_static_token_value(value, context.imports),
3225 }),
3226 },
3227 }
3228 }
3229 true
3230}
3231
3232fn is_stylex_conditional_object(
3233 obj: &ObjectExpression<'_>,
3234 const_strings: &FxHashMap<ReferenceId, (u32, &str)>,
3235 before: u32,
3236) -> bool {
3237 let mut has_default = false;
3238 for prop in &obj.properties {
3239 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3240 return false;
3241 };
3242 let key = if prop.computed {
3243 match prop.key.as_expression() {
3244 Some(Expression::Identifier(id)) => {
3245 let Some(reference_id) = id.reference_id.get() else {
3246 return false;
3247 };
3248 let Some((declaration_start, condition)) = const_strings.get(&reference_id)
3249 else {
3250 return false;
3251 };
3252 if *declaration_start >= before {
3253 return false;
3254 }
3255 (*condition).to_string()
3256 }
3257 Some(expression) => {
3258 let Some(condition) = static_computed_key(expression) else {
3259 return false;
3260 };
3261 condition
3262 }
3263 None => return false,
3264 }
3265 } else {
3266 let Some(key) = prop.key.static_name() else {
3267 return false;
3268 };
3269 key.to_string()
3270 };
3271 if key == "default" {
3272 has_default = true;
3273 } else if !key.starts_with('@') {
3274 return false;
3275 }
3276 }
3277 has_default
3278}
3279
3280fn stylex_static_token_value(
3281 value: &Expression<'_>,
3282 imports: &FxHashMap<&str, (Lib, &str)>,
3283) -> Option<String> {
3284 match unwrap_transparent_expression(value) {
3285 Expression::ObjectExpression(obj) => obj.properties.iter().find_map(|prop| {
3286 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3287 return None;
3288 };
3289 (prop.key.static_name().as_deref() == Some("default"))
3290 .then(|| stylex_static_token_value(&prop.value, imports))
3291 .flatten()
3292 }),
3293 Expression::CallExpression(call) if is_stylex_static_value_call(&call.callee, imports) => {
3294 call.arguments
3295 .first()
3296 .and_then(Argument::as_expression)
3297 .and_then(|value| stylex_static_token_value(value, imports))
3298 }
3299 _ => static_token_value(value),
3300 }
3301}
3302
3303fn is_stylex_static_value_call(
3304 callee: &Expression<'_>,
3305 imports: &FxHashMap<&str, (Lib, &str)>,
3306) -> bool {
3307 match callee {
3308 Expression::Identifier(id) => imports
3309 .get(id.name.as_str())
3310 .is_some_and(|(lib, role)| *lib == Lib::StyleX && *role == "unstable_conditional"),
3311 Expression::StaticMemberExpression(member) => match &member.object {
3312 Expression::Identifier(object) => {
3313 imports
3314 .get(object.name.as_str())
3315 .is_some_and(|(lib, role)| {
3316 *lib == Lib::StyleX
3317 && (*role == "types"
3318 || member.property.name.as_str() == "unstable_conditional")
3319 })
3320 }
3321 Expression::StaticMemberExpression(namespace) => {
3322 let Expression::Identifier(object) = &namespace.object else {
3323 return false;
3324 };
3325 imports.get(object.name.as_str()).is_some_and(|(lib, _)| {
3326 *lib == Lib::StyleX && namespace.property.name.as_str() == "types"
3327 })
3328 }
3329 _ => false,
3330 },
3331 _ => false,
3332 }
3333}
3334
3335fn collect_token_leaves(
3345 lines: &mut LineCounter<'_>,
3346 obj: &ObjectExpression<'_>,
3347 prefix: &str,
3348 origin: CssInJsTokenOrigin,
3349 out: &mut Vec<CssInJsToken>,
3350) {
3351 for prop in &obj.properties {
3352 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3353 continue;
3354 };
3355 let Some(key) = prop.key.static_name() else {
3356 continue;
3357 };
3358 let path = if prefix.is_empty() {
3359 key.to_string()
3360 } else {
3361 format!("{prefix}.{key}")
3362 };
3363 match &prop.value {
3364 Expression::ObjectExpression(nested)
3365 if origin == CssInJsTokenOrigin::Panda
3366 && !prefix.is_empty()
3367 && object_has_static_key(nested, "value") =>
3368 {
3369 out.push(CssInJsToken {
3370 path,
3371 def_line: lines.line_at(prop.key.span().start),
3372 value: object_static_property_value(nested, "value"),
3373 });
3374 }
3375 Expression::ObjectExpression(nested) => {
3376 collect_token_leaves(lines, nested, &path, origin, out);
3377 }
3378 Expression::Identifier(_) => {}
3381 _ => out.push(CssInJsToken {
3382 value: static_token_value(&prop.value),
3383 path,
3384 def_line: lines.line_at(prop.key.span().start),
3385 }),
3386 }
3387 }
3388}
3389
3390fn object_static_property_value(obj: &ObjectExpression<'_>, wanted: &str) -> Option<String> {
3391 obj.properties.iter().find_map(|prop| {
3392 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3393 return None;
3394 };
3395 (prop.key.static_name().as_deref() == Some(wanted))
3396 .then(|| static_token_value(&prop.value))
3397 .flatten()
3398 })
3399}
3400
3401fn static_token_value(value: &Expression<'_>) -> Option<String> {
3402 match value {
3403 Expression::StringLiteral(lit) => {
3404 let text = lit.value.as_str().trim();
3405 (!text.is_empty()).then(|| text.to_string())
3406 }
3407 Expression::NumericLiteral(num) => Some(format_numeric_token(num)),
3408 Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
3409 if let Expression::NumericLiteral(num) = &unary.argument {
3410 Some(format!("-{}", format_numeric_token(num)))
3411 } else {
3412 None
3413 }
3414 }
3415 _ => None,
3416 }
3417}
3418
3419fn format_numeric_token(num: &NumericLiteral<'_>) -> String {
3420 if num.value.fract() == 0.0 {
3421 format!("{:.0}", num.value)
3422 } else {
3423 num.value.to_string()
3424 }
3425}
3426
3427fn is_theme_binding_name(name: &str) -> bool {
3428 let lower = name.to_ascii_lowercase();
3429 lower == "theme" || lower.ends_with("theme")
3430}
3431
3432fn object_has_static_key(obj: &ObjectExpression<'_>, wanted: &str) -> bool {
3433 obj.properties.iter().any(|prop| {
3434 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3435 return false;
3436 };
3437 prop.key.static_name().is_some_and(|key| key == wanted)
3438 })
3439}
3440
3441fn object_static_property_object<'a>(
3442 obj: &'a ObjectExpression<'a>,
3443 wanted: &str,
3444) -> Option<&'a ObjectExpression<'a>> {
3445 obj.properties.iter().find_map(|prop| {
3446 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
3447 return None;
3448 };
3449 if prop.key.static_name().as_deref() == Some(wanted)
3450 && let Expression::ObjectExpression(value) = &prop.value
3451 {
3452 Some(&**value)
3453 } else {
3454 None
3455 }
3456 })
3457}
3458
3459fn collect_panda_config_token_leaves(
3460 lines: &mut LineCounter<'_>,
3461 obj: &ObjectExpression<'_>,
3462 out: &mut Vec<CssInJsToken>,
3463) {
3464 let Some(theme) = object_static_property_object(obj, "theme") else {
3465 return;
3466 };
3467 for key in ["tokens", "semanticTokens"] {
3468 if let Some(tokens) = object_static_property_object(theme, key) {
3469 collect_token_leaves(lines, tokens, "", CssInJsTokenOrigin::Panda, out);
3470 }
3471 }
3472}
3473
3474impl<'a> Visit<'a> for TokenDefCollector<'a> {
3475 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
3476 if !self.collecting_mutations && self.nested_depth == 0 {
3477 self.process_declarator(decl);
3478 }
3479 walk::walk_variable_declarator(self, decl);
3480 }
3481
3482 fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
3483 self.nested_depth = self.nested_depth.saturating_add(1);
3484 walk::walk_function(self, function, flags);
3485 self.nested_depth = self.nested_depth.saturating_sub(1);
3486 }
3487
3488 fn visit_arrow_function_expression(&mut self, function: &ArrowFunctionExpression<'a>) {
3489 self.nested_depth = self.nested_depth.saturating_add(1);
3490 walk::walk_arrow_function_expression(self, function);
3491 self.nested_depth = self.nested_depth.saturating_sub(1);
3492 }
3493
3494 fn visit_block_statement(&mut self, block: &BlockStatement<'a>) {
3495 self.nested_depth = self.nested_depth.saturating_add(1);
3496 walk::walk_block_statement(self, block);
3497 self.nested_depth = self.nested_depth.saturating_sub(1);
3498 }
3499
3500 fn visit_assignment_expression(&mut self, assignment: &AssignmentExpression<'a>) {
3501 if self.collecting_mutations {
3502 for binding in assignment_target_root_bindings(&assignment.left) {
3503 self.invalidate_const_object(Some(binding));
3504 }
3505 }
3506 walk::walk_assignment_expression(self, assignment);
3507 }
3508
3509 fn visit_update_expression(&mut self, update: &UpdateExpression<'a>) {
3510 if self.collecting_mutations {
3511 self.invalidate_const_object(simple_assignment_target_root_binding(&update.argument));
3512 }
3513 walk::walk_update_expression(self, update);
3514 }
3515
3516 fn visit_unary_expression(&mut self, expression: &UnaryExpression<'a>) {
3517 if self.collecting_mutations && expression.operator.is_delete() {
3518 self.invalidate_const_object(expression_root_binding(&expression.argument));
3519 }
3520 walk::walk_unary_expression(self, expression);
3521 }
3522
3523 fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
3524 if self.collecting_mutations && self.callee_role(&call.callee).is_none() {
3525 if let Some(receiver) = call_receiver_root(&call.callee) {
3526 self.invalidate_const_object(Some(receiver));
3527 }
3528 let possibly_mutated: Vec<RootBinding<'_>> = call
3529 .arguments
3530 .iter()
3531 .filter_map(Argument::as_expression)
3532 .filter_map(expression_root_binding)
3533 .collect();
3534 for binding in possibly_mutated {
3535 self.invalidate_const_object(Some(binding));
3536 }
3537 }
3538 walk::walk_call_expression(self, call);
3539 }
3540
3541 fn visit_export_default_declaration(
3542 &mut self,
3543 decl: &oxc_ast::ast::ExportDefaultDeclaration<'a>,
3544 ) {
3545 if !self.collecting_mutations
3546 && let Some(Expression::CallExpression(call)) = decl.declaration.as_expression()
3547 {
3548 self.process_panda_config_call(call);
3549 }
3550 walk::walk_export_default_declaration(self, decl);
3551 }
3552}
3553
3554fn count_newlines_u32(s: &str) -> u32 {
3556 u32::try_from(s.bytes().filter(|&b| b == b'\n').count()).unwrap_or(u32::MAX)
3557}
3558
3559struct LineCounter<'a> {
3571 source: &'a str,
3572 last_offset: usize,
3575 last_line: u32,
3578}
3579
3580impl<'a> LineCounter<'a> {
3581 fn new(source: &'a str) -> Self {
3582 Self {
3583 source,
3584 last_offset: 0,
3585 last_line: 1,
3586 }
3587 }
3588
3589 fn line_at(&mut self, offset: u32) -> u32 {
3592 let end = (offset as usize).min(self.source.len());
3593 if !self.source.is_char_boundary(end) {
3596 return 1;
3597 }
3598 if end >= self.last_offset {
3599 let delta = count_newlines_u32(&self.source[self.last_offset..end]);
3600 self.last_line = self.last_line.saturating_add(delta);
3601 } else {
3602 let delta = count_newlines_u32(&self.source[end..self.last_offset]);
3603 self.last_line = self.last_line.saturating_sub(delta);
3604 }
3605 self.last_offset = end;
3606 self.last_line
3607 }
3608}
3609
3610#[cfg(all(test, not(miri)))]
3611mod tests {
3612 use super::*;
3613
3614 fn defs(source: &str) -> Vec<CssInJsTokenDef> {
3615 css_in_js_token_defs(source, Path::new("tokens.ts"))
3616 }
3617
3618 fn paths(defs: &[CssInJsTokenDef], binding: &str) -> Vec<String> {
3619 defs.iter()
3620 .find(|d| d.binding == binding)
3621 .map(|d| d.tokens.iter().map(|t| t.path.clone()).collect())
3622 .unwrap_or_default()
3623 }
3624
3625 fn token_values(defs: &[CssInJsTokenDef], binding: &str) -> Vec<(String, Option<String>)> {
3626 defs.iter()
3627 .find(|d| d.binding == binding)
3628 .map(|d| {
3629 d.tokens
3630 .iter()
3631 .map(|t| (t.path.clone(), t.value.clone()))
3632 .collect()
3633 })
3634 .unwrap_or_default()
3635 }
3636
3637 fn theme_defs(source: &str) -> Vec<CssInJsTokenDef> {
3638 css_in_js_theme_token_defs(source, Path::new("theme.ts"))
3639 }
3640
3641 #[test]
3642 fn incremental_def_lines_match_source_order() {
3643 let src = "import { defineVars } from '@stylexjs/stylex';\n\
3649export const colors = defineVars({\n\
3650primary: '#000',\n\
3651secondary: '#fff',\n\
3652});\n\
3653export const space = defineVars({\n\
3654sm: '4px',\n\
3655lg: '16px',\n\
3656});\n";
3657 let d = defs(src);
3658 let line_of = |binding: &str, path: &str| {
3659 d.iter()
3660 .find(|def| def.binding == binding)
3661 .and_then(|def| def.tokens.iter().find(|t| t.path == path))
3662 .unwrap_or_else(|| panic!("token {binding}.{path} present"))
3663 .def_line
3664 };
3665 assert_eq!(line_of("colors", "primary"), 3);
3666 assert_eq!(line_of("colors", "secondary"), 4);
3667 assert_eq!(line_of("space", "sm"), 7);
3668 assert_eq!(line_of("space", "lg"), 8);
3669 }
3670
3671 #[test]
3672 fn stylex_define_vars_flat_namespace_call() {
3673 let d = defs(
3674 r"
3675import * as stylex from '@stylexjs/stylex';
3676export const vars = stylex.defineVars({ primaryColor: '#3b82f6', spacingSm: '4px' });
3677",
3678 );
3679 assert_eq!(paths(&d, "vars"), vec!["primaryColor", "spacingSm"]);
3680 assert_eq!(
3681 token_values(&d, "vars"),
3682 vec![
3683 ("primaryColor".to_string(), Some("#3b82f6".to_string())),
3684 ("spacingSm".to_string(), Some("4px".to_string())),
3685 ]
3686 );
3687 }
3688
3689 #[test]
3690 fn stylex_define_vars_conditional_value_is_one_flat_token() {
3691 let d = defs(
3692 r"
3693import { defineVars } from '@stylexjs/stylex';
3694const DARK = '@media (prefers-color-scheme: dark)';
3695export const vars = defineVars({ color: { default: '#000', [DARK]: '#fff' } });
3696",
3697 );
3698 assert_eq!(paths(&d, "vars"), vec!["color"]);
3699 assert_eq!(
3700 token_values(&d, "vars"),
3701 vec![("color".to_string(), Some("#000".to_string()))]
3702 );
3703 }
3704
3705 #[test]
3706 fn stylex_nested_vars_recurse_and_stop_at_conditional_leaves() {
3707 let d = defs(
3708 r"
3709import * as stylex from 'stylex';
3710import { unstable_conditional as cond } from 'stylex';
3711const DARK = '@media (prefers-color-scheme: dark)';
3712export const vars = stylex.unstable_defineVarsNested({
3713 surface: {
3714 bg: { default: '#fff', [DARK]: '#111' },
3715 text: cond({ default: '#000', [DARK]: '#eee' }),
3716 },
3717 typed: stylex.types.color('red'),
3718});
3719",
3720 );
3721 assert_eq!(
3722 paths(&d, "vars"),
3723 vec!["surface.bg", "surface.text", "typed"]
3724 );
3725 assert_eq!(
3726 token_values(&d, "vars"),
3727 vec![
3728 ("surface.bg".to_string(), Some("#fff".to_string())),
3729 ("surface.text".to_string(), Some("#000".to_string())),
3730 ("typed".to_string(), Some("red".to_string())),
3731 ]
3732 );
3733 }
3734
3735 #[test]
3736 fn stylex_nested_default_token_namespace_is_not_a_conditional_leaf() {
3737 let d = defs(
3738 r"
3739import * as stylex from '@stylexjs/stylex';
3740export const vars = stylex.unstable_defineVarsNested({
3741 button: {
3742 primary: {
3743 background: {
3744 default: stylex.unstable_conditional({ default: 'blue' }),
3745 hovered: stylex.unstable_conditional({ default: 'navy' }),
3746 },
3747 },
3748 },
3749});
3750",
3751 );
3752 assert_eq!(
3753 paths(&d, "vars"),
3754 vec![
3755 "button.primary.background.default",
3756 "button.primary.background.hovered",
3757 ]
3758 );
3759 }
3760
3761 #[test]
3762 fn stylex_nested_static_computed_conditions_remain_one_leaf() {
3763 let d = defs(
3764 r"
3765import * as stylex from '@stylexjs/stylex';
3766export const vars = stylex.unstable_defineVarsNested({
3767 surface: {
3768 color: {
3769 ['default']: '#fff',
3770 [`@media (prefers-color-scheme: dark)`]: '#111',
3771 },
3772 },
3773});
3774",
3775 );
3776 assert_eq!(paths(&d, "vars"), vec!["surface.color"]);
3777 }
3778
3779 #[test]
3780 fn stylex_define_vars_resolves_local_const_object() {
3781 let d = defs(
3782 r"
3783import { defineVars as define } from '@stylexjs/stylex';
3784const values = { foreground: '#111', background: '#fff' };
3785export const vars = define(values);
3786",
3787 );
3788 assert_eq!(paths(&d, "vars"), vec!["foreground", "background"]);
3789 }
3790
3791 #[test]
3792 fn stylex_define_vars_resolves_transparent_typescript_wrappers() {
3793 let d = defs(
3794 r"
3795import { defineVars } from '@stylexjs/stylex';
3796const values = { foreground: '#111' } as const;
3797export const vars = defineVars(values);
3798export const more = defineVars(
3799 { background: '#fff' } satisfies Record<string, string>,
3800);
3801",
3802 );
3803 assert_eq!(paths(&d, "vars"), vec!["foreground"]);
3804 assert_eq!(paths(&d, "more"), vec!["background"]);
3805 }
3806
3807 #[test]
3808 fn stylex_define_vars_abstains_after_local_const_object_mutation() {
3809 let d = defs(
3810 r"
3811import { defineVars } from '@stylexjs/stylex';
3812const values = { foreground: '#111' };
3813values.foreground = getColor();
3814export const vars = defineVars(values);
3815",
3816 );
3817 assert!(d.is_empty(), "mutated token objects must abstain: {d:?}");
3818 }
3819
3820 #[test]
3821 fn stylex_define_vars_abstains_when_local_object_is_mutated_after_definition() {
3822 let d = defs(
3823 r"
3824import { defineVars } from '@stylexjs/stylex';
3825const values = { foreground: '#111' };
3826export const vars = defineVars(values);
3827values.foreground = getColor();
3828",
3829 );
3830 assert!(
3831 d.is_empty(),
3832 "later mutation must invalidate the definition: {d:?}"
3833 );
3834 }
3835
3836 #[test]
3837 fn stylex_define_vars_abstains_after_delete_or_receiver_call() {
3838 for mutation in ["delete values.foreground;", "values.mutate();"] {
3839 let source = format!(
3840 "import {{ defineVars }} from '@stylexjs/stylex';\nconst values = {{ foreground: '#111' }};\n{mutation}\nexport const vars = defineVars(values);"
3841 );
3842 let d = defs(&source);
3843 assert!(d.is_empty(), "mutated token objects must abstain: {source}");
3844 }
3845 }
3846
3847 #[test]
3848 fn stylex_define_vars_requires_declared_unmutated_root_object() {
3849 for source in [
3850 r"
3851import { defineVars } from '@stylexjs/stylex';
3852export const vars = defineVars(values);
3853const values = { foreground: '#111' };
3854",
3855 r"
3856import { defineVars } from '@stylexjs/stylex';
3857const values = { foreground: '#111' };
3858({ foreground: values.foreground } = next);
3859export const vars = defineVars(values);
3860",
3861 r"
3862import { defineVars } from '@stylexjs/stylex';
3863const values = { foreground: '#111' };
3864[values.foreground] = next;
3865export const vars = defineVars(values);
3866",
3867 ] {
3868 let d = defs(source);
3869 assert!(d.is_empty(), "invalid static object must abstain: {source}");
3870 }
3871 }
3872
3873 #[test]
3874 fn stylex_define_vars_ignores_shadowed_mutation() {
3875 let d = defs(
3876 r"
3877import { defineVars } from '@stylexjs/stylex';
3878const values = { foreground: '#111' };
3879function mutate(values) { values.foreground = '#fff'; }
3880export const vars = defineVars(values);
3881",
3882 );
3883 assert_eq!(paths(&d, "vars"), vec!["foreground"]);
3884 }
3885
3886 #[test]
3887 fn stylex_nested_condition_must_be_declared_before_use() {
3888 let d = defs(
3889 r"
3890import { unstable_defineVarsNested } from '@stylexjs/stylex';
3891export const vars = unstable_defineVarsNested({
3892 surface: { color: { default: '#fff', [DARK]: '#111' } },
3893});
3894const DARK = '@media (prefers-color-scheme: dark)';
3895",
3896 );
3897 assert!(d.is_empty(), "TDZ condition must abstain: {d:?}");
3898 }
3899
3900 #[test]
3901 fn stylex_nested_scope_shadow_does_not_define_tokens() {
3902 let d = defs(
3903 r"
3904import * as stylex from '@stylexjs/stylex';
3905export const makeVars = (stylex) => {
3906 const vars = stylex.defineVars({ foreground: '#111' });
3907 return vars;
3908};
3909",
3910 );
3911 assert!(d.is_empty(), "nested shadowed calls must abstain: {d:?}");
3912 }
3913
3914 #[test]
3915 fn stylex_arbitrary_value_call_does_not_invent_comparable_value() {
3916 let d = defs(
3917 r"
3918import { defineVars } from '@stylexjs/stylex';
3919const dynamic = value => value;
3920export const vars = defineVars({ color: dynamic({ default: '#111' }) });
3921",
3922 );
3923 assert_eq!(paths(&d, "vars"), vec!["color"]);
3924 assert_eq!(token_values(&d, "vars"), vec![("color".to_string(), None)]);
3925 }
3926
3927 #[test]
3928 fn panda_define_tokens_collapses_value_objects() {
3929 let d = defs(
3930 r"
3931import { defineTokens } from '@pandacss/dev';
3932export const tokens = defineTokens({
3933 colors: {
3934 brand: { value: '#f05a28' },
3935 accent: { value: '{colors.brand}' },
3936 },
3937 spacing: { card: { value: '1rem' } },
3938});
3939",
3940 );
3941 assert_eq!(
3942 paths(&d, "tokens"),
3943 vec!["colors.brand", "colors.accent", "spacing.card"]
3944 );
3945 assert_eq!(
3946 token_values(&d, "tokens"),
3947 vec![
3948 ("colors.brand".to_string(), Some("#f05a28".to_string())),
3949 (
3950 "colors.accent".to_string(),
3951 Some("{colors.brand}".to_string())
3952 ),
3953 ("spacing.card".to_string(), Some("1rem".to_string())),
3954 ]
3955 );
3956 assert_eq!(
3957 d.iter().find(|d| d.binding == "tokens").unwrap().origin,
3958 CssInJsTokenOrigin::Panda
3959 );
3960 }
3961
3962 #[test]
3963 fn panda_define_config_extracts_tokens_and_semantic_tokens() {
3964 let d = defs(
3965 r"
3966import { defineConfig } from '@pandacss/dev';
3967
3968export default defineConfig({
3969 theme: {
3970 tokens: {
3971 colors: {
3972 brand: { value: '#f05a28' },
3973 },
3974 },
3975 semanticTokens: {
3976 colors: {
3977 surface: { value: { base: '{colors.brand}', _dark: '#111111' } },
3978 },
3979 },
3980 recipes: {
3981 card: { base: { color: 'colors.brand' } },
3982 },
3983 },
3984});
3985",
3986 );
3987 assert_eq!(
3988 paths(&d, "pandaConfig"),
3989 vec!["colors.brand", "colors.surface"]
3990 );
3991 assert_eq!(
3992 token_values(&d, "pandaConfig"),
3993 vec![
3994 ("colors.brand".to_string(), Some("#f05a28".to_string())),
3995 ("colors.surface".to_string(), None),
3996 ]
3997 );
3998 assert_eq!(
3999 d.iter()
4000 .find(|d| d.binding == "pandaConfig")
4001 .unwrap()
4002 .origin,
4003 CssInJsTokenOrigin::Panda
4004 );
4005 }
4006
4007 #[test]
4008 fn theme_object_definitions_flatten_static_leaves() {
4009 let d = theme_defs(
4010 r"
4011export const appTheme = {
4012 colors: { brand: '#f05a28', accent: '#111' },
4013 space: { card: '1rem' },
4014 dynamic: palette,
4015};
4016",
4017 );
4018 assert_eq!(
4019 paths(&d, "appTheme"),
4020 vec!["colors.brand", "colors.accent", "space.card"]
4021 );
4022 assert_eq!(
4023 token_values(&d, "appTheme"),
4024 vec![
4025 ("colors.brand".to_string(), Some("#f05a28".to_string())),
4026 ("colors.accent".to_string(), Some("#111".to_string())),
4027 ("space.card".to_string(), Some("1rem".to_string())),
4028 ]
4029 );
4030 assert_eq!(
4031 d.iter().find(|d| d.binding == "appTheme").unwrap().origin,
4032 CssInJsTokenOrigin::Theme
4033 );
4034 }
4035
4036 #[test]
4037 fn theme_consumers_credit_props_and_destructured_theme_reads() {
4038 let leaves = ["colors.brand", "space.card"]
4039 .into_iter()
4040 .map(str::to_owned)
4041 .collect();
4042 let hits = scan_one(
4043 r"
4044import styled from 'styled-components';
4045export const Card = styled.div`
4046 color: ${({ theme }) => theme.colors.brand};
4047 margin: ${props => props.theme.space.card};
4048`;
4049",
4050 Path::new("card.tsx"),
4051 ConsumerQuery::ThemeReads {
4052 leaf_paths: &leaves,
4053 },
4054 );
4055 let mut token_paths: Vec<String> = hits.into_iter().map(|hit| hit.token_path).collect();
4056 token_paths.sort();
4057 assert_eq!(token_paths, vec!["colors.brand", "space.card"]);
4058 }
4059
4060 #[test]
4061 fn ve_create_theme_tuple_destructure_binds_element_one() {
4062 let d = defs(
4063 r"
4064import { createTheme } from '@vanilla-extract/css';
4065export const [themeClass, vars] = createTheme({
4066 color: { brand: 'red', accent: 'blue' },
4067 space: { small: '4px' },
4068});
4069",
4070 );
4071 assert_eq!(
4073 paths(&d, "vars"),
4074 vec!["color.brand", "color.accent", "space.small"]
4075 );
4076 assert!(paths(&d, "themeClass").is_empty());
4077 }
4078
4079 #[test]
4080 fn ve_create_theme_contract_null_leaves() {
4081 let d = defs(
4082 r"
4083import { createThemeContract } from '@vanilla-extract/css';
4084export const vars = createThemeContract({ color: { brand: null, accent: null } });
4085",
4086 );
4087 assert_eq!(paths(&d, "vars"), vec!["color.brand", "color.accent"]);
4089 }
4090
4091 #[test]
4092 fn ve_create_global_theme_two_arg_binds_lhs_tokens_in_second_arg() {
4093 let d = defs(
4094 r"
4095import { createGlobalTheme } from '@vanilla-extract/css';
4096export const vars = createGlobalTheme(':root', { color: { brand: 'red' } });
4097",
4098 );
4099 assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
4100 }
4101
4102 #[test]
4103 fn ve_create_theme_two_arg_contract_impl_is_not_a_definition_site() {
4104 let d = defs(
4107 r"
4108import { createTheme } from '@vanilla-extract/css';
4109export const themeClass = createTheme(vars, { color: { brand: 'red' } });
4110",
4111 );
4112 assert!(
4113 d.is_empty(),
4114 "2-arg createTheme must not define tokens, got {d:?}"
4115 );
4116 }
4117
4118 #[test]
4119 fn ve_create_global_theme_three_arg_contract_impl_is_not_a_definition_site() {
4120 let d = defs(
4121 r"
4122import { createGlobalTheme } from '@vanilla-extract/css';
4123createGlobalTheme(':root', vars, { color: { brand: 'red' } });
4124",
4125 );
4126 assert!(
4127 d.is_empty(),
4128 "3-arg createGlobalTheme must not define tokens, got {d:?}"
4129 );
4130 }
4131
4132 #[test]
4133 fn aliased_named_import_still_fires() {
4134 let d = defs(
4135 r"
4136import { createThemeContract as ct } from '@vanilla-extract/css';
4137export const vars = ct({ color: { brand: null } });
4138",
4139 );
4140 assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
4141 }
4142
4143 #[test]
4144 fn local_helper_not_from_library_does_not_fire() {
4145 let d = defs(
4147 r"
4148function defineVars(o) { return o; }
4149export const vars = defineVars({ color: { primary: '#000' } });
4150",
4151 );
4152 assert!(d.is_empty(), "local defineVars must not fire, got {d:?}");
4153 }
4154
4155 #[test]
4156 fn unrelated_create_theme_import_does_not_fire() {
4157 let d = defs(
4158 r"
4159import { createTheme } from '@mui/material/styles';
4160export const theme = createTheme({ palette: { primary: { main: '#000' } } });
4161",
4162 );
4163 assert!(d.is_empty(), "non-VE createTheme must not fire, got {d:?}");
4164 }
4165
4166 #[test]
4167 fn type_only_import_does_not_fire() {
4168 let d = defs(
4169 r"
4170import type { defineVars } from '@stylexjs/stylex';
4171export const vars = defineVars({ color: { primary: '#000' } });
4172",
4173 );
4174 assert!(
4175 d.is_empty(),
4176 "type-only import must not gate recognition, got {d:?}"
4177 );
4178 }
4179
4180 #[test]
4181 fn token_def_lines_are_per_leaf() {
4182 let src = "import { unstable_defineVarsNested } from '@stylexjs/stylex';\nexport const vars = unstable_defineVarsNested({\n color: {\n primary: '#000',\n secondary: '#fff',\n },\n});\n";
4183 let d = defs(src);
4184 let def = d.iter().find(|d| d.binding == "vars").unwrap();
4185 let primary = def
4186 .tokens
4187 .iter()
4188 .find(|t| t.path == "color.primary")
4189 .unwrap();
4190 let secondary = def
4191 .tokens
4192 .iter()
4193 .find(|t| t.path == "color.secondary")
4194 .unwrap();
4195 assert_eq!(primary.def_line, 4);
4196 assert_eq!(secondary.def_line, 5);
4197 }
4198
4199 #[test]
4200 fn stylex_spread_or_dynamic_key_abstains_for_whole_definition() {
4201 let d = defs(
4202 r"
4203import { defineVars } from '@stylexjs/stylex';
4204const base = { a: '1' };
4205export const vars = defineVars({ ...base, ['x' + 'y']: '2', real: '#000' });
4206",
4207 );
4208 assert!(d.is_empty(), "partial StyleX shapes must abstain: {d:?}");
4209 }
4210
4211 #[test]
4212 fn stylex_nested_dynamic_condition_key_abstains_for_whole_definition() {
4213 let d = defs(
4214 r"
4215import * as stylex from '@stylexjs/stylex';
4216export const vars = stylex.unstable_defineVarsNested({
4217 surface: { default: '#fff', [getCondition()]: '#111' },
4218});
4219",
4220 );
4221 assert!(
4222 d.is_empty(),
4223 "dynamic StyleX conditions must abstain: {d:?}"
4224 );
4225 }
4226
4227 #[test]
4228 fn identifier_valued_key_is_not_a_leaf_but_call_and_member_values_are() {
4229 let d = defs(
4233 r"
4234import { createGlobalTheme } from '@vanilla-extract/css';
4235export const vars = createGlobalTheme(':root', {
4236 palette: tailwindPalette,
4237 radius: px(2),
4238 red: colors.red['500'],
4239});
4240",
4241 );
4242 let p = paths(&d, "vars");
4243 assert!(
4244 !p.contains(&"palette".to_string()),
4245 "identifier-valued key must not be a leaf: {p:?}"
4246 );
4247 assert!(
4248 p.contains(&"radius".to_string()),
4249 "call-valued key is a leaf: {p:?}"
4250 );
4251 assert!(
4252 p.contains(&"red".to_string()),
4253 "member-valued key is a leaf: {p:?}"
4254 );
4255 }
4256
4257 #[test]
4258 fn no_css_in_js_import_returns_empty() {
4259 let d = defs("export const vars = { color: { primary: '#000' } };");
4260 assert!(d.is_empty());
4261 }
4262
4263 fn leaves(paths: &[&str]) -> FxHashSet<String> {
4264 paths.iter().map(|s| (*s).to_string()).collect()
4265 }
4266
4267 fn scan_one(source: &str, path: &Path, query: ConsumerQuery<'_>) -> Vec<TokenConsumerHit> {
4269 css_in_js_consumer_scan(source, path, &[query])
4270 .into_iter()
4271 .map(|(_, hit)| hit)
4272 .collect()
4273 }
4274
4275 fn consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
4276 let leaf_paths = leaves(paths);
4277 scan_one(
4278 source,
4279 Path::new("card.ts"),
4280 ConsumerQuery::MemberBinding {
4281 alias,
4282 leaf_paths: &leaf_paths,
4283 },
4284 )
4285 }
4286
4287 fn panda_consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
4288 let leaf_paths = leaves(paths);
4289 scan_one(
4290 source,
4291 Path::new("card.ts"),
4292 ConsumerQuery::PandaTokenCall {
4293 alias,
4294 leaf_paths: &leaf_paths,
4295 },
4296 )
4297 }
4298
4299 fn panda_style_consumers(
4300 source: &str,
4301 aliases: &[&str],
4302 paths: &[&str],
4303 ) -> Vec<TokenConsumerHit> {
4304 let aliases = aliases.iter().map(|s| (*s).to_string()).collect();
4305 let leaf_paths = leaves(paths);
4306 scan_one(
4307 source,
4308 Path::new("card.ts"),
4309 ConsumerQuery::PandaStyleValues {
4310 aliases: &aliases,
4311 leaf_paths: &leaf_paths,
4312 },
4313 )
4314 }
4315
4316 #[test]
4317 fn consumer_matches_deepest_leaf_not_intermediate_group() {
4318 let hits = consumers(
4321 "const a = vars.color.primary;",
4322 "vars",
4323 &["color.primary", "color.secondary"],
4324 );
4325 assert_eq!(hits.len(), 1);
4326 assert_eq!(hits[0].token_path, "color.primary");
4327 assert_eq!(hits[0].line, 1);
4328 }
4329
4330 #[test]
4331 fn consumer_aliased_receiver() {
4332 let hits = consumers("const a = v.color.primary;", "v", &["color.primary"]);
4334 assert_eq!(hits.len(), 1);
4335 assert_eq!(hits[0].token_path, "color.primary");
4336 }
4337
4338 #[test]
4339 fn consumer_multiple_sites_distinct_lines() {
4340 let src = "const a = vars.color.primary;\nconst b = vars.space.sm;\nconst c = vars.color.primary;";
4341 let hits = consumers(src, "vars", &["color.primary", "space.sm"]);
4342 assert_eq!(hits.len(), 3);
4343 let lines: Vec<u32> = hits.iter().map(|h| h.line).collect();
4344 assert_eq!(lines, vec![1, 2, 3]);
4345 }
4346
4347 #[test]
4348 fn consumer_in_style_object_value_position() {
4349 let hits = consumers(
4351 "export const s = stylex.create({ root: { color: vars.color.primary } });",
4352 "vars",
4353 &["color.primary"],
4354 );
4355 assert_eq!(hits.len(), 1);
4356 assert_eq!(hits[0].token_path, "color.primary");
4357 }
4358
4359 #[test]
4360 fn panda_token_call_consumer_matches_string_literal() {
4361 let hits = panda_consumers(
4362 "export const c = css({ color: token('colors.brand') });",
4363 "token",
4364 &["colors.brand", "colors.accent"],
4365 );
4366 assert_eq!(hits.len(), 1);
4367 assert_eq!(hits[0].token_path, "colors.brand");
4368 }
4369
4370 #[test]
4371 fn panda_style_value_consumer_matches_known_token_string() {
4372 let hits = panda_style_consumers(
4373 "export const c = css({ color: 'colors.brand', _hover: { bg: 'colors.accent' } });",
4374 &["css"],
4375 &["colors.brand", "colors.accent", "colors.unused"],
4376 );
4377 let paths: Vec<_> = hits.iter().map(|hit| hit.token_path.as_str()).collect();
4378 assert_eq!(paths, vec!["colors.brand", "colors.accent"]);
4379 }
4380
4381 #[test]
4382 fn panda_style_value_consumer_ignores_unimported_alias() {
4383 let hits = panda_style_consumers(
4384 "export const c = notPanda({ color: 'colors.brand' });",
4385 &["css"],
4386 &["colors.brand"],
4387 );
4388 assert!(hits.is_empty());
4389 }
4390
4391 #[test]
4392 fn consumer_flat_stylex_depth_one() {
4393 let hits = consumers("const a = vars.primaryColor;", "vars", &["primaryColor"]);
4394 assert_eq!(hits.len(), 1);
4395 assert_eq!(hits[0].token_path, "primaryColor");
4396 }
4397
4398 #[test]
4399 fn stylex_theme_call_consumes_complete_group_once() {
4400 let leaf_paths = leaves(&["surface.bg", "surface.text"]);
4401 let queries = [ConsumerQuery::StyleXThemeGroup {
4402 contract_alias: "tokens",
4403 leaf_paths: &leaf_paths,
4404 }];
4405 let hits = css_in_js_consumer_scan(
4406 "import { createTheme as theme } from 'stylex';\nconst reset = theme(tokens, {});",
4407 Path::new("theme.ts"),
4408 &queries,
4409 );
4410 let mut paths: Vec<_> = hits
4411 .into_iter()
4412 .map(|(_, hit)| (hit.token_path, hit.line))
4413 .collect();
4414 paths.sort();
4415 assert_eq!(
4416 paths,
4417 vec![
4418 ("surface.bg".to_string(), 2),
4419 ("surface.text".to_string(), 2),
4420 ]
4421 );
4422 }
4423
4424 #[test]
4425 fn stylex_theme_call_abstains_for_type_only_or_unknown_contract() {
4426 let leaf_paths = leaves(&["surface.bg"]);
4427 let queries = [ConsumerQuery::StyleXThemeGroup {
4428 contract_alias: "tokens",
4429 leaf_paths: &leaf_paths,
4430 }];
4431 for source in [
4432 "import { type createTheme } from '@stylexjs/stylex'; createTheme(tokens, {});",
4433 "import * as stylex from '@stylexjs/stylex'; stylex.createTheme(other, {});",
4434 "const createTheme = (a, b) => b; createTheme(tokens, {});",
4435 "import { createTheme } from 'stylex'; const run = (createTheme) => createTheme(tokens, {});",
4436 "import * as stylex from 'stylex'; const run = (stylex) => stylex.createTheme(tokens, {});",
4437 "import { createTheme } from 'stylex'; const run = (tokens) => createTheme(tokens, {});",
4438 ] {
4439 assert!(
4440 css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4441 "must abstain: {source}"
4442 );
4443 }
4444 }
4445
4446 #[test]
4447 fn stylex_theme_call_requires_bound_exact_static_shape() {
4448 let leaf_paths = leaves(&["surface.bg"]);
4449 let queries = [ConsumerQuery::StyleXThemeGroup {
4450 contract_alias: "tokens",
4451 leaf_paths: &leaf_paths,
4452 }];
4453 for source in [
4454 "import { createTheme } from 'stylex'; createTheme(tokens, {});",
4455 "import { createTheme } from 'stylex'; const theme = createTheme(tokens);",
4456 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, {}, {});",
4457 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, '#fff');",
4458 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, getOverrides());",
4459 "import { createTheme } from 'stylex'; const { theme } = createTheme(tokens, {});",
4460 ] {
4461 assert!(
4462 css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4463 "must abstain: {source}"
4464 );
4465 }
4466 }
4467
4468 #[test]
4469 fn stylex_theme_call_accepts_unmutated_local_static_override_and_wrapped_contract() {
4470 let leaf_paths = leaves(&["surface.bg"]);
4471 let queries = [ConsumerQuery::StyleXThemeGroup {
4472 contract_alias: "tokens",
4473 leaf_paths: &leaf_paths,
4474 }];
4475 let source = r"
4476import { createTheme, unstable_conditional } from 'stylex';
4477const overrides = {
4478 surface: unstable_conditional({ default: '#fff', '@media (prefers-color-scheme: dark)': '#111' }),
4479} as const;
4480const theme = createTheme(tokens as typeof tokens, overrides);
4481";
4482 let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4483 assert_eq!(hits.len(), 1);
4484 assert_eq!(hits[0].1.token_path, "surface.bg");
4485 }
4486
4487 #[test]
4488 fn stylex_theme_call_accepts_official_static_expression_shapes() {
4489 let leaf_paths = leaves(&["surface.bg"]);
4490 let queries = [ConsumerQuery::StyleXThemeGroup {
4491 contract_alias: "tokens",
4492 leaf_paths: &leaf_paths,
4493 }];
4494 let source = r"
4495import { createTheme, types } from 'stylex';
4496const DARK = '@media (prefers-color-scheme: dark)';
4497const name = 'light';
4498const RADIUS = 4;
4499const theme = createTheme(tokens, {
4500 [DARK]: `${name}green`,
4501 radius: RADIUS * 2,
4502 typed: types.length({ default: RADIUS * 2 }),
4503});
4504";
4505 let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4506 assert_eq!(hits.len(), 1);
4507 assert_eq!(hits[0].1.token_path, "surface.bg");
4508 }
4509
4510 #[test]
4511 fn stylex_theme_call_accepts_generic_evaluator_shapes() {
4512 let leaf_paths = leaves(&["surface.bg"]);
4513 let queries = [ConsumerQuery::StyleXThemeGroup {
4514 contract_alias: "tokens",
4515 leaf_paths: &leaf_paths,
4516 }];
4517 let source = r"
4518import { createTheme } from 'stylex';
4519const palette = { green: 'green' };
4520const base = { color: 'red' };
4521const alias = base;
4522const FLAG = true;
4523const RADIUS = '4';
4524const choose = value => value ? 'red' : 'blue';
4525const pick = value => value.color;
4526consume(RADIUS);
4527const theme = createTheme(tokens, {
4528 ...alias,
4529 member: palette.green,
4530 conditional: FLAG ? 'red' : 'blue',
4531 logical: FLAG && 'red',
4532 sequence: (0, 'red'),
4533 raw: String.raw`red-${2}`,
4534 math: Math.max(1, 2),
4535 stringMethod: 'red'.toUpperCase(),
4536 entries: Object.fromEntries([['color', 'red']]),
4537 arrow: choose(FLAG),
4538 arrowMember: pick({ color: 'red' }),
4539 coercedRadius: +RADIUS,
4540 bigintEquality: (1n === 1n) ? 8 : 4,
4541});
4542";
4543 let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4544 assert_eq!(hits.len(), 1);
4545 assert_eq!(hits[0].1.token_path, "surface.bg");
4546 }
4547
4548 #[test]
4549 fn stylex_theme_call_resolves_static_contract_aliases_and_scalar_receivers() {
4550 let leaf_paths = leaves(&["surface.bg"]);
4551 let queries = [ConsumerQuery::StyleXThemeGroup {
4552 contract_alias: "tokens",
4553 leaf_paths: &leaf_paths,
4554 }];
4555 let source = r"
4556import { createTheme } from 'stylex';
4557const alias = tokens;
4558const secondAlias = alias;
4559const color = 'red';
4560const digits = 2;
4561const theme = createTheme(secondAlias, {
4562 color: color.toUpperCase().toLowerCase(),
4563 radius: (1).toFixed(digits),
4564});
4565color.toUpperCase();
4566const second = createTheme(alias, { color });
4567";
4568 let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4569 assert_eq!(hits.len(), 2);
4570 }
4571
4572 #[test]
4573 fn stylex_pure_call_arguments_remain_static_across_theme_calls() {
4574 let leaf_paths = leaves(&["surface.bg"]);
4575 let queries = [ConsumerQuery::StyleXThemeGroup {
4576 contract_alias: "tokens",
4577 leaf_paths: &leaf_paths,
4578 }];
4579 let source = r"
4580import { createTheme } from 'stylex';
4581const RADIUS = 4;
4582const first = createTheme(tokens, { radius: Math.max(RADIUS, 2) });
4583const second = createTheme(tokens, { radius: RADIUS * 2 });
4584";
4585 let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4586 assert_eq!(hits.len(), 2);
4587 }
4588
4589 #[test]
4590 fn stylex_theme_call_rejects_transitive_tdz_cycles_and_dynamic_helpers() {
4591 let leaf_paths = leaves(&["surface.bg"]);
4592 let queries = [ConsumerQuery::StyleXThemeGroup {
4593 contract_alias: "tokens",
4594 leaf_paths: &leaf_paths,
4595 }];
4596 for source in [
4597 "import { createTheme } from 'stylex'; const palette = { green: later }; const later = 'red'; const theme = createTheme(tokens, { color: palette.green });",
4598 "import { createTheme } from 'stylex'; const palette = { green: palette.green }; const theme = createTheme(tokens, { color: palette.green });",
4599 "import { createTheme } from 'stylex'; const choose = value => getColor(value); const theme = createTheme(tokens, { color: choose(true) });",
4600 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Object.assign({}, { color: 'red' }) });",
4601 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Object.fromEntries() });",
4602 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Object.fromEntries([['color', ['red']]]) });",
4603 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Math.notAFunction(1) });",
4604 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x'.repeat(-1) });",
4605 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: (1).toFixed(1000) });",
4606 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { radius: 1n + 1 });",
4607 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { radius: +1n });",
4608 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { radius: Math.max(1n, 2n) });",
4609 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x' in 1 });",
4610 "import { createTheme } from 'stylex'; const base = { color: 'red' }; const alias = base; alias.color = getColor(); const theme = createTheme(tokens, base);",
4611 "import { createTheme } from 'stylex'; const big = () => 1n; const theme = createTheme(tokens, { radius: Math.max(big(), 2) });",
4612 "import { createTheme } from 'stylex'; const big = () => LATER; const LATER = 1n; const theme = createTheme(tokens, { radius: Math.max(big(), 2) });",
4613 "import { createTheme } from 'stylex'; const base = { color: 'red' }; const get = () => base; get().color = getColor(); const theme = createTheme(tokens, base);",
4614 "import { createTheme } from 'stylex'; const base = { color: 'red' }; const get = () => base; mutate(get()); const theme = createTheme(tokens, base);",
4615 "import { createTheme } from 'stylex'; const base = { nested: { color: 'red' } }; const member = value => value.nested; member(base).color = getColor(); const theme = createTheme(tokens, base);",
4616 "import { createTheme } from 'stylex'; const base = { nested: { color: 'red' } }; const member = value => value.nested; mutate(member(base)); const theme = createTheme(tokens, base);",
4617 "import { createTheme } from 'stylex'; const base = { color: 'red' }; const theme = createTheme(tokens, base); base.color = getColor();",
4618 "import { createTheme } from 'stylex'; const choose = value => { return value; }; const theme = createTheme(tokens, { color: choose('red') });",
4619 "import { createTheme } from 'stylex'; const choose = value => { return value; mutate(); }; const theme = createTheme(tokens, { color: choose('red') });",
4620 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'a'.localeCompare('b', 'not_a_locale') });",
4621 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x'.padStart(1 / 0, 'a') });",
4622 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'abc'.charAt(1n) });",
4623 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'abc'.includes('a', 1n) });",
4624 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String(getFlag() ? 'a' : 'b') });",
4625 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String((mutate(), 'red')) });",
4626 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String(+1n) });",
4627 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: 'x'.concat(1n + 1n) });",
4628 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: String('x' in 1) });",
4629 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { color: Math.max(+1n, 2) });",
4630 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { [1n + 1]: 'red' });",
4631 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { [1n / 0n]: 'red' });",
4632 "import { createTheme } from 'stylex'; const alias = tokens; alias = other; const theme = createTheme(alias, { color: 'red' });",
4633 ] {
4634 assert!(
4635 css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4636 "must abstain: {source}"
4637 );
4638 }
4639 }
4640
4641 #[test]
4642 fn stylex_theme_call_rejects_noncompiler_static_shapes() {
4643 let leaf_paths = leaves(&["surface.bg"]);
4644 let queries = [ConsumerQuery::StyleXThemeGroup {
4645 contract_alias: "tokens",
4646 leaf_paths: &leaf_paths,
4647 }];
4648 for source in [
4649 "import { createTheme } from 'stylex'; const theme = createTheme(tokens, { value: [] });",
4650 "import stylex, { createTheme } from 'stylex'; const theme = createTheme(tokens, { value: stylex.types.notAType({}) });",
4651 "import stylex, { createTheme } from 'stylex'; const theme = createTheme(tokens, { value: stylex.keyframes() });",
4652 "import { createTheme } from 'stylex'; const overrides = {}; const run = (overrides) => { const theme = createTheme(tokens, overrides); };",
4653 "import { createTheme } from 'stylex'; const overrides = {}; ({ value: overrides.value } = next); const theme = createTheme(tokens, overrides);",
4654 ] {
4655 assert!(
4656 css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries).is_empty(),
4657 "must abstain: {source}"
4658 );
4659 }
4660 }
4661
4662 #[test]
4663 fn consumer_other_binding_not_matched() {
4664 let hits = consumers("const a = other.color.primary;", "vars", &["color.primary"]);
4666 assert!(hits.is_empty());
4667 }
4668
4669 #[test]
4670 fn consumer_shadowed_token_alias_is_not_matched() {
4671 let hits = consumers(
4672 "const read = (vars) => vars.color.primary;",
4673 "vars",
4674 &["color.primary"],
4675 );
4676 assert!(hits.is_empty());
4677 }
4678
4679 #[test]
4680 fn consumer_lexical_shadowing_respects_loops_catch_and_tdz() {
4681 let source = r"
4682import { vars } from './tokens.stylex';
4683for (const vars of groups) { use(vars.color.primary); }
4684try { work(); } catch (vars) { use(vars.color.primary); }
4685{ use(vars.color.primary); const vars = fallback; }
4686use((vars as typeof vars).color.primary);
4687";
4688 let hits = consumers(source, "vars", &["color.primary"]);
4689 assert_eq!(hits.len(), 1);
4690 assert_eq!(hits[0].line, 6);
4691 }
4692
4693 #[test]
4694 fn stylex_theme_callee_loop_shadow_does_not_escape_loop_scope() {
4695 let leaf_paths = leaves(&["surface.bg"]);
4696 let queries = [ConsumerQuery::StyleXThemeGroup {
4697 contract_alias: "tokens",
4698 leaf_paths: &leaf_paths,
4699 }];
4700 let source = r"
4701import { createTheme } from 'stylex';
4702for (const createTheme of factories) {
4703 const bad = createTheme(tokens, {});
4704}
4705const good = createTheme(tokens, {});
4706";
4707 let hits = css_in_js_consumer_scan(source, Path::new("theme.ts"), &queries);
4708 assert_eq!(hits.len(), 1);
4709 assert_eq!(hits[0].1.line, 6);
4710 }
4711
4712 #[test]
4713 fn consumer_deeper_access_past_leaf_matches_leaf_subexpression_once() {
4714 let hits = consumers(
4717 "const a = vars.color.primary.toString();",
4718 "vars",
4719 &["color.primary"],
4720 );
4721 assert_eq!(hits.len(), 1);
4722 assert_eq!(hits[0].token_path, "color.primary");
4723 }
4724
4725 #[test]
4726 fn consumer_undefined_path_not_matched() {
4727 let hits = consumers("const a = vars.color.tertiary;", "vars", &["color.primary"]);
4728 assert!(hits.is_empty());
4729 }
4730
4731 #[test]
4732 fn consumer_bracket_notation_hyphenated_key() {
4733 let hits = consumers(
4736 "const a = vars.color['gray-100'];\nconst b = vars.borderRadius['0x'];",
4737 "vars",
4738 &["color.gray-100", "borderRadius.0x"],
4739 );
4740 let paths: Vec<&str> = hits.iter().map(|h| h.token_path.as_str()).collect();
4741 assert!(paths.contains(&"color.gray-100"));
4742 assert!(paths.contains(&"borderRadius.0x"));
4743 assert_eq!(hits.len(), 2);
4744 }
4745
4746 #[test]
4747 fn consumer_mixed_dot_and_bracket_chain() {
4748 let hits = consumers(
4751 "const a = vars['color'].primary;\nconst b = vars.color['primary'];",
4752 "vars",
4753 &["color.primary"],
4754 );
4755 assert_eq!(hits.len(), 2);
4756 assert!(hits.iter().all(|h| h.token_path == "color.primary"));
4757 }
4758
4759 #[test]
4760 fn consumer_numeric_computed_key_is_matched() {
4761 let hits = consumers("const a = vars.color.gray[50];", "vars", &["color.gray.50"]);
4762 assert_eq!(hits.len(), 1);
4763 assert_eq!(hits[0].token_path, "color.gray.50");
4764 }
4765
4766 #[test]
4767 fn consumer_non_literal_computed_key_not_matched() {
4768 let hits = consumers(
4770 "const k = 'primary'; const a = vars.color[k];",
4771 "vars",
4772 &["color.primary"],
4773 );
4774 assert!(hits.is_empty());
4775 }
4776
4777 #[test]
4778 fn consumer_empty_inputs_short_circuit() {
4779 assert!(consumers("const a = vars.color.primary;", "", &["color.primary"]).is_empty());
4780 assert!(consumers("const a = vars.color.primary;", "vars", &[]).is_empty());
4781 }
4782
4783 #[test]
4784 fn consumer_scan_matches_individual_calls() {
4785 let source = "const a = vars.color.primary;\nconst b = css({ color: token('colors.brand'), background: 'colors.accent' });\nconst c = theme.space.card;";
4789 let path = Path::new("card.tsx");
4790
4791 let member_leaves = leaves(&["color.primary"]);
4792 let panda_call_leaves = leaves(&["colors.brand"]);
4793 let panda_style_aliases = leaves(&["css"]);
4794 let panda_style_leaves = leaves(&["colors.accent"]);
4795 let theme_leaves = leaves(&["space.card"]);
4796
4797 let queries = [
4798 ConsumerQuery::MemberBinding {
4799 alias: "vars",
4800 leaf_paths: &member_leaves,
4801 },
4802 ConsumerQuery::PandaTokenCall {
4803 alias: "token",
4804 leaf_paths: &panda_call_leaves,
4805 },
4806 ConsumerQuery::PandaStyleValues {
4807 aliases: &panda_style_aliases,
4808 leaf_paths: &panda_style_leaves,
4809 },
4810 ConsumerQuery::ThemeReads {
4811 leaf_paths: &theme_leaves,
4812 },
4813 ];
4814 let scanned = css_in_js_consumer_scan(source, path, &queries);
4815
4816 let individual: Vec<(usize, TokenConsumerHit)> = scan_one(
4817 source,
4818 path,
4819 ConsumerQuery::MemberBinding {
4820 alias: "vars",
4821 leaf_paths: &member_leaves,
4822 },
4823 )
4824 .into_iter()
4825 .map(|hit| (0, hit))
4826 .chain(
4827 scan_one(
4828 source,
4829 path,
4830 ConsumerQuery::PandaTokenCall {
4831 alias: "token",
4832 leaf_paths: &panda_call_leaves,
4833 },
4834 )
4835 .into_iter()
4836 .map(|hit| (1, hit)),
4837 )
4838 .chain(
4839 scan_one(
4840 source,
4841 path,
4842 ConsumerQuery::PandaStyleValues {
4843 aliases: &panda_style_aliases,
4844 leaf_paths: &panda_style_leaves,
4845 },
4846 )
4847 .into_iter()
4848 .map(|hit| (2, hit)),
4849 )
4850 .chain(
4851 scan_one(
4852 source,
4853 path,
4854 ConsumerQuery::ThemeReads {
4855 leaf_paths: &theme_leaves,
4856 },
4857 )
4858 .into_iter()
4859 .map(|hit| (3, hit)),
4860 )
4861 .collect();
4862
4863 assert_eq!(scanned, individual);
4864 assert_eq!(scanned.len(), 4);
4865 assert_eq!(
4866 scanned[0],
4867 (
4868 0,
4869 TokenConsumerHit {
4870 token_path: "color.primary".to_string(),
4871 line: 1,
4872 }
4873 )
4874 );
4875 assert_eq!(
4876 scanned[3],
4877 (
4878 3,
4879 TokenConsumerHit {
4880 token_path: "space.card".to_string(),
4881 line: 3,
4882 }
4883 )
4884 );
4885 }
4886
4887 #[test]
4888 fn consumer_scan_empty_query_is_isolated() {
4889 let source = "const a = vars.color.primary;";
4892 let path = Path::new("card.ts");
4893 let empty_leaves = leaves(&["color.primary"]);
4894 let valid_leaves = leaves(&["color.primary"]);
4895 let queries = [
4896 ConsumerQuery::MemberBinding {
4897 alias: "",
4898 leaf_paths: &empty_leaves,
4899 },
4900 ConsumerQuery::MemberBinding {
4901 alias: "vars",
4902 leaf_paths: &valid_leaves,
4903 },
4904 ];
4905 let scanned = css_in_js_consumer_scan(source, path, &queries);
4906 assert_eq!(scanned.len(), 1);
4907 assert_eq!(scanned[0].0, 1);
4908 assert_eq!(scanned[0].1.token_path, "color.primary");
4909 }
4910
4911 #[test]
4912 fn consumer_scan_two_member_queries_same_source() {
4913 let source = "const a = brand.color.primary;\nconst b = accent.color.primary;";
4916 let path = Path::new("card.ts");
4917 let brand_leaves = leaves(&["color.primary"]);
4918 let accent_leaves = leaves(&["color.primary"]);
4919 let queries = [
4920 ConsumerQuery::MemberBinding {
4921 alias: "brand",
4922 leaf_paths: &brand_leaves,
4923 },
4924 ConsumerQuery::MemberBinding {
4925 alias: "accent",
4926 leaf_paths: &accent_leaves,
4927 },
4928 ];
4929 let scanned = css_in_js_consumer_scan(source, path, &queries);
4930 assert_eq!(scanned.len(), 2);
4931 assert!(scanned.contains(&(
4932 0,
4933 TokenConsumerHit {
4934 token_path: "color.primary".to_string(),
4935 line: 1,
4936 }
4937 )));
4938 assert!(scanned.contains(&(
4939 1,
4940 TokenConsumerHit {
4941 token_path: "color.primary".to_string(),
4942 line: 2,
4943 }
4944 )));
4945 }
4946}