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            // Every expression-kind default export (`export default <expr>`) delegates to
588            // `collect_expression`, which already descends arrow/function/parenthesized bodies and
589            // JSX. Previously only member/call kinds were matched and the catch-all silently
590            // dropped `export default () => <JSX/>` (and parenthesized/JSX/conditional forms),
591            // so their className usages were never collected -> unusedSelector false positives.
592            // Non-expression kinds (`TSInterfaceDeclaration`) yield `None` and are correctly ignored.
593            _ => {
594                if let Some(expression) = declaration.as_expression() {
595                    self.collect_expression(expression);
596                }
597            }
598        }
599    }
600
601    fn collect_for_statement_init(&mut self, init: &oxc_ast::ast::ForStatementInit<'a>) {
602        match init {
603            oxc_ast::ast::ForStatementInit::VariableDeclaration(declaration) => {
604                self.collect_variable_declaration(declaration);
605            }
606            oxc_ast::ast::ForStatementInit::StaticMemberExpression(member) => {
607                self.collect_static_member_expression(member);
608            }
609            oxc_ast::ast::ForStatementInit::ComputedMemberExpression(member) => {
610                self.collect_computed_member_expression(member);
611            }
612            oxc_ast::ast::ForStatementInit::CallExpression(expression) => {
613                self.collect_call_expression(expression);
614            }
615            _ => {}
616        }
617    }
618
619    fn collect_variable_declaration(
620        &mut self,
621        declaration: &oxc_ast::ast::VariableDeclaration<'a>,
622    ) {
623        for declarator in &declaration.declarations {
624            if let Some(binding) = self.classnames_bind_utility_binding_from_declarator(declarator)
625            {
626                self.classnames_bind_utility_bindings.push(binding);
627            }
628            if let Some(init) = &declarator.init {
629                self.collect_expression(init);
630            }
631        }
632    }
633
634    fn classnames_bind_utility_binding_from_declarator(
635        &self,
636        declarator: &VariableDeclarator<'a>,
637    ) -> Option<ClassnamesBindUtilityBinding> {
638        if self.style_targets.is_empty() || self.classnames_bind_imports.is_empty() {
639            return None;
640        }
641        let binding = binding_pattern_identifier_name(&declarator.id)?;
642        let init = declarator.init.as_ref()?;
643        let Expression::CallExpression(call) = init else {
644            return None;
645        };
646        let Expression::StaticMemberExpression(callee) = &call.callee else {
647            return None;
648        };
649        if callee.property.name.as_str() != "bind" {
650            return None;
651        }
652        let callee_binding = expression_identifier_name(&callee.object)?;
653        if !self
654            .classnames_bind_imports
655            .iter()
656            .any(|import_binding| import_binding == callee_binding)
657        {
658            return None;
659        }
660        let style_binding = call.arguments.first().and_then(argument_identifier_name)?;
661        let style_uri = self
662            .style_targets
663            .iter()
664            .find(|target| target.binding == style_binding)?
665            .target_style_uri
666            .clone()?;
667
668        Some(ClassnamesBindUtilityBinding {
669            binding: binding.to_string(),
670            style_uri,
671        })
672    }
673
674    fn collect_function_body(&mut self, body: Option<&oxc_ast::ast::FunctionBody<'a>>) {
675        let Some(body) = body else {
676            return;
677        };
678        for statement in &body.statements {
679            self.collect_statement(statement);
680        }
681    }
682
683    fn collect_class(&mut self, class: &Class<'a>) {
684        if let Some(super_class) = &class.super_class {
685            self.collect_expression(super_class);
686        }
687        for element in &class.body.body {
688            match element {
689                ClassElement::MethodDefinition(method) => {
690                    self.collect_function_body(method.value.body.as_deref());
691                }
692                ClassElement::PropertyDefinition(property) => {
693                    if property.computed {
694                        self.collect_property_key(&property.key);
695                    }
696                    if let Some(value) = &property.value {
697                        self.collect_expression(value);
698                    }
699                }
700                ClassElement::AccessorProperty(property) => {
701                    if property.computed {
702                        self.collect_property_key(&property.key);
703                    }
704                    if let Some(value) = &property.value {
705                        self.collect_expression(value);
706                    }
707                }
708                ClassElement::StaticBlock(block) => {
709                    for statement in &block.body {
710                        self.collect_statement(statement);
711                    }
712                }
713                ClassElement::TSIndexSignature(_) => {}
714            }
715        }
716    }
717
718    fn collect_expression(&mut self, expression: &Expression<'a>) {
719        match expression {
720            Expression::StaticMemberExpression(member) => {
721                self.collect_static_member_expression(member);
722            }
723            Expression::ComputedMemberExpression(member) => {
724                self.collect_computed_member_expression(member);
725            }
726            Expression::PrivateFieldExpression(member) => {
727                self.collect_expression(&member.object);
728            }
729            Expression::ArrayExpression(expression) => {
730                self.collect_array_expression(expression);
731            }
732            Expression::ObjectExpression(expression) => {
733                self.collect_object_expression(expression);
734            }
735            Expression::CallExpression(expression) => {
736                self.collect_call_expression(expression);
737            }
738            Expression::NewExpression(expression) => {
739                self.collect_expression(&expression.callee);
740                for argument in &expression.arguments {
741                    self.collect_argument(argument);
742                }
743            }
744            Expression::ChainExpression(expression) => {
745                self.collect_chain_element(&expression.expression);
746            }
747            Expression::ConditionalExpression(expression) => {
748                self.collect_conditional_expression(expression);
749            }
750            Expression::BinaryExpression(expression) => {
751                self.collect_expression(&expression.left);
752                self.collect_expression(&expression.right);
753            }
754            Expression::LogicalExpression(expression) => {
755                self.collect_logical_expression(expression);
756            }
757            Expression::AssignmentExpression(expression) => {
758                self.collect_expression(&expression.right);
759            }
760            Expression::SequenceExpression(expression) => {
761                for expression in &expression.expressions {
762                    self.collect_expression(expression);
763                }
764            }
765            Expression::ParenthesizedExpression(expression) => {
766                self.collect_parenthesized_expression(expression);
767            }
768            Expression::UnaryExpression(expression) => {
769                self.collect_expression(&expression.argument);
770            }
771            Expression::AwaitExpression(expression) => {
772                self.collect_expression(&expression.argument);
773            }
774            Expression::TemplateLiteral(expression) => {
775                for expression in &expression.expressions {
776                    self.collect_expression(expression);
777                }
778            }
779            Expression::TaggedTemplateExpression(expression) => {
780                self.collect_expression(&expression.tag);
781                for expression in &expression.quasi.expressions {
782                    self.collect_expression(expression);
783                }
784            }
785            Expression::ArrowFunctionExpression(expression) => {
786                self.collect_function_body(Some(&expression.body));
787            }
788            Expression::FunctionExpression(expression) => {
789                self.collect_function_body(expression.body.as_deref());
790            }
791            Expression::ClassExpression(class) => {
792                self.collect_class(class);
793            }
794            Expression::ImportExpression(expression) => {
795                self.collect_expression(&expression.source);
796                if let Some(options) = &expression.options {
797                    self.collect_expression(options);
798                }
799            }
800            Expression::JSXElement(element) => {
801                self.collect_jsx_element(element);
802            }
803            Expression::JSXFragment(fragment) => {
804                for child in &fragment.children {
805                    self.collect_jsx_child(child);
806                }
807            }
808            Expression::TSAsExpression(expression) => {
809                self.collect_ts_as_expression(expression);
810            }
811            Expression::TSSatisfiesExpression(expression) => {
812                self.collect_ts_satisfies_expression(expression);
813            }
814            Expression::TSTypeAssertion(expression) => {
815                self.collect_expression(&expression.expression);
816            }
817            Expression::TSNonNullExpression(expression) => {
818                self.collect_ts_non_null_expression(expression);
819            }
820            Expression::TSInstantiationExpression(expression) => {
821                self.collect_expression(&expression.expression);
822            }
823            _ => {}
824        }
825    }
826
827    fn collect_array_expression_element(&mut self, element: &ArrayExpressionElement<'a>) {
828        match element {
829            ArrayExpressionElement::SpreadElement(spread) => {
830                self.collect_expression(&spread.argument);
831            }
832            ArrayExpressionElement::Elision(_) => {}
833            _ => {
834                if let Some(expression) = element.as_expression() {
835                    self.collect_expression(expression);
836                }
837            }
838        }
839    }
840
841    fn collect_argument(&mut self, argument: &Argument<'a>) {
842        match argument {
843            Argument::SpreadElement(spread) => {
844                self.collect_expression(&spread.argument);
845            }
846            _ => {
847                if let Some(expression) = argument.as_expression() {
848                    self.collect_expression(expression);
849                }
850            }
851        }
852    }
853
854    fn collect_chain_element(&mut self, element: &ChainElement<'a>) {
855        match element {
856            ChainElement::CallExpression(expression) => {
857                self.collect_expression(&expression.callee);
858                for argument in &expression.arguments {
859                    self.collect_argument(argument);
860                }
861            }
862            ChainElement::StaticMemberExpression(member) => {
863                self.collect_static_member_expression(member);
864            }
865            ChainElement::ComputedMemberExpression(member) => {
866                self.collect_computed_member_expression(member);
867            }
868            ChainElement::PrivateFieldExpression(member) => {
869                self.collect_expression(&member.object);
870            }
871            ChainElement::TSNonNullExpression(expression) => {
872                self.collect_expression(&expression.expression);
873            }
874        }
875    }
876
877    fn collect_property_key(&mut self, key: &oxc_ast::ast::PropertyKey<'a>) {
878        match key {
879            oxc_ast::ast::PropertyKey::StaticIdentifier(_)
880            | oxc_ast::ast::PropertyKey::PrivateIdentifier(_) => {}
881            _ => {
882                if let Some(expression) = key.as_expression() {
883                    self.collect_expression(expression);
884                }
885            }
886        }
887    }
888
889    fn collect_jsx_element(&mut self, element: &oxc_ast::ast::JSXElement<'a>) {
890        for attribute in &element.opening_element.attributes {
891            match attribute {
892                oxc_ast::ast::JSXAttributeItem::Attribute(attribute) => {
893                    if is_jsx_class_name_attribute(&attribute.name)
894                        && let Some(value) = &attribute.value
895                    {
896                        self.collect_class_name_string_literal_attribute(value);
897                        self.collect_class_name_expression_attribute(value);
898                    }
899                    if let Some(value) = &attribute.value {
900                        self.collect_jsx_attribute_value(value);
901                    }
902                }
903                oxc_ast::ast::JSXAttributeItem::SpreadAttribute(attribute) => {
904                    self.collect_expression(&attribute.argument);
905                }
906            }
907        }
908        for child in &element.children {
909            self.collect_jsx_child(child);
910        }
911    }
912
913    fn collect_jsx_attribute_value(&mut self, value: &JSXAttributeValue<'a>) {
914        match value {
915            JSXAttributeValue::ExpressionContainer(container) => {
916                self.collect_jsx_expression(&container.expression);
917            }
918            JSXAttributeValue::Element(element) => {
919                self.collect_jsx_element(element);
920            }
921            JSXAttributeValue::Fragment(fragment) => {
922                for child in &fragment.children {
923                    self.collect_jsx_child(child);
924                }
925            }
926            JSXAttributeValue::StringLiteral(_) => {}
927        }
928    }
929
930    fn collect_class_name_string_literal_attribute(&mut self, value: &JSXAttributeValue<'a>) {
931        let JSXAttributeValue::StringLiteral(literal) = value else {
932            return;
933        };
934        if let Some(span) = self.string_literal_content_span(literal.span) {
935            self.class_string_literals.push(span);
936        }
937    }
938
939    fn collect_class_name_expression_attribute(&mut self, value: &JSXAttributeValue<'a>) {
940        let JSXAttributeValue::ExpressionContainer(container) = value else {
941            return;
942        };
943        if let Some(span) = jsx_expression_span(&container.expression) {
944            self.class_name_expression_spans.push(span);
945        }
946    }
947
948    fn collect_jsx_child(&mut self, child: &JSXChild<'a>) {
949        match child {
950            JSXChild::Element(element) => {
951                self.collect_jsx_element(element);
952            }
953            JSXChild::Fragment(fragment) => {
954                for child in &fragment.children {
955                    self.collect_jsx_child(child);
956                }
957            }
958            JSXChild::ExpressionContainer(container) => {
959                self.collect_jsx_expression(&container.expression);
960            }
961            JSXChild::Spread(spread) => {
962                self.collect_expression(&spread.expression);
963            }
964            JSXChild::Text(_) => {}
965        }
966    }
967
968    fn collect_jsx_expression(&mut self, expression: &JSXExpression<'a>) {
969        match expression {
970            JSXExpression::StaticMemberExpression(member) => {
971                self.collect_static_member_expression(member);
972            }
973            JSXExpression::ComputedMemberExpression(member) => {
974                self.collect_computed_member_expression(member);
975            }
976            JSXExpression::CallExpression(expression) => {
977                self.collect_call_expression(expression);
978            }
979            JSXExpression::ConditionalExpression(expression) => {
980                self.collect_conditional_expression(expression);
981            }
982            JSXExpression::LogicalExpression(expression) => {
983                self.collect_logical_expression(expression);
984            }
985            JSXExpression::ArrayExpression(expression) => {
986                self.collect_array_expression(expression);
987            }
988            JSXExpression::ObjectExpression(expression) => {
989                self.collect_object_expression(expression);
990            }
991            JSXExpression::ParenthesizedExpression(expression) => {
992                self.collect_parenthesized_expression(expression);
993            }
994            JSXExpression::TSAsExpression(expression) => {
995                self.collect_ts_as_expression(expression);
996            }
997            JSXExpression::TSSatisfiesExpression(expression) => {
998                self.collect_ts_satisfies_expression(expression);
999            }
1000            JSXExpression::TSNonNullExpression(expression) => {
1001                self.collect_ts_non_null_expression(expression);
1002            }
1003            JSXExpression::JSXElement(element) => {
1004                self.collect_jsx_element(element);
1005            }
1006            JSXExpression::JSXFragment(fragment) => {
1007                for child in &fragment.children {
1008                    self.collect_jsx_child(child);
1009                }
1010            }
1011            _ => {}
1012        }
1013    }
1014
1015    fn collect_array_expression(&mut self, expression: &ArrayExpression<'a>) {
1016        for element in &expression.elements {
1017            self.collect_array_expression_element(element);
1018        }
1019    }
1020
1021    fn collect_object_expression(&mut self, expression: &ObjectExpression<'a>) {
1022        for property in &expression.properties {
1023            match property {
1024                ObjectPropertyKind::ObjectProperty(property) => {
1025                    if property.computed {
1026                        self.collect_property_key(&property.key);
1027                    }
1028                    self.collect_expression(&property.value);
1029                }
1030                ObjectPropertyKind::SpreadProperty(spread) => {
1031                    self.collect_expression(&spread.argument);
1032                }
1033            }
1034        }
1035    }
1036
1037    fn collect_call_expression(&mut self, expression: &CallExpression<'a>) {
1038        if let Some(binding) = expression_identifier_name(&expression.callee) {
1039            for argument in &expression.arguments {
1040                if let Some(byte_span) = argument_expression_span(argument) {
1041                    self.classnames_bind_call_arguments
1042                        .push(ClassnamesBindCallArgument {
1043                            binding: binding.to_string(),
1044                            byte_span,
1045                        });
1046                }
1047            }
1048        }
1049        self.collect_expression(&expression.callee);
1050        for argument in &expression.arguments {
1051            self.collect_argument(argument);
1052        }
1053    }
1054
1055    fn collect_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
1056        self.collect_expression(&expression.test);
1057        self.collect_expression(&expression.consequent);
1058        self.collect_expression(&expression.alternate);
1059    }
1060
1061    fn collect_logical_expression(&mut self, expression: &LogicalExpression<'a>) {
1062        self.collect_expression(&expression.left);
1063        self.collect_expression(&expression.right);
1064    }
1065
1066    fn collect_parenthesized_expression(&mut self, expression: &ParenthesizedExpression<'a>) {
1067        self.collect_expression(&expression.expression);
1068    }
1069
1070    fn collect_ts_as_expression(&mut self, expression: &TSAsExpression<'a>) {
1071        self.collect_expression(&expression.expression);
1072    }
1073
1074    fn collect_ts_satisfies_expression(&mut self, expression: &TSSatisfiesExpression<'a>) {
1075        self.collect_expression(&expression.expression);
1076    }
1077
1078    fn collect_ts_non_null_expression(&mut self, expression: &TSNonNullExpression<'a>) {
1079        self.collect_expression(&expression.expression);
1080    }
1081
1082    fn collect_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
1083        if let Some(target) = self.target_for_object(&member.object)
1084            && let Some(byte_span) = self.css_identifier_span(member.property.span)
1085        {
1086            self.style_property_accesses
1087                .push(SourceStylePropertyAccessFactV0 {
1088                    byte_span,
1089                    target_style_uri: target.target_style_uri.clone(),
1090                });
1091        }
1092        self.collect_expression(&member.object);
1093    }
1094
1095    fn collect_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
1096        if let Some(target) = self.target_for_object(&member.object)
1097            && let Some(byte_span) = self.static_string_expression_content_span(&member.expression)
1098        {
1099            self.style_property_accesses
1100                .push(SourceStylePropertyAccessFactV0 {
1101                    byte_span,
1102                    target_style_uri: target.target_style_uri.clone(),
1103                });
1104        }
1105        self.collect_expression(&member.object);
1106        self.collect_expression(&member.expression);
1107    }
1108
1109    fn target_for_object(&self, expression: &Expression<'a>) -> Option<&SourceStyleBindingTarget> {
1110        match expression {
1111            Expression::Identifier(identifier) => self
1112                .property_access_targets
1113                .iter()
1114                .find(|target| target.binding == identifier.name.as_str()),
1115            Expression::ParenthesizedExpression(expression) => {
1116                self.target_for_object(&expression.expression)
1117            }
1118            Expression::TSAsExpression(expression) => {
1119                self.target_for_object(&expression.expression)
1120            }
1121            Expression::TSSatisfiesExpression(expression) => {
1122                self.target_for_object(&expression.expression)
1123            }
1124            Expression::TSTypeAssertion(expression) => {
1125                self.target_for_object(&expression.expression)
1126            }
1127            Expression::TSNonNullExpression(expression) => {
1128                self.target_for_object(&expression.expression)
1129            }
1130            Expression::TSInstantiationExpression(expression) => {
1131                self.target_for_object(&expression.expression)
1132            }
1133            _ => None,
1134        }
1135    }
1136
1137    fn static_string_expression_content_span(
1138        &self,
1139        expression: &Expression<'a>,
1140    ) -> Option<ParserByteSpanV0> {
1141        match expression {
1142            Expression::StringLiteral(literal) => self.css_identifier_content_span(literal.span),
1143            Expression::TemplateLiteral(literal) if literal.expressions.is_empty() => {
1144                self.css_identifier_content_span(literal.span)
1145            }
1146            _ => None,
1147        }
1148    }
1149
1150    fn css_identifier_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1151        let span = parser_byte_span(span);
1152        let text = self.source.get(span.start..span.end)?;
1153        (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(span)
1154    }
1155
1156    fn css_identifier_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1157        let span = parser_byte_span(span);
1158        if span.end <= span.start + 1 {
1159            return None;
1160        }
1161        let content = ParserByteSpanV0 {
1162            start: span.start + 1,
1163            end: span.end - 1,
1164        };
1165        let text = self.source.get(content.start..content.end)?;
1166        (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(content)
1167    }
1168
1169    fn string_literal_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1170        let span = parser_byte_span(span);
1171        if span.end <= span.start + 1 {
1172            return None;
1173        }
1174        let content = ParserByteSpanV0 {
1175            start: span.start + 1,
1176            end: span.end - 1,
1177        };
1178        self.source.get(content.start..content.end)?;
1179        Some(content)
1180    }
1181
1182    fn canonicalize(&mut self) {
1183        self.class_string_literals.sort_by(|left, right| {
1184            left.start
1185                .cmp(&right.start)
1186                .then_with(|| left.end.cmp(&right.end))
1187        });
1188        self.class_string_literals.dedup();
1189        self.style_property_accesses.sort_by(|left, right| {
1190            left.byte_span
1191                .start
1192                .cmp(&right.byte_span.start)
1193                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1194                .then_with(|| left.target_style_uri.cmp(&right.target_style_uri))
1195        });
1196        self.style_property_accesses.dedup();
1197        self.classnames_bind_utility_bindings
1198            .sort_by(|left, right| {
1199                left.binding
1200                    .cmp(&right.binding)
1201                    .then_with(|| left.style_uri.cmp(&right.style_uri))
1202            });
1203        self.classnames_bind_utility_bindings
1204            .dedup_by(|left, right| {
1205                left.binding == right.binding && left.style_uri == right.style_uri
1206            });
1207        self.classnames_bind_call_arguments.sort_by(|left, right| {
1208            left.binding
1209                .cmp(&right.binding)
1210                .then_with(|| left.byte_span.start.cmp(&right.byte_span.start))
1211                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1212        });
1213        self.classnames_bind_call_arguments.dedup_by(|left, right| {
1214            left.binding == right.binding && left.byte_span == right.byte_span
1215        });
1216    }
1217}
1218
1219fn parser_byte_span(span: Span) -> ParserByteSpanV0 {
1220    ParserByteSpanV0 {
1221        start: span.start as usize,
1222        end: span.end as usize,
1223    }
1224}
1225
1226fn is_jsx_class_name_attribute(name: &JSXAttributeName<'_>) -> bool {
1227    matches!(name, JSXAttributeName::Identifier(identifier) if identifier.name.as_str() == "className")
1228}
1229
1230fn jsx_expression_span(expression: &JSXExpression<'_>) -> Option<ParserByteSpanV0> {
1231    match expression {
1232        JSXExpression::EmptyExpression(_) => None,
1233        _ => Some(parser_byte_span(expression.span())),
1234    }
1235}
1236
1237fn argument_expression_span(argument: &Argument<'_>) -> Option<ParserByteSpanV0> {
1238    match argument {
1239        Argument::SpreadElement(spread) => Some(parser_byte_span(spread.argument.span())),
1240        _ => Some(parser_byte_span(argument.span())),
1241    }
1242}
1243
1244fn binding_pattern_identifier_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
1245    match pattern {
1246        BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.as_str()),
1247        _ => None,
1248    }
1249}
1250
1251fn expression_identifier_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1252    match expression {
1253        Expression::Identifier(identifier) => Some(identifier.name.as_str()),
1254        Expression::ParenthesizedExpression(expression) => {
1255            expression_identifier_name(&expression.expression)
1256        }
1257        Expression::TSAsExpression(expression) => {
1258            expression_identifier_name(&expression.expression)
1259        }
1260        Expression::TSSatisfiesExpression(expression) => {
1261            expression_identifier_name(&expression.expression)
1262        }
1263        Expression::TSTypeAssertion(expression) => {
1264            expression_identifier_name(&expression.expression)
1265        }
1266        Expression::TSNonNullExpression(expression) => {
1267            expression_identifier_name(&expression.expression)
1268        }
1269        Expression::TSInstantiationExpression(expression) => {
1270            expression_identifier_name(&expression.expression)
1271        }
1272        _ => None,
1273    }
1274}
1275
1276fn argument_identifier_name<'a>(argument: &'a Argument<'a>) -> Option<&'a str> {
1277    match argument {
1278        Argument::Identifier(identifier) => Some(identifier.name.as_str()),
1279        Argument::ParenthesizedExpression(expression) => {
1280            expression_identifier_name(&expression.expression)
1281        }
1282        Argument::TSAsExpression(expression) => expression_identifier_name(&expression.expression),
1283        Argument::TSSatisfiesExpression(expression) => {
1284            expression_identifier_name(&expression.expression)
1285        }
1286        Argument::TSNonNullExpression(expression) => {
1287            expression_identifier_name(&expression.expression)
1288        }
1289        Argument::TSInstantiationExpression(expression) => {
1290            expression_identifier_name(&expression.expression)
1291        }
1292        _ => None,
1293    }
1294}
1295
1296fn collect_selector_references_from_js_expression(
1297    source: &str,
1298    start: usize,
1299    end: usize,
1300    target_style_uri: Option<&str>,
1301    local_class_values: &BTreeMap<String, SourceClassValue>,
1302    references: &mut Vec<SourceSelectorReferenceFactV0>,
1303    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1304) {
1305    let (start, end) = trim_js_expression(source, start, end);
1306    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1307    if start >= end {
1308        return;
1309    }
1310
1311    if let Some((literal_start, literal_end, next_offset)) =
1312        js_string_literal_span(source, start, end)
1313        && trim_js_expression(source, next_offset, end).0 >= end
1314    {
1315        push_js_literal_selector_references(
1316            source,
1317            literal_start,
1318            literal_end,
1319            source.as_bytes().get(start).copied() == Some(b'`'),
1320            target_style_uri,
1321            references,
1322        );
1323        if source.as_bytes().get(start).copied() == Some(b'`') {
1324            collect_template_type_fact_targets(
1325                source,
1326                literal_start,
1327                literal_end,
1328                target_style_uri,
1329                type_fact_targets,
1330            );
1331        }
1332        return;
1333    }
1334
1335    if source.as_bytes().get(start) == Some(&b'{')
1336        && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1337    {
1338        collect_object_literal_selector_references(
1339            source,
1340            start,
1341            end,
1342            target_style_uri,
1343            local_class_values,
1344            references,
1345            type_fact_targets,
1346        );
1347        return;
1348    }
1349
1350    if source.as_bytes().get(start) == Some(&b'[')
1351        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1352    {
1353        for (element_start, element_end) in
1354            split_top_level_js_segments(source, start + 1, end - 1, b',')
1355        {
1356            let element_start = skip_js_trivia_until(source, element_start, element_end);
1357            let element_start = if source[element_start..element_end].starts_with("...") {
1358                element_start + 3
1359            } else {
1360                element_start
1361            };
1362            collect_selector_references_from_js_expression(
1363                source,
1364                element_start,
1365                element_end,
1366                target_style_uri,
1367                local_class_values,
1368                references,
1369                type_fact_targets,
1370            );
1371        }
1372        return;
1373    }
1374
1375    if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1376    {
1377        for (argument_start, argument_end) in
1378            split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1379        {
1380            collect_selector_references_from_js_expression(
1381                source,
1382                argument_start,
1383                argument_end,
1384                target_style_uri,
1385                local_class_values,
1386                references,
1387                type_fact_targets,
1388            );
1389        }
1390        return;
1391    }
1392
1393    if let Some((_, true_start, true_end, false_start, false_end)) =
1394        top_level_conditional_parts(source, start, end)
1395    {
1396        collect_selector_references_from_js_expression(
1397            source,
1398            true_start,
1399            true_end,
1400            target_style_uri,
1401            local_class_values,
1402            references,
1403            type_fact_targets,
1404        );
1405        collect_selector_references_from_js_expression(
1406            source,
1407            false_start,
1408            false_end,
1409            target_style_uri,
1410            local_class_values,
1411            references,
1412            type_fact_targets,
1413        );
1414        return;
1415    }
1416
1417    if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1418        .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1419    {
1420        collect_selector_references_from_js_expression(
1421            source,
1422            operator_offset + 2,
1423            end,
1424            target_style_uri,
1425            local_class_values,
1426            references,
1427            type_fact_targets,
1428        );
1429        return;
1430    }
1431
1432    let expression_path = js_expression_path(source, start, end);
1433    if let Some(value) =
1434        source_class_value_from_js_expression(source, start, end, local_class_values)
1435        && !value.is_empty()
1436    {
1437        if let Some(path) = expression_path.as_deref() {
1438            push_source_type_fact_target(
1439                ParserByteSpanV0 { start, end },
1440                path,
1441                target_style_uri,
1442                "",
1443                "",
1444                type_fact_targets,
1445            );
1446        }
1447        push_source_class_value_reference(
1448            ParserByteSpanV0 { start, end },
1449            value,
1450            target_style_uri,
1451            references,
1452        );
1453        return;
1454    }
1455
1456    if let Some(prefix) =
1457        static_string_prefix_for_js_expression(source, start, end, local_class_values)
1458        && !prefix.is_empty()
1459    {
1460        push_selector_reference(
1461            ParserByteSpanV0 { start, end },
1462            Some(prefix),
1463            SourceSelectorReferenceMatchKindV0::Prefix,
1464            target_style_uri,
1465            references,
1466        );
1467        return;
1468    }
1469
1470    if let Some(path) = expression_path {
1471        push_source_type_fact_target(
1472            ParserByteSpanV0 { start, end },
1473            path.as_str(),
1474            target_style_uri,
1475            "",
1476            "",
1477            type_fact_targets,
1478        );
1479    }
1480}
1481
1482fn collect_local_class_value_bindings(source: &str) -> BTreeMap<String, SourceClassValue> {
1483    let mut values = BTreeMap::new();
1484    let mut cursor = 0usize;
1485    while let Some(keyword) = next_code_identifier(source, cursor) {
1486        cursor = keyword.end;
1487        if !matches!(keyword.text, "const" | "let" | "var") {
1488            continue;
1489        }
1490        let binding_start = skip_js_trivia(source, keyword.end);
1491        let Some((binding, binding_end)) = read_js_identifier(source, binding_start) else {
1492            continue;
1493        };
1494        let equals_offset = skip_js_trivia(source, binding_end);
1495        if source.as_bytes().get(equals_offset) != Some(&b'=') {
1496            continue;
1497        }
1498        let expression_start = skip_js_trivia(source, equals_offset + 1);
1499        let expression_end = js_statement_expression_end(source, expression_start);
1500        if let Some(value) =
1501            source_class_value_from_js_expression(source, expression_start, expression_end, &values)
1502            && !value.is_empty()
1503        {
1504            values.insert(binding.to_string(), value);
1505        }
1506        let (_, property_values) = source_class_value_from_object_literal(
1507            source,
1508            expression_start,
1509            expression_end,
1510            &values,
1511        );
1512        for (property, value) in property_values {
1513            if !value.is_empty() {
1514                values.insert(format!("{binding}.{property}"), value);
1515            }
1516        }
1517        cursor = expression_end.min(source.len());
1518    }
1519    values
1520}
1521
1522fn source_class_value_from_js_expression(
1523    source: &str,
1524    start: usize,
1525    end: usize,
1526    local_class_values: &BTreeMap<String, SourceClassValue>,
1527) -> Option<SourceClassValue> {
1528    let (start, end) = trim_js_expression(source, start, end);
1529    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1530    if start >= end {
1531        return None;
1532    }
1533
1534    if let Some((literal_start, literal_end, next_offset)) =
1535        js_string_literal_span(source, start, end)
1536        && trim_js_expression(source, next_offset, end).0 >= end
1537    {
1538        return Some(source_class_value_from_js_literal(
1539            source,
1540            literal_start,
1541            literal_end,
1542            source.as_bytes().get(start).copied() == Some(b'`'),
1543        ));
1544    }
1545
1546    if source.as_bytes().get(start) == Some(&b'{')
1547        && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1548    {
1549        let (value, _) =
1550            source_class_value_from_object_literal(source, start, end, local_class_values);
1551        return Some(value);
1552    }
1553
1554    if source.as_bytes().get(start) == Some(&b'[')
1555        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1556    {
1557        let mut value = SourceClassValue::default();
1558        for (element_start, element_end) in
1559            split_top_level_js_segments(source, start + 1, end - 1, b',')
1560        {
1561            let element_start = skip_js_trivia_until(source, element_start, element_end);
1562            let element_start = if source[element_start..element_end].starts_with("...") {
1563                element_start + 3
1564            } else {
1565                element_start
1566            };
1567            if let Some(element_value) = source_class_value_from_js_expression(
1568                source,
1569                element_start,
1570                element_end,
1571                local_class_values,
1572            ) {
1573                value.merge(element_value);
1574            }
1575        }
1576        return Some(value);
1577    }
1578
1579    if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1580    {
1581        let mut value = SourceClassValue::default();
1582        for (argument_start, argument_end) in
1583            split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1584        {
1585            if let Some(argument_value) = source_class_value_from_js_expression(
1586                source,
1587                argument_start,
1588                argument_end,
1589                local_class_values,
1590            ) {
1591                value.merge(argument_value);
1592            }
1593        }
1594        return Some(value);
1595    }
1596
1597    if let Some((_, true_start, true_end, false_start, false_end)) =
1598        top_level_conditional_parts(source, start, end)
1599    {
1600        let mut value = SourceClassValue::default();
1601        if let Some(true_value) =
1602            source_class_value_from_js_expression(source, true_start, true_end, local_class_values)
1603        {
1604            value.merge(true_value);
1605        }
1606        if let Some(false_value) = source_class_value_from_js_expression(
1607            source,
1608            false_start,
1609            false_end,
1610            local_class_values,
1611        ) {
1612            value.merge(false_value);
1613        }
1614        return Some(value);
1615    }
1616
1617    if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1618        .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1619    {
1620        return source_class_value_from_js_expression(
1621            source,
1622            operator_offset + 2,
1623            end,
1624            local_class_values,
1625        );
1626    }
1627
1628    if let Some(path) = js_expression_path(source, start, end)
1629        && let Some(value) = local_class_values.get(path.as_str())
1630    {
1631        return Some(value.clone());
1632    }
1633
1634    static_string_prefix_for_js_expression(source, start, end, local_class_values).map(|prefix| {
1635        let mut value = SourceClassValue::default();
1636        if !prefix.is_empty() {
1637            value.prefixes.push(prefix);
1638        }
1639        value
1640    })
1641}
1642
1643fn source_class_value_from_js_literal(
1644    source: &str,
1645    literal_start: usize,
1646    literal_end: usize,
1647    is_template: bool,
1648) -> SourceClassValue {
1649    let mut value = SourceClassValue::default();
1650    if is_template
1651        && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
1652    {
1653        let prefix_end = literal_start + relative_interpolation;
1654        push_template_prefix_value(source, literal_start, prefix_end, &mut value);
1655    } else {
1656        value
1657            .exact
1658            .extend(class_token_strings(source, literal_start, literal_end));
1659    }
1660    value.canonicalize();
1661    value
1662}
1663
1664fn source_class_value_from_object_literal(
1665    source: &str,
1666    start: usize,
1667    end: usize,
1668    local_class_values: &BTreeMap<String, SourceClassValue>,
1669) -> (SourceClassValue, BTreeMap<String, SourceClassValue>) {
1670    let (start, end) = trim_js_expression(source, start, end);
1671    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1672    let mut object_value = SourceClassValue::default();
1673    let mut property_values = BTreeMap::new();
1674    if source.as_bytes().get(start) != Some(&b'{')
1675        || matching_js_block_end(source, start, b'{', b'}') != Some(end.saturating_sub(1))
1676    {
1677        return (object_value, property_values);
1678    }
1679
1680    for (property_start, property_end) in
1681        split_top_level_js_segments(source, start + 1, end - 1, b',')
1682    {
1683        let (property_start, property_end) =
1684            trim_js_expression(source, property_start, property_end);
1685        if property_start >= property_end {
1686            continue;
1687        }
1688        if source[property_start..property_end].starts_with("...") {
1689            if let Some(spread_value) = source_class_value_from_js_expression(
1690                source,
1691                property_start + 3,
1692                property_end,
1693                local_class_values,
1694            ) {
1695                object_value.merge(spread_value);
1696            }
1697            continue;
1698        }
1699        let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1700        let key_end = colon.unwrap_or(property_end);
1701        let key_value =
1702            source_class_value_from_object_key(source, property_start, key_end, local_class_values);
1703        object_value.merge(key_value.clone());
1704        if let Some(property_name) = object_property_name(source, property_start, key_end)
1705            && let Some(property_value) = colon
1706                .and_then(|colon| {
1707                    source_class_value_from_js_expression(
1708                        source,
1709                        colon + 1,
1710                        property_end,
1711                        local_class_values,
1712                    )
1713                })
1714                .filter(|value| !value.is_empty())
1715        {
1716            property_values.insert(property_name, property_value);
1717        }
1718    }
1719    object_value.canonicalize();
1720    (object_value, property_values)
1721}
1722
1723fn collect_object_literal_selector_references(
1724    source: &str,
1725    start: usize,
1726    end: usize,
1727    target_style_uri: Option<&str>,
1728    local_class_values: &BTreeMap<String, SourceClassValue>,
1729    references: &mut Vec<SourceSelectorReferenceFactV0>,
1730    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1731) {
1732    for (property_start, property_end) in
1733        split_top_level_js_segments(source, start + 1, end - 1, b',')
1734    {
1735        let (property_start, property_end) =
1736            trim_js_expression(source, property_start, property_end);
1737        if property_start >= property_end {
1738            continue;
1739        }
1740        if source[property_start..property_end].starts_with("...") {
1741            collect_selector_references_from_js_expression(
1742                source,
1743                property_start + 3,
1744                property_end,
1745                target_style_uri,
1746                local_class_values,
1747                references,
1748                type_fact_targets,
1749            );
1750            continue;
1751        }
1752        let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1753        let key_end = colon.unwrap_or(property_end);
1754        collect_selector_references_from_object_key(
1755            source,
1756            property_start,
1757            key_end,
1758            target_style_uri,
1759            local_class_values,
1760            references,
1761            type_fact_targets,
1762        );
1763    }
1764}
1765
1766fn class_utility_call_arguments(source: &str, start: usize, end: usize) -> Option<(usize, usize)> {
1767    let (callee, callee_end) = read_js_identifier(source, start)?;
1768    if !is_class_utility_callee(callee) {
1769        return None;
1770    }
1771    let open_paren = skip_js_trivia_until(source, callee_end, end);
1772    if source.as_bytes().get(open_paren) != Some(&b'(') {
1773        return None;
1774    }
1775    let call_end = js_call_end(source, open_paren)?;
1776    if call_end > end || trim_js_expression(source, call_end + 1, end).0 < end {
1777        return None;
1778    }
1779    Some((open_paren + 1, call_end))
1780}
1781
1782fn is_class_utility_callee(callee: &str) -> bool {
1783    matches!(callee, "classnames" | "classNames" | "clsx" | "cn")
1784}
1785
1786fn collect_selector_references_from_object_key(
1787    source: &str,
1788    start: usize,
1789    end: usize,
1790    target_style_uri: Option<&str>,
1791    local_class_values: &BTreeMap<String, SourceClassValue>,
1792    references: &mut Vec<SourceSelectorReferenceFactV0>,
1793    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1794) {
1795    let (start, end) = trim_js_expression(source, start, end);
1796    if start >= end {
1797        return;
1798    }
1799    if source.as_bytes().get(start) == Some(&b'[')
1800        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1801    {
1802        collect_selector_references_from_js_expression(
1803            source,
1804            start + 1,
1805            end - 1,
1806            target_style_uri,
1807            local_class_values,
1808            references,
1809            type_fact_targets,
1810        );
1811        return;
1812    }
1813    if let Some((literal_start, literal_end, next_offset)) =
1814        js_string_literal_span(source, start, end)
1815        && trim_js_expression(source, next_offset, end).0 >= end
1816    {
1817        push_js_literal_selector_references(
1818            source,
1819            literal_start,
1820            literal_end,
1821            source.as_bytes().get(start).copied() == Some(b'`'),
1822            target_style_uri,
1823            references,
1824        );
1825        if source.as_bytes().get(start).copied() == Some(b'`') {
1826            collect_template_type_fact_targets(
1827                source,
1828                literal_start,
1829                literal_end,
1830                target_style_uri,
1831                type_fact_targets,
1832            );
1833        }
1834        return;
1835    }
1836    if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1837        && trim_js_expression(source, identifier_end, end).0 >= end
1838    {
1839        push_selector_reference(
1840            ParserByteSpanV0 { start, end },
1841            Some(identifier.to_string()),
1842            SourceSelectorReferenceMatchKindV0::Exact,
1843            target_style_uri,
1844            references,
1845        );
1846    }
1847}
1848
1849fn source_class_value_from_object_key(
1850    source: &str,
1851    start: usize,
1852    end: usize,
1853    local_class_values: &BTreeMap<String, SourceClassValue>,
1854) -> SourceClassValue {
1855    let (start, end) = trim_js_expression(source, start, end);
1856    if start >= end {
1857        return SourceClassValue::default();
1858    }
1859    if source.as_bytes().get(start) == Some(&b'[')
1860        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1861    {
1862        return source_class_value_from_js_expression(
1863            source,
1864            start + 1,
1865            end - 1,
1866            local_class_values,
1867        )
1868        .unwrap_or_default();
1869    }
1870    if let Some((literal_start, literal_end, next_offset)) =
1871        js_string_literal_span(source, start, end)
1872        && trim_js_expression(source, next_offset, end).0 >= end
1873    {
1874        return source_class_value_from_js_literal(
1875            source,
1876            literal_start,
1877            literal_end,
1878            source.as_bytes().get(start).copied() == Some(b'`'),
1879        );
1880    }
1881    if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1882        && trim_js_expression(source, identifier_end, end).0 >= end
1883    {
1884        let mut value = SourceClassValue::default();
1885        value.exact.push(identifier.to_string());
1886        return value;
1887    }
1888    SourceClassValue::default()
1889}
1890
1891fn object_property_name(source: &str, start: usize, end: usize) -> Option<String> {
1892    let (start, end) = trim_js_expression(source, start, end);
1893    if let Some((literal_start, literal_end, next_offset)) =
1894        js_string_literal_span(source, start, end)
1895        && trim_js_expression(source, next_offset, end).0 >= end
1896    {
1897        return source.get(literal_start..literal_end).map(str::to_string);
1898    }
1899    let (identifier, identifier_end) = read_js_identifier(source, start)?;
1900    (trim_js_expression(source, identifier_end, end).0 >= end).then(|| identifier.to_string())
1901}
1902
1903fn push_source_class_value_reference(
1904    byte_span: ParserByteSpanV0,
1905    value: SourceClassValue,
1906    target_style_uri: Option<&str>,
1907    references: &mut Vec<SourceSelectorReferenceFactV0>,
1908) {
1909    for selector_name in value.exact {
1910        push_selector_reference(
1911            byte_span,
1912            Some(selector_name),
1913            SourceSelectorReferenceMatchKindV0::Exact,
1914            target_style_uri,
1915            references,
1916        );
1917    }
1918    for prefix in value.prefixes {
1919        push_selector_reference(
1920            byte_span,
1921            Some(prefix),
1922            SourceSelectorReferenceMatchKindV0::Prefix,
1923            target_style_uri,
1924            references,
1925        );
1926    }
1927}
1928
1929fn collect_template_type_fact_targets(
1930    source: &str,
1931    literal_start: usize,
1932    literal_end: usize,
1933    target_style_uri: Option<&str>,
1934    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1935) {
1936    let Some((prefix, expression_span, suffix)) =
1937        single_template_interpolation_projection(source, literal_start, literal_end)
1938    else {
1939        return;
1940    };
1941    let Some(path) = js_expression_path(source, expression_span.start, expression_span.end) else {
1942        return;
1943    };
1944    push_source_type_fact_target(
1945        expression_span,
1946        path.as_str(),
1947        target_style_uri,
1948        prefix.as_str(),
1949        suffix.as_str(),
1950        type_fact_targets,
1951    );
1952}
1953
1954fn single_template_interpolation_projection(
1955    source: &str,
1956    literal_start: usize,
1957    literal_end: usize,
1958) -> Option<(String, ParserByteSpanV0, String)> {
1959    let relative_open = source.get(literal_start..literal_end)?.find("${")?;
1960    let open = literal_start + relative_open;
1961    if source.get(open + 2..literal_end)?.contains("${") {
1962        return None;
1963    }
1964    let expression_start = open + 2;
1965    let close = matching_js_block_end(source, open + 1, b'{', b'}')?;
1966    if close > literal_end {
1967        return None;
1968    }
1969    let (expression_start, expression_end) = trim_js_expression(source, expression_start, close);
1970    if expression_start >= expression_end {
1971        return None;
1972    }
1973    let prefix_start = template_token_start(source, literal_start, open);
1974    let suffix_end = template_token_end(source, close + 1, literal_end);
1975    let prefix = source.get(prefix_start..open)?.to_string();
1976    let suffix = source.get(close + 1..suffix_end)?.to_string();
1977    if !prefix.chars().all(is_css_identifier_continue)
1978        || !suffix.chars().all(is_css_identifier_continue)
1979    {
1980        return None;
1981    }
1982    Some((
1983        prefix,
1984        ParserByteSpanV0 {
1985            start: expression_start,
1986            end: expression_end,
1987        },
1988        suffix,
1989    ))
1990}
1991
1992fn template_token_start(source: &str, literal_start: usize, prefix_end: usize) -> usize {
1993    source
1994        .get(literal_start..prefix_end)
1995        .and_then(|value| {
1996            value
1997                .char_indices()
1998                .rev()
1999                .find(|(_, ch)| ch.is_ascii_whitespace())
2000                .map(|(index, ch)| literal_start + index + ch.len_utf8())
2001        })
2002        .unwrap_or(literal_start)
2003}
2004
2005fn template_token_end(source: &str, suffix_start: usize, literal_end: usize) -> usize {
2006    source
2007        .get(suffix_start..literal_end)
2008        .and_then(|value| {
2009            value
2010                .char_indices()
2011                .find(|(_, ch)| ch.is_ascii_whitespace())
2012                .map(|(index, _)| suffix_start + index)
2013        })
2014        .unwrap_or(literal_end)
2015}
2016
2017fn push_source_type_fact_target(
2018    byte_span: ParserByteSpanV0,
2019    expression_path: &str,
2020    target_style_uri: Option<&str>,
2021    prefix: &str,
2022    suffix: &str,
2023    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
2024) {
2025    type_fact_targets.push(SourceTypeFactTargetV0 {
2026        byte_span,
2027        expression_id: source_type_fact_expression_id(expression_path, byte_span),
2028        target_style_uri: target_style_uri.map(ToString::to_string),
2029        prefix: prefix.to_string(),
2030        suffix: suffix.to_string(),
2031    });
2032}
2033
2034fn source_type_fact_expression_id(expression_path: &str, byte_span: ParserByteSpanV0) -> String {
2035    format!(
2036        "omena-bridge-source-type-fact:{expression_path}:{}:{}",
2037        byte_span.start, byte_span.end
2038    )
2039}
2040
2041fn push_selector_reference(
2042    byte_span: ParserByteSpanV0,
2043    selector_name: Option<String>,
2044    match_kind: SourceSelectorReferenceMatchKindV0,
2045    target_style_uri: Option<&str>,
2046    references: &mut Vec<SourceSelectorReferenceFactV0>,
2047) {
2048    references.push(SourceSelectorReferenceFactV0 {
2049        byte_span,
2050        selector_name,
2051        match_kind,
2052        target_style_uri: target_style_uri.map(ToString::to_string),
2053    });
2054}
2055
2056fn push_js_literal_selector_references(
2057    source: &str,
2058    literal_start: usize,
2059    literal_end: usize,
2060    is_template: bool,
2061    target_style_uri: Option<&str>,
2062    references: &mut Vec<SourceSelectorReferenceFactV0>,
2063) {
2064    if is_template
2065        && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2066    {
2067        push_template_prefix_selector_references(
2068            source,
2069            literal_start,
2070            literal_start + relative_interpolation,
2071            target_style_uri,
2072            references,
2073        );
2074        return;
2075    }
2076
2077    push_string_literal_selector_references(
2078        source,
2079        ParserByteSpanV0 {
2080            start: literal_start,
2081            end: literal_end,
2082        },
2083        target_style_uri.map(ToString::to_string),
2084        references,
2085    );
2086}
2087
2088fn push_template_prefix_selector_references(
2089    source: &str,
2090    literal_start: usize,
2091    prefix_end: usize,
2092    target_style_uri: Option<&str>,
2093    references: &mut Vec<SourceSelectorReferenceFactV0>,
2094) {
2095    let spans = class_token_byte_spans(source, literal_start, prefix_end);
2096    let prefix_ends_with_space = source[..prefix_end]
2097        .chars()
2098        .last()
2099        .is_none_or(char::is_whitespace);
2100    for (index, span) in spans.iter().enumerate() {
2101        let is_open_prefix = index + 1 == spans.len() && !prefix_ends_with_space;
2102        push_selector_reference(
2103            *span,
2104            Some(source[span.start..span.end].to_string()),
2105            if is_open_prefix {
2106                SourceSelectorReferenceMatchKindV0::Prefix
2107            } else {
2108                SourceSelectorReferenceMatchKindV0::Exact
2109            },
2110            target_style_uri,
2111            references,
2112        );
2113    }
2114}
2115
2116fn push_template_prefix_value(
2117    source: &str,
2118    literal_start: usize,
2119    prefix_end: usize,
2120    value: &mut SourceClassValue,
2121) {
2122    let spans = class_token_byte_spans(source, literal_start, prefix_end);
2123    let prefix_ends_with_space = source[..prefix_end]
2124        .chars()
2125        .last()
2126        .is_none_or(char::is_whitespace);
2127    for (index, span) in spans.iter().enumerate() {
2128        let token = source[span.start..span.end].to_string();
2129        if index + 1 == spans.len() && !prefix_ends_with_space {
2130            value.prefixes.push(token);
2131        } else {
2132            value.exact.push(token);
2133        }
2134    }
2135}
2136
2137fn class_token_strings(source: &str, literal_start: usize, literal_end: usize) -> Vec<String> {
2138    class_token_byte_spans(source, literal_start, literal_end)
2139        .into_iter()
2140        .map(|span| source[span.start..span.end].to_string())
2141        .collect()
2142}
2143
2144fn push_string_literal_selector_references(
2145    source: &str,
2146    literal_span: ParserByteSpanV0,
2147    target_style_uri: Option<String>,
2148    references: &mut Vec<SourceSelectorReferenceFactV0>,
2149) {
2150    for span in class_token_byte_spans(source, literal_span.start, literal_span.end) {
2151        references.push(SourceSelectorReferenceFactV0 {
2152            byte_span: span,
2153            selector_name: None,
2154            match_kind: SourceSelectorReferenceMatchKindV0::Exact,
2155            target_style_uri: target_style_uri.clone(),
2156        });
2157    }
2158}
2159
2160fn trim_js_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2161    let mut start = char_boundary_ceil(source, start);
2162    let mut end = char_boundary_floor(source, end);
2163    start = skip_js_trivia_until(source, start, end);
2164    while end > start
2165        && source
2166            .as_bytes()
2167            .get(end - 1)
2168            .is_some_and(u8::is_ascii_whitespace)
2169    {
2170        end -= 1;
2171    }
2172    (start, end)
2173}
2174
2175fn char_boundary_floor(source: &str, index: usize) -> usize {
2176    let mut index = index.min(source.len());
2177    while index > 0 && !source.is_char_boundary(index) {
2178        index -= 1;
2179    }
2180    index
2181}
2182
2183fn char_boundary_ceil(source: &str, index: usize) -> usize {
2184    let mut index = index.min(source.len());
2185    while index < source.len() && !source.is_char_boundary(index) {
2186        index += 1;
2187    }
2188    index
2189}
2190
2191fn advance_js_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
2192    let cursor = char_boundary_ceil(source, cursor);
2193    let limit = char_boundary_floor(source, limit);
2194    if cursor >= limit {
2195        return limit;
2196    }
2197    char_boundary_ceil(source, cursor + 1).min(limit)
2198}
2199
2200fn advance_js_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
2201    let after_slash = advance_js_scan_cursor(source, slash_offset, limit);
2202    advance_js_scan_cursor(source, after_slash, limit)
2203}
2204
2205fn unwrap_js_parenthesized_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2206    let mut current_start = start;
2207    let mut current_end = end;
2208    loop {
2209        let (trimmed_start, trimmed_end) = trim_js_expression(source, current_start, current_end);
2210        if source.as_bytes().get(trimmed_start) == Some(&b'(')
2211            && matching_js_block_end(source, trimmed_start, b'(', b')')
2212                == Some(trimmed_end.saturating_sub(1))
2213        {
2214            current_start = trimmed_start + 1;
2215            current_end = trimmed_end - 1;
2216            continue;
2217        }
2218        return (trimmed_start, trimmed_end);
2219    }
2220}
2221
2222fn js_statement_expression_end(source: &str, start: usize) -> usize {
2223    let mut cursor = char_boundary_ceil(source, start);
2224    let mut depth = 0usize;
2225    while cursor < source.len() {
2226        match source.as_bytes().get(cursor).copied() {
2227            Some(b'\'' | b'"' | b'`') => {
2228                cursor =
2229                    skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2230            }
2231            Some(b'(' | b'[' | b'{') => {
2232                depth += 1;
2233                cursor = advance_js_scan_cursor(source, cursor, source.len());
2234            }
2235            Some(b')' | b']' | b'}') => {
2236                depth = depth.saturating_sub(1);
2237                cursor = advance_js_scan_cursor(source, cursor, source.len());
2238            }
2239            Some(b';') if depth == 0 => return cursor,
2240            Some(b'\n') if depth == 0 => return cursor,
2241            Some(_) => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2242            None => break,
2243        }
2244    }
2245    source.len()
2246}
2247
2248fn matching_js_block_end(source: &str, open_offset: usize, open: u8, close: u8) -> Option<usize> {
2249    if source.as_bytes().get(open_offset) != Some(&open) {
2250        return None;
2251    }
2252    let mut cursor = advance_js_scan_cursor(source, open_offset, source.len());
2253    let mut depth = 1usize;
2254    while cursor < source.len() {
2255        match source.as_bytes().get(cursor).copied()? {
2256            b'\'' | b'"' | b'`' => {
2257                cursor = skip_js_string_literal(source, cursor, source.len())?;
2258            }
2259            byte if byte == open => {
2260                depth += 1;
2261                cursor = advance_js_scan_cursor(source, cursor, source.len());
2262            }
2263            byte if byte == close => {
2264                depth -= 1;
2265                if depth == 0 {
2266                    return Some(cursor);
2267                }
2268                cursor = advance_js_scan_cursor(source, cursor, source.len());
2269            }
2270            _ => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2271        }
2272    }
2273    None
2274}
2275
2276fn split_top_level_js_segments(
2277    source: &str,
2278    start: usize,
2279    end: usize,
2280    delimiter: u8,
2281) -> Vec<(usize, usize)> {
2282    let mut segments = Vec::new();
2283    let end = char_boundary_floor(source, end);
2284    let mut segment_start = char_boundary_ceil(source, start).min(end);
2285    let mut cursor = segment_start;
2286    let mut depth = 0usize;
2287    while cursor < end {
2288        match source.as_bytes().get(cursor).copied() {
2289            Some(b'\'' | b'"' | b'`') => {
2290                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2291            }
2292            Some(b'(' | b'[' | b'{') => {
2293                depth += 1;
2294                cursor = advance_js_scan_cursor(source, cursor, end);
2295            }
2296            Some(b')' | b']' | b'}') => {
2297                depth = depth.saturating_sub(1);
2298                cursor = advance_js_scan_cursor(source, cursor, end);
2299            }
2300            Some(byte) if byte == delimiter && depth == 0 => {
2301                segments.push((segment_start, cursor));
2302                cursor = advance_js_scan_cursor(source, cursor, end);
2303                segment_start = cursor;
2304            }
2305            Some(_) => cursor = advance_js_scan_cursor(source, cursor, end),
2306            None => break,
2307        }
2308    }
2309    if segment_start <= end {
2310        segments.push((segment_start, end));
2311    }
2312    segments
2313}
2314
2315fn find_top_level_js_byte(source: &str, start: usize, end: usize, needle: u8) -> Option<usize> {
2316    let end = char_boundary_floor(source, end);
2317    let mut cursor = char_boundary_ceil(source, start).min(end);
2318    let mut depth = 0usize;
2319    while cursor < end {
2320        match source.as_bytes().get(cursor).copied()? {
2321            b'\'' | b'"' | b'`' => {
2322                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2323            }
2324            b'(' | b'[' | b'{' => {
2325                depth += 1;
2326                cursor = advance_js_scan_cursor(source, cursor, end);
2327            }
2328            b')' | b']' | b'}' => {
2329                depth = depth.saturating_sub(1);
2330                cursor = advance_js_scan_cursor(source, cursor, end);
2331            }
2332            byte if byte == needle && depth == 0 => return Some(cursor),
2333            _ => cursor = advance_js_scan_cursor(source, cursor, end),
2334        }
2335    }
2336    None
2337}
2338
2339fn find_top_level_js_operator(
2340    source: &str,
2341    start: usize,
2342    end: usize,
2343    operator: &str,
2344) -> Option<usize> {
2345    let end = char_boundary_floor(source, end);
2346    let mut cursor = char_boundary_ceil(source, start).min(end);
2347    let mut depth = 0usize;
2348    while cursor < end {
2349        match source.as_bytes().get(cursor).copied()? {
2350            b'\'' | b'"' | b'`' => {
2351                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2352            }
2353            b'(' | b'[' | b'{' => {
2354                depth += 1;
2355                cursor = advance_js_scan_cursor(source, cursor, end);
2356            }
2357            b')' | b']' | b'}' => {
2358                depth = depth.saturating_sub(1);
2359                cursor = advance_js_scan_cursor(source, cursor, end);
2360            }
2361            _ if depth == 0
2362                && source
2363                    .get(cursor..end)
2364                    .is_some_and(|rest| rest.starts_with(operator)) =>
2365            {
2366                return Some(cursor);
2367            }
2368            _ => cursor = advance_js_scan_cursor(source, cursor, end),
2369        }
2370    }
2371    None
2372}
2373
2374fn top_level_conditional_parts(
2375    source: &str,
2376    start: usize,
2377    end: usize,
2378) -> Option<(usize, usize, usize, usize, usize)> {
2379    let question = find_top_level_js_byte(source, start, end, b'?')?;
2380    let end = char_boundary_floor(source, end);
2381    let mut cursor = advance_js_scan_cursor(source, question, end);
2382    let mut depth = 0usize;
2383    let mut nested_conditional_depth = 0usize;
2384    while cursor < end {
2385        match source.as_bytes().get(cursor).copied()? {
2386            b'\'' | b'"' | b'`' => {
2387                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2388            }
2389            b'(' | b'[' | b'{' => {
2390                depth += 1;
2391                cursor = advance_js_scan_cursor(source, cursor, end);
2392            }
2393            b')' | b']' | b'}' => {
2394                depth = depth.saturating_sub(1);
2395                cursor = advance_js_scan_cursor(source, cursor, end);
2396            }
2397            b'?' if depth == 0 => {
2398                nested_conditional_depth += 1;
2399                cursor = advance_js_scan_cursor(source, cursor, end);
2400            }
2401            b':' if depth == 0 && nested_conditional_depth == 0 => {
2402                return Some((
2403                    question,
2404                    advance_js_scan_cursor(source, question, end),
2405                    cursor,
2406                    advance_js_scan_cursor(source, cursor, end),
2407                    end,
2408                ));
2409            }
2410            b':' if depth == 0 => {
2411                nested_conditional_depth = nested_conditional_depth.saturating_sub(1);
2412                cursor = advance_js_scan_cursor(source, cursor, end);
2413            }
2414            _ => cursor = advance_js_scan_cursor(source, cursor, end),
2415        }
2416    }
2417    None
2418}
2419
2420fn js_expression_path(source: &str, start: usize, end: usize) -> Option<String> {
2421    let (start, end) = trim_js_expression(source, start, end);
2422    let (first, mut cursor) = read_js_identifier(source, start)?;
2423    let mut path = vec![first.to_string()];
2424    loop {
2425        cursor = skip_js_trivia_until(source, cursor, end);
2426        match source.as_bytes().get(cursor).copied() {
2427            Some(b'.') => {
2428                let member_start = skip_js_trivia_until(source, cursor + 1, end);
2429                let (member, member_end) = read_js_identifier(source, member_start)?;
2430                path.push(member.to_string());
2431                cursor = member_end;
2432            }
2433            Some(b'[') => {
2434                if let Some((literal_start, literal_end, bracket_end)) =
2435                    bracket_string_literal_access(source, cursor)
2436                    && bracket_end <= end
2437                {
2438                    path.push(source[literal_start..literal_end].to_string());
2439                    cursor = bracket_end;
2440                } else {
2441                    return None;
2442                }
2443            }
2444            _ => break,
2445        }
2446    }
2447    (trim_js_expression(source, cursor, end).0 >= end).then(|| path.join("."))
2448}
2449
2450fn static_string_prefix_for_js_expression(
2451    source: &str,
2452    start: usize,
2453    end: usize,
2454    local_class_values: &BTreeMap<String, SourceClassValue>,
2455) -> Option<String> {
2456    let (start, end) = trim_js_expression(source, start, end);
2457    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
2458    if let Some((literal_start, literal_end, next_offset)) =
2459        js_string_literal_span(source, start, end)
2460        && trim_js_expression(source, next_offset, end).0 >= end
2461    {
2462        if source.as_bytes().get(start).copied() == Some(b'`')
2463            && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2464        {
2465            return Some(source[literal_start..literal_start + relative_interpolation].to_string());
2466        }
2467        return Some(source[literal_start..literal_end].to_string());
2468    }
2469    if let Some(path) = js_expression_path(source, start, end)
2470        && let Some(value) = local_class_values.get(path.as_str())
2471    {
2472        if value.exact.len() == 1 && value.prefixes.is_empty() {
2473            return value.exact.first().cloned();
2474        }
2475        if value.prefixes.len() == 1 && value.exact.is_empty() {
2476            return value.prefixes.first().cloned();
2477        }
2478    }
2479    if let Some(plus_offset) = find_top_level_js_operator(source, start, end, "+") {
2480        let left =
2481            static_string_prefix_for_js_expression(source, start, plus_offset, local_class_values)?;
2482        let right = static_string_prefix_for_js_expression(
2483            source,
2484            plus_offset + 1,
2485            end,
2486            local_class_values,
2487        )
2488        .unwrap_or_default();
2489        return Some(format!("{left}{right}"));
2490    }
2491    None
2492}
2493
2494fn js_call_end(source: &str, open_paren: usize) -> Option<usize> {
2495    if source.as_bytes().get(open_paren) != Some(&b'(') {
2496        return None;
2497    }
2498    let mut cursor = advance_js_scan_cursor(source, open_paren, source.len());
2499    let mut depth = 1usize;
2500    while cursor < source.len() {
2501        match source.as_bytes().get(cursor).copied()? {
2502            b'\'' | b'"' | b'`' => {
2503                cursor = skip_js_string_literal(source, cursor, source.len())?;
2504            }
2505            b'(' => {
2506                depth += 1;
2507                cursor = advance_js_scan_cursor(source, cursor, source.len());
2508            }
2509            b')' => {
2510                depth -= 1;
2511                if depth == 0 {
2512                    return Some(cursor);
2513                }
2514                cursor = advance_js_scan_cursor(source, cursor, source.len());
2515            }
2516            _ => {
2517                cursor = advance_js_scan_cursor(source, cursor, source.len());
2518            }
2519        }
2520    }
2521    None
2522}
2523
2524fn class_token_byte_spans(
2525    source: &str,
2526    literal_start: usize,
2527    literal_end: usize,
2528) -> Vec<ParserByteSpanV0> {
2529    let mut spans = Vec::new();
2530    let mut token_start: Option<usize> = None;
2531    for (relative_index, ch) in source[literal_start..literal_end].char_indices() {
2532        let index = literal_start + relative_index;
2533        if ch.is_ascii_whitespace() {
2534            if let Some(start) = token_start.take() {
2535                push_class_token_span(source, start, index, &mut spans);
2536            }
2537        } else if token_start.is_none() {
2538            token_start = Some(index);
2539        }
2540    }
2541    if let Some(start) = token_start {
2542        push_class_token_span(source, start, literal_end, &mut spans);
2543    }
2544    spans
2545}
2546
2547fn push_class_token_span(
2548    source: &str,
2549    start: usize,
2550    end: usize,
2551    spans: &mut Vec<ParserByteSpanV0>,
2552) {
2553    if start < end && source[start..end].chars().all(is_css_identifier_continue) {
2554        spans.push(ParserByteSpanV0 { start, end });
2555    }
2556}
2557
2558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2559struct CodeIdentifier<'a> {
2560    text: &'a str,
2561    end: usize,
2562}
2563
2564fn next_code_identifier(source: &str, mut cursor: usize) -> Option<CodeIdentifier<'_>> {
2565    while cursor < source.len() {
2566        cursor = skip_js_trivia(source, cursor);
2567        let byte = source.as_bytes().get(cursor).copied()?;
2568        if matches!(byte, b'\'' | b'"' | b'`') {
2569            cursor = skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2570            continue;
2571        }
2572        if byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$') {
2573            let (text, end) = read_js_identifier(source, cursor)?;
2574            return Some(CodeIdentifier { text, end });
2575        }
2576        cursor = advance_js_scan_cursor(source, cursor, source.len());
2577    }
2578    None
2579}
2580
2581fn skip_js_trivia(source: &str, cursor: usize) -> usize {
2582    skip_js_trivia_until(source, cursor, source.len())
2583}
2584
2585fn skip_js_trivia_until(source: &str, mut cursor: usize, limit: usize) -> usize {
2586    loop {
2587        cursor = skip_ascii_whitespace_until(source, cursor, limit);
2588        if source.as_bytes().get(cursor) == Some(&b'/') {
2589            match source.as_bytes().get(cursor + 1).copied() {
2590                Some(b'/') => {
2591                    cursor = skip_js_line_comment(source, cursor + 2, limit);
2592                    continue;
2593                }
2594                Some(b'*') => {
2595                    cursor = skip_js_block_comment(source, cursor + 2, limit);
2596                    continue;
2597                }
2598                _ => {}
2599            }
2600        }
2601        return cursor;
2602    }
2603}
2604
2605fn skip_ascii_whitespace_until(source: &str, mut offset: usize, limit: usize) -> usize {
2606    while offset < limit
2607        && source
2608            .as_bytes()
2609            .get(offset)
2610            .is_some_and(u8::is_ascii_whitespace)
2611    {
2612        offset += 1;
2613    }
2614    offset
2615}
2616
2617fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
2618    while source
2619        .as_bytes()
2620        .get(offset)
2621        .is_some_and(u8::is_ascii_whitespace)
2622    {
2623        offset += 1;
2624    }
2625    offset
2626}
2627
2628fn skip_js_line_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2629    let limit = char_boundary_floor(source, limit);
2630    while cursor < limit {
2631        if source.as_bytes().get(cursor) == Some(&b'\n') {
2632            return advance_js_scan_cursor(source, cursor, limit);
2633        }
2634        cursor = advance_js_scan_cursor(source, cursor, limit);
2635    }
2636    limit
2637}
2638
2639fn skip_js_block_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2640    let limit = char_boundary_floor(source, limit);
2641    while cursor + 1 < limit {
2642        if source.as_bytes().get(cursor) == Some(&b'*')
2643            && source.as_bytes().get(cursor + 1) == Some(&b'/')
2644        {
2645            return cursor + 2;
2646        }
2647        cursor = advance_js_scan_cursor(source, cursor, limit);
2648    }
2649    limit
2650}
2651
2652fn js_string_literal_span(
2653    source: &str,
2654    quote_offset: usize,
2655    limit: usize,
2656) -> Option<(usize, usize, usize)> {
2657    let quote = source.as_bytes().get(quote_offset).copied()?;
2658    if !matches!(quote, b'\'' | b'"' | b'`') {
2659        return None;
2660    }
2661    let literal_start = quote_offset + 1;
2662    let next_offset = skip_js_string_literal(source, quote_offset, limit)?;
2663    Some((literal_start, next_offset - 1, next_offset))
2664}
2665
2666fn skip_js_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
2667    let quote = source.as_bytes().get(quote_offset).copied()?;
2668    let limit = char_boundary_floor(source, limit);
2669    let mut cursor = quote_offset + 1;
2670    while cursor < limit {
2671        let byte = source.as_bytes().get(cursor).copied()?;
2672        if byte == b'\\' {
2673            cursor = advance_js_escaped_char(source, cursor, limit);
2674            continue;
2675        }
2676        if byte == quote {
2677            return Some(cursor + 1);
2678        }
2679        cursor = advance_js_scan_cursor(source, cursor, limit);
2680    }
2681    None
2682}
2683
2684fn bracket_string_literal_access(
2685    source: &str,
2686    bracket_offset: usize,
2687) -> Option<(usize, usize, usize)> {
2688    if source.as_bytes().get(bracket_offset) != Some(&b'[') {
2689        return None;
2690    }
2691    let quote_offset = skip_ascii_whitespace(source, bracket_offset + 1);
2692    let quote = source.as_bytes().get(quote_offset).copied()?;
2693    if !matches!(quote, b'\'' | b'"') {
2694        return None;
2695    }
2696    let (literal_start, literal_end, literal_next) =
2697        js_string_literal_span(source, quote_offset, source.len())?;
2698    if literal_next > source.len() {
2699        return None;
2700    }
2701    let closing_bracket = skip_ascii_whitespace(source, literal_end + 1);
2702    if source.as_bytes().get(closing_bracket) != Some(&b']') {
2703        return None;
2704    }
2705    Some((literal_start, literal_end, closing_bracket + 1))
2706}
2707
2708fn read_js_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
2709    let start = char_boundary_ceil(source, start);
2710    let first = source.get(start..)?.chars().next()?;
2711    if !is_js_identifier_start(first) {
2712        return None;
2713    }
2714    let mut end = start + first.len_utf8();
2715    let scan_start = end;
2716    for (relative_index, ch) in source.get(scan_start..)?.char_indices() {
2717        if !is_js_identifier_continue(ch) {
2718            break;
2719        }
2720        end = scan_start + relative_index + ch.len_utf8();
2721    }
2722    Some((&source[start..end], end))
2723}
2724
2725fn is_js_identifier_start(ch: char) -> bool {
2726    ch.is_ascii_alphabetic() || matches!(ch, '_' | '$')
2727}
2728
2729fn is_js_identifier_continue(ch: char) -> bool {
2730    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')
2731}
2732
2733fn is_css_identifier_continue(ch: char) -> bool {
2734    ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
2735}
2736
2737#[cfg(test)]
2738mod tests;