Skip to main content

omena_bridge/
source_syntax.rs

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