Skip to main content

omena_bridge/
source_syntax.rs

1use omena_parser::ParserByteSpanV0;
2use oxc_allocator::Allocator;
3use oxc_ast::ast::{
4    Argument, ArrayExpression, ArrayExpressionElement, BindingPattern, CallExpression,
5    ChainElement, Class, ClassElement, ComputedMemberExpression, ConditionalExpression,
6    Declaration, Expression, ImportDeclarationSpecifier, ImportOrExportKind, JSXAttributeName,
7    JSXAttributeValue, JSXChild, JSXExpression, LogicalExpression, ObjectExpression,
8    ObjectPropertyKind, ParenthesizedExpression, Program, Statement, StaticMemberExpression,
9    TSAsExpression, TSNonNullExpression, TSSatisfiesExpression, VariableDeclarator,
10};
11use oxc_parser::{Parser, ParserReturn};
12use oxc_span::{GetSpan, SourceType, Span};
13use serde::Serialize;
14use std::collections::{BTreeMap, BTreeSet};
15
16use crate::source_language::{project_source_for_language, source_type_for_language};
17
18#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SourceSyntaxIndexV0 {
21    pub schema_version: &'static str,
22    pub product: &'static str,
23    pub imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
24    pub class_string_literals: Vec<ParserByteSpanV0>,
25    pub style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
26    pub selector_references: Vec<SourceSelectorReferenceFactV0>,
27    pub type_fact_targets: Vec<SourceTypeFactTargetV0>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct SourceImportedStyleBindingV0 {
33    pub binding: String,
34    pub style_uri: String,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct SourceStylePropertyAccessFactV0 {
40    pub byte_span: ParserByteSpanV0,
41    pub target_style_uri: Option<String>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct SourceSelectorReferenceFactV0 {
47    pub byte_span: ParserByteSpanV0,
48    pub selector_name: Option<String>,
49    pub match_kind: SourceSelectorReferenceMatchKindV0,
50    pub target_style_uri: Option<String>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct SourceTypeFactTargetV0 {
56    pub byte_span: ParserByteSpanV0,
57    pub expression_id: String,
58    pub target_style_uri: Option<String>,
59    pub prefix: String,
60    pub suffix: String,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub enum SourceSelectorReferenceMatchKindV0 {
66    Exact,
67    Prefix,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71struct SourceStyleBindingTarget {
72    binding: String,
73    target_style_uri: Option<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77struct ClassnamesBindUtilityBinding {
78    binding: String,
79    style_uri: String,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83struct ClassnamesBindCallArgument {
84    binding: String,
85    byte_span: ParserByteSpanV0,
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Eq)]
89struct SourceClassValue {
90    exact: Vec<String>,
91    prefixes: Vec<String>,
92}
93
94impl SourceClassValue {
95    fn is_empty(&self) -> bool {
96        self.exact.is_empty() && self.prefixes.is_empty()
97    }
98
99    fn merge(&mut self, other: SourceClassValue) {
100        self.exact.extend(other.exact);
101        self.prefixes.extend(other.prefixes);
102        self.canonicalize();
103    }
104
105    fn canonicalize(&mut self) {
106        self.exact.sort();
107        self.exact.dedup();
108        self.prefixes.sort();
109        self.prefixes.dedup();
110    }
111}
112
113type SourceReferenceDedupeKey = (
114    usize,
115    usize,
116    Option<String>,
117    SourceSelectorReferenceMatchKindV0,
118);
119type SourceReferenceTargetMap = BTreeMap<SourceReferenceDedupeKey, BTreeSet<Option<String>>>;
120
121pub fn summarize_omena_bridge_source_syntax_index(
122    source: &str,
123    imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
124    classnames_bind_bindings: Vec<String>,
125) -> SourceSyntaxIndexV0 {
126    summarize_omena_bridge_source_syntax_index_for_source_language(
127        "source.tsx",
128        source,
129        None,
130        imported_style_bindings,
131        classnames_bind_bindings,
132    )
133}
134
135pub fn summarize_omena_bridge_source_syntax_index_for_source_language(
136    source_path: &str,
137    source: &str,
138    source_language: Option<&str>,
139    imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
140    classnames_bind_bindings: Vec<String>,
141) -> SourceSyntaxIndexV0 {
142    let projected_source = project_source_for_language(source_path, source, source_language);
143    let imported_style_targets = imported_style_targets(imported_style_bindings.as_slice());
144    let property_access_targets = property_access_style_targets(imported_style_bindings.as_slice());
145    let ast_facts = collect_source_syntax_ast_facts(
146        projected_source.as_ref(),
147        source_type_for_language(source_path, source_language),
148        property_access_targets.as_slice(),
149        imported_style_targets.as_slice(),
150        classnames_bind_bindings.as_slice(),
151    );
152    let class_string_literals = ast_facts.class_string_literals;
153    let style_property_accesses = ast_facts.style_property_accesses;
154    let class_name_expression_spans = ast_facts.class_name_expression_spans;
155    let classnames_bind_targets = ast_facts.classnames_bind_utility_bindings;
156    let classnames_bind_call_arguments = ast_facts.classnames_bind_call_arguments;
157    let local_class_values = collect_local_class_value_bindings(projected_source.as_ref());
158
159    let mut index = SourceSyntaxIndexV0 {
160        schema_version: "0",
161        product: "omena-bridge.source-syntax-index",
162        imported_style_bindings,
163        class_string_literals,
164        style_property_accesses,
165        selector_references: Vec::new(),
166        type_fact_targets: Vec::new(),
167    };
168
169    for span in &index.class_string_literals {
170        push_string_literal_selector_references(
171            source,
172            *span,
173            None,
174            &mut index.selector_references,
175        );
176    }
177    for span in class_name_expression_spans {
178        collect_selector_references_from_js_expression(
179            source,
180            span.start,
181            span.end,
182            None,
183            &local_class_values,
184            &mut index.selector_references,
185            &mut index.type_fact_targets,
186        );
187    }
188    for access in &index.style_property_accesses {
189        index
190            .selector_references
191            .push(SourceSelectorReferenceFactV0 {
192                byte_span: access.byte_span,
193                selector_name: None,
194                match_kind: SourceSelectorReferenceMatchKindV0::Exact,
195                target_style_uri: access.target_style_uri.clone(),
196            });
197    }
198    for argument in classnames_bind_call_arguments {
199        if let Some(binding) = classnames_bind_targets
200            .iter()
201            .find(|binding| binding.binding == argument.binding)
202        {
203            collect_selector_references_from_js_expression(
204                source,
205                argument.byte_span.start,
206                argument.byte_span.end,
207                Some(binding.style_uri.as_str()),
208                &local_class_values,
209                &mut index.selector_references,
210                &mut index.type_fact_targets,
211            );
212        }
213    }
214    canonicalize_source_selector_references(&mut index.selector_references);
215
216    index
217}
218
219pub fn collect_omena_bridge_vue_style_module_bindings(
220    source_path: &str,
221    source: &str,
222    source_language: Option<&str>,
223) -> Vec<String> {
224    let projected_source = project_source_for_language(source_path, source, source_language);
225    let allocator = Allocator::default();
226    let ParserReturn {
227        program, panicked, ..
228    } = Parser::new(
229        &allocator,
230        projected_source.as_ref(),
231        source_type_for_language(source_path, source_language),
232    )
233    .parse();
234    if panicked {
235        return Vec::new();
236    }
237    collect_vue_use_css_module_bindings(&program)
238}
239
240pub fn canonicalize_source_selector_references(
241    references: &mut Vec<SourceSelectorReferenceFactV0>,
242) {
243    let mut targets_by_reference: SourceReferenceTargetMap = BTreeMap::new();
244    for reference in references.iter() {
245        targets_by_reference
246            .entry((
247                reference.byte_span.start,
248                reference.byte_span.end,
249                reference.selector_name.clone(),
250                reference.match_kind,
251            ))
252            .or_default()
253            .insert(reference.target_style_uri.clone());
254    }
255
256    let mut canonical = Vec::new();
257    for ((start, end, selector_name, match_kind), targets) in targets_by_reference {
258        let has_targeted_reference = targets.iter().any(Option::is_some);
259        for target_style_uri in targets {
260            if has_targeted_reference && target_style_uri.is_none() {
261                continue;
262            }
263            canonical.push(SourceSelectorReferenceFactV0 {
264                byte_span: ParserByteSpanV0 { start, end },
265                selector_name: selector_name.clone(),
266                match_kind,
267                target_style_uri,
268            });
269        }
270    }
271    *references = canonical;
272}
273
274fn imported_style_targets(
275    bindings: &[SourceImportedStyleBindingV0],
276) -> Vec<SourceStyleBindingTarget> {
277    bindings
278        .iter()
279        .map(|binding| SourceStyleBindingTarget {
280            binding: binding.binding.clone(),
281            target_style_uri: Some(binding.style_uri.clone()),
282        })
283        .collect()
284}
285
286fn property_access_style_targets(
287    bindings: &[SourceImportedStyleBindingV0],
288) -> Vec<SourceStyleBindingTarget> {
289    let imported = imported_style_targets(bindings);
290    if imported.is_empty() {
291        vec![SourceStyleBindingTarget {
292            binding: "styles".to_string(),
293            target_style_uri: None,
294        }]
295    } else {
296        imported
297    }
298}
299
300struct SourceSyntaxAstFacts {
301    class_string_literals: Vec<ParserByteSpanV0>,
302    style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
303    class_name_expression_spans: Vec<ParserByteSpanV0>,
304    classnames_bind_utility_bindings: Vec<ClassnamesBindUtilityBinding>,
305    classnames_bind_call_arguments: Vec<ClassnamesBindCallArgument>,
306}
307
308fn collect_source_syntax_ast_facts(
309    source: &str,
310    source_type: SourceType,
311    property_access_targets: &[SourceStyleBindingTarget],
312    style_targets: &[SourceStyleBindingTarget],
313    classnames_bind_imports: &[String],
314) -> SourceSyntaxAstFacts {
315    let allocator = Allocator::default();
316    let ParserReturn {
317        program, panicked, ..
318    } = Parser::new(&allocator, source, source_type).parse();
319    if panicked {
320        return SourceSyntaxAstFacts {
321            class_string_literals: Vec::new(),
322            style_property_accesses: Vec::new(),
323            class_name_expression_spans: Vec::new(),
324            classnames_bind_utility_bindings: Vec::new(),
325            classnames_bind_call_arguments: Vec::new(),
326        };
327    }
328
329    let mut collector = SourceSyntaxAstCollector {
330        source,
331        property_access_targets,
332        style_targets,
333        classnames_bind_imports,
334        class_string_literals: Vec::new(),
335        style_property_accesses: Vec::new(),
336        class_name_expression_spans: Vec::new(),
337        classnames_bind_utility_bindings: Vec::new(),
338        classnames_bind_call_arguments: Vec::new(),
339    };
340    collector.collect_program(&program);
341    collector.canonicalize();
342    SourceSyntaxAstFacts {
343        class_string_literals: collector.class_string_literals,
344        style_property_accesses: collector.style_property_accesses,
345        class_name_expression_spans: collector.class_name_expression_spans,
346        classnames_bind_utility_bindings: collector.classnames_bind_utility_bindings,
347        classnames_bind_call_arguments: collector.classnames_bind_call_arguments,
348    }
349}
350
351fn collect_vue_use_css_module_import_names(program: &Program<'_>) -> BTreeSet<String> {
352    let mut names = BTreeSet::new();
353    for statement in &program.body {
354        let Statement::ImportDeclaration(import) = statement else {
355            continue;
356        };
357        if import.import_kind != ImportOrExportKind::Value || import.source.value.as_str() != "vue"
358        {
359            continue;
360        }
361        let Some(specifiers) = import.specifiers.as_ref() else {
362            continue;
363        };
364        for specifier in specifiers {
365            if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier {
366                let imported_name = specifier.imported.name().as_str();
367                if imported_name == "useCssModule" {
368                    names.insert(specifier.local.name.as_str().to_string());
369                }
370            }
371        }
372    }
373    names
374}
375
376fn collect_vue_use_css_module_bindings(program: &Program<'_>) -> Vec<String> {
377    let use_css_module_names = collect_vue_use_css_module_import_names(program);
378    if use_css_module_names.is_empty() {
379        return Vec::new();
380    }
381    let mut bindings = BTreeSet::new();
382    for statement in &program.body {
383        collect_vue_use_css_module_bindings_from_statement(
384            statement,
385            &use_css_module_names,
386            &mut bindings,
387        );
388    }
389    bindings.into_iter().collect()
390}
391
392fn collect_vue_use_css_module_bindings_from_statement(
393    statement: &Statement<'_>,
394    use_css_module_names: &BTreeSet<String>,
395    bindings: &mut BTreeSet<String>,
396) {
397    match statement {
398        Statement::VariableDeclaration(declaration) => {
399            collect_vue_use_css_module_bindings_from_variable_declaration(
400                declaration,
401                use_css_module_names,
402                bindings,
403            );
404        }
405        Statement::ExportNamedDeclaration(declaration) => {
406            if let Some(Declaration::VariableDeclaration(declaration)) = &declaration.declaration {
407                collect_vue_use_css_module_bindings_from_variable_declaration(
408                    declaration,
409                    use_css_module_names,
410                    bindings,
411                );
412            }
413        }
414        _ => {}
415    }
416}
417
418fn collect_vue_use_css_module_bindings_from_variable_declaration(
419    declaration: &oxc_ast::ast::VariableDeclaration<'_>,
420    use_css_module_names: &BTreeSet<String>,
421    bindings: &mut BTreeSet<String>,
422) {
423    for declarator in &declaration.declarations {
424        let Some(binding) = binding_pattern_identifier_name(&declarator.id) else {
425            continue;
426        };
427        let Some(Expression::CallExpression(call)) = &declarator.init else {
428            continue;
429        };
430        let Some(callee) = expression_identifier_name(&call.callee) else {
431            continue;
432        };
433        if use_css_module_names.contains(callee) {
434            bindings.insert(binding.to_string());
435        }
436    }
437}
438
439struct SourceSyntaxAstCollector<'a> {
440    source: &'a str,
441    property_access_targets: &'a [SourceStyleBindingTarget],
442    style_targets: &'a [SourceStyleBindingTarget],
443    classnames_bind_imports: &'a [String],
444    class_string_literals: Vec<ParserByteSpanV0>,
445    style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
446    class_name_expression_spans: Vec<ParserByteSpanV0>,
447    classnames_bind_utility_bindings: Vec<ClassnamesBindUtilityBinding>,
448    classnames_bind_call_arguments: Vec<ClassnamesBindCallArgument>,
449}
450
451impl<'a> SourceSyntaxAstCollector<'a> {
452    fn collect_program(&mut self, program: &Program<'a>) {
453        for statement in &program.body {
454            self.collect_statement(statement);
455        }
456    }
457
458    fn collect_statement(&mut self, statement: &Statement<'a>) {
459        match statement {
460            Statement::BlockStatement(statement) => {
461                for statement in &statement.body {
462                    self.collect_statement(statement);
463                }
464            }
465            Statement::ExpressionStatement(statement) => {
466                self.collect_expression(&statement.expression);
467            }
468            Statement::ReturnStatement(statement) => {
469                if let Some(argument) = &statement.argument {
470                    self.collect_expression(argument);
471                }
472            }
473            Statement::IfStatement(statement) => {
474                self.collect_expression(&statement.test);
475                self.collect_statement(&statement.consequent);
476                if let Some(alternate) = &statement.alternate {
477                    self.collect_statement(alternate);
478                }
479            }
480            Statement::ForStatement(statement) => {
481                if let Some(init) = &statement.init {
482                    self.collect_for_statement_init(init);
483                }
484                if let Some(test) = &statement.test {
485                    self.collect_expression(test);
486                }
487                if let Some(update) = &statement.update {
488                    self.collect_expression(update);
489                }
490                self.collect_statement(&statement.body);
491            }
492            Statement::ForInStatement(statement) => {
493                self.collect_expression(&statement.right);
494                self.collect_statement(&statement.body);
495            }
496            Statement::ForOfStatement(statement) => {
497                self.collect_expression(&statement.right);
498                self.collect_statement(&statement.body);
499            }
500            Statement::WhileStatement(statement) => {
501                self.collect_expression(&statement.test);
502                self.collect_statement(&statement.body);
503            }
504            Statement::DoWhileStatement(statement) => {
505                self.collect_statement(&statement.body);
506                self.collect_expression(&statement.test);
507            }
508            Statement::SwitchStatement(statement) => {
509                self.collect_expression(&statement.discriminant);
510                for switch_case in &statement.cases {
511                    if let Some(test) = &switch_case.test {
512                        self.collect_expression(test);
513                    }
514                    for consequent in &switch_case.consequent {
515                        self.collect_statement(consequent);
516                    }
517                }
518            }
519            Statement::ThrowStatement(statement) => {
520                self.collect_expression(&statement.argument);
521            }
522            Statement::TryStatement(statement) => {
523                for statement in &statement.block.body {
524                    self.collect_statement(statement);
525                }
526                if let Some(handler) = &statement.handler {
527                    for statement in &handler.body.body {
528                        self.collect_statement(statement);
529                    }
530                }
531                if let Some(finalizer) = &statement.finalizer {
532                    for statement in &finalizer.body {
533                        self.collect_statement(statement);
534                    }
535                }
536            }
537            Statement::VariableDeclaration(declaration) => {
538                self.collect_variable_declaration(declaration);
539            }
540            Statement::FunctionDeclaration(function) => {
541                self.collect_function_body(function.body.as_deref());
542            }
543            Statement::ClassDeclaration(class) => {
544                self.collect_class(class);
545            }
546            Statement::ExportNamedDeclaration(declaration) => {
547                if let Some(declaration) = &declaration.declaration {
548                    self.collect_declaration(declaration);
549                }
550            }
551            Statement::ExportDefaultDeclaration(declaration) => {
552                self.collect_export_default_declaration(&declaration.declaration);
553            }
554            Statement::TSExportAssignment(declaration) => {
555                self.collect_expression(&declaration.expression);
556            }
557            _ => {}
558        }
559    }
560
561    fn collect_declaration(&mut self, declaration: &Declaration<'a>) {
562        match declaration {
563            Declaration::VariableDeclaration(declaration) => {
564                self.collect_variable_declaration(declaration);
565            }
566            Declaration::FunctionDeclaration(function) => {
567                self.collect_function_body(function.body.as_deref());
568            }
569            Declaration::ClassDeclaration(class) => {
570                self.collect_class(class);
571            }
572            _ => {}
573        }
574    }
575
576    fn collect_export_default_declaration(
577        &mut self,
578        declaration: &oxc_ast::ast::ExportDefaultDeclarationKind<'a>,
579    ) {
580        match declaration {
581            oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
582                self.collect_function_body(function.body.as_deref());
583            }
584            oxc_ast::ast::ExportDefaultDeclarationKind::ClassDeclaration(class) => {
585                self.collect_class(class);
586            }
587            oxc_ast::ast::ExportDefaultDeclarationKind::StaticMemberExpression(member) => {
588                self.collect_static_member_expression(member);
589            }
590            oxc_ast::ast::ExportDefaultDeclarationKind::ComputedMemberExpression(member) => {
591                self.collect_computed_member_expression(member);
592            }
593            oxc_ast::ast::ExportDefaultDeclarationKind::CallExpression(expression) => {
594                self.collect_call_expression(expression);
595            }
596            _ => {}
597        }
598    }
599
600    fn collect_for_statement_init(&mut self, init: &oxc_ast::ast::ForStatementInit<'a>) {
601        match init {
602            oxc_ast::ast::ForStatementInit::VariableDeclaration(declaration) => {
603                self.collect_variable_declaration(declaration);
604            }
605            oxc_ast::ast::ForStatementInit::StaticMemberExpression(member) => {
606                self.collect_static_member_expression(member);
607            }
608            oxc_ast::ast::ForStatementInit::ComputedMemberExpression(member) => {
609                self.collect_computed_member_expression(member);
610            }
611            oxc_ast::ast::ForStatementInit::CallExpression(expression) => {
612                self.collect_call_expression(expression);
613            }
614            _ => {}
615        }
616    }
617
618    fn collect_variable_declaration(
619        &mut self,
620        declaration: &oxc_ast::ast::VariableDeclaration<'a>,
621    ) {
622        for declarator in &declaration.declarations {
623            if let Some(binding) = self.classnames_bind_utility_binding_from_declarator(declarator)
624            {
625                self.classnames_bind_utility_bindings.push(binding);
626            }
627            if let Some(init) = &declarator.init {
628                self.collect_expression(init);
629            }
630        }
631    }
632
633    fn classnames_bind_utility_binding_from_declarator(
634        &self,
635        declarator: &VariableDeclarator<'a>,
636    ) -> Option<ClassnamesBindUtilityBinding> {
637        if self.style_targets.is_empty() || self.classnames_bind_imports.is_empty() {
638            return None;
639        }
640        let binding = binding_pattern_identifier_name(&declarator.id)?;
641        let init = declarator.init.as_ref()?;
642        let Expression::CallExpression(call) = init else {
643            return None;
644        };
645        let Expression::StaticMemberExpression(callee) = &call.callee else {
646            return None;
647        };
648        if callee.property.name.as_str() != "bind" {
649            return None;
650        }
651        let callee_binding = expression_identifier_name(&callee.object)?;
652        if !self
653            .classnames_bind_imports
654            .iter()
655            .any(|import_binding| import_binding == callee_binding)
656        {
657            return None;
658        }
659        let style_binding = call.arguments.first().and_then(argument_identifier_name)?;
660        let style_uri = self
661            .style_targets
662            .iter()
663            .find(|target| target.binding == style_binding)?
664            .target_style_uri
665            .clone()?;
666
667        Some(ClassnamesBindUtilityBinding {
668            binding: binding.to_string(),
669            style_uri,
670        })
671    }
672
673    fn collect_function_body(&mut self, body: Option<&oxc_ast::ast::FunctionBody<'a>>) {
674        let Some(body) = body else {
675            return;
676        };
677        for statement in &body.statements {
678            self.collect_statement(statement);
679        }
680    }
681
682    fn collect_class(&mut self, class: &Class<'a>) {
683        if let Some(super_class) = &class.super_class {
684            self.collect_expression(super_class);
685        }
686        for element in &class.body.body {
687            match element {
688                ClassElement::MethodDefinition(method) => {
689                    self.collect_function_body(method.value.body.as_deref());
690                }
691                ClassElement::PropertyDefinition(property) => {
692                    if property.computed {
693                        self.collect_property_key(&property.key);
694                    }
695                    if let Some(value) = &property.value {
696                        self.collect_expression(value);
697                    }
698                }
699                ClassElement::AccessorProperty(property) => {
700                    if property.computed {
701                        self.collect_property_key(&property.key);
702                    }
703                    if let Some(value) = &property.value {
704                        self.collect_expression(value);
705                    }
706                }
707                ClassElement::StaticBlock(block) => {
708                    for statement in &block.body {
709                        self.collect_statement(statement);
710                    }
711                }
712                ClassElement::TSIndexSignature(_) => {}
713            }
714        }
715    }
716
717    fn collect_expression(&mut self, expression: &Expression<'a>) {
718        match expression {
719            Expression::StaticMemberExpression(member) => {
720                self.collect_static_member_expression(member);
721            }
722            Expression::ComputedMemberExpression(member) => {
723                self.collect_computed_member_expression(member);
724            }
725            Expression::PrivateFieldExpression(member) => {
726                self.collect_expression(&member.object);
727            }
728            Expression::ArrayExpression(expression) => {
729                self.collect_array_expression(expression);
730            }
731            Expression::ObjectExpression(expression) => {
732                self.collect_object_expression(expression);
733            }
734            Expression::CallExpression(expression) => {
735                self.collect_call_expression(expression);
736            }
737            Expression::NewExpression(expression) => {
738                self.collect_expression(&expression.callee);
739                for argument in &expression.arguments {
740                    self.collect_argument(argument);
741                }
742            }
743            Expression::ChainExpression(expression) => {
744                self.collect_chain_element(&expression.expression);
745            }
746            Expression::ConditionalExpression(expression) => {
747                self.collect_conditional_expression(expression);
748            }
749            Expression::BinaryExpression(expression) => {
750                self.collect_expression(&expression.left);
751                self.collect_expression(&expression.right);
752            }
753            Expression::LogicalExpression(expression) => {
754                self.collect_logical_expression(expression);
755            }
756            Expression::AssignmentExpression(expression) => {
757                self.collect_expression(&expression.right);
758            }
759            Expression::SequenceExpression(expression) => {
760                for expression in &expression.expressions {
761                    self.collect_expression(expression);
762                }
763            }
764            Expression::ParenthesizedExpression(expression) => {
765                self.collect_parenthesized_expression(expression);
766            }
767            Expression::UnaryExpression(expression) => {
768                self.collect_expression(&expression.argument);
769            }
770            Expression::AwaitExpression(expression) => {
771                self.collect_expression(&expression.argument);
772            }
773            Expression::TemplateLiteral(expression) => {
774                for expression in &expression.expressions {
775                    self.collect_expression(expression);
776                }
777            }
778            Expression::TaggedTemplateExpression(expression) => {
779                self.collect_expression(&expression.tag);
780                for expression in &expression.quasi.expressions {
781                    self.collect_expression(expression);
782                }
783            }
784            Expression::ArrowFunctionExpression(expression) => {
785                self.collect_function_body(Some(&expression.body));
786            }
787            Expression::FunctionExpression(expression) => {
788                self.collect_function_body(expression.body.as_deref());
789            }
790            Expression::ClassExpression(class) => {
791                self.collect_class(class);
792            }
793            Expression::ImportExpression(expression) => {
794                self.collect_expression(&expression.source);
795                if let Some(options) = &expression.options {
796                    self.collect_expression(options);
797                }
798            }
799            Expression::JSXElement(element) => {
800                self.collect_jsx_element(element);
801            }
802            Expression::JSXFragment(fragment) => {
803                for child in &fragment.children {
804                    self.collect_jsx_child(child);
805                }
806            }
807            Expression::TSAsExpression(expression) => {
808                self.collect_ts_as_expression(expression);
809            }
810            Expression::TSSatisfiesExpression(expression) => {
811                self.collect_ts_satisfies_expression(expression);
812            }
813            Expression::TSTypeAssertion(expression) => {
814                self.collect_expression(&expression.expression);
815            }
816            Expression::TSNonNullExpression(expression) => {
817                self.collect_ts_non_null_expression(expression);
818            }
819            Expression::TSInstantiationExpression(expression) => {
820                self.collect_expression(&expression.expression);
821            }
822            _ => {}
823        }
824    }
825
826    fn collect_array_expression_element(&mut self, element: &ArrayExpressionElement<'a>) {
827        match element {
828            ArrayExpressionElement::SpreadElement(spread) => {
829                self.collect_expression(&spread.argument);
830            }
831            ArrayExpressionElement::Elision(_) => {}
832            _ => {
833                if let Some(expression) = element.as_expression() {
834                    self.collect_expression(expression);
835                }
836            }
837        }
838    }
839
840    fn collect_argument(&mut self, argument: &Argument<'a>) {
841        match argument {
842            Argument::SpreadElement(spread) => {
843                self.collect_expression(&spread.argument);
844            }
845            _ => {
846                if let Some(expression) = argument.as_expression() {
847                    self.collect_expression(expression);
848                }
849            }
850        }
851    }
852
853    fn collect_chain_element(&mut self, element: &ChainElement<'a>) {
854        match element {
855            ChainElement::CallExpression(expression) => {
856                self.collect_expression(&expression.callee);
857                for argument in &expression.arguments {
858                    self.collect_argument(argument);
859                }
860            }
861            ChainElement::StaticMemberExpression(member) => {
862                self.collect_static_member_expression(member);
863            }
864            ChainElement::ComputedMemberExpression(member) => {
865                self.collect_computed_member_expression(member);
866            }
867            ChainElement::PrivateFieldExpression(member) => {
868                self.collect_expression(&member.object);
869            }
870            ChainElement::TSNonNullExpression(expression) => {
871                self.collect_expression(&expression.expression);
872            }
873        }
874    }
875
876    fn collect_property_key(&mut self, key: &oxc_ast::ast::PropertyKey<'a>) {
877        match key {
878            oxc_ast::ast::PropertyKey::StaticIdentifier(_)
879            | oxc_ast::ast::PropertyKey::PrivateIdentifier(_) => {}
880            _ => {
881                if let Some(expression) = key.as_expression() {
882                    self.collect_expression(expression);
883                }
884            }
885        }
886    }
887
888    fn collect_jsx_element(&mut self, element: &oxc_ast::ast::JSXElement<'a>) {
889        for attribute in &element.opening_element.attributes {
890            match attribute {
891                oxc_ast::ast::JSXAttributeItem::Attribute(attribute) => {
892                    if is_jsx_class_name_attribute(&attribute.name)
893                        && let Some(value) = &attribute.value
894                    {
895                        self.collect_class_name_string_literal_attribute(value);
896                        self.collect_class_name_expression_attribute(value);
897                    }
898                    if let Some(value) = &attribute.value {
899                        self.collect_jsx_attribute_value(value);
900                    }
901                }
902                oxc_ast::ast::JSXAttributeItem::SpreadAttribute(attribute) => {
903                    self.collect_expression(&attribute.argument);
904                }
905            }
906        }
907        for child in &element.children {
908            self.collect_jsx_child(child);
909        }
910    }
911
912    fn collect_jsx_attribute_value(&mut self, value: &JSXAttributeValue<'a>) {
913        match value {
914            JSXAttributeValue::ExpressionContainer(container) => {
915                self.collect_jsx_expression(&container.expression);
916            }
917            JSXAttributeValue::Element(element) => {
918                self.collect_jsx_element(element);
919            }
920            JSXAttributeValue::Fragment(fragment) => {
921                for child in &fragment.children {
922                    self.collect_jsx_child(child);
923                }
924            }
925            JSXAttributeValue::StringLiteral(_) => {}
926        }
927    }
928
929    fn collect_class_name_string_literal_attribute(&mut self, value: &JSXAttributeValue<'a>) {
930        let JSXAttributeValue::StringLiteral(literal) = value else {
931            return;
932        };
933        if let Some(span) = self.string_literal_content_span(literal.span) {
934            self.class_string_literals.push(span);
935        }
936    }
937
938    fn collect_class_name_expression_attribute(&mut self, value: &JSXAttributeValue<'a>) {
939        let JSXAttributeValue::ExpressionContainer(container) = value else {
940            return;
941        };
942        if let Some(span) = jsx_expression_span(&container.expression) {
943            self.class_name_expression_spans.push(span);
944        }
945    }
946
947    fn collect_jsx_child(&mut self, child: &JSXChild<'a>) {
948        match child {
949            JSXChild::Element(element) => {
950                self.collect_jsx_element(element);
951            }
952            JSXChild::Fragment(fragment) => {
953                for child in &fragment.children {
954                    self.collect_jsx_child(child);
955                }
956            }
957            JSXChild::ExpressionContainer(container) => {
958                self.collect_jsx_expression(&container.expression);
959            }
960            JSXChild::Spread(spread) => {
961                self.collect_expression(&spread.expression);
962            }
963            JSXChild::Text(_) => {}
964        }
965    }
966
967    fn collect_jsx_expression(&mut self, expression: &JSXExpression<'a>) {
968        match expression {
969            JSXExpression::StaticMemberExpression(member) => {
970                self.collect_static_member_expression(member);
971            }
972            JSXExpression::ComputedMemberExpression(member) => {
973                self.collect_computed_member_expression(member);
974            }
975            JSXExpression::CallExpression(expression) => {
976                self.collect_call_expression(expression);
977            }
978            JSXExpression::ConditionalExpression(expression) => {
979                self.collect_conditional_expression(expression);
980            }
981            JSXExpression::LogicalExpression(expression) => {
982                self.collect_logical_expression(expression);
983            }
984            JSXExpression::ArrayExpression(expression) => {
985                self.collect_array_expression(expression);
986            }
987            JSXExpression::ObjectExpression(expression) => {
988                self.collect_object_expression(expression);
989            }
990            JSXExpression::ParenthesizedExpression(expression) => {
991                self.collect_parenthesized_expression(expression);
992            }
993            JSXExpression::TSAsExpression(expression) => {
994                self.collect_ts_as_expression(expression);
995            }
996            JSXExpression::TSSatisfiesExpression(expression) => {
997                self.collect_ts_satisfies_expression(expression);
998            }
999            JSXExpression::TSNonNullExpression(expression) => {
1000                self.collect_ts_non_null_expression(expression);
1001            }
1002            JSXExpression::JSXElement(element) => {
1003                self.collect_jsx_element(element);
1004            }
1005            JSXExpression::JSXFragment(fragment) => {
1006                for child in &fragment.children {
1007                    self.collect_jsx_child(child);
1008                }
1009            }
1010            _ => {}
1011        }
1012    }
1013
1014    fn collect_array_expression(&mut self, expression: &ArrayExpression<'a>) {
1015        for element in &expression.elements {
1016            self.collect_array_expression_element(element);
1017        }
1018    }
1019
1020    fn collect_object_expression(&mut self, expression: &ObjectExpression<'a>) {
1021        for property in &expression.properties {
1022            match property {
1023                ObjectPropertyKind::ObjectProperty(property) => {
1024                    if property.computed {
1025                        self.collect_property_key(&property.key);
1026                    }
1027                    self.collect_expression(&property.value);
1028                }
1029                ObjectPropertyKind::SpreadProperty(spread) => {
1030                    self.collect_expression(&spread.argument);
1031                }
1032            }
1033        }
1034    }
1035
1036    fn collect_call_expression(&mut self, expression: &CallExpression<'a>) {
1037        if let Some(binding) = expression_identifier_name(&expression.callee) {
1038            for argument in &expression.arguments {
1039                if let Some(byte_span) = argument_expression_span(argument) {
1040                    self.classnames_bind_call_arguments
1041                        .push(ClassnamesBindCallArgument {
1042                            binding: binding.to_string(),
1043                            byte_span,
1044                        });
1045                }
1046            }
1047        }
1048        self.collect_expression(&expression.callee);
1049        for argument in &expression.arguments {
1050            self.collect_argument(argument);
1051        }
1052    }
1053
1054    fn collect_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
1055        self.collect_expression(&expression.test);
1056        self.collect_expression(&expression.consequent);
1057        self.collect_expression(&expression.alternate);
1058    }
1059
1060    fn collect_logical_expression(&mut self, expression: &LogicalExpression<'a>) {
1061        self.collect_expression(&expression.left);
1062        self.collect_expression(&expression.right);
1063    }
1064
1065    fn collect_parenthesized_expression(&mut self, expression: &ParenthesizedExpression<'a>) {
1066        self.collect_expression(&expression.expression);
1067    }
1068
1069    fn collect_ts_as_expression(&mut self, expression: &TSAsExpression<'a>) {
1070        self.collect_expression(&expression.expression);
1071    }
1072
1073    fn collect_ts_satisfies_expression(&mut self, expression: &TSSatisfiesExpression<'a>) {
1074        self.collect_expression(&expression.expression);
1075    }
1076
1077    fn collect_ts_non_null_expression(&mut self, expression: &TSNonNullExpression<'a>) {
1078        self.collect_expression(&expression.expression);
1079    }
1080
1081    fn collect_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
1082        if let Some(target) = self.target_for_object(&member.object)
1083            && let Some(byte_span) = self.css_identifier_span(member.property.span)
1084        {
1085            self.style_property_accesses
1086                .push(SourceStylePropertyAccessFactV0 {
1087                    byte_span,
1088                    target_style_uri: target.target_style_uri.clone(),
1089                });
1090        }
1091        self.collect_expression(&member.object);
1092    }
1093
1094    fn collect_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
1095        if let Some(target) = self.target_for_object(&member.object)
1096            && let Some(byte_span) = self.static_string_expression_content_span(&member.expression)
1097        {
1098            self.style_property_accesses
1099                .push(SourceStylePropertyAccessFactV0 {
1100                    byte_span,
1101                    target_style_uri: target.target_style_uri.clone(),
1102                });
1103        }
1104        self.collect_expression(&member.object);
1105        self.collect_expression(&member.expression);
1106    }
1107
1108    fn target_for_object(&self, expression: &Expression<'a>) -> Option<&SourceStyleBindingTarget> {
1109        match expression {
1110            Expression::Identifier(identifier) => self
1111                .property_access_targets
1112                .iter()
1113                .find(|target| target.binding == identifier.name.as_str()),
1114            Expression::ParenthesizedExpression(expression) => {
1115                self.target_for_object(&expression.expression)
1116            }
1117            Expression::TSAsExpression(expression) => {
1118                self.target_for_object(&expression.expression)
1119            }
1120            Expression::TSSatisfiesExpression(expression) => {
1121                self.target_for_object(&expression.expression)
1122            }
1123            Expression::TSTypeAssertion(expression) => {
1124                self.target_for_object(&expression.expression)
1125            }
1126            Expression::TSNonNullExpression(expression) => {
1127                self.target_for_object(&expression.expression)
1128            }
1129            Expression::TSInstantiationExpression(expression) => {
1130                self.target_for_object(&expression.expression)
1131            }
1132            _ => None,
1133        }
1134    }
1135
1136    fn static_string_expression_content_span(
1137        &self,
1138        expression: &Expression<'a>,
1139    ) -> Option<ParserByteSpanV0> {
1140        match expression {
1141            Expression::StringLiteral(literal) => self.css_identifier_content_span(literal.span),
1142            Expression::TemplateLiteral(literal) if literal.expressions.is_empty() => {
1143                self.css_identifier_content_span(literal.span)
1144            }
1145            _ => None,
1146        }
1147    }
1148
1149    fn css_identifier_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1150        let span = parser_byte_span(span);
1151        let text = self.source.get(span.start..span.end)?;
1152        (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(span)
1153    }
1154
1155    fn css_identifier_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1156        let span = parser_byte_span(span);
1157        if span.end <= span.start + 1 {
1158            return None;
1159        }
1160        let content = ParserByteSpanV0 {
1161            start: span.start + 1,
1162            end: span.end - 1,
1163        };
1164        let text = self.source.get(content.start..content.end)?;
1165        (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(content)
1166    }
1167
1168    fn string_literal_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1169        let span = parser_byte_span(span);
1170        if span.end <= span.start + 1 {
1171            return None;
1172        }
1173        let content = ParserByteSpanV0 {
1174            start: span.start + 1,
1175            end: span.end - 1,
1176        };
1177        self.source.get(content.start..content.end)?;
1178        Some(content)
1179    }
1180
1181    fn canonicalize(&mut self) {
1182        self.class_string_literals.sort_by(|left, right| {
1183            left.start
1184                .cmp(&right.start)
1185                .then_with(|| left.end.cmp(&right.end))
1186        });
1187        self.class_string_literals.dedup();
1188        self.style_property_accesses.sort_by(|left, right| {
1189            left.byte_span
1190                .start
1191                .cmp(&right.byte_span.start)
1192                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1193                .then_with(|| left.target_style_uri.cmp(&right.target_style_uri))
1194        });
1195        self.style_property_accesses.dedup();
1196        self.classnames_bind_utility_bindings
1197            .sort_by(|left, right| {
1198                left.binding
1199                    .cmp(&right.binding)
1200                    .then_with(|| left.style_uri.cmp(&right.style_uri))
1201            });
1202        self.classnames_bind_utility_bindings
1203            .dedup_by(|left, right| {
1204                left.binding == right.binding && left.style_uri == right.style_uri
1205            });
1206        self.classnames_bind_call_arguments.sort_by(|left, right| {
1207            left.binding
1208                .cmp(&right.binding)
1209                .then_with(|| left.byte_span.start.cmp(&right.byte_span.start))
1210                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1211        });
1212        self.classnames_bind_call_arguments.dedup_by(|left, right| {
1213            left.binding == right.binding && left.byte_span == right.byte_span
1214        });
1215    }
1216}
1217
1218fn parser_byte_span(span: Span) -> ParserByteSpanV0 {
1219    ParserByteSpanV0 {
1220        start: span.start as usize,
1221        end: span.end as usize,
1222    }
1223}
1224
1225fn is_jsx_class_name_attribute(name: &JSXAttributeName<'_>) -> bool {
1226    matches!(name, JSXAttributeName::Identifier(identifier) if identifier.name.as_str() == "className")
1227}
1228
1229fn jsx_expression_span(expression: &JSXExpression<'_>) -> Option<ParserByteSpanV0> {
1230    match expression {
1231        JSXExpression::EmptyExpression(_) => None,
1232        _ => Some(parser_byte_span(expression.span())),
1233    }
1234}
1235
1236fn argument_expression_span(argument: &Argument<'_>) -> Option<ParserByteSpanV0> {
1237    match argument {
1238        Argument::SpreadElement(spread) => Some(parser_byte_span(spread.argument.span())),
1239        _ => Some(parser_byte_span(argument.span())),
1240    }
1241}
1242
1243fn binding_pattern_identifier_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
1244    match pattern {
1245        BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.as_str()),
1246        _ => None,
1247    }
1248}
1249
1250fn expression_identifier_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1251    match expression {
1252        Expression::Identifier(identifier) => Some(identifier.name.as_str()),
1253        Expression::ParenthesizedExpression(expression) => {
1254            expression_identifier_name(&expression.expression)
1255        }
1256        Expression::TSAsExpression(expression) => {
1257            expression_identifier_name(&expression.expression)
1258        }
1259        Expression::TSSatisfiesExpression(expression) => {
1260            expression_identifier_name(&expression.expression)
1261        }
1262        Expression::TSTypeAssertion(expression) => {
1263            expression_identifier_name(&expression.expression)
1264        }
1265        Expression::TSNonNullExpression(expression) => {
1266            expression_identifier_name(&expression.expression)
1267        }
1268        Expression::TSInstantiationExpression(expression) => {
1269            expression_identifier_name(&expression.expression)
1270        }
1271        _ => None,
1272    }
1273}
1274
1275fn argument_identifier_name<'a>(argument: &'a Argument<'a>) -> Option<&'a str> {
1276    match argument {
1277        Argument::Identifier(identifier) => Some(identifier.name.as_str()),
1278        Argument::ParenthesizedExpression(expression) => {
1279            expression_identifier_name(&expression.expression)
1280        }
1281        Argument::TSAsExpression(expression) => expression_identifier_name(&expression.expression),
1282        Argument::TSSatisfiesExpression(expression) => {
1283            expression_identifier_name(&expression.expression)
1284        }
1285        Argument::TSNonNullExpression(expression) => {
1286            expression_identifier_name(&expression.expression)
1287        }
1288        Argument::TSInstantiationExpression(expression) => {
1289            expression_identifier_name(&expression.expression)
1290        }
1291        _ => None,
1292    }
1293}
1294
1295fn collect_selector_references_from_js_expression(
1296    source: &str,
1297    start: usize,
1298    end: usize,
1299    target_style_uri: Option<&str>,
1300    local_class_values: &BTreeMap<String, SourceClassValue>,
1301    references: &mut Vec<SourceSelectorReferenceFactV0>,
1302    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1303) {
1304    let (start, end) = trim_js_expression(source, start, end);
1305    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1306    if start >= end {
1307        return;
1308    }
1309
1310    if let Some((literal_start, literal_end, next_offset)) =
1311        js_string_literal_span(source, start, end)
1312        && trim_js_expression(source, next_offset, end).0 >= end
1313    {
1314        push_js_literal_selector_references(
1315            source,
1316            literal_start,
1317            literal_end,
1318            source.as_bytes().get(start).copied() == Some(b'`'),
1319            target_style_uri,
1320            references,
1321        );
1322        if source.as_bytes().get(start).copied() == Some(b'`') {
1323            collect_template_type_fact_targets(
1324                source,
1325                literal_start,
1326                literal_end,
1327                target_style_uri,
1328                type_fact_targets,
1329            );
1330        }
1331        return;
1332    }
1333
1334    if source.as_bytes().get(start) == Some(&b'{')
1335        && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1336    {
1337        collect_object_literal_selector_references(
1338            source,
1339            start,
1340            end,
1341            target_style_uri,
1342            local_class_values,
1343            references,
1344            type_fact_targets,
1345        );
1346        return;
1347    }
1348
1349    if source.as_bytes().get(start) == Some(&b'[')
1350        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1351    {
1352        for (element_start, element_end) in
1353            split_top_level_js_segments(source, start + 1, end - 1, b',')
1354        {
1355            let element_start = skip_js_trivia_until(source, element_start, element_end);
1356            let element_start = if source[element_start..element_end].starts_with("...") {
1357                element_start + 3
1358            } else {
1359                element_start
1360            };
1361            collect_selector_references_from_js_expression(
1362                source,
1363                element_start,
1364                element_end,
1365                target_style_uri,
1366                local_class_values,
1367                references,
1368                type_fact_targets,
1369            );
1370        }
1371        return;
1372    }
1373
1374    if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1375    {
1376        for (argument_start, argument_end) in
1377            split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1378        {
1379            collect_selector_references_from_js_expression(
1380                source,
1381                argument_start,
1382                argument_end,
1383                target_style_uri,
1384                local_class_values,
1385                references,
1386                type_fact_targets,
1387            );
1388        }
1389        return;
1390    }
1391
1392    if let Some((_, true_start, true_end, false_start, false_end)) =
1393        top_level_conditional_parts(source, start, end)
1394    {
1395        collect_selector_references_from_js_expression(
1396            source,
1397            true_start,
1398            true_end,
1399            target_style_uri,
1400            local_class_values,
1401            references,
1402            type_fact_targets,
1403        );
1404        collect_selector_references_from_js_expression(
1405            source,
1406            false_start,
1407            false_end,
1408            target_style_uri,
1409            local_class_values,
1410            references,
1411            type_fact_targets,
1412        );
1413        return;
1414    }
1415
1416    if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1417        .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1418    {
1419        collect_selector_references_from_js_expression(
1420            source,
1421            operator_offset + 2,
1422            end,
1423            target_style_uri,
1424            local_class_values,
1425            references,
1426            type_fact_targets,
1427        );
1428        return;
1429    }
1430
1431    let expression_path = js_expression_path(source, start, end);
1432    if let Some(value) =
1433        source_class_value_from_js_expression(source, start, end, local_class_values)
1434        && !value.is_empty()
1435    {
1436        if let Some(path) = expression_path.as_deref() {
1437            push_source_type_fact_target(
1438                ParserByteSpanV0 { start, end },
1439                path,
1440                target_style_uri,
1441                "",
1442                "",
1443                type_fact_targets,
1444            );
1445        }
1446        push_source_class_value_reference(
1447            ParserByteSpanV0 { start, end },
1448            value,
1449            target_style_uri,
1450            references,
1451        );
1452        return;
1453    }
1454
1455    if let Some(prefix) =
1456        static_string_prefix_for_js_expression(source, start, end, local_class_values)
1457        && !prefix.is_empty()
1458    {
1459        push_selector_reference(
1460            ParserByteSpanV0 { start, end },
1461            Some(prefix),
1462            SourceSelectorReferenceMatchKindV0::Prefix,
1463            target_style_uri,
1464            references,
1465        );
1466        return;
1467    }
1468
1469    if let Some(path) = expression_path {
1470        push_source_type_fact_target(
1471            ParserByteSpanV0 { start, end },
1472            path.as_str(),
1473            target_style_uri,
1474            "",
1475            "",
1476            type_fact_targets,
1477        );
1478    }
1479}
1480
1481fn collect_local_class_value_bindings(source: &str) -> BTreeMap<String, SourceClassValue> {
1482    let mut values = BTreeMap::new();
1483    let mut cursor = 0usize;
1484    while let Some(keyword) = next_code_identifier(source, cursor) {
1485        cursor = keyword.end;
1486        if !matches!(keyword.text, "const" | "let" | "var") {
1487            continue;
1488        }
1489        let binding_start = skip_js_trivia(source, keyword.end);
1490        let Some((binding, binding_end)) = read_js_identifier(source, binding_start) else {
1491            continue;
1492        };
1493        let equals_offset = skip_js_trivia(source, binding_end);
1494        if source.as_bytes().get(equals_offset) != Some(&b'=') {
1495            continue;
1496        }
1497        let expression_start = skip_js_trivia(source, equals_offset + 1);
1498        let expression_end = js_statement_expression_end(source, expression_start);
1499        if let Some(value) =
1500            source_class_value_from_js_expression(source, expression_start, expression_end, &values)
1501            && !value.is_empty()
1502        {
1503            values.insert(binding.to_string(), value);
1504        }
1505        let (_, property_values) = source_class_value_from_object_literal(
1506            source,
1507            expression_start,
1508            expression_end,
1509            &values,
1510        );
1511        for (property, value) in property_values {
1512            if !value.is_empty() {
1513                values.insert(format!("{binding}.{property}"), value);
1514            }
1515        }
1516        cursor = expression_end.min(source.len());
1517    }
1518    values
1519}
1520
1521fn source_class_value_from_js_expression(
1522    source: &str,
1523    start: usize,
1524    end: usize,
1525    local_class_values: &BTreeMap<String, SourceClassValue>,
1526) -> Option<SourceClassValue> {
1527    let (start, end) = trim_js_expression(source, start, end);
1528    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1529    if start >= end {
1530        return None;
1531    }
1532
1533    if let Some((literal_start, literal_end, next_offset)) =
1534        js_string_literal_span(source, start, end)
1535        && trim_js_expression(source, next_offset, end).0 >= end
1536    {
1537        return Some(source_class_value_from_js_literal(
1538            source,
1539            literal_start,
1540            literal_end,
1541            source.as_bytes().get(start).copied() == Some(b'`'),
1542        ));
1543    }
1544
1545    if source.as_bytes().get(start) == Some(&b'{')
1546        && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1547    {
1548        let (value, _) =
1549            source_class_value_from_object_literal(source, start, end, local_class_values);
1550        return Some(value);
1551    }
1552
1553    if source.as_bytes().get(start) == Some(&b'[')
1554        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1555    {
1556        let mut value = SourceClassValue::default();
1557        for (element_start, element_end) in
1558            split_top_level_js_segments(source, start + 1, end - 1, b',')
1559        {
1560            let element_start = skip_js_trivia_until(source, element_start, element_end);
1561            let element_start = if source[element_start..element_end].starts_with("...") {
1562                element_start + 3
1563            } else {
1564                element_start
1565            };
1566            if let Some(element_value) = source_class_value_from_js_expression(
1567                source,
1568                element_start,
1569                element_end,
1570                local_class_values,
1571            ) {
1572                value.merge(element_value);
1573            }
1574        }
1575        return Some(value);
1576    }
1577
1578    if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1579    {
1580        let mut value = SourceClassValue::default();
1581        for (argument_start, argument_end) in
1582            split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1583        {
1584            if let Some(argument_value) = source_class_value_from_js_expression(
1585                source,
1586                argument_start,
1587                argument_end,
1588                local_class_values,
1589            ) {
1590                value.merge(argument_value);
1591            }
1592        }
1593        return Some(value);
1594    }
1595
1596    if let Some((_, true_start, true_end, false_start, false_end)) =
1597        top_level_conditional_parts(source, start, end)
1598    {
1599        let mut value = SourceClassValue::default();
1600        if let Some(true_value) =
1601            source_class_value_from_js_expression(source, true_start, true_end, local_class_values)
1602        {
1603            value.merge(true_value);
1604        }
1605        if let Some(false_value) = source_class_value_from_js_expression(
1606            source,
1607            false_start,
1608            false_end,
1609            local_class_values,
1610        ) {
1611            value.merge(false_value);
1612        }
1613        return Some(value);
1614    }
1615
1616    if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1617        .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1618    {
1619        return source_class_value_from_js_expression(
1620            source,
1621            operator_offset + 2,
1622            end,
1623            local_class_values,
1624        );
1625    }
1626
1627    if let Some(path) = js_expression_path(source, start, end)
1628        && let Some(value) = local_class_values.get(path.as_str())
1629    {
1630        return Some(value.clone());
1631    }
1632
1633    static_string_prefix_for_js_expression(source, start, end, local_class_values).map(|prefix| {
1634        let mut value = SourceClassValue::default();
1635        if !prefix.is_empty() {
1636            value.prefixes.push(prefix);
1637        }
1638        value
1639    })
1640}
1641
1642fn source_class_value_from_js_literal(
1643    source: &str,
1644    literal_start: usize,
1645    literal_end: usize,
1646    is_template: bool,
1647) -> SourceClassValue {
1648    let mut value = SourceClassValue::default();
1649    if is_template
1650        && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
1651    {
1652        let prefix_end = literal_start + relative_interpolation;
1653        push_template_prefix_value(source, literal_start, prefix_end, &mut value);
1654    } else {
1655        value
1656            .exact
1657            .extend(class_token_strings(source, literal_start, literal_end));
1658    }
1659    value.canonicalize();
1660    value
1661}
1662
1663fn source_class_value_from_object_literal(
1664    source: &str,
1665    start: usize,
1666    end: usize,
1667    local_class_values: &BTreeMap<String, SourceClassValue>,
1668) -> (SourceClassValue, BTreeMap<String, SourceClassValue>) {
1669    let (start, end) = trim_js_expression(source, start, end);
1670    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1671    let mut object_value = SourceClassValue::default();
1672    let mut property_values = BTreeMap::new();
1673    if source.as_bytes().get(start) != Some(&b'{')
1674        || matching_js_block_end(source, start, b'{', b'}') != Some(end.saturating_sub(1))
1675    {
1676        return (object_value, property_values);
1677    }
1678
1679    for (property_start, property_end) in
1680        split_top_level_js_segments(source, start + 1, end - 1, b',')
1681    {
1682        let (property_start, property_end) =
1683            trim_js_expression(source, property_start, property_end);
1684        if property_start >= property_end {
1685            continue;
1686        }
1687        if source[property_start..property_end].starts_with("...") {
1688            if let Some(spread_value) = source_class_value_from_js_expression(
1689                source,
1690                property_start + 3,
1691                property_end,
1692                local_class_values,
1693            ) {
1694                object_value.merge(spread_value);
1695            }
1696            continue;
1697        }
1698        let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1699        let key_end = colon.unwrap_or(property_end);
1700        let key_value =
1701            source_class_value_from_object_key(source, property_start, key_end, local_class_values);
1702        object_value.merge(key_value.clone());
1703        if let Some(property_name) = object_property_name(source, property_start, key_end)
1704            && let Some(property_value) = colon
1705                .and_then(|colon| {
1706                    source_class_value_from_js_expression(
1707                        source,
1708                        colon + 1,
1709                        property_end,
1710                        local_class_values,
1711                    )
1712                })
1713                .filter(|value| !value.is_empty())
1714        {
1715            property_values.insert(property_name, property_value);
1716        }
1717    }
1718    object_value.canonicalize();
1719    (object_value, property_values)
1720}
1721
1722fn collect_object_literal_selector_references(
1723    source: &str,
1724    start: usize,
1725    end: usize,
1726    target_style_uri: Option<&str>,
1727    local_class_values: &BTreeMap<String, SourceClassValue>,
1728    references: &mut Vec<SourceSelectorReferenceFactV0>,
1729    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1730) {
1731    for (property_start, property_end) in
1732        split_top_level_js_segments(source, start + 1, end - 1, b',')
1733    {
1734        let (property_start, property_end) =
1735            trim_js_expression(source, property_start, property_end);
1736        if property_start >= property_end {
1737            continue;
1738        }
1739        if source[property_start..property_end].starts_with("...") {
1740            collect_selector_references_from_js_expression(
1741                source,
1742                property_start + 3,
1743                property_end,
1744                target_style_uri,
1745                local_class_values,
1746                references,
1747                type_fact_targets,
1748            );
1749            continue;
1750        }
1751        let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1752        let key_end = colon.unwrap_or(property_end);
1753        collect_selector_references_from_object_key(
1754            source,
1755            property_start,
1756            key_end,
1757            target_style_uri,
1758            local_class_values,
1759            references,
1760            type_fact_targets,
1761        );
1762    }
1763}
1764
1765fn class_utility_call_arguments(source: &str, start: usize, end: usize) -> Option<(usize, usize)> {
1766    let (callee, callee_end) = read_js_identifier(source, start)?;
1767    if !is_class_utility_callee(callee) {
1768        return None;
1769    }
1770    let open_paren = skip_js_trivia_until(source, callee_end, end);
1771    if source.as_bytes().get(open_paren) != Some(&b'(') {
1772        return None;
1773    }
1774    let call_end = js_call_end(source, open_paren)?;
1775    if call_end > end || trim_js_expression(source, call_end + 1, end).0 < end {
1776        return None;
1777    }
1778    Some((open_paren + 1, call_end))
1779}
1780
1781fn is_class_utility_callee(callee: &str) -> bool {
1782    matches!(callee, "classnames" | "classNames" | "clsx" | "cn")
1783}
1784
1785fn collect_selector_references_from_object_key(
1786    source: &str,
1787    start: usize,
1788    end: usize,
1789    target_style_uri: Option<&str>,
1790    local_class_values: &BTreeMap<String, SourceClassValue>,
1791    references: &mut Vec<SourceSelectorReferenceFactV0>,
1792    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1793) {
1794    let (start, end) = trim_js_expression(source, start, end);
1795    if start >= end {
1796        return;
1797    }
1798    if source.as_bytes().get(start) == Some(&b'[')
1799        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1800    {
1801        collect_selector_references_from_js_expression(
1802            source,
1803            start + 1,
1804            end - 1,
1805            target_style_uri,
1806            local_class_values,
1807            references,
1808            type_fact_targets,
1809        );
1810        return;
1811    }
1812    if let Some((literal_start, literal_end, next_offset)) =
1813        js_string_literal_span(source, start, end)
1814        && trim_js_expression(source, next_offset, end).0 >= end
1815    {
1816        push_js_literal_selector_references(
1817            source,
1818            literal_start,
1819            literal_end,
1820            source.as_bytes().get(start).copied() == Some(b'`'),
1821            target_style_uri,
1822            references,
1823        );
1824        if source.as_bytes().get(start).copied() == Some(b'`') {
1825            collect_template_type_fact_targets(
1826                source,
1827                literal_start,
1828                literal_end,
1829                target_style_uri,
1830                type_fact_targets,
1831            );
1832        }
1833        return;
1834    }
1835    if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1836        && trim_js_expression(source, identifier_end, end).0 >= end
1837    {
1838        push_selector_reference(
1839            ParserByteSpanV0 { start, end },
1840            Some(identifier.to_string()),
1841            SourceSelectorReferenceMatchKindV0::Exact,
1842            target_style_uri,
1843            references,
1844        );
1845    }
1846}
1847
1848fn source_class_value_from_object_key(
1849    source: &str,
1850    start: usize,
1851    end: usize,
1852    local_class_values: &BTreeMap<String, SourceClassValue>,
1853) -> SourceClassValue {
1854    let (start, end) = trim_js_expression(source, start, end);
1855    if start >= end {
1856        return SourceClassValue::default();
1857    }
1858    if source.as_bytes().get(start) == Some(&b'[')
1859        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1860    {
1861        return source_class_value_from_js_expression(
1862            source,
1863            start + 1,
1864            end - 1,
1865            local_class_values,
1866        )
1867        .unwrap_or_default();
1868    }
1869    if let Some((literal_start, literal_end, next_offset)) =
1870        js_string_literal_span(source, start, end)
1871        && trim_js_expression(source, next_offset, end).0 >= end
1872    {
1873        return source_class_value_from_js_literal(
1874            source,
1875            literal_start,
1876            literal_end,
1877            source.as_bytes().get(start).copied() == Some(b'`'),
1878        );
1879    }
1880    if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1881        && trim_js_expression(source, identifier_end, end).0 >= end
1882    {
1883        let mut value = SourceClassValue::default();
1884        value.exact.push(identifier.to_string());
1885        return value;
1886    }
1887    SourceClassValue::default()
1888}
1889
1890fn object_property_name(source: &str, start: usize, end: usize) -> Option<String> {
1891    let (start, end) = trim_js_expression(source, start, end);
1892    if let Some((literal_start, literal_end, next_offset)) =
1893        js_string_literal_span(source, start, end)
1894        && trim_js_expression(source, next_offset, end).0 >= end
1895    {
1896        return source.get(literal_start..literal_end).map(str::to_string);
1897    }
1898    let (identifier, identifier_end) = read_js_identifier(source, start)?;
1899    (trim_js_expression(source, identifier_end, end).0 >= end).then(|| identifier.to_string())
1900}
1901
1902fn push_source_class_value_reference(
1903    byte_span: ParserByteSpanV0,
1904    value: SourceClassValue,
1905    target_style_uri: Option<&str>,
1906    references: &mut Vec<SourceSelectorReferenceFactV0>,
1907) {
1908    for selector_name in value.exact {
1909        push_selector_reference(
1910            byte_span,
1911            Some(selector_name),
1912            SourceSelectorReferenceMatchKindV0::Exact,
1913            target_style_uri,
1914            references,
1915        );
1916    }
1917    for prefix in value.prefixes {
1918        push_selector_reference(
1919            byte_span,
1920            Some(prefix),
1921            SourceSelectorReferenceMatchKindV0::Prefix,
1922            target_style_uri,
1923            references,
1924        );
1925    }
1926}
1927
1928fn collect_template_type_fact_targets(
1929    source: &str,
1930    literal_start: usize,
1931    literal_end: usize,
1932    target_style_uri: Option<&str>,
1933    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1934) {
1935    let Some((prefix, expression_span, suffix)) =
1936        single_template_interpolation_projection(source, literal_start, literal_end)
1937    else {
1938        return;
1939    };
1940    let Some(path) = js_expression_path(source, expression_span.start, expression_span.end) else {
1941        return;
1942    };
1943    push_source_type_fact_target(
1944        expression_span,
1945        path.as_str(),
1946        target_style_uri,
1947        prefix.as_str(),
1948        suffix.as_str(),
1949        type_fact_targets,
1950    );
1951}
1952
1953fn single_template_interpolation_projection(
1954    source: &str,
1955    literal_start: usize,
1956    literal_end: usize,
1957) -> Option<(String, ParserByteSpanV0, String)> {
1958    let relative_open = source.get(literal_start..literal_end)?.find("${")?;
1959    let open = literal_start + relative_open;
1960    if source.get(open + 2..literal_end)?.contains("${") {
1961        return None;
1962    }
1963    let expression_start = open + 2;
1964    let close = matching_js_block_end(source, open + 1, b'{', b'}')?;
1965    if close > literal_end {
1966        return None;
1967    }
1968    let (expression_start, expression_end) = trim_js_expression(source, expression_start, close);
1969    if expression_start >= expression_end {
1970        return None;
1971    }
1972    let prefix_start = template_token_start(source, literal_start, open);
1973    let suffix_end = template_token_end(source, close + 1, literal_end);
1974    let prefix = source.get(prefix_start..open)?.to_string();
1975    let suffix = source.get(close + 1..suffix_end)?.to_string();
1976    if !prefix.chars().all(is_css_identifier_continue)
1977        || !suffix.chars().all(is_css_identifier_continue)
1978    {
1979        return None;
1980    }
1981    Some((
1982        prefix,
1983        ParserByteSpanV0 {
1984            start: expression_start,
1985            end: expression_end,
1986        },
1987        suffix,
1988    ))
1989}
1990
1991fn template_token_start(source: &str, literal_start: usize, prefix_end: usize) -> usize {
1992    source
1993        .get(literal_start..prefix_end)
1994        .and_then(|value| {
1995            value
1996                .char_indices()
1997                .rev()
1998                .find(|(_, ch)| ch.is_ascii_whitespace())
1999                .map(|(index, ch)| literal_start + index + ch.len_utf8())
2000        })
2001        .unwrap_or(literal_start)
2002}
2003
2004fn template_token_end(source: &str, suffix_start: usize, literal_end: usize) -> usize {
2005    source
2006        .get(suffix_start..literal_end)
2007        .and_then(|value| {
2008            value
2009                .char_indices()
2010                .find(|(_, ch)| ch.is_ascii_whitespace())
2011                .map(|(index, _)| suffix_start + index)
2012        })
2013        .unwrap_or(literal_end)
2014}
2015
2016fn push_source_type_fact_target(
2017    byte_span: ParserByteSpanV0,
2018    expression_path: &str,
2019    target_style_uri: Option<&str>,
2020    prefix: &str,
2021    suffix: &str,
2022    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
2023) {
2024    type_fact_targets.push(SourceTypeFactTargetV0 {
2025        byte_span,
2026        expression_id: source_type_fact_expression_id(expression_path, byte_span),
2027        target_style_uri: target_style_uri.map(ToString::to_string),
2028        prefix: prefix.to_string(),
2029        suffix: suffix.to_string(),
2030    });
2031}
2032
2033fn source_type_fact_expression_id(expression_path: &str, byte_span: ParserByteSpanV0) -> String {
2034    format!(
2035        "omena-bridge-source-type-fact:{expression_path}:{}:{}",
2036        byte_span.start, byte_span.end
2037    )
2038}
2039
2040fn push_selector_reference(
2041    byte_span: ParserByteSpanV0,
2042    selector_name: Option<String>,
2043    match_kind: SourceSelectorReferenceMatchKindV0,
2044    target_style_uri: Option<&str>,
2045    references: &mut Vec<SourceSelectorReferenceFactV0>,
2046) {
2047    references.push(SourceSelectorReferenceFactV0 {
2048        byte_span,
2049        selector_name,
2050        match_kind,
2051        target_style_uri: target_style_uri.map(ToString::to_string),
2052    });
2053}
2054
2055fn push_js_literal_selector_references(
2056    source: &str,
2057    literal_start: usize,
2058    literal_end: usize,
2059    is_template: bool,
2060    target_style_uri: Option<&str>,
2061    references: &mut Vec<SourceSelectorReferenceFactV0>,
2062) {
2063    if is_template
2064        && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2065    {
2066        push_template_prefix_selector_references(
2067            source,
2068            literal_start,
2069            literal_start + relative_interpolation,
2070            target_style_uri,
2071            references,
2072        );
2073        return;
2074    }
2075
2076    push_string_literal_selector_references(
2077        source,
2078        ParserByteSpanV0 {
2079            start: literal_start,
2080            end: literal_end,
2081        },
2082        target_style_uri.map(ToString::to_string),
2083        references,
2084    );
2085}
2086
2087fn push_template_prefix_selector_references(
2088    source: &str,
2089    literal_start: usize,
2090    prefix_end: usize,
2091    target_style_uri: Option<&str>,
2092    references: &mut Vec<SourceSelectorReferenceFactV0>,
2093) {
2094    let spans = class_token_byte_spans(source, literal_start, prefix_end);
2095    let prefix_ends_with_space = source[..prefix_end]
2096        .chars()
2097        .last()
2098        .is_none_or(char::is_whitespace);
2099    for (index, span) in spans.iter().enumerate() {
2100        let is_open_prefix = index + 1 == spans.len() && !prefix_ends_with_space;
2101        push_selector_reference(
2102            *span,
2103            Some(source[span.start..span.end].to_string()),
2104            if is_open_prefix {
2105                SourceSelectorReferenceMatchKindV0::Prefix
2106            } else {
2107                SourceSelectorReferenceMatchKindV0::Exact
2108            },
2109            target_style_uri,
2110            references,
2111        );
2112    }
2113}
2114
2115fn push_template_prefix_value(
2116    source: &str,
2117    literal_start: usize,
2118    prefix_end: usize,
2119    value: &mut SourceClassValue,
2120) {
2121    let spans = class_token_byte_spans(source, literal_start, prefix_end);
2122    let prefix_ends_with_space = source[..prefix_end]
2123        .chars()
2124        .last()
2125        .is_none_or(char::is_whitespace);
2126    for (index, span) in spans.iter().enumerate() {
2127        let token = source[span.start..span.end].to_string();
2128        if index + 1 == spans.len() && !prefix_ends_with_space {
2129            value.prefixes.push(token);
2130        } else {
2131            value.exact.push(token);
2132        }
2133    }
2134}
2135
2136fn class_token_strings(source: &str, literal_start: usize, literal_end: usize) -> Vec<String> {
2137    class_token_byte_spans(source, literal_start, literal_end)
2138        .into_iter()
2139        .map(|span| source[span.start..span.end].to_string())
2140        .collect()
2141}
2142
2143fn push_string_literal_selector_references(
2144    source: &str,
2145    literal_span: ParserByteSpanV0,
2146    target_style_uri: Option<String>,
2147    references: &mut Vec<SourceSelectorReferenceFactV0>,
2148) {
2149    for span in class_token_byte_spans(source, literal_span.start, literal_span.end) {
2150        references.push(SourceSelectorReferenceFactV0 {
2151            byte_span: span,
2152            selector_name: None,
2153            match_kind: SourceSelectorReferenceMatchKindV0::Exact,
2154            target_style_uri: target_style_uri.clone(),
2155        });
2156    }
2157}
2158
2159fn trim_js_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2160    let mut start = char_boundary_ceil(source, start);
2161    let mut end = char_boundary_floor(source, end);
2162    start = skip_js_trivia_until(source, start, end);
2163    while end > start
2164        && source
2165            .as_bytes()
2166            .get(end - 1)
2167            .is_some_and(u8::is_ascii_whitespace)
2168    {
2169        end -= 1;
2170    }
2171    (start, end)
2172}
2173
2174fn char_boundary_floor(source: &str, index: usize) -> usize {
2175    let mut index = index.min(source.len());
2176    while index > 0 && !source.is_char_boundary(index) {
2177        index -= 1;
2178    }
2179    index
2180}
2181
2182fn char_boundary_ceil(source: &str, index: usize) -> usize {
2183    let mut index = index.min(source.len());
2184    while index < source.len() && !source.is_char_boundary(index) {
2185        index += 1;
2186    }
2187    index
2188}
2189
2190fn advance_js_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
2191    let cursor = char_boundary_ceil(source, cursor);
2192    let limit = char_boundary_floor(source, limit);
2193    if cursor >= limit {
2194        return limit;
2195    }
2196    char_boundary_ceil(source, cursor + 1).min(limit)
2197}
2198
2199fn advance_js_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
2200    let after_slash = advance_js_scan_cursor(source, slash_offset, limit);
2201    advance_js_scan_cursor(source, after_slash, limit)
2202}
2203
2204fn unwrap_js_parenthesized_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2205    let mut current_start = start;
2206    let mut current_end = end;
2207    loop {
2208        let (trimmed_start, trimmed_end) = trim_js_expression(source, current_start, current_end);
2209        if source.as_bytes().get(trimmed_start) == Some(&b'(')
2210            && matching_js_block_end(source, trimmed_start, b'(', b')')
2211                == Some(trimmed_end.saturating_sub(1))
2212        {
2213            current_start = trimmed_start + 1;
2214            current_end = trimmed_end - 1;
2215            continue;
2216        }
2217        return (trimmed_start, trimmed_end);
2218    }
2219}
2220
2221fn js_statement_expression_end(source: &str, start: usize) -> usize {
2222    let mut cursor = char_boundary_ceil(source, start);
2223    let mut depth = 0usize;
2224    while cursor < source.len() {
2225        match source.as_bytes().get(cursor).copied() {
2226            Some(b'\'' | b'"' | b'`') => {
2227                cursor =
2228                    skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2229            }
2230            Some(b'(' | b'[' | b'{') => {
2231                depth += 1;
2232                cursor = advance_js_scan_cursor(source, cursor, source.len());
2233            }
2234            Some(b')' | b']' | b'}') => {
2235                depth = depth.saturating_sub(1);
2236                cursor = advance_js_scan_cursor(source, cursor, source.len());
2237            }
2238            Some(b';') if depth == 0 => return cursor,
2239            Some(b'\n') if depth == 0 => return cursor,
2240            Some(_) => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2241            None => break,
2242        }
2243    }
2244    source.len()
2245}
2246
2247fn matching_js_block_end(source: &str, open_offset: usize, open: u8, close: u8) -> Option<usize> {
2248    if source.as_bytes().get(open_offset) != Some(&open) {
2249        return None;
2250    }
2251    let mut cursor = advance_js_scan_cursor(source, open_offset, source.len());
2252    let mut depth = 1usize;
2253    while cursor < source.len() {
2254        match source.as_bytes().get(cursor).copied()? {
2255            b'\'' | b'"' | b'`' => {
2256                cursor = skip_js_string_literal(source, cursor, source.len())?;
2257            }
2258            byte if byte == open => {
2259                depth += 1;
2260                cursor = advance_js_scan_cursor(source, cursor, source.len());
2261            }
2262            byte if byte == close => {
2263                depth -= 1;
2264                if depth == 0 {
2265                    return Some(cursor);
2266                }
2267                cursor = advance_js_scan_cursor(source, cursor, source.len());
2268            }
2269            _ => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2270        }
2271    }
2272    None
2273}
2274
2275fn split_top_level_js_segments(
2276    source: &str,
2277    start: usize,
2278    end: usize,
2279    delimiter: u8,
2280) -> Vec<(usize, usize)> {
2281    let mut segments = Vec::new();
2282    let end = char_boundary_floor(source, end);
2283    let mut segment_start = char_boundary_ceil(source, start).min(end);
2284    let mut cursor = segment_start;
2285    let mut depth = 0usize;
2286    while cursor < end {
2287        match source.as_bytes().get(cursor).copied() {
2288            Some(b'\'' | b'"' | b'`') => {
2289                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2290            }
2291            Some(b'(' | b'[' | b'{') => {
2292                depth += 1;
2293                cursor = advance_js_scan_cursor(source, cursor, end);
2294            }
2295            Some(b')' | b']' | b'}') => {
2296                depth = depth.saturating_sub(1);
2297                cursor = advance_js_scan_cursor(source, cursor, end);
2298            }
2299            Some(byte) if byte == delimiter && depth == 0 => {
2300                segments.push((segment_start, cursor));
2301                cursor = advance_js_scan_cursor(source, cursor, end);
2302                segment_start = cursor;
2303            }
2304            Some(_) => cursor = advance_js_scan_cursor(source, cursor, end),
2305            None => break,
2306        }
2307    }
2308    if segment_start <= end {
2309        segments.push((segment_start, end));
2310    }
2311    segments
2312}
2313
2314fn find_top_level_js_byte(source: &str, start: usize, end: usize, needle: u8) -> Option<usize> {
2315    let end = char_boundary_floor(source, end);
2316    let mut cursor = char_boundary_ceil(source, start).min(end);
2317    let mut depth = 0usize;
2318    while cursor < end {
2319        match source.as_bytes().get(cursor).copied()? {
2320            b'\'' | b'"' | b'`' => {
2321                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2322            }
2323            b'(' | b'[' | b'{' => {
2324                depth += 1;
2325                cursor = advance_js_scan_cursor(source, cursor, end);
2326            }
2327            b')' | b']' | b'}' => {
2328                depth = depth.saturating_sub(1);
2329                cursor = advance_js_scan_cursor(source, cursor, end);
2330            }
2331            byte if byte == needle && depth == 0 => return Some(cursor),
2332            _ => cursor = advance_js_scan_cursor(source, cursor, end),
2333        }
2334    }
2335    None
2336}
2337
2338fn find_top_level_js_operator(
2339    source: &str,
2340    start: usize,
2341    end: usize,
2342    operator: &str,
2343) -> Option<usize> {
2344    let end = char_boundary_floor(source, end);
2345    let mut cursor = char_boundary_ceil(source, start).min(end);
2346    let mut depth = 0usize;
2347    while cursor < end {
2348        match source.as_bytes().get(cursor).copied()? {
2349            b'\'' | b'"' | b'`' => {
2350                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2351            }
2352            b'(' | b'[' | b'{' => {
2353                depth += 1;
2354                cursor = advance_js_scan_cursor(source, cursor, end);
2355            }
2356            b')' | b']' | b'}' => {
2357                depth = depth.saturating_sub(1);
2358                cursor = advance_js_scan_cursor(source, cursor, end);
2359            }
2360            _ if depth == 0
2361                && source
2362                    .get(cursor..end)
2363                    .is_some_and(|rest| rest.starts_with(operator)) =>
2364            {
2365                return Some(cursor);
2366            }
2367            _ => cursor = advance_js_scan_cursor(source, cursor, end),
2368        }
2369    }
2370    None
2371}
2372
2373fn top_level_conditional_parts(
2374    source: &str,
2375    start: usize,
2376    end: usize,
2377) -> Option<(usize, usize, usize, usize, usize)> {
2378    let question = find_top_level_js_byte(source, start, end, b'?')?;
2379    let end = char_boundary_floor(source, end);
2380    let mut cursor = advance_js_scan_cursor(source, question, end);
2381    let mut depth = 0usize;
2382    let mut nested_conditional_depth = 0usize;
2383    while cursor < end {
2384        match source.as_bytes().get(cursor).copied()? {
2385            b'\'' | b'"' | b'`' => {
2386                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2387            }
2388            b'(' | b'[' | b'{' => {
2389                depth += 1;
2390                cursor = advance_js_scan_cursor(source, cursor, end);
2391            }
2392            b')' | b']' | b'}' => {
2393                depth = depth.saturating_sub(1);
2394                cursor = advance_js_scan_cursor(source, cursor, end);
2395            }
2396            b'?' if depth == 0 => {
2397                nested_conditional_depth += 1;
2398                cursor = advance_js_scan_cursor(source, cursor, end);
2399            }
2400            b':' if depth == 0 && nested_conditional_depth == 0 => {
2401                return Some((
2402                    question,
2403                    advance_js_scan_cursor(source, question, end),
2404                    cursor,
2405                    advance_js_scan_cursor(source, cursor, end),
2406                    end,
2407                ));
2408            }
2409            b':' if depth == 0 => {
2410                nested_conditional_depth = nested_conditional_depth.saturating_sub(1);
2411                cursor = advance_js_scan_cursor(source, cursor, end);
2412            }
2413            _ => cursor = advance_js_scan_cursor(source, cursor, end),
2414        }
2415    }
2416    None
2417}
2418
2419fn js_expression_path(source: &str, start: usize, end: usize) -> Option<String> {
2420    let (start, end) = trim_js_expression(source, start, end);
2421    let (first, mut cursor) = read_js_identifier(source, start)?;
2422    let mut path = vec![first.to_string()];
2423    loop {
2424        cursor = skip_js_trivia_until(source, cursor, end);
2425        match source.as_bytes().get(cursor).copied() {
2426            Some(b'.') => {
2427                let member_start = skip_js_trivia_until(source, cursor + 1, end);
2428                let (member, member_end) = read_js_identifier(source, member_start)?;
2429                path.push(member.to_string());
2430                cursor = member_end;
2431            }
2432            Some(b'[') => {
2433                if let Some((literal_start, literal_end, bracket_end)) =
2434                    bracket_string_literal_access(source, cursor)
2435                    && bracket_end <= end
2436                {
2437                    path.push(source[literal_start..literal_end].to_string());
2438                    cursor = bracket_end;
2439                } else {
2440                    return None;
2441                }
2442            }
2443            _ => break,
2444        }
2445    }
2446    (trim_js_expression(source, cursor, end).0 >= end).then(|| path.join("."))
2447}
2448
2449fn static_string_prefix_for_js_expression(
2450    source: &str,
2451    start: usize,
2452    end: usize,
2453    local_class_values: &BTreeMap<String, SourceClassValue>,
2454) -> Option<String> {
2455    let (start, end) = trim_js_expression(source, start, end);
2456    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
2457    if let Some((literal_start, literal_end, next_offset)) =
2458        js_string_literal_span(source, start, end)
2459        && trim_js_expression(source, next_offset, end).0 >= end
2460    {
2461        if source.as_bytes().get(start).copied() == Some(b'`')
2462            && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2463        {
2464            return Some(source[literal_start..literal_start + relative_interpolation].to_string());
2465        }
2466        return Some(source[literal_start..literal_end].to_string());
2467    }
2468    if let Some(path) = js_expression_path(source, start, end)
2469        && let Some(value) = local_class_values.get(path.as_str())
2470    {
2471        if value.exact.len() == 1 && value.prefixes.is_empty() {
2472            return value.exact.first().cloned();
2473        }
2474        if value.prefixes.len() == 1 && value.exact.is_empty() {
2475            return value.prefixes.first().cloned();
2476        }
2477    }
2478    if let Some(plus_offset) = find_top_level_js_operator(source, start, end, "+") {
2479        let left =
2480            static_string_prefix_for_js_expression(source, start, plus_offset, local_class_values)?;
2481        let right = static_string_prefix_for_js_expression(
2482            source,
2483            plus_offset + 1,
2484            end,
2485            local_class_values,
2486        )
2487        .unwrap_or_default();
2488        return Some(format!("{left}{right}"));
2489    }
2490    None
2491}
2492
2493fn js_call_end(source: &str, open_paren: usize) -> Option<usize> {
2494    if source.as_bytes().get(open_paren) != Some(&b'(') {
2495        return None;
2496    }
2497    let mut cursor = advance_js_scan_cursor(source, open_paren, source.len());
2498    let mut depth = 1usize;
2499    while cursor < source.len() {
2500        match source.as_bytes().get(cursor).copied()? {
2501            b'\'' | b'"' | b'`' => {
2502                cursor = skip_js_string_literal(source, cursor, source.len())?;
2503            }
2504            b'(' => {
2505                depth += 1;
2506                cursor = advance_js_scan_cursor(source, cursor, source.len());
2507            }
2508            b')' => {
2509                depth -= 1;
2510                if depth == 0 {
2511                    return Some(cursor);
2512                }
2513                cursor = advance_js_scan_cursor(source, cursor, source.len());
2514            }
2515            _ => {
2516                cursor = advance_js_scan_cursor(source, cursor, source.len());
2517            }
2518        }
2519    }
2520    None
2521}
2522
2523fn class_token_byte_spans(
2524    source: &str,
2525    literal_start: usize,
2526    literal_end: usize,
2527) -> Vec<ParserByteSpanV0> {
2528    let mut spans = Vec::new();
2529    let mut token_start: Option<usize> = None;
2530    for (relative_index, ch) in source[literal_start..literal_end].char_indices() {
2531        let index = literal_start + relative_index;
2532        if ch.is_ascii_whitespace() {
2533            if let Some(start) = token_start.take() {
2534                push_class_token_span(source, start, index, &mut spans);
2535            }
2536        } else if token_start.is_none() {
2537            token_start = Some(index);
2538        }
2539    }
2540    if let Some(start) = token_start {
2541        push_class_token_span(source, start, literal_end, &mut spans);
2542    }
2543    spans
2544}
2545
2546fn push_class_token_span(
2547    source: &str,
2548    start: usize,
2549    end: usize,
2550    spans: &mut Vec<ParserByteSpanV0>,
2551) {
2552    if start < end && source[start..end].chars().all(is_css_identifier_continue) {
2553        spans.push(ParserByteSpanV0 { start, end });
2554    }
2555}
2556
2557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2558struct CodeIdentifier<'a> {
2559    text: &'a str,
2560    end: usize,
2561}
2562
2563fn next_code_identifier(source: &str, mut cursor: usize) -> Option<CodeIdentifier<'_>> {
2564    while cursor < source.len() {
2565        cursor = skip_js_trivia(source, cursor);
2566        let byte = source.as_bytes().get(cursor).copied()?;
2567        if matches!(byte, b'\'' | b'"' | b'`') {
2568            cursor = skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2569            continue;
2570        }
2571        if byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$') {
2572            let (text, end) = read_js_identifier(source, cursor)?;
2573            return Some(CodeIdentifier { text, end });
2574        }
2575        cursor = advance_js_scan_cursor(source, cursor, source.len());
2576    }
2577    None
2578}
2579
2580fn skip_js_trivia(source: &str, cursor: usize) -> usize {
2581    skip_js_trivia_until(source, cursor, source.len())
2582}
2583
2584fn skip_js_trivia_until(source: &str, mut cursor: usize, limit: usize) -> usize {
2585    loop {
2586        cursor = skip_ascii_whitespace_until(source, cursor, limit);
2587        if source.as_bytes().get(cursor) == Some(&b'/') {
2588            match source.as_bytes().get(cursor + 1).copied() {
2589                Some(b'/') => {
2590                    cursor = skip_js_line_comment(source, cursor + 2, limit);
2591                    continue;
2592                }
2593                Some(b'*') => {
2594                    cursor = skip_js_block_comment(source, cursor + 2, limit);
2595                    continue;
2596                }
2597                _ => {}
2598            }
2599        }
2600        return cursor;
2601    }
2602}
2603
2604fn skip_ascii_whitespace_until(source: &str, mut offset: usize, limit: usize) -> usize {
2605    while offset < limit
2606        && source
2607            .as_bytes()
2608            .get(offset)
2609            .is_some_and(u8::is_ascii_whitespace)
2610    {
2611        offset += 1;
2612    }
2613    offset
2614}
2615
2616fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
2617    while source
2618        .as_bytes()
2619        .get(offset)
2620        .is_some_and(u8::is_ascii_whitespace)
2621    {
2622        offset += 1;
2623    }
2624    offset
2625}
2626
2627fn skip_js_line_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2628    let limit = char_boundary_floor(source, limit);
2629    while cursor < limit {
2630        if source.as_bytes().get(cursor) == Some(&b'\n') {
2631            return advance_js_scan_cursor(source, cursor, limit);
2632        }
2633        cursor = advance_js_scan_cursor(source, cursor, limit);
2634    }
2635    limit
2636}
2637
2638fn skip_js_block_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2639    let limit = char_boundary_floor(source, limit);
2640    while cursor + 1 < limit {
2641        if source.as_bytes().get(cursor) == Some(&b'*')
2642            && source.as_bytes().get(cursor + 1) == Some(&b'/')
2643        {
2644            return cursor + 2;
2645        }
2646        cursor = advance_js_scan_cursor(source, cursor, limit);
2647    }
2648    limit
2649}
2650
2651fn js_string_literal_span(
2652    source: &str,
2653    quote_offset: usize,
2654    limit: usize,
2655) -> Option<(usize, usize, usize)> {
2656    let quote = source.as_bytes().get(quote_offset).copied()?;
2657    if !matches!(quote, b'\'' | b'"' | b'`') {
2658        return None;
2659    }
2660    let literal_start = quote_offset + 1;
2661    let next_offset = skip_js_string_literal(source, quote_offset, limit)?;
2662    Some((literal_start, next_offset - 1, next_offset))
2663}
2664
2665fn skip_js_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
2666    let quote = source.as_bytes().get(quote_offset).copied()?;
2667    let limit = char_boundary_floor(source, limit);
2668    let mut cursor = quote_offset + 1;
2669    while cursor < limit {
2670        let byte = source.as_bytes().get(cursor).copied()?;
2671        if byte == b'\\' {
2672            cursor = advance_js_escaped_char(source, cursor, limit);
2673            continue;
2674        }
2675        if byte == quote {
2676            return Some(cursor + 1);
2677        }
2678        cursor = advance_js_scan_cursor(source, cursor, limit);
2679    }
2680    None
2681}
2682
2683fn bracket_string_literal_access(
2684    source: &str,
2685    bracket_offset: usize,
2686) -> Option<(usize, usize, usize)> {
2687    if source.as_bytes().get(bracket_offset) != Some(&b'[') {
2688        return None;
2689    }
2690    let quote_offset = skip_ascii_whitespace(source, bracket_offset + 1);
2691    let quote = source.as_bytes().get(quote_offset).copied()?;
2692    if !matches!(quote, b'\'' | b'"') {
2693        return None;
2694    }
2695    let (literal_start, literal_end, literal_next) =
2696        js_string_literal_span(source, quote_offset, source.len())?;
2697    if literal_next > source.len() {
2698        return None;
2699    }
2700    let closing_bracket = skip_ascii_whitespace(source, literal_end + 1);
2701    if source.as_bytes().get(closing_bracket) != Some(&b']') {
2702        return None;
2703    }
2704    Some((literal_start, literal_end, closing_bracket + 1))
2705}
2706
2707fn read_js_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
2708    let start = char_boundary_ceil(source, start);
2709    let first = source.get(start..)?.chars().next()?;
2710    if !is_js_identifier_start(first) {
2711        return None;
2712    }
2713    let mut end = start + first.len_utf8();
2714    let scan_start = end;
2715    for (relative_index, ch) in source.get(scan_start..)?.char_indices() {
2716        if !is_js_identifier_continue(ch) {
2717            break;
2718        }
2719        end = scan_start + relative_index + ch.len_utf8();
2720    }
2721    Some((&source[start..end], end))
2722}
2723
2724fn is_js_identifier_start(ch: char) -> bool {
2725    ch.is_ascii_alphabetic() || matches!(ch, '_' | '$')
2726}
2727
2728fn is_js_identifier_continue(ch: char) -> bool {
2729    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')
2730}
2731
2732fn is_css_identifier_continue(ch: char) -> bool {
2733    ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
2734}
2735
2736#[cfg(test)]
2737mod tests;