1use std::path::Path;
48
49use oxc_allocator::Allocator;
50use oxc_ast::ast::{
51 Argument, BindingPattern, ComputedMemberExpression, Expression, ImportDeclarationSpecifier,
52 NumericLiteral, ObjectExpression, ObjectPropertyKind, Program, Statement,
53 StaticMemberExpression, UnaryOperator, VariableDeclarator,
54};
55use oxc_ast_visit::{Visit, walk};
56use oxc_parser::Parser;
57use oxc_span::{GetSpan, SourceType};
58use rustc_hash::{FxHashMap, FxHashSet};
59
60use super::object::{Lib, module_library};
61
62const PANDA_CONFIG_BINDING: &str = "pandaConfig";
63
64#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CssInJsToken {
69 pub path: String,
71 pub def_line: u32,
73 pub value: Option<String>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct CssInJsTokenDef {
82 pub binding: String,
85 pub origin: CssInJsTokenOrigin,
87 pub tokens: Vec<CssInJsToken>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum CssInJsTokenOrigin {
94 StyleX,
96 VanillaExtract,
98 Panda,
100 Theme,
102}
103
104#[must_use]
108pub fn css_in_js_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
109 let source_type = SourceType::from_path(path).unwrap_or_default();
110 let allocator = Allocator::default();
111 let ret = Parser::new(&allocator, source, source_type).parse();
112
113 let mut collector = TokenDefCollector::new(source);
114 collector.build_import_map(&ret.program);
115 if collector.imports.is_empty() {
116 return Vec::new();
117 }
118 collector.visit_program(&ret.program);
119 collector.defs
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct TokenConsumerHit {
127 pub token_path: String,
130 pub line: u32,
132}
133
134#[must_use]
142#[expect(
143 clippy::implicit_hasher,
144 reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
145)]
146pub fn css_in_js_token_consumers(
147 source: &str,
148 path: &Path,
149 alias: &str,
150 leaf_paths: &FxHashSet<String>,
151) -> Vec<TokenConsumerHit> {
152 css_in_js_consumer_scan(
153 source,
154 path,
155 &[ConsumerQuery::MemberBinding { alias, leaf_paths }],
156 )
157 .into_iter()
158 .map(|(_, hit)| hit)
159 .collect()
160}
161
162#[must_use]
166#[expect(
167 clippy::implicit_hasher,
168 reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
169)]
170pub fn panda_token_call_consumers(
171 source: &str,
172 path: &Path,
173 alias: &str,
174 leaf_paths: &FxHashSet<String>,
175) -> Vec<TokenConsumerHit> {
176 css_in_js_consumer_scan(
177 source,
178 path,
179 &[ConsumerQuery::PandaTokenCall { alias, leaf_paths }],
180 )
181 .into_iter()
182 .map(|(_, hit)| hit)
183 .collect()
184}
185
186#[must_use]
189#[expect(
190 clippy::implicit_hasher,
191 reason = "callers build FxHashSet values; std HashSet is a disallowed type here"
192)]
193pub fn panda_style_value_consumers(
194 source: &str,
195 path: &Path,
196 aliases: &FxHashSet<String>,
197 leaf_paths: &FxHashSet<String>,
198) -> Vec<TokenConsumerHit> {
199 css_in_js_consumer_scan(
200 source,
201 path,
202 &[ConsumerQuery::PandaStyleValues {
203 aliases,
204 leaf_paths,
205 }],
206 )
207 .into_iter()
208 .map(|(_, hit)| hit)
209 .collect()
210}
211
212#[must_use]
217pub fn css_in_js_theme_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
218 let source_type = SourceType::from_path(path).unwrap_or_default();
219 let allocator = Allocator::default();
220 let ret = Parser::new(&allocator, source, source_type).parse();
221
222 let mut collector = ThemeDefCollector {
223 source,
224 defs: Vec::new(),
225 };
226 collector.visit_program(&ret.program);
227 collector.defs
228}
229
230#[must_use]
233#[expect(
234 clippy::implicit_hasher,
235 reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
236)]
237pub fn css_in_js_theme_consumers(
238 source: &str,
239 path: &Path,
240 leaf_paths: &FxHashSet<String>,
241) -> Vec<TokenConsumerHit> {
242 css_in_js_consumer_scan(source, path, &[ConsumerQuery::ThemeReads { leaf_paths }])
243 .into_iter()
244 .map(|(_, hit)| hit)
245 .collect()
246}
247
248pub enum ConsumerQuery<'a> {
252 MemberBinding {
255 alias: &'a str,
257 leaf_paths: &'a FxHashSet<String>,
259 },
260 PandaTokenCall {
263 alias: &'a str,
265 leaf_paths: &'a FxHashSet<String>,
267 },
268 PandaStyleValues {
271 aliases: &'a FxHashSet<String>,
273 leaf_paths: &'a FxHashSet<String>,
275 },
276 ThemeReads {
279 leaf_paths: &'a FxHashSet<String>,
281 },
282}
283
284#[must_use]
291pub fn css_in_js_consumer_scan(
292 source: &str,
293 path: &Path,
294 queries: &[ConsumerQuery<'_>],
295) -> Vec<(usize, TokenConsumerHit)> {
296 if queries.is_empty() {
297 return Vec::new();
298 }
299 let source_type = SourceType::from_path(path).unwrap_or_default();
300 let allocator = Allocator::default();
301 let ret = Parser::new(&allocator, source, source_type).parse();
302 let mut out = Vec::new();
303 for (idx, query) in queries.iter().enumerate() {
304 run_consumer_query(query, source, &ret.program, idx, &mut out);
305 }
306 out
307}
308
309fn run_consumer_query<'a>(
313 query: &ConsumerQuery<'_>,
314 source: &'a str,
315 program: &Program<'a>,
316 idx: usize,
317 out: &mut Vec<(usize, TokenConsumerHit)>,
318) {
319 match query {
320 ConsumerQuery::MemberBinding { alias, leaf_paths } => {
321 if alias.is_empty() || leaf_paths.is_empty() {
322 return;
323 }
324 let mut collector = ConsumerCollector {
325 source,
326 alias,
327 leaf_paths,
328 hits: Vec::new(),
329 };
330 collector.visit_program(program);
331 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
332 }
333 ConsumerQuery::PandaTokenCall { alias, leaf_paths } => {
334 if alias.is_empty() || leaf_paths.is_empty() {
335 return;
336 }
337 let mut collector = PandaTokenCallCollector {
338 source,
339 alias,
340 leaf_paths,
341 hits: Vec::new(),
342 };
343 collector.visit_program(program);
344 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
345 }
346 ConsumerQuery::PandaStyleValues {
347 aliases,
348 leaf_paths,
349 } => {
350 if aliases.is_empty() || leaf_paths.is_empty() {
351 return;
352 }
353 let mut collector = PandaStyleValueCollector {
354 source,
355 aliases,
356 leaf_paths,
357 hits: Vec::new(),
358 };
359 collector.visit_program(program);
360 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
361 }
362 ConsumerQuery::ThemeReads { leaf_paths } => {
363 if leaf_paths.is_empty() {
364 return;
365 }
366 let mut collector = ThemeConsumerCollector {
367 source,
368 leaf_paths,
369 hits: Vec::new(),
370 };
371 collector.visit_program(program);
372 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
373 }
374 }
375}
376
377struct ConsumerCollector<'a, 'b> {
379 source: &'a str,
380 alias: &'b str,
381 leaf_paths: &'b FxHashSet<String>,
382 hits: Vec<TokenConsumerHit>,
383}
384
385impl<'a> ConsumerCollector<'a, '_> {
386 fn record(&mut self, chain: Option<(&'a str, Vec<&'a str>)>, span_start: u32) {
391 if let Some((base, segments)) = chain
392 && base == self.alias
393 && !segments.is_empty()
394 {
395 let token_path = segments.join(".");
396 if self.leaf_paths.contains(&token_path) {
397 self.hits.push(TokenConsumerHit {
398 token_path,
399 line: line_at(self.source, span_start),
400 });
401 }
402 }
403 }
404}
405
406impl<'a> Visit<'a> for ConsumerCollector<'a, '_> {
407 fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
408 let mut chain = access_object_chain(&member.object);
409 if let Some((_, segments)) = chain.as_mut() {
410 segments.push(member.property.name.as_str());
411 }
412 self.record(chain, member.span().start);
413 walk::walk_static_member_expression(self, member);
414 }
415
416 fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
417 let mut chain = access_object_chain(&member.object);
423 if let (Some((_, segments)), Some(key)) =
424 (chain.as_mut(), string_literal_key(&member.expression))
425 {
426 segments.push(key);
427 } else {
428 chain = None;
429 }
430 self.record(chain, member.span().start);
431 walk::walk_computed_member_expression(self, member);
432 }
433}
434
435struct PandaTokenCallCollector<'a, 'b> {
436 source: &'a str,
437 alias: &'b str,
438 leaf_paths: &'b FxHashSet<String>,
439 hits: Vec<TokenConsumerHit>,
440}
441
442impl<'a> Visit<'a> for PandaTokenCallCollector<'a, '_> {
443 fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
444 let Expression::Identifier(callee) = &call.callee else {
445 walk::walk_call_expression(self, call);
446 return;
447 };
448 if callee.name.as_str() == self.alias
449 && let Some(Argument::StringLiteral(lit)) = call.arguments.first()
450 {
451 let token_path = lit.value.as_str();
452 if self.leaf_paths.contains(token_path) {
453 self.hits.push(TokenConsumerHit {
454 token_path: token_path.to_owned(),
455 line: line_at(self.source, call.span().start),
456 });
457 }
458 }
459 walk::walk_call_expression(self, call);
460 }
461}
462
463struct PandaStyleValueCollector<'a, 'b> {
464 source: &'a str,
465 aliases: &'b FxHashSet<String>,
466 leaf_paths: &'b FxHashSet<String>,
467 hits: Vec<TokenConsumerHit>,
468}
469
470impl<'a> PandaStyleValueCollector<'a, '_> {
471 fn record_object(&mut self, obj: &ObjectExpression<'a>) {
472 for prop in &obj.properties {
473 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
474 continue;
475 };
476 self.record_expression(&prop.value);
477 }
478 }
479
480 fn record_expression(&mut self, expr: &Expression<'a>) {
481 match expr {
482 Expression::StringLiteral(lit) => {
483 let token_path = lit.value.as_str();
484 if self.leaf_paths.contains(token_path) {
485 self.hits.push(TokenConsumerHit {
486 token_path: token_path.to_owned(),
487 line: line_at(self.source, lit.span().start),
488 });
489 }
490 }
491 Expression::ObjectExpression(obj) => self.record_object(obj),
492 _ => {}
493 }
494 }
495}
496
497impl<'a> Visit<'a> for PandaStyleValueCollector<'a, '_> {
498 fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
499 let Expression::Identifier(callee) = &call.callee else {
500 walk::walk_call_expression(self, call);
501 return;
502 };
503 if self.aliases.contains(callee.name.as_str()) {
504 for arg in &call.arguments {
505 if let Argument::ObjectExpression(obj) = arg {
506 self.record_object(obj);
507 }
508 }
509 }
510 walk::walk_call_expression(self, call);
511 }
512}
513
514struct ThemeDefCollector<'a> {
515 source: &'a str,
516 defs: Vec<CssInJsTokenDef>,
517}
518
519impl<'a> ThemeDefCollector<'a> {
520 fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
521 let BindingPattern::BindingIdentifier(binding) = &decl.id else {
522 return;
523 };
524 let binding_name = binding.name.as_str();
525 if !is_theme_binding_name(binding_name) {
526 return;
527 }
528 let Some(Expression::ObjectExpression(obj)) = &decl.init else {
529 return;
530 };
531 let mut tokens = Vec::new();
532 collect_token_leaves(self.source, obj, "", CssInJsTokenOrigin::Theme, &mut tokens);
533 if tokens.is_empty() {
534 return;
535 }
536 self.defs.push(CssInJsTokenDef {
537 binding: binding_name.to_owned(),
538 origin: CssInJsTokenOrigin::Theme,
539 tokens,
540 });
541 }
542}
543
544impl<'a> Visit<'a> for ThemeDefCollector<'a> {
545 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
546 self.process_declarator(decl);
547 walk::walk_variable_declarator(self, decl);
548 }
549}
550
551struct ThemeConsumerCollector<'a, 'b> {
552 source: &'a str,
553 leaf_paths: &'b FxHashSet<String>,
554 hits: Vec<TokenConsumerHit>,
555}
556
557impl<'a> ThemeConsumerCollector<'a, '_> {
558 fn record(&mut self, chain: Option<(&'a str, Vec<&'a str>)>, span_start: u32) {
559 let Some((base, segments)) = chain else {
560 return;
561 };
562 let token_segments: &[&str] = match base {
563 "theme" => &segments,
564 "props" if segments.first().copied() == Some("theme") => &segments[1..],
565 _ => return,
566 };
567 if token_segments.is_empty() {
568 return;
569 }
570 let token_path = token_segments.join(".");
571 if self.leaf_paths.contains(&token_path) {
572 self.hits.push(TokenConsumerHit {
573 token_path,
574 line: line_at(self.source, span_start),
575 });
576 }
577 }
578}
579
580impl<'a> Visit<'a> for ThemeConsumerCollector<'a, '_> {
581 fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
582 let mut chain = access_object_chain(&member.object);
583 if let Some((_, segments)) = chain.as_mut() {
584 segments.push(member.property.name.as_str());
585 }
586 self.record(chain, member.span().start);
587 walk::walk_static_member_expression(self, member);
588 }
589
590 fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
591 let mut chain = access_object_chain(&member.object);
592 if let (Some((_, segments)), Some(key)) =
593 (chain.as_mut(), string_literal_key(&member.expression))
594 {
595 segments.push(key);
596 } else {
597 chain = None;
598 }
599 self.record(chain, member.span().start);
600 walk::walk_computed_member_expression(self, member);
601 }
602}
603
604fn access_object_chain<'a>(expr: &Expression<'a>) -> Option<(&'a str, Vec<&'a str>)> {
610 match expr {
611 Expression::Identifier(id) => Some((id.name.as_str(), Vec::new())),
612 Expression::StaticMemberExpression(inner) => {
613 let (base, mut segments) = access_object_chain(&inner.object)?;
614 segments.push(inner.property.name.as_str());
615 Some((base, segments))
616 }
617 Expression::ComputedMemberExpression(inner) => {
618 let (base, mut segments) = access_object_chain(&inner.object)?;
619 segments.push(string_literal_key(&inner.expression)?);
620 Some((base, segments))
621 }
622 _ => None,
623 }
624}
625
626fn string_literal_key<'a>(expr: &Expression<'a>) -> Option<&'a str> {
629 match expr {
630 Expression::StringLiteral(lit) => Some(lit.value.as_str()),
631 _ => None,
632 }
633}
634
635#[derive(Clone, Copy)]
637enum BindingSource {
638 LhsIdent,
640 TupleElement(usize),
642}
643
644#[derive(Clone, Copy)]
647struct Recognized {
648 binding_source: BindingSource,
649 tokens_arg: usize,
650 origin: CssInJsTokenOrigin,
651}
652
653struct TokenDefCollector<'a> {
655 source: &'a str,
656 imports: FxHashMap<&'a str, (Lib, &'a str)>,
659 defs: Vec<CssInJsTokenDef>,
660}
661
662impl<'a> TokenDefCollector<'a> {
663 fn new(source: &'a str) -> Self {
664 Self {
665 source,
666 imports: FxHashMap::default(),
667 defs: Vec::new(),
668 }
669 }
670
671 fn build_import_map(&mut self, program: &Program<'a>) {
676 for stmt in &program.body {
677 let Statement::ImportDeclaration(decl) = stmt else {
678 continue;
679 };
680 if decl.import_kind.is_type() {
681 continue;
682 }
683 let Some(lib) = module_library(decl.source.value.as_str()) else {
684 continue;
685 };
686 let Some(specifiers) = &decl.specifiers else {
687 continue;
688 };
689 for specifier in specifiers {
690 let (local, role) = match specifier {
691 ImportDeclarationSpecifier::ImportSpecifier(s) => {
692 (s.local.name.as_str(), s.imported.name().as_str())
693 }
694 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
695 (s.local.name.as_str(), s.local.name.as_str())
696 }
697 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
698 (s.local.name.as_str(), s.local.name.as_str())
699 }
700 };
701 self.imports.insert(local, (lib, role));
702 }
703 }
704 }
705
706 fn callee_role(&self, callee: &Expression<'a>) -> Option<(Lib, &'a str)> {
710 match callee {
711 Expression::Identifier(id) => self.imports.get(id.name.as_str()).copied(),
712 Expression::StaticMemberExpression(member) => {
713 let Expression::Identifier(obj) = &member.object else {
714 return None;
715 };
716 let (lib, _) = *self.imports.get(obj.name.as_str())?;
717 Some((lib, member.property.name.as_str()))
719 }
720 _ => None,
721 }
722 }
723
724 fn recognize(lib: Lib, role: &str, arg_count: usize) -> Option<Recognized> {
728 let single = |tokens_arg, origin| {
729 Some(Recognized {
730 binding_source: BindingSource::LhsIdent,
731 tokens_arg,
732 origin,
733 })
734 };
735 match (lib, role) {
736 (Lib::StyleX, "defineVars") if arg_count >= 1 => single(0, CssInJsTokenOrigin::StyleX),
739 (Lib::VanillaExtract, "createThemeContract") if arg_count >= 1 => {
740 single(0, CssInJsTokenOrigin::VanillaExtract)
741 }
742 (Lib::VanillaExtract, "createTheme") if arg_count == 1 => Some(Recognized {
746 binding_source: BindingSource::TupleElement(1),
747 tokens_arg: 0,
748 origin: CssInJsTokenOrigin::VanillaExtract,
749 }),
750 (Lib::VanillaExtract, "createGlobalTheme") if arg_count == 2 => {
754 single(1, CssInJsTokenOrigin::VanillaExtract)
755 }
756 (Lib::Panda, "defineTokens") if arg_count >= 1 => single(0, CssInJsTokenOrigin::Panda),
757 _ => None,
758 }
759 }
760
761 fn binding_name(decl: &VariableDeclarator<'a>, source: BindingSource) -> Option<&'a str> {
764 match source {
765 BindingSource::LhsIdent => match &decl.id {
766 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
767 _ => None,
768 },
769 BindingSource::TupleElement(index) => {
770 let BindingPattern::ArrayPattern(arr) = &decl.id else {
771 return None;
772 };
773 let element = arr.elements.get(index)?.as_ref()?;
774 match element {
775 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
776 _ => None,
777 }
778 }
779 }
780 }
781
782 fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
783 let Some(Expression::CallExpression(call)) = &decl.init else {
784 return;
785 };
786 if self.process_panda_config_call(call) {
787 return;
788 }
789 let Some((lib, role)) = self.callee_role(&call.callee) else {
790 return;
791 };
792 let Some(recognized) = Self::recognize(lib, role, call.arguments.len()) else {
793 return;
794 };
795 let Some(binding) = Self::binding_name(decl, recognized.binding_source) else {
796 return;
797 };
798 let Some(Argument::ObjectExpression(obj)) = call.arguments.get(recognized.tokens_arg)
799 else {
800 return;
801 };
802 let mut tokens = Vec::new();
803 collect_token_leaves(self.source, obj, "", recognized.origin, &mut tokens);
804 if tokens.is_empty() {
805 return;
806 }
807 self.defs.push(CssInJsTokenDef {
808 binding: binding.to_owned(),
809 origin: recognized.origin,
810 tokens,
811 });
812 }
813
814 fn process_panda_config_call(&mut self, call: &oxc_ast::ast::CallExpression<'a>) -> bool {
815 let Some((Lib::Panda, "defineConfig")) = self.callee_role(&call.callee) else {
816 return false;
817 };
818 let Some(Argument::ObjectExpression(obj)) = call.arguments.first() else {
819 return true;
820 };
821 let mut tokens = Vec::new();
822 collect_panda_config_token_leaves(self.source, obj, &mut tokens);
823 if !tokens.is_empty() {
824 self.defs.push(CssInJsTokenDef {
825 binding: PANDA_CONFIG_BINDING.to_string(),
826 origin: CssInJsTokenOrigin::Panda,
827 tokens,
828 });
829 }
830 true
831 }
832}
833
834fn collect_token_leaves(
844 source: &str,
845 obj: &ObjectExpression<'_>,
846 prefix: &str,
847 origin: CssInJsTokenOrigin,
848 out: &mut Vec<CssInJsToken>,
849) {
850 for prop in &obj.properties {
851 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
852 continue;
853 };
854 let Some(key) = prop.key.static_name() else {
855 continue;
856 };
857 let path = if prefix.is_empty() {
858 key.to_string()
859 } else {
860 format!("{prefix}.{key}")
861 };
862 match &prop.value {
863 Expression::ObjectExpression(nested)
864 if origin == CssInJsTokenOrigin::Panda
865 && !prefix.is_empty()
866 && object_has_static_key(nested, "value") =>
867 {
868 out.push(CssInJsToken {
869 path,
870 def_line: line_at(source, prop.key.span().start),
871 value: object_static_property_value(nested, "value"),
872 });
873 }
874 Expression::ObjectExpression(nested) => {
875 collect_token_leaves(source, nested, &path, origin, out);
876 }
877 Expression::Identifier(_) => {}
880 _ => out.push(CssInJsToken {
881 value: static_token_value(&prop.value),
882 path,
883 def_line: line_at(source, prop.key.span().start),
884 }),
885 }
886 }
887}
888
889fn object_static_property_value(obj: &ObjectExpression<'_>, wanted: &str) -> Option<String> {
890 obj.properties.iter().find_map(|prop| {
891 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
892 return None;
893 };
894 (prop.key.static_name().as_deref() == Some(wanted))
895 .then(|| static_token_value(&prop.value))
896 .flatten()
897 })
898}
899
900fn static_token_value(value: &Expression<'_>) -> Option<String> {
901 match value {
902 Expression::StringLiteral(lit) => {
903 let text = lit.value.as_str().trim();
904 (!text.is_empty()).then(|| text.to_string())
905 }
906 Expression::NumericLiteral(num) => Some(format_numeric_token(num)),
907 Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
908 if let Expression::NumericLiteral(num) = &unary.argument {
909 Some(format!("-{}", format_numeric_token(num)))
910 } else {
911 None
912 }
913 }
914 _ => None,
915 }
916}
917
918fn format_numeric_token(num: &NumericLiteral<'_>) -> String {
919 if num.value.fract() == 0.0 {
920 format!("{:.0}", num.value)
921 } else {
922 num.value.to_string()
923 }
924}
925
926fn is_theme_binding_name(name: &str) -> bool {
927 let lower = name.to_ascii_lowercase();
928 lower == "theme" || lower.ends_with("theme")
929}
930
931fn object_has_static_key(obj: &ObjectExpression<'_>, wanted: &str) -> bool {
932 obj.properties.iter().any(|prop| {
933 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
934 return false;
935 };
936 prop.key.static_name().is_some_and(|key| key == wanted)
937 })
938}
939
940fn object_static_property_object<'a>(
941 obj: &'a ObjectExpression<'a>,
942 wanted: &str,
943) -> Option<&'a ObjectExpression<'a>> {
944 obj.properties.iter().find_map(|prop| {
945 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
946 return None;
947 };
948 if prop.key.static_name().as_deref() == Some(wanted)
949 && let Expression::ObjectExpression(value) = &prop.value
950 {
951 Some(&**value)
952 } else {
953 None
954 }
955 })
956}
957
958fn collect_panda_config_token_leaves(
959 source: &str,
960 obj: &ObjectExpression<'_>,
961 out: &mut Vec<CssInJsToken>,
962) {
963 let Some(theme) = object_static_property_object(obj, "theme") else {
964 return;
965 };
966 for key in ["tokens", "semanticTokens"] {
967 if let Some(tokens) = object_static_property_object(theme, key) {
968 collect_token_leaves(source, tokens, "", CssInJsTokenOrigin::Panda, out);
969 }
970 }
971}
972
973impl<'a> Visit<'a> for TokenDefCollector<'a> {
974 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
975 self.process_declarator(decl);
976 walk::walk_variable_declarator(self, decl);
977 }
978
979 fn visit_export_default_declaration(
980 &mut self,
981 decl: &oxc_ast::ast::ExportDefaultDeclaration<'a>,
982 ) {
983 if let Some(Expression::CallExpression(call)) = decl.declaration.as_expression() {
984 self.process_panda_config_call(call);
985 }
986 walk::walk_export_default_declaration(self, decl);
987 }
988}
989
990fn line_at(source: &str, offset: u32) -> u32 {
994 let end = (offset as usize).min(source.len());
995 let count = source
996 .get(..end)
997 .map_or(0, |s| s.bytes().filter(|&b| b == b'\n').count());
998 u32::try_from(1 + count).unwrap_or(u32::MAX)
999}
1000
1001#[cfg(all(test, not(miri)))]
1002mod tests {
1003 use super::*;
1004
1005 fn defs(source: &str) -> Vec<CssInJsTokenDef> {
1006 css_in_js_token_defs(source, Path::new("tokens.ts"))
1007 }
1008
1009 fn paths(defs: &[CssInJsTokenDef], binding: &str) -> Vec<String> {
1010 defs.iter()
1011 .find(|d| d.binding == binding)
1012 .map(|d| d.tokens.iter().map(|t| t.path.clone()).collect())
1013 .unwrap_or_default()
1014 }
1015
1016 fn token_values(defs: &[CssInJsTokenDef], binding: &str) -> Vec<(String, Option<String>)> {
1017 defs.iter()
1018 .find(|d| d.binding == binding)
1019 .map(|d| {
1020 d.tokens
1021 .iter()
1022 .map(|t| (t.path.clone(), t.value.clone()))
1023 .collect()
1024 })
1025 .unwrap_or_default()
1026 }
1027
1028 fn theme_defs(source: &str) -> Vec<CssInJsTokenDef> {
1029 css_in_js_theme_token_defs(source, Path::new("theme.ts"))
1030 }
1031
1032 #[test]
1033 fn stylex_define_vars_flat_namespace_call() {
1034 let d = defs(
1035 r"
1036import * as stylex from '@stylexjs/stylex';
1037export const vars = stylex.defineVars({ primaryColor: '#3b82f6', spacingSm: '4px' });
1038",
1039 );
1040 assert_eq!(paths(&d, "vars"), vec!["primaryColor", "spacingSm"]);
1041 assert_eq!(
1042 token_values(&d, "vars"),
1043 vec![
1044 ("primaryColor".to_string(), Some("#3b82f6".to_string())),
1045 ("spacingSm".to_string(), Some("4px".to_string())),
1046 ]
1047 );
1048 }
1049
1050 #[test]
1051 fn stylex_define_vars_named_import_nested() {
1052 let d = defs(
1053 r"
1054import { defineVars } from '@stylexjs/stylex';
1055export const vars = defineVars({ color: { primary: '#000', secondary: '#fff' } });
1056",
1057 );
1058 assert_eq!(paths(&d, "vars"), vec!["color.primary", "color.secondary"]);
1059 }
1060
1061 #[test]
1062 fn panda_define_tokens_collapses_value_objects() {
1063 let d = defs(
1064 r"
1065import { defineTokens } from '@pandacss/dev';
1066export const tokens = defineTokens({
1067 colors: {
1068 brand: { value: '#f05a28' },
1069 accent: { value: '{colors.brand}' },
1070 },
1071 spacing: { card: { value: '1rem' } },
1072});
1073",
1074 );
1075 assert_eq!(
1076 paths(&d, "tokens"),
1077 vec!["colors.brand", "colors.accent", "spacing.card"]
1078 );
1079 assert_eq!(
1080 token_values(&d, "tokens"),
1081 vec![
1082 ("colors.brand".to_string(), Some("#f05a28".to_string())),
1083 (
1084 "colors.accent".to_string(),
1085 Some("{colors.brand}".to_string())
1086 ),
1087 ("spacing.card".to_string(), Some("1rem".to_string())),
1088 ]
1089 );
1090 assert_eq!(
1091 d.iter().find(|d| d.binding == "tokens").unwrap().origin,
1092 CssInJsTokenOrigin::Panda
1093 );
1094 }
1095
1096 #[test]
1097 fn panda_define_config_extracts_tokens_and_semantic_tokens() {
1098 let d = defs(
1099 r"
1100import { defineConfig } from '@pandacss/dev';
1101
1102export default defineConfig({
1103 theme: {
1104 tokens: {
1105 colors: {
1106 brand: { value: '#f05a28' },
1107 },
1108 },
1109 semanticTokens: {
1110 colors: {
1111 surface: { value: { base: '{colors.brand}', _dark: '#111111' } },
1112 },
1113 },
1114 recipes: {
1115 card: { base: { color: 'colors.brand' } },
1116 },
1117 },
1118});
1119",
1120 );
1121 assert_eq!(
1122 paths(&d, "pandaConfig"),
1123 vec!["colors.brand", "colors.surface"]
1124 );
1125 assert_eq!(
1126 token_values(&d, "pandaConfig"),
1127 vec![
1128 ("colors.brand".to_string(), Some("#f05a28".to_string())),
1129 ("colors.surface".to_string(), None),
1130 ]
1131 );
1132 assert_eq!(
1133 d.iter()
1134 .find(|d| d.binding == "pandaConfig")
1135 .unwrap()
1136 .origin,
1137 CssInJsTokenOrigin::Panda
1138 );
1139 }
1140
1141 #[test]
1142 fn theme_object_definitions_flatten_static_leaves() {
1143 let d = theme_defs(
1144 r"
1145export const appTheme = {
1146 colors: { brand: '#f05a28', accent: '#111' },
1147 space: { card: '1rem' },
1148 dynamic: palette,
1149};
1150",
1151 );
1152 assert_eq!(
1153 paths(&d, "appTheme"),
1154 vec!["colors.brand", "colors.accent", "space.card"]
1155 );
1156 assert_eq!(
1157 token_values(&d, "appTheme"),
1158 vec![
1159 ("colors.brand".to_string(), Some("#f05a28".to_string())),
1160 ("colors.accent".to_string(), Some("#111".to_string())),
1161 ("space.card".to_string(), Some("1rem".to_string())),
1162 ]
1163 );
1164 assert_eq!(
1165 d.iter().find(|d| d.binding == "appTheme").unwrap().origin,
1166 CssInJsTokenOrigin::Theme
1167 );
1168 }
1169
1170 #[test]
1171 fn theme_consumers_credit_props_and_destructured_theme_reads() {
1172 let leaves = ["colors.brand", "space.card"]
1173 .into_iter()
1174 .map(str::to_owned)
1175 .collect();
1176 let hits = css_in_js_theme_consumers(
1177 r"
1178import styled from 'styled-components';
1179export const Card = styled.div`
1180 color: ${({ theme }) => theme.colors.brand};
1181 margin: ${props => props.theme.space.card};
1182`;
1183",
1184 Path::new("card.tsx"),
1185 &leaves,
1186 );
1187 let mut token_paths: Vec<String> = hits.into_iter().map(|hit| hit.token_path).collect();
1188 token_paths.sort();
1189 assert_eq!(token_paths, vec!["colors.brand", "space.card"]);
1190 }
1191
1192 #[test]
1193 fn ve_create_theme_tuple_destructure_binds_element_one() {
1194 let d = defs(
1195 r"
1196import { createTheme } from '@vanilla-extract/css';
1197export const [themeClass, vars] = createTheme({
1198 color: { brand: 'red', accent: 'blue' },
1199 space: { small: '4px' },
1200});
1201",
1202 );
1203 assert_eq!(
1205 paths(&d, "vars"),
1206 vec!["color.brand", "color.accent", "space.small"]
1207 );
1208 assert!(paths(&d, "themeClass").is_empty());
1209 }
1210
1211 #[test]
1212 fn ve_create_theme_contract_null_leaves() {
1213 let d = defs(
1214 r"
1215import { createThemeContract } from '@vanilla-extract/css';
1216export const vars = createThemeContract({ color: { brand: null, accent: null } });
1217",
1218 );
1219 assert_eq!(paths(&d, "vars"), vec!["color.brand", "color.accent"]);
1221 }
1222
1223 #[test]
1224 fn ve_create_global_theme_two_arg_binds_lhs_tokens_in_second_arg() {
1225 let d = defs(
1226 r"
1227import { createGlobalTheme } from '@vanilla-extract/css';
1228export const vars = createGlobalTheme(':root', { color: { brand: 'red' } });
1229",
1230 );
1231 assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
1232 }
1233
1234 #[test]
1235 fn ve_create_theme_two_arg_contract_impl_is_not_a_definition_site() {
1236 let d = defs(
1239 r"
1240import { createTheme } from '@vanilla-extract/css';
1241export const themeClass = createTheme(vars, { color: { brand: 'red' } });
1242",
1243 );
1244 assert!(
1245 d.is_empty(),
1246 "2-arg createTheme must not define tokens, got {d:?}"
1247 );
1248 }
1249
1250 #[test]
1251 fn ve_create_global_theme_three_arg_contract_impl_is_not_a_definition_site() {
1252 let d = defs(
1253 r"
1254import { createGlobalTheme } from '@vanilla-extract/css';
1255createGlobalTheme(':root', vars, { color: { brand: 'red' } });
1256",
1257 );
1258 assert!(
1259 d.is_empty(),
1260 "3-arg createGlobalTheme must not define tokens, got {d:?}"
1261 );
1262 }
1263
1264 #[test]
1265 fn aliased_named_import_still_fires() {
1266 let d = defs(
1267 r"
1268import { createThemeContract as ct } from '@vanilla-extract/css';
1269export const vars = ct({ color: { brand: null } });
1270",
1271 );
1272 assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
1273 }
1274
1275 #[test]
1276 fn local_helper_not_from_library_does_not_fire() {
1277 let d = defs(
1279 r"
1280function defineVars(o) { return o; }
1281export const vars = defineVars({ color: { primary: '#000' } });
1282",
1283 );
1284 assert!(d.is_empty(), "local defineVars must not fire, got {d:?}");
1285 }
1286
1287 #[test]
1288 fn unrelated_create_theme_import_does_not_fire() {
1289 let d = defs(
1290 r"
1291import { createTheme } from '@mui/material/styles';
1292export const theme = createTheme({ palette: { primary: { main: '#000' } } });
1293",
1294 );
1295 assert!(d.is_empty(), "non-VE createTheme must not fire, got {d:?}");
1296 }
1297
1298 #[test]
1299 fn type_only_import_does_not_fire() {
1300 let d = defs(
1301 r"
1302import type { defineVars } from '@stylexjs/stylex';
1303export const vars = defineVars({ color: { primary: '#000' } });
1304",
1305 );
1306 assert!(
1307 d.is_empty(),
1308 "type-only import must not gate recognition, got {d:?}"
1309 );
1310 }
1311
1312 #[test]
1313 fn token_def_lines_are_per_leaf() {
1314 let src = "import { defineVars } from '@stylexjs/stylex';\nexport const vars = defineVars({\n color: {\n primary: '#000',\n secondary: '#fff',\n },\n});\n";
1315 let d = defs(src);
1316 let def = d.iter().find(|d| d.binding == "vars").unwrap();
1317 let primary = def
1318 .tokens
1319 .iter()
1320 .find(|t| t.path == "color.primary")
1321 .unwrap();
1322 let secondary = def
1323 .tokens
1324 .iter()
1325 .find(|t| t.path == "color.secondary")
1326 .unwrap();
1327 assert_eq!(primary.def_line, 4);
1328 assert_eq!(secondary.def_line, 5);
1329 }
1330
1331 #[test]
1332 fn spread_and_computed_keys_are_skipped() {
1333 let d = defs(
1334 r"
1335import { defineVars } from '@stylexjs/stylex';
1336const base = { a: '1' };
1337export const vars = defineVars({ ...base, ['x' + 'y']: '2', real: '#000' });
1338",
1339 );
1340 assert_eq!(paths(&d, "vars"), vec!["real"]);
1342 }
1343
1344 #[test]
1345 fn identifier_valued_key_is_not_a_leaf_but_call_and_member_values_are() {
1346 let d = defs(
1350 r"
1351import { createGlobalTheme } from '@vanilla-extract/css';
1352export const vars = createGlobalTheme(':root', {
1353 palette: tailwindPalette,
1354 radius: px(2),
1355 red: colors.red['500'],
1356});
1357",
1358 );
1359 let p = paths(&d, "vars");
1360 assert!(
1361 !p.contains(&"palette".to_string()),
1362 "identifier-valued key must not be a leaf: {p:?}"
1363 );
1364 assert!(
1365 p.contains(&"radius".to_string()),
1366 "call-valued key is a leaf: {p:?}"
1367 );
1368 assert!(
1369 p.contains(&"red".to_string()),
1370 "member-valued key is a leaf: {p:?}"
1371 );
1372 }
1373
1374 #[test]
1375 fn no_css_in_js_import_returns_empty() {
1376 let d = defs("export const vars = { color: { primary: '#000' } };");
1377 assert!(d.is_empty());
1378 }
1379
1380 fn leaves(paths: &[&str]) -> FxHashSet<String> {
1381 paths.iter().map(|s| (*s).to_string()).collect()
1382 }
1383
1384 fn consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
1385 css_in_js_token_consumers(source, Path::new("card.ts"), alias, &leaves(paths))
1386 }
1387
1388 fn panda_consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
1389 panda_token_call_consumers(source, Path::new("card.ts"), alias, &leaves(paths))
1390 }
1391
1392 fn panda_style_consumers(
1393 source: &str,
1394 aliases: &[&str],
1395 paths: &[&str],
1396 ) -> Vec<TokenConsumerHit> {
1397 let aliases = aliases.iter().map(|s| (*s).to_string()).collect();
1398 panda_style_value_consumers(source, Path::new("card.ts"), &aliases, &leaves(paths))
1399 }
1400
1401 #[test]
1402 fn consumer_matches_deepest_leaf_not_intermediate_group() {
1403 let hits = consumers(
1406 "const a = vars.color.primary;",
1407 "vars",
1408 &["color.primary", "color.secondary"],
1409 );
1410 assert_eq!(hits.len(), 1);
1411 assert_eq!(hits[0].token_path, "color.primary");
1412 assert_eq!(hits[0].line, 1);
1413 }
1414
1415 #[test]
1416 fn consumer_aliased_receiver() {
1417 let hits = consumers("const a = v.color.primary;", "v", &["color.primary"]);
1419 assert_eq!(hits.len(), 1);
1420 assert_eq!(hits[0].token_path, "color.primary");
1421 }
1422
1423 #[test]
1424 fn consumer_multiple_sites_distinct_lines() {
1425 let src = "const a = vars.color.primary;\nconst b = vars.space.sm;\nconst c = vars.color.primary;";
1426 let hits = consumers(src, "vars", &["color.primary", "space.sm"]);
1427 assert_eq!(hits.len(), 3);
1428 let lines: Vec<u32> = hits.iter().map(|h| h.line).collect();
1429 assert_eq!(lines, vec![1, 2, 3]);
1430 }
1431
1432 #[test]
1433 fn consumer_in_style_object_value_position() {
1434 let hits = consumers(
1436 "export const s = stylex.create({ root: { color: vars.color.primary } });",
1437 "vars",
1438 &["color.primary"],
1439 );
1440 assert_eq!(hits.len(), 1);
1441 assert_eq!(hits[0].token_path, "color.primary");
1442 }
1443
1444 #[test]
1445 fn panda_token_call_consumer_matches_string_literal() {
1446 let hits = panda_consumers(
1447 "export const c = css({ color: token('colors.brand') });",
1448 "token",
1449 &["colors.brand", "colors.accent"],
1450 );
1451 assert_eq!(hits.len(), 1);
1452 assert_eq!(hits[0].token_path, "colors.brand");
1453 }
1454
1455 #[test]
1456 fn panda_style_value_consumer_matches_known_token_string() {
1457 let hits = panda_style_consumers(
1458 "export const c = css({ color: 'colors.brand', _hover: { bg: 'colors.accent' } });",
1459 &["css"],
1460 &["colors.brand", "colors.accent", "colors.unused"],
1461 );
1462 let paths: Vec<_> = hits.iter().map(|hit| hit.token_path.as_str()).collect();
1463 assert_eq!(paths, vec!["colors.brand", "colors.accent"]);
1464 }
1465
1466 #[test]
1467 fn panda_style_value_consumer_ignores_unimported_alias() {
1468 let hits = panda_style_consumers(
1469 "export const c = notPanda({ color: 'colors.brand' });",
1470 &["css"],
1471 &["colors.brand"],
1472 );
1473 assert!(hits.is_empty());
1474 }
1475
1476 #[test]
1477 fn consumer_flat_stylex_depth_one() {
1478 let hits = consumers("const a = vars.primaryColor;", "vars", &["primaryColor"]);
1479 assert_eq!(hits.len(), 1);
1480 assert_eq!(hits[0].token_path, "primaryColor");
1481 }
1482
1483 #[test]
1484 fn consumer_other_binding_not_matched() {
1485 let hits = consumers("const a = other.color.primary;", "vars", &["color.primary"]);
1487 assert!(hits.is_empty());
1488 }
1489
1490 #[test]
1491 fn consumer_deeper_access_past_leaf_matches_leaf_subexpression_once() {
1492 let hits = consumers(
1495 "const a = vars.color.primary.toString();",
1496 "vars",
1497 &["color.primary"],
1498 );
1499 assert_eq!(hits.len(), 1);
1500 assert_eq!(hits[0].token_path, "color.primary");
1501 }
1502
1503 #[test]
1504 fn consumer_undefined_path_not_matched() {
1505 let hits = consumers("const a = vars.color.tertiary;", "vars", &["color.primary"]);
1506 assert!(hits.is_empty());
1507 }
1508
1509 #[test]
1510 fn consumer_bracket_notation_hyphenated_key() {
1511 let hits = consumers(
1514 "const a = vars.color['gray-100'];\nconst b = vars.borderRadius['0x'];",
1515 "vars",
1516 &["color.gray-100", "borderRadius.0x"],
1517 );
1518 let paths: Vec<&str> = hits.iter().map(|h| h.token_path.as_str()).collect();
1519 assert!(paths.contains(&"color.gray-100"));
1520 assert!(paths.contains(&"borderRadius.0x"));
1521 assert_eq!(hits.len(), 2);
1522 }
1523
1524 #[test]
1525 fn consumer_mixed_dot_and_bracket_chain() {
1526 let hits = consumers(
1529 "const a = vars['color'].primary;\nconst b = vars.color['primary'];",
1530 "vars",
1531 &["color.primary"],
1532 );
1533 assert_eq!(hits.len(), 2);
1534 assert!(hits.iter().all(|h| h.token_path == "color.primary"));
1535 }
1536
1537 #[test]
1538 fn consumer_non_literal_computed_key_not_matched() {
1539 let hits = consumers(
1541 "const k = 'primary'; const a = vars.color[k];",
1542 "vars",
1543 &["color.primary"],
1544 );
1545 assert!(hits.is_empty());
1546 }
1547
1548 #[test]
1549 fn consumer_empty_inputs_short_circuit() {
1550 assert!(consumers("const a = vars.color.primary;", "", &["color.primary"]).is_empty());
1551 assert!(consumers("const a = vars.color.primary;", "vars", &[]).is_empty());
1552 }
1553
1554 #[test]
1555 fn consumer_scan_matches_individual_calls() {
1556 let source = "const a = vars.color.primary;\nconst b = css({ color: token('colors.brand'), background: 'colors.accent' });\nconst c = theme.space.card;";
1560 let path = Path::new("card.tsx");
1561
1562 let member_leaves = leaves(&["color.primary"]);
1563 let panda_call_leaves = leaves(&["colors.brand"]);
1564 let panda_style_aliases = leaves(&["css"]);
1565 let panda_style_leaves = leaves(&["colors.accent"]);
1566 let theme_leaves = leaves(&["space.card"]);
1567
1568 let queries = [
1569 ConsumerQuery::MemberBinding {
1570 alias: "vars",
1571 leaf_paths: &member_leaves,
1572 },
1573 ConsumerQuery::PandaTokenCall {
1574 alias: "token",
1575 leaf_paths: &panda_call_leaves,
1576 },
1577 ConsumerQuery::PandaStyleValues {
1578 aliases: &panda_style_aliases,
1579 leaf_paths: &panda_style_leaves,
1580 },
1581 ConsumerQuery::ThemeReads {
1582 leaf_paths: &theme_leaves,
1583 },
1584 ];
1585 let scanned = css_in_js_consumer_scan(source, path, &queries);
1586
1587 let individual: Vec<(usize, TokenConsumerHit)> =
1588 css_in_js_token_consumers(source, path, "vars", &member_leaves)
1589 .into_iter()
1590 .map(|hit| (0, hit))
1591 .chain(
1592 panda_token_call_consumers(source, path, "token", &panda_call_leaves)
1593 .into_iter()
1594 .map(|hit| (1, hit)),
1595 )
1596 .chain(
1597 panda_style_value_consumers(
1598 source,
1599 path,
1600 &panda_style_aliases,
1601 &panda_style_leaves,
1602 )
1603 .into_iter()
1604 .map(|hit| (2, hit)),
1605 )
1606 .chain(
1607 css_in_js_theme_consumers(source, path, &theme_leaves)
1608 .into_iter()
1609 .map(|hit| (3, hit)),
1610 )
1611 .collect();
1612
1613 assert_eq!(scanned, individual);
1614 assert_eq!(scanned.len(), 4);
1615 assert_eq!(
1616 scanned[0],
1617 (
1618 0,
1619 TokenConsumerHit {
1620 token_path: "color.primary".to_string(),
1621 line: 1,
1622 }
1623 )
1624 );
1625 assert_eq!(
1626 scanned[3],
1627 (
1628 3,
1629 TokenConsumerHit {
1630 token_path: "space.card".to_string(),
1631 line: 3,
1632 }
1633 )
1634 );
1635 }
1636
1637 #[test]
1638 fn consumer_scan_empty_query_is_isolated() {
1639 let source = "const a = vars.color.primary;";
1642 let path = Path::new("card.ts");
1643 let empty_leaves = leaves(&["color.primary"]);
1644 let valid_leaves = leaves(&["color.primary"]);
1645 let queries = [
1646 ConsumerQuery::MemberBinding {
1647 alias: "",
1648 leaf_paths: &empty_leaves,
1649 },
1650 ConsumerQuery::MemberBinding {
1651 alias: "vars",
1652 leaf_paths: &valid_leaves,
1653 },
1654 ];
1655 let scanned = css_in_js_consumer_scan(source, path, &queries);
1656 assert_eq!(scanned.len(), 1);
1657 assert_eq!(scanned[0].0, 1);
1658 assert_eq!(scanned[0].1.token_path, "color.primary");
1659 }
1660
1661 #[test]
1662 fn consumer_scan_two_member_queries_same_source() {
1663 let source = "const a = brand.color.primary;\nconst b = accent.color.primary;";
1666 let path = Path::new("card.ts");
1667 let brand_leaves = leaves(&["color.primary"]);
1668 let accent_leaves = leaves(&["color.primary"]);
1669 let queries = [
1670 ConsumerQuery::MemberBinding {
1671 alias: "brand",
1672 leaf_paths: &brand_leaves,
1673 },
1674 ConsumerQuery::MemberBinding {
1675 alias: "accent",
1676 leaf_paths: &accent_leaves,
1677 },
1678 ];
1679 let scanned = css_in_js_consumer_scan(source, path, &queries);
1680 assert_eq!(scanned.len(), 2);
1681 assert!(scanned.contains(&(
1682 0,
1683 TokenConsumerHit {
1684 token_path: "color.primary".to_string(),
1685 line: 1,
1686 }
1687 )));
1688 assert!(scanned.contains(&(
1689 1,
1690 TokenConsumerHit {
1691 token_path: "color.primary".to_string(),
1692 line: 2,
1693 }
1694 )));
1695 }
1696}