use std::path::Path;
use oxc_allocator::Allocator;
use oxc_ast::ast::{
Argument, BindingPattern, ComputedMemberExpression, Expression, ImportDeclarationSpecifier,
ObjectExpression, ObjectPropertyKind, Program, Statement, StaticMemberExpression,
VariableDeclarator,
};
use oxc_ast_visit::{Visit, walk};
use oxc_parser::Parser;
use oxc_span::{GetSpan, SourceType};
use rustc_hash::{FxHashMap, FxHashSet};
use super::object::{Lib, module_library};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CssInJsToken {
pub path: String,
pub def_line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CssInJsTokenDef {
pub binding: String,
pub tokens: Vec<CssInJsToken>,
}
#[must_use]
pub fn css_in_js_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = TokenDefCollector::new(source);
collector.build_import_map(&ret.program);
if collector.imports.is_empty() {
return Vec::new();
}
collector.visit_program(&ret.program);
collector.defs
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenConsumerHit {
pub token_path: String,
pub line: u32,
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
)]
pub fn css_in_js_token_consumers(
source: &str,
path: &Path,
alias: &str,
leaf_paths: &FxHashSet<String>,
) -> Vec<TokenConsumerHit> {
if alias.is_empty() || leaf_paths.is_empty() {
return Vec::new();
}
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = ConsumerCollector {
source,
alias,
leaf_paths,
hits: Vec::new(),
};
collector.visit_program(&ret.program);
collector.hits
}
struct ConsumerCollector<'a, 'b> {
source: &'a str,
alias: &'b str,
leaf_paths: &'b FxHashSet<String>,
hits: Vec<TokenConsumerHit>,
}
impl<'a> ConsumerCollector<'a, '_> {
fn record(&mut self, chain: Option<(&'a str, Vec<&'a str>)>, span_start: u32) {
if let Some((base, segments)) = chain
&& base == self.alias
&& !segments.is_empty()
{
let token_path = segments.join(".");
if self.leaf_paths.contains(&token_path) {
self.hits.push(TokenConsumerHit {
token_path,
line: line_at(self.source, span_start),
});
}
}
}
}
impl<'a> Visit<'a> for ConsumerCollector<'a, '_> {
fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
let mut chain = access_object_chain(&member.object);
if let Some((_, segments)) = chain.as_mut() {
segments.push(member.property.name.as_str());
}
self.record(chain, member.span().start);
walk::walk_static_member_expression(self, member);
}
fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
let mut chain = access_object_chain(&member.object);
if let (Some((_, segments)), Some(key)) =
(chain.as_mut(), string_literal_key(&member.expression))
{
segments.push(key);
} else {
chain = None;
}
self.record(chain, member.span().start);
walk::walk_computed_member_expression(self, member);
}
}
fn access_object_chain<'a>(expr: &Expression<'a>) -> Option<(&'a str, Vec<&'a str>)> {
match expr {
Expression::Identifier(id) => Some((id.name.as_str(), Vec::new())),
Expression::StaticMemberExpression(inner) => {
let (base, mut segments) = access_object_chain(&inner.object)?;
segments.push(inner.property.name.as_str());
Some((base, segments))
}
Expression::ComputedMemberExpression(inner) => {
let (base, mut segments) = access_object_chain(&inner.object)?;
segments.push(string_literal_key(&inner.expression)?);
Some((base, segments))
}
_ => None,
}
}
fn string_literal_key<'a>(expr: &Expression<'a>) -> Option<&'a str> {
match expr {
Expression::StringLiteral(lit) => Some(lit.value.as_str()),
_ => None,
}
}
#[derive(Clone, Copy)]
enum BindingSource {
LhsIdent,
TupleElement(usize),
}
#[derive(Clone, Copy)]
struct Recognized {
binding_source: BindingSource,
tokens_arg: usize,
}
struct TokenDefCollector<'a> {
source: &'a str,
imports: FxHashMap<&'a str, (Lib, &'a str)>,
defs: Vec<CssInJsTokenDef>,
}
impl<'a> TokenDefCollector<'a> {
fn new(source: &'a str) -> Self {
Self {
source,
imports: FxHashMap::default(),
defs: Vec::new(),
}
}
fn build_import_map(&mut self, program: &Program<'a>) {
for stmt in &program.body {
let Statement::ImportDeclaration(decl) = stmt else {
continue;
};
if decl.import_kind.is_type() {
continue;
}
let Some(lib) = module_library(decl.source.value.as_str()) else {
continue;
};
let Some(specifiers) = &decl.specifiers else {
continue;
};
for specifier in specifiers {
let (local, role) = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
(s.local.name.as_str(), s.imported.name().as_str())
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
(s.local.name.as_str(), s.local.name.as_str())
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
(s.local.name.as_str(), s.local.name.as_str())
}
};
self.imports.insert(local, (lib, role));
}
}
}
fn callee_role(&self, callee: &Expression<'a>) -> Option<(Lib, &'a str)> {
match callee {
Expression::Identifier(id) => self.imports.get(id.name.as_str()).copied(),
Expression::StaticMemberExpression(member) => {
let Expression::Identifier(obj) = &member.object else {
return None;
};
let (lib, _) = *self.imports.get(obj.name.as_str())?;
Some((lib, member.property.name.as_str()))
}
_ => None,
}
}
fn recognize(lib: Lib, role: &str, arg_count: usize) -> Option<Recognized> {
let single = |tokens_arg| {
Some(Recognized {
binding_source: BindingSource::LhsIdent,
tokens_arg,
})
};
match (lib, role) {
(Lib::StyleX, "defineVars") | (Lib::VanillaExtract, "createThemeContract")
if arg_count >= 1 =>
{
single(0)
}
(Lib::VanillaExtract, "createTheme") if arg_count == 1 => Some(Recognized {
binding_source: BindingSource::TupleElement(1),
tokens_arg: 0,
}),
(Lib::VanillaExtract, "createGlobalTheme") if arg_count == 2 => single(1),
_ => None,
}
}
fn binding_name(decl: &VariableDeclarator<'a>, source: BindingSource) -> Option<&'a str> {
match source {
BindingSource::LhsIdent => match &decl.id {
BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
_ => None,
},
BindingSource::TupleElement(index) => {
let BindingPattern::ArrayPattern(arr) = &decl.id else {
return None;
};
let element = arr.elements.get(index)?.as_ref()?;
match element {
BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
_ => None,
}
}
}
}
fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
let Some(Expression::CallExpression(call)) = &decl.init else {
return;
};
let Some((lib, role)) = self.callee_role(&call.callee) else {
return;
};
let Some(recognized) = Self::recognize(lib, role, call.arguments.len()) else {
return;
};
let Some(binding) = Self::binding_name(decl, recognized.binding_source) else {
return;
};
let Some(Argument::ObjectExpression(obj)) = call.arguments.get(recognized.tokens_arg)
else {
return;
};
let mut tokens = Vec::new();
self.collect_leaves(obj, "", &mut tokens);
if tokens.is_empty() {
return;
}
self.defs.push(CssInJsTokenDef {
binding: binding.to_owned(),
tokens,
});
}
fn collect_leaves(
&self,
obj: &ObjectExpression<'a>,
prefix: &str,
out: &mut Vec<CssInJsToken>,
) {
for prop in &obj.properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
continue;
};
let Some(key) = prop.key.static_name() else {
continue;
};
let path = if prefix.is_empty() {
key.to_string()
} else {
format!("{prefix}.{key}")
};
match &prop.value {
Expression::ObjectExpression(nested) => self.collect_leaves(nested, &path, out),
Expression::Identifier(_) => {}
_ => out.push(CssInJsToken {
path,
def_line: line_at(self.source, prop.key.span().start),
}),
}
}
}
}
impl<'a> Visit<'a> for TokenDefCollector<'a> {
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
self.process_declarator(decl);
walk::walk_variable_declarator(self, decl);
}
}
fn line_at(source: &str, offset: u32) -> u32 {
let end = (offset as usize).min(source.len());
let count = source
.get(..end)
.map_or(0, |s| s.bytes().filter(|&b| b == b'\n').count());
u32::try_from(1 + count).unwrap_or(u32::MAX)
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
fn defs(source: &str) -> Vec<CssInJsTokenDef> {
css_in_js_token_defs(source, Path::new("tokens.ts"))
}
fn paths(defs: &[CssInJsTokenDef], binding: &str) -> Vec<String> {
defs.iter()
.find(|d| d.binding == binding)
.map(|d| d.tokens.iter().map(|t| t.path.clone()).collect())
.unwrap_or_default()
}
#[test]
fn stylex_define_vars_flat_namespace_call() {
let d = defs(
r"
import * as stylex from '@stylexjs/stylex';
export const vars = stylex.defineVars({ primaryColor: '#3b82f6', spacingSm: '4px' });
",
);
assert_eq!(paths(&d, "vars"), vec!["primaryColor", "spacingSm"]);
}
#[test]
fn stylex_define_vars_named_import_nested() {
let d = defs(
r"
import { defineVars } from '@stylexjs/stylex';
export const vars = defineVars({ color: { primary: '#000', secondary: '#fff' } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.primary", "color.secondary"]);
}
#[test]
fn ve_create_theme_tuple_destructure_binds_element_one() {
let d = defs(
r"
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
color: { brand: 'red', accent: 'blue' },
space: { small: '4px' },
});
",
);
assert_eq!(
paths(&d, "vars"),
vec!["color.brand", "color.accent", "space.small"]
);
assert!(paths(&d, "themeClass").is_empty());
}
#[test]
fn ve_create_theme_contract_null_leaves() {
let d = defs(
r"
import { createThemeContract } from '@vanilla-extract/css';
export const vars = createThemeContract({ color: { brand: null, accent: null } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.brand", "color.accent"]);
}
#[test]
fn ve_create_global_theme_two_arg_binds_lhs_tokens_in_second_arg() {
let d = defs(
r"
import { createGlobalTheme } from '@vanilla-extract/css';
export const vars = createGlobalTheme(':root', { color: { brand: 'red' } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
}
#[test]
fn ve_create_theme_two_arg_contract_impl_is_not_a_definition_site() {
let d = defs(
r"
import { createTheme } from '@vanilla-extract/css';
export const themeClass = createTheme(vars, { color: { brand: 'red' } });
",
);
assert!(
d.is_empty(),
"2-arg createTheme must not define tokens, got {d:?}"
);
}
#[test]
fn ve_create_global_theme_three_arg_contract_impl_is_not_a_definition_site() {
let d = defs(
r"
import { createGlobalTheme } from '@vanilla-extract/css';
createGlobalTheme(':root', vars, { color: { brand: 'red' } });
",
);
assert!(
d.is_empty(),
"3-arg createGlobalTheme must not define tokens, got {d:?}"
);
}
#[test]
fn aliased_named_import_still_fires() {
let d = defs(
r"
import { createThemeContract as ct } from '@vanilla-extract/css';
export const vars = ct({ color: { brand: null } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
}
#[test]
fn local_helper_not_from_library_does_not_fire() {
let d = defs(
r"
function defineVars(o) { return o; }
export const vars = defineVars({ color: { primary: '#000' } });
",
);
assert!(d.is_empty(), "local defineVars must not fire, got {d:?}");
}
#[test]
fn unrelated_create_theme_import_does_not_fire() {
let d = defs(
r"
import { createTheme } from '@mui/material/styles';
export const theme = createTheme({ palette: { primary: { main: '#000' } } });
",
);
assert!(d.is_empty(), "non-VE createTheme must not fire, got {d:?}");
}
#[test]
fn type_only_import_does_not_fire() {
let d = defs(
r"
import type { defineVars } from '@stylexjs/stylex';
export const vars = defineVars({ color: { primary: '#000' } });
",
);
assert!(
d.is_empty(),
"type-only import must not gate recognition, got {d:?}"
);
}
#[test]
fn token_def_lines_are_per_leaf() {
let src = "import { defineVars } from '@stylexjs/stylex';\nexport const vars = defineVars({\n color: {\n primary: '#000',\n secondary: '#fff',\n },\n});\n";
let d = defs(src);
let def = d.iter().find(|d| d.binding == "vars").unwrap();
let primary = def
.tokens
.iter()
.find(|t| t.path == "color.primary")
.unwrap();
let secondary = def
.tokens
.iter()
.find(|t| t.path == "color.secondary")
.unwrap();
assert_eq!(primary.def_line, 4);
assert_eq!(secondary.def_line, 5);
}
#[test]
fn spread_and_computed_keys_are_skipped() {
let d = defs(
r"
import { defineVars } from '@stylexjs/stylex';
const base = { a: '1' };
export const vars = defineVars({ ...base, ['x' + 'y']: '2', real: '#000' });
",
);
assert_eq!(paths(&d, "vars"), vec!["real"]);
}
#[test]
fn identifier_valued_key_is_not_a_leaf_but_call_and_member_values_are() {
let d = defs(
r"
import { createGlobalTheme } from '@vanilla-extract/css';
export const vars = createGlobalTheme(':root', {
palette: tailwindPalette,
radius: px(2),
red: colors.red['500'],
});
",
);
let p = paths(&d, "vars");
assert!(
!p.contains(&"palette".to_string()),
"identifier-valued key must not be a leaf: {p:?}"
);
assert!(
p.contains(&"radius".to_string()),
"call-valued key is a leaf: {p:?}"
);
assert!(
p.contains(&"red".to_string()),
"member-valued key is a leaf: {p:?}"
);
}
#[test]
fn no_css_in_js_import_returns_empty() {
let d = defs("export const vars = { color: { primary: '#000' } };");
assert!(d.is_empty());
}
fn leaves(paths: &[&str]) -> FxHashSet<String> {
paths.iter().map(|s| (*s).to_string()).collect()
}
fn consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
css_in_js_token_consumers(source, Path::new("card.ts"), alias, &leaves(paths))
}
#[test]
fn consumer_matches_deepest_leaf_not_intermediate_group() {
let hits = consumers(
"const a = vars.color.primary;",
"vars",
&["color.primary", "color.secondary"],
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
assert_eq!(hits[0].line, 1);
}
#[test]
fn consumer_aliased_receiver() {
let hits = consumers("const a = v.color.primary;", "v", &["color.primary"]);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
}
#[test]
fn consumer_multiple_sites_distinct_lines() {
let src = "const a = vars.color.primary;\nconst b = vars.space.sm;\nconst c = vars.color.primary;";
let hits = consumers(src, "vars", &["color.primary", "space.sm"]);
assert_eq!(hits.len(), 3);
let lines: Vec<u32> = hits.iter().map(|h| h.line).collect();
assert_eq!(lines, vec![1, 2, 3]);
}
#[test]
fn consumer_in_style_object_value_position() {
let hits = consumers(
"export const s = stylex.create({ root: { color: vars.color.primary } });",
"vars",
&["color.primary"],
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
}
#[test]
fn consumer_flat_stylex_depth_one() {
let hits = consumers("const a = vars.primaryColor;", "vars", &["primaryColor"]);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "primaryColor");
}
#[test]
fn consumer_other_binding_not_matched() {
let hits = consumers("const a = other.color.primary;", "vars", &["color.primary"]);
assert!(hits.is_empty());
}
#[test]
fn consumer_deeper_access_past_leaf_matches_leaf_subexpression_once() {
let hits = consumers(
"const a = vars.color.primary.toString();",
"vars",
&["color.primary"],
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
}
#[test]
fn consumer_undefined_path_not_matched() {
let hits = consumers("const a = vars.color.tertiary;", "vars", &["color.primary"]);
assert!(hits.is_empty());
}
#[test]
fn consumer_bracket_notation_hyphenated_key() {
let hits = consumers(
"const a = vars.color['gray-100'];\nconst b = vars.borderRadius['0x'];",
"vars",
&["color.gray-100", "borderRadius.0x"],
);
let paths: Vec<&str> = hits.iter().map(|h| h.token_path.as_str()).collect();
assert!(paths.contains(&"color.gray-100"));
assert!(paths.contains(&"borderRadius.0x"));
assert_eq!(hits.len(), 2);
}
#[test]
fn consumer_mixed_dot_and_bracket_chain() {
let hits = consumers(
"const a = vars['color'].primary;\nconst b = vars.color['primary'];",
"vars",
&["color.primary"],
);
assert_eq!(hits.len(), 2);
assert!(hits.iter().all(|h| h.token_path == "color.primary"));
}
#[test]
fn consumer_non_literal_computed_key_not_matched() {
let hits = consumers(
"const k = 'primary'; const a = vars.color[k];",
"vars",
&["color.primary"],
);
assert!(hits.is_empty());
}
#[test]
fn consumer_empty_inputs_short_circuit() {
assert!(consumers("const a = vars.color.primary;", "", &["color.primary"]).is_empty());
assert!(consumers("const a = vars.color.primary;", "vars", &[]).is_empty());
}
}