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