Skip to main content

omena_bridge/
source_syntax.rs

1use omena_parser::ParserByteSpanV0;
2use oxc_allocator::Allocator;
3use oxc_ast::ast::TSModuleReference;
4use oxc_ast::ast::{
5    Argument, ArrayExpression, ArrayExpressionElement, BindingIdentifier, BindingPattern,
6    CallExpression, ChainElement, Class, ClassElement, ComputedMemberExpression,
7    ConditionalExpression, Declaration, Expression, IdentifierReference, ImportDeclaration,
8    ImportDeclarationSpecifier, ImportOrExportKind, JSXAttributeName, JSXAttributeValue, JSXChild,
9    JSXExpression, LogicalExpression, ObjectExpression, ObjectPropertyKind,
10    ParenthesizedExpression, Program, Statement, StaticMemberExpression, TSAsExpression,
11    TSNonNullExpression, TSSatisfiesExpression, VariableDeclarator,
12};
13use oxc_parser::{Parser, ParserReturn};
14use oxc_semantic::{Scoping, SemanticBuilder, SymbolId};
15use oxc_span::{GetSpan, SourceType, Span};
16use serde::Serialize;
17use std::collections::{BTreeMap, BTreeSet};
18
19use crate::source_language::{
20    ServerTemplateDelimiterFamilyV0, is_astro_source, is_html_source, is_markdown_source,
21    is_server_template_source, is_svelte_source, is_vue_source, project_source_for_language,
22    server_template_delimiter_family, source_type_for_language, tag_content_ranges,
23};
24use crate::style_intelligence::{
25    BuiltInRecipeCallShapeV0 as VariantRecipeCallShape,
26    BuiltInRecipeProviderConfigV0 as VariantRecipeConfigV0, built_in_recipe_provider_configs,
27};
28
29#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct SourceSyntaxIndexV0 {
32    pub schema_version: &'static str,
33    pub product: &'static str,
34    pub imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
35    pub class_string_literals: Vec<ParserByteSpanV0>,
36    pub style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
37    pub inline_style_declarations: Vec<SourceInlineStyleDeclarationFactV0>,
38    pub selector_references: Vec<SourceSelectorReferenceFactV0>,
39    pub type_fact_targets: Vec<SourceTypeFactTargetV0>,
40    #[serde(skip_serializing_if = "Vec::is_empty")]
41    pub type_fact_provider_unavailable: Vec<SourceTypeFactProviderUnavailableFactV0>,
42    pub class_value_universes: Vec<SourceClassValueUniverseEntryV0>,
43    pub domain_class_references: Vec<SourceDomainClassReferenceFactV0>,
44    #[serde(skip_serializing_if = "Vec::is_empty")]
45    pub source_elements: Vec<SourceElementFactV0>,
46    #[serde(skip_serializing_if = "Vec::is_empty")]
47    pub element_parent_edges: Vec<SourceElementParentFactV0>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub struct SourceElementIdentityFactV0 {
53    pub source_path: String,
54    pub byte_span: ParserByteSpanV0,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct SourceElementFactV0 {
60    pub identity: SourceElementIdentityFactV0,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub intrinsic_tag_name: Option<String>,
63    #[serde(skip_serializing_if = "Vec::is_empty")]
64    pub static_class_names: Vec<String>,
65    pub classes_are_exact: bool,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
69#[serde(rename_all = "camelCase")]
70pub struct SourceElementParentFactV0 {
71    pub child: SourceElementIdentityFactV0,
72    pub parent: SourceElementIdentityFactV0,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct SourceImportedStyleBindingV0 {
78    pub binding: String,
79    pub style_uri: String,
80}
81
82#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct SourceBindingIndexV0 {
85    pub schema_version: &'static str,
86    pub product: &'static str,
87    pub binding_scopes: Vec<SourceBindingScopeFactV0>,
88    pub scope_parent_edges: Vec<SourceScopeParentFactV0>,
89    pub binding_decls: Vec<SourceBindingDeclFactV0>,
90    pub scope_contains_decls: Vec<SourceScopeContainsDeclFactV0>,
91    pub style_import_bindings: Vec<SourceBindingStyleImportFactV0>,
92    pub declares_style_imports: Vec<SourceDeclaresStyleImportFactV0>,
93    pub style_import_resolves_modules: Vec<SourceStyleImportResolvesModuleFactV0>,
94    pub class_expression_nodes: Vec<SourceClassExpressionNodeFactV0>,
95    pub expression_targets_modules: Vec<SourceExpressionTargetsModuleFactV0>,
96    pub classnames_bind_utility_bindings: Vec<SourceClassnamesBindUtilityBindingFactV0>,
97    pub class_util_bindings: Vec<SourceClassUtilityBindingFactV0>,
98    pub declares_utility_bindings: Vec<SourceDeclaresUtilityBindingFactV0>,
99    pub utility_uses_style_imports: Vec<SourceUtilityUsesStyleImportFactV0>,
100    pub style_access_uses_style_imports: Vec<SourceStyleAccessUsesStyleImportFactV0>,
101    pub symbol_ref_uses_decls: Vec<SourceSymbolRefUsesDeclFactV0>,
102    pub module_specifiers: Vec<SourceModuleSpecifierFactV0>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
106#[serde(rename_all = "camelCase")]
107pub struct SourceBindingScopeFactV0 {
108    pub kind: &'static str,
109    pub byte_span: ParserByteSpanV0,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct SourceScopeParentFactV0 {
115    pub child_kind: &'static str,
116    pub child_byte_span: ParserByteSpanV0,
117    pub parent_kind: &'static str,
118    pub parent_byte_span: ParserByteSpanV0,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct SourceBindingDeclFactV0 {
124    pub kind: &'static str,
125    pub name: String,
126    pub byte_span: ParserByteSpanV0,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub import_path: Option<String>,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct SourceScopeContainsDeclFactV0 {
134    pub scope_kind: &'static str,
135    pub scope_byte_span: ParserByteSpanV0,
136    pub decl_kind: &'static str,
137    pub decl_name: String,
138    pub decl_byte_span: ParserByteSpanV0,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub import_path: Option<String>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct SourceBindingStyleImportFactV0 {
146    pub local_name: String,
147    pub style_uri: String,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
151#[serde(rename_all = "camelCase")]
152pub struct SourceDeclaresStyleImportFactV0 {
153    pub decl_name: String,
154    pub styles_local_name: String,
155    pub style_uri: String,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
159#[serde(rename_all = "camelCase")]
160pub struct SourceStyleImportResolvesModuleFactV0 {
161    pub styles_local_name: String,
162    pub style_uri: String,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
166#[serde(rename_all = "camelCase")]
167pub struct SourceClassExpressionNodeFactV0 {
168    pub byte_span: ParserByteSpanV0,
169    pub kind: &'static str,
170    pub target_style_uri: String,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct SourceExpressionTargetsModuleFactV0 {
176    pub byte_span: ParserByteSpanV0,
177    pub target_style_uri: String,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
181#[serde(rename_all = "camelCase")]
182pub struct SourceClassnamesBindUtilityBindingFactV0 {
183    pub local_name: String,
184    pub styles_local_name: String,
185    pub style_uri: String,
186    pub classnames_import_name: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
190#[serde(rename_all = "camelCase")]
191pub struct SourceClassUtilityBindingFactV0 {
192    pub local_name: String,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
196#[serde(rename_all = "camelCase")]
197pub struct SourceDeclaresUtilityBindingFactV0 {
198    pub decl_name: String,
199    pub utility_local_name: String,
200    pub utility_kind: &'static str,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
204#[serde(rename_all = "camelCase")]
205pub struct SourceUtilityUsesStyleImportFactV0 {
206    pub utility_local_name: String,
207    pub styles_local_name: String,
208    pub style_uri: String,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
212#[serde(rename_all = "camelCase")]
213pub struct SourceStyleAccessUsesStyleImportFactV0 {
214    pub byte_span: ParserByteSpanV0,
215    pub decl_name: String,
216    pub styles_local_name: String,
217    pub style_uri: String,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
221#[serde(rename_all = "camelCase")]
222pub struct SourceSymbolRefUsesDeclFactV0 {
223    pub byte_span: ParserByteSpanV0,
224    pub raw_reference: String,
225    pub root_name: String,
226    pub decl_name: String,
227    pub style_uri: String,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
231#[serde(rename_all = "camelCase")]
232pub struct SourceModuleSpecifierFactV0 {
233    pub kind: &'static str,
234    pub specifier: String,
235    pub byte_span: ParserByteSpanV0,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
239#[serde(rename_all = "camelCase")]
240pub struct SourceStylePropertyAccessFactV0 {
241    pub byte_span: ParserByteSpanV0,
242    pub target_style_uri: Option<String>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
246#[serde(rename_all = "camelCase")]
247pub struct SourceInlineStyleDeclarationFactV0 {
248    pub byte_span: ParserByteSpanV0,
249    pub value_byte_span: Option<ParserByteSpanV0>,
250    pub property_name: String,
251    pub value: Option<String>,
252    pub target_style_uri: Option<String>,
253    pub cascade_tier: &'static str,
254    pub static_value: bool,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct SourceSelectorReferenceFactV0 {
260    pub byte_span: ParserByteSpanV0,
261    pub selector_name: Option<String>,
262    pub match_kind: SourceSelectorReferenceMatchKindV0,
263    pub target_style_uri: Option<String>,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
267#[serde(rename_all = "camelCase")]
268pub struct SourceTypeFactTargetV0 {
269    pub byte_span: ParserByteSpanV0,
270    pub expression_id: String,
271    pub target_style_uri: Option<String>,
272    pub prefix: String,
273    pub suffix: String,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
277#[serde(rename_all = "camelCase")]
278pub struct SourceTypeFactProviderUnavailableFactV0 {
279    pub byte_span: ParserByteSpanV0,
280    pub expression_id: String,
281    pub target_style_uri: Option<String>,
282    pub provider_id: &'static str,
283    pub reason: &'static str,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
287#[serde(rename_all = "camelCase")]
288pub struct SourceClassValueUniverseEntryV0 {
289    pub plugin_id: &'static str,
290    pub domain: &'static str,
291    pub owner_name: String,
292    pub class_names: Vec<String>,
293    pub axes: Vec<SourceClassValueUniverseAxisV0>,
294    #[serde(skip_serializing_if = "Vec::is_empty")]
295    pub patterns: Vec<SourceClassValuePatternV0>,
296    #[serde(skip_serializing_if = "Vec::is_empty")]
297    pub unresolved: Vec<SourceClassValueUnresolvedV0>,
298    pub byte_span: ParserByteSpanV0,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
302#[serde(rename_all = "camelCase")]
303pub struct SourceClassValueUniverseAxisV0 {
304    pub axis_name: String,
305    pub values: Vec<String>,
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
309#[serde(rename_all = "camelCase")]
310pub enum SourceClassValuePatternMatcherV0 {
311    PrefixSuffix,
312    RegexSource,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
316#[serde(rename_all = "camelCase")]
317pub struct SourceClassValuePatternV0 {
318    pub matcher: SourceClassValuePatternMatcherV0,
319    pub source: String,
320    pub completion_hint: String,
321    pub prefix: Option<String>,
322    pub suffix: Option<String>,
323}
324
325impl SourceClassValuePatternV0 {
326    pub fn matches(&self, class_name: &str) -> Option<bool> {
327        match self.matcher {
328            SourceClassValuePatternMatcherV0::PrefixSuffix => {
329                let prefix = self.prefix.as_deref().unwrap_or_default();
330                let suffix = self.suffix.as_deref().unwrap_or_default();
331                let Some(rest) = class_name.strip_prefix(prefix) else {
332                    return Some(false);
333                };
334                let Some(value) = rest.strip_suffix(suffix) else {
335                    return Some(false);
336                };
337                Some(!value.is_empty())
338            }
339            SourceClassValuePatternMatcherV0::RegexSource => None,
340        }
341    }
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
345#[serde(rename_all = "camelCase")]
346pub struct SourceClassValueUnresolvedV0 {
347    pub path: String,
348    pub reason: String,
349    pub detail: String,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
353#[serde(rename_all = "camelCase")]
354pub struct SourceDomainClassReferenceFactV0 {
355    pub byte_span: ParserByteSpanV0,
356    pub plugin_id: &'static str,
357    pub domain: &'static str,
358    pub owner_name: String,
359    pub axis_name: String,
360    pub option_name: Option<String>,
361    pub prefix: Option<String>,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
365#[serde(rename_all = "camelCase")]
366pub enum SourceSelectorReferenceMatchKindV0 {
367    Exact,
368    Prefix,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372struct SourceStyleBindingTarget {
373    binding: String,
374    target_style_uri: Option<String>,
375    binding_symbol_id: Option<SymbolId>,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
379struct ClassnamesBindUtilityBinding {
380    binding: String,
381    binding_symbol_id: SymbolId,
382    styles_binding: String,
383    style_uri: String,
384    classnames_import_binding: String,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq)]
388struct ClassnamesBindCallArgument {
389    binding: String,
390    binding_symbol_id: SymbolId,
391    byte_span: ParserByteSpanV0,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
395struct SymbolRefClassValueBinding {
396    classnames_binding_symbol_id: SymbolId,
397    byte_span: ParserByteSpanV0,
398    raw_reference: String,
399    root_name: String,
400    decl_name: String,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
404enum TemplateScanScope {
405    WholeDocument,
406    Ranges(Vec<(usize, usize)>),
407}
408
409impl TemplateScanScope {
410    fn as_ranges(&self) -> Option<&[(usize, usize)]> {
411        match self {
412            Self::WholeDocument => None,
413            Self::Ranges(ranges) => Some(ranges.as_slice()),
414        }
415    }
416}
417
418#[derive(Debug, Clone, Default, PartialEq, Eq)]
419struct SourceClassValue {
420    exact: Vec<String>,
421    prefixes: Vec<String>,
422}
423
424impl SourceClassValue {
425    fn is_empty(&self) -> bool {
426        self.exact.is_empty() && self.prefixes.is_empty()
427    }
428
429    fn merge(&mut self, other: SourceClassValue) {
430        self.exact.extend(other.exact);
431        self.prefixes.extend(other.prefixes);
432        self.canonicalize();
433    }
434
435    fn canonicalize(&mut self) {
436        self.exact.sort();
437        self.exact.dedup();
438        self.prefixes.sort();
439        self.prefixes.dedup();
440    }
441}
442
443type SourceReferenceDedupeKey = (
444    usize,
445    usize,
446    Option<String>,
447    SourceSelectorReferenceMatchKindV0,
448);
449type SourceReferenceTargetMap = BTreeMap<SourceReferenceDedupeKey, BTreeSet<Option<String>>>;
450
451pub fn summarize_omena_bridge_source_syntax_index(
452    source: &str,
453    imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
454    classnames_bind_bindings: Vec<String>,
455) -> SourceSyntaxIndexV0 {
456    summarize_omena_bridge_source_syntax_index_for_source_language(
457        "source.tsx",
458        source,
459        None,
460        imported_style_bindings,
461        classnames_bind_bindings,
462    )
463}
464
465pub fn summarize_omena_bridge_source_syntax_index_for_source_language(
466    source_path: &str,
467    source: &str,
468    source_language: Option<&str>,
469    imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
470    classnames_bind_bindings: Vec<String>,
471) -> SourceSyntaxIndexV0 {
472    let projected_source = project_source_for_language(source_path, source, source_language);
473    let imported_style_targets = imported_style_targets(imported_style_bindings.as_slice());
474    let property_access_targets = property_access_style_targets(imported_style_bindings.as_slice());
475    let ast_facts = collect_source_syntax_ast_facts(
476        source_path,
477        projected_source.as_ref(),
478        source_type_for_language(source_path, source_language),
479        property_access_targets.as_slice(),
480        imported_style_targets.as_slice(),
481        classnames_bind_bindings.as_slice(),
482    );
483    let class_string_literals = ast_facts.class_string_literals;
484    let style_property_accesses = ast_facts.style_property_accesses;
485    let class_name_expression_spans = ast_facts.class_name_expression_spans;
486    let classnames_bind_targets = ast_facts.classnames_bind_utility_bindings;
487    let classnames_bind_call_arguments = ast_facts.classnames_bind_call_arguments;
488    let local_class_values = collect_local_class_value_bindings(projected_source.as_ref());
489
490    let mut index = SourceSyntaxIndexV0 {
491        schema_version: "0",
492        product: "omena-bridge.source-syntax-index",
493        imported_style_bindings,
494        class_string_literals,
495        style_property_accesses,
496        inline_style_declarations: ast_facts.inline_style_declarations,
497        selector_references: Vec::new(),
498        type_fact_targets: Vec::new(),
499        type_fact_provider_unavailable: Vec::new(),
500        class_value_universes: ast_facts.class_value_universes,
501        domain_class_references: ast_facts.domain_class_references,
502        source_elements: ast_facts.source_elements,
503        element_parent_edges: ast_facts.element_parent_edges,
504    };
505
506    for span in &index.class_string_literals {
507        push_string_literal_selector_references(
508            source,
509            *span,
510            None,
511            &mut index.selector_references,
512        );
513    }
514    for span in class_name_expression_spans {
515        collect_selector_references_from_js_expression(
516            source,
517            span.start,
518            span.end,
519            None,
520            &local_class_values,
521            &mut index.selector_references,
522            &mut index.type_fact_targets,
523        );
524    }
525    for access in &index.style_property_accesses {
526        index
527            .selector_references
528            .push(SourceSelectorReferenceFactV0 {
529                byte_span: access.byte_span,
530                selector_name: None,
531                match_kind: SourceSelectorReferenceMatchKindV0::Exact,
532                target_style_uri: access.target_style_uri.clone(),
533            });
534    }
535    for argument in classnames_bind_call_arguments {
536        if let Some(binding) = classnames_bind_targets
537            .iter()
538            .find(|binding| binding.binding_symbol_id == argument.binding_symbol_id)
539        {
540            collect_selector_references_from_js_expression(
541                source,
542                argument.byte_span.start,
543                argument.byte_span.end,
544                Some(binding.style_uri.as_str()),
545                &local_class_values,
546                &mut index.selector_references,
547                &mut index.type_fact_targets,
548            );
549        }
550    }
551    collect_template_class_attribute_selector_references(
552        source_path,
553        source,
554        source_language,
555        &mut index.selector_references,
556    );
557    collect_template_class_expression_selector_references(
558        source_path,
559        source,
560        source_language,
561        imported_style_targets.as_slice(),
562        &mut index.selector_references,
563    );
564    canonicalize_source_selector_references(&mut index.selector_references);
565
566    index
567}
568
569pub(crate) fn summarize_source_control_flow_graph_with_semantic(
570    source_path: &str,
571    source: &str,
572    source_language: Option<&str>,
573    variable_name: &str,
574    reference_byte_offset: usize,
575) -> Option<crate::source_cfg::SourceControlFlowGraphCaptureV0> {
576    let projected_source = project_source_for_language(source_path, source, source_language);
577    let allocator = Allocator::default();
578    let ParserReturn {
579        program, panicked, ..
580    } = Parser::new(
581        &allocator,
582        projected_source.as_ref(),
583        source_type_for_language(source_path, source_language),
584    )
585    .parse();
586    if panicked {
587        return None;
588    }
589
590    let semantic = SemanticBuilder::new().build(&program).semantic;
591    crate::source_cfg::summarize_source_control_flow_graph_from_program(
592        &program,
593        semantic.scoping(),
594        variable_name,
595        reference_byte_offset,
596    )
597}
598
599pub fn summarize_omena_bridge_source_binding_index(
600    source: &str,
601    imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
602    classnames_bind_bindings: Vec<String>,
603) -> SourceBindingIndexV0 {
604    summarize_omena_bridge_source_binding_index_for_source_language(
605        "source.tsx",
606        source,
607        None,
608        imported_style_bindings,
609        classnames_bind_bindings,
610    )
611}
612
613pub fn summarize_omena_bridge_source_binding_index_for_source_language(
614    source_path: &str,
615    source: &str,
616    source_language: Option<&str>,
617    imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
618    classnames_bind_bindings: Vec<String>,
619) -> SourceBindingIndexV0 {
620    let syntax_index = summarize_omena_bridge_source_syntax_index_for_source_language(
621        source_path,
622        source,
623        source_language,
624        imported_style_bindings.clone(),
625        classnames_bind_bindings.clone(),
626    );
627    let projected_source = project_source_for_language(source_path, source, source_language);
628    let imported_style_targets = imported_style_targets(imported_style_bindings.as_slice());
629    let property_access_targets = property_access_style_targets(imported_style_bindings.as_slice());
630    let ast_facts = collect_source_syntax_ast_facts(
631        source_path,
632        projected_source.as_ref(),
633        source_type_for_language(source_path, source_language),
634        property_access_targets.as_slice(),
635        imported_style_targets.as_slice(),
636        classnames_bind_bindings.as_slice(),
637    );
638    let mut binding_scopes = ast_facts.binding_scopes;
639    binding_scopes.sort();
640    binding_scopes.dedup();
641    let mut scope_parent_edges = ast_facts.scope_parent_edges;
642    scope_parent_edges.sort();
643    scope_parent_edges.dedup();
644    let mut binding_decls = ast_facts.binding_decls;
645    binding_decls.sort();
646    binding_decls.dedup();
647    let mut scope_contains_decls = ast_facts.scope_contains_decls;
648    scope_contains_decls.sort();
649    scope_contains_decls.dedup();
650    let mut style_import_bindings = imported_style_targets
651        .iter()
652        .filter_map(|target| {
653            target
654                .target_style_uri
655                .as_ref()
656                .map(|style_uri| SourceBindingStyleImportFactV0 {
657                    local_name: target.binding.clone(),
658                    style_uri: style_uri.clone(),
659                })
660        })
661        .collect::<Vec<_>>();
662    style_import_bindings.sort();
663    style_import_bindings.dedup();
664    let style_import_local_names_by_uri =
665        style_import_local_names_by_uri(style_import_bindings.as_slice());
666    let mut declares_style_imports = style_import_bindings
667        .iter()
668        .map(|binding| SourceDeclaresStyleImportFactV0 {
669            decl_name: binding.local_name.clone(),
670            styles_local_name: binding.local_name.clone(),
671            style_uri: binding.style_uri.clone(),
672        })
673        .collect::<Vec<_>>();
674    declares_style_imports.sort();
675    declares_style_imports.dedup();
676    let mut style_import_resolves_modules = style_import_bindings
677        .iter()
678        .map(|binding| SourceStyleImportResolvesModuleFactV0 {
679            styles_local_name: binding.local_name.clone(),
680            style_uri: binding.style_uri.clone(),
681        })
682        .collect::<Vec<_>>();
683    style_import_resolves_modules.sort();
684    style_import_resolves_modules.dedup();
685    let classnames_bind_targets = ast_facts.classnames_bind_utility_bindings.clone();
686    let style_access_expression_keys = syntax_index
687        .style_property_accesses
688        .iter()
689        .filter_map(|access| {
690            access.target_style_uri.as_ref().map(|style_uri| {
691                (
692                    access.byte_span.start,
693                    access.byte_span.end,
694                    style_uri.clone(),
695                )
696            })
697        })
698        .collect::<BTreeSet<_>>();
699    let symbol_ref_expression_keys = ast_facts
700        .symbol_ref_class_value_bindings
701        .iter()
702        .filter_map(|reference| {
703            let binding = classnames_bind_targets.iter().find(|binding| {
704                binding.binding_symbol_id == reference.classnames_binding_symbol_id
705            })?;
706            Some((
707                reference.byte_span.start,
708                reference.byte_span.end,
709                binding.style_uri.clone(),
710            ))
711        })
712        .collect::<BTreeSet<_>>();
713    let mut class_expression_nodes = syntax_index
714        .selector_references
715        .iter()
716        .filter_map(|reference| {
717            let target_style_uri = reference.target_style_uri.clone()?;
718            let expression_key = (
719                reference.byte_span.start,
720                reference.byte_span.end,
721                target_style_uri.clone(),
722            );
723            let kind = if style_access_expression_keys.contains(&expression_key) {
724                "styleAccess"
725            } else if symbol_ref_expression_keys.contains(&expression_key) {
726                "symbolRef"
727            } else {
728                match reference.match_kind {
729                    SourceSelectorReferenceMatchKindV0::Exact => "literal",
730                    SourceSelectorReferenceMatchKindV0::Prefix => "template",
731                }
732            };
733            Some(SourceClassExpressionNodeFactV0 {
734                kind,
735                byte_span: reference.byte_span,
736                target_style_uri,
737            })
738        })
739        .collect::<Vec<_>>();
740    class_expression_nodes.extend(ast_facts.symbol_ref_class_value_bindings.iter().filter_map(
741        |reference| {
742            let binding = classnames_bind_targets.iter().find(|binding| {
743                binding.binding_symbol_id == reference.classnames_binding_symbol_id
744            })?;
745            Some(SourceClassExpressionNodeFactV0 {
746                kind: "symbolRef",
747                byte_span: reference.byte_span,
748                target_style_uri: binding.style_uri.clone(),
749            })
750        },
751    ));
752    class_expression_nodes.sort();
753    class_expression_nodes.dedup();
754    let mut expression_targets_modules = syntax_index
755        .selector_references
756        .iter()
757        .filter_map(|reference| {
758            reference.target_style_uri.clone().map(|target_style_uri| {
759                SourceExpressionTargetsModuleFactV0 {
760                    byte_span: reference.byte_span,
761                    target_style_uri,
762                }
763            })
764        })
765        .collect::<Vec<_>>();
766    expression_targets_modules.extend(ast_facts.symbol_ref_class_value_bindings.iter().filter_map(
767        |reference| {
768            let binding = classnames_bind_targets.iter().find(|binding| {
769                binding.binding_symbol_id == reference.classnames_binding_symbol_id
770            })?;
771            Some(SourceExpressionTargetsModuleFactV0 {
772                byte_span: reference.byte_span,
773                target_style_uri: binding.style_uri.clone(),
774            })
775        },
776    ));
777    expression_targets_modules.sort();
778    expression_targets_modules.dedup();
779    let mut classnames_bind_utility_bindings = ast_facts
780        .classnames_bind_utility_bindings
781        .into_iter()
782        .map(|binding| SourceClassnamesBindUtilityBindingFactV0 {
783            local_name: binding.binding,
784            styles_local_name: binding.styles_binding,
785            style_uri: binding.style_uri,
786            classnames_import_name: binding.classnames_import_binding,
787        })
788        .collect::<Vec<_>>();
789    classnames_bind_utility_bindings.sort();
790    classnames_bind_utility_bindings.dedup();
791    let mut class_util_bindings = binding_decls
792        .iter()
793        .filter_map(|decl| {
794            let import_path = decl.import_path.as_deref()?;
795            if decl.kind == "import" && is_class_utility_import_path(import_path) {
796                Some(SourceClassUtilityBindingFactV0 {
797                    local_name: decl.name.clone(),
798                })
799            } else {
800                None
801            }
802        })
803        .collect::<Vec<_>>();
804    class_util_bindings.sort();
805    class_util_bindings.dedup();
806    let mut declares_utility_bindings = classnames_bind_utility_bindings
807        .iter()
808        .map(|binding| SourceDeclaresUtilityBindingFactV0 {
809            decl_name: binding.local_name.clone(),
810            utility_local_name: binding.local_name.clone(),
811            utility_kind: "classnamesBind",
812        })
813        .collect::<Vec<_>>();
814    declares_utility_bindings.extend(class_util_bindings.iter().map(|binding| {
815        SourceDeclaresUtilityBindingFactV0 {
816            decl_name: binding.local_name.clone(),
817            utility_local_name: binding.local_name.clone(),
818            utility_kind: "classUtil",
819        }
820    }));
821    declares_utility_bindings.sort();
822    declares_utility_bindings.dedup();
823    let mut utility_uses_style_imports = classnames_bind_utility_bindings
824        .iter()
825        .map(|binding| SourceUtilityUsesStyleImportFactV0 {
826            utility_local_name: binding.local_name.clone(),
827            styles_local_name: binding.styles_local_name.clone(),
828            style_uri: binding.style_uri.clone(),
829        })
830        .collect::<Vec<_>>();
831    utility_uses_style_imports.sort();
832    utility_uses_style_imports.dedup();
833    let mut style_access_uses_style_imports = syntax_index
834        .style_property_accesses
835        .iter()
836        .filter_map(|access| {
837            let style_uri = access.target_style_uri.as_ref()?;
838            let local_names = style_import_local_names_by_uri.get(style_uri)?;
839            let styles_local_name = single_btree_set_item(local_names)?;
840            Some(SourceStyleAccessUsesStyleImportFactV0 {
841                byte_span: access.byte_span,
842                decl_name: styles_local_name.clone(),
843                styles_local_name: styles_local_name.clone(),
844                style_uri: style_uri.clone(),
845            })
846        })
847        .collect::<Vec<_>>();
848    style_access_uses_style_imports.sort();
849    style_access_uses_style_imports.dedup();
850    let mut symbol_ref_uses_decls = ast_facts
851        .symbol_ref_class_value_bindings
852        .into_iter()
853        .filter_map(|reference| {
854            let binding = classnames_bind_targets.iter().find(|binding| {
855                binding.binding_symbol_id == reference.classnames_binding_symbol_id
856            })?;
857            Some(SourceSymbolRefUsesDeclFactV0 {
858                byte_span: reference.byte_span,
859                raw_reference: reference.raw_reference,
860                root_name: reference.root_name,
861                decl_name: reference.decl_name,
862                style_uri: binding.style_uri.clone(),
863            })
864        })
865        .collect::<Vec<_>>();
866    symbol_ref_uses_decls.sort();
867    symbol_ref_uses_decls.dedup();
868    let mut module_specifiers = ast_facts.module_specifiers;
869    module_specifiers.sort();
870    module_specifiers.dedup();
871
872    SourceBindingIndexV0 {
873        schema_version: "0",
874        product: "omena-bridge.source-binding-index",
875        binding_scopes,
876        scope_parent_edges,
877        binding_decls,
878        scope_contains_decls,
879        style_import_bindings,
880        declares_style_imports,
881        style_import_resolves_modules,
882        class_expression_nodes,
883        expression_targets_modules,
884        classnames_bind_utility_bindings,
885        class_util_bindings,
886        declares_utility_bindings,
887        utility_uses_style_imports,
888        style_access_uses_style_imports,
889        symbol_ref_uses_decls,
890        module_specifiers,
891    }
892}
893
894pub fn collect_omena_bridge_vue_style_module_bindings(
895    source_path: &str,
896    source: &str,
897    source_language: Option<&str>,
898) -> Vec<String> {
899    let projected_source = project_source_for_language(source_path, source, source_language);
900    let allocator = Allocator::default();
901    let ParserReturn {
902        program, panicked, ..
903    } = Parser::new(
904        &allocator,
905        projected_source.as_ref(),
906        source_type_for_language(source_path, source_language),
907    )
908    .parse();
909    if panicked {
910        return Vec::new();
911    }
912    collect_vue_use_css_module_bindings(&program)
913}
914
915pub fn canonicalize_source_selector_references(
916    references: &mut Vec<SourceSelectorReferenceFactV0>,
917) {
918    let mut targets_by_reference: SourceReferenceTargetMap = BTreeMap::new();
919    for reference in references.iter() {
920        targets_by_reference
921            .entry((
922                reference.byte_span.start,
923                reference.byte_span.end,
924                reference.selector_name.clone(),
925                reference.match_kind,
926            ))
927            .or_default()
928            .insert(reference.target_style_uri.clone());
929    }
930
931    let mut canonical = Vec::new();
932    for ((start, end, selector_name, match_kind), targets) in targets_by_reference {
933        let has_targeted_reference = targets.iter().any(Option::is_some);
934        for target_style_uri in targets {
935            if has_targeted_reference && target_style_uri.is_none() {
936                continue;
937            }
938            canonical.push(SourceSelectorReferenceFactV0 {
939                byte_span: ParserByteSpanV0 { start, end },
940                selector_name: selector_name.clone(),
941                match_kind,
942                target_style_uri,
943            });
944        }
945    }
946    *references = canonical;
947}
948
949fn collect_template_class_attribute_selector_references(
950    source_path: &str,
951    source: &str,
952    source_language: Option<&str>,
953    references: &mut Vec<SourceSelectorReferenceFactV0>,
954) {
955    let Some(scan_scope) = template_source_scan_scope(source_path, source, source_language) else {
956        return;
957    };
958    let delimiter_family = server_template_delimiter_family(source_path, source_language);
959
960    let mut suppressed_ranges = tag_content_ranges(source, "<script", "</script>");
961    suppressed_ranges.extend(tag_content_ranges(source, "<style", "</style>"));
962    suppressed_ranges.sort_unstable();
963
964    for value_span in template_class_attribute_value_spans(
965        source,
966        scan_scope.as_ranges(),
967        suppressed_ranges.as_slice(),
968        delimiter_family.is_some(),
969    ) {
970        if let Some(family) = delimiter_family {
971            push_server_template_class_attribute_selector_references(
972                source, value_span, family, references,
973            );
974        } else {
975            push_string_literal_selector_references(source, value_span, None, references);
976        }
977    }
978}
979
980fn collect_template_class_expression_selector_references(
981    source_path: &str,
982    source: &str,
983    source_language: Option<&str>,
984    style_targets: &[SourceStyleBindingTarget],
985    references: &mut Vec<SourceSelectorReferenceFactV0>,
986) {
987    if style_targets.is_empty() {
988        return;
989    }
990    let Some(scan_scope) = template_source_scan_scope(source_path, source, source_language) else {
991        return;
992    };
993
994    let mut suppressed_ranges = tag_content_ranges(source, "<script", "</script>");
995    suppressed_ranges.extend(tag_content_ranges(source, "<style", "</style>"));
996    suppressed_ranges.sort_unstable();
997
998    for expression_span in template_class_expression_spans(
999        source,
1000        scan_scope.as_ranges(),
1001        suppressed_ranges.as_slice(),
1002    ) {
1003        for target in style_targets {
1004            push_style_binding_selector_references_from_expression(
1005                source,
1006                expression_span,
1007                target,
1008                references,
1009            );
1010        }
1011    }
1012}
1013
1014fn is_html_like_template_source(source_path: &str, source_language: Option<&str>) -> bool {
1015    is_vue_source(source_path, source_language)
1016        || is_html_source(source_path, source_language)
1017        || is_svelte_source(source_path, source_language)
1018        || is_astro_source(source_path, source_language)
1019        || is_server_template_source(source_path, source_language)
1020}
1021
1022fn template_source_scan_scope(
1023    source_path: &str,
1024    source: &str,
1025    source_language: Option<&str>,
1026) -> Option<TemplateScanScope> {
1027    if is_html_like_template_source(source_path, source_language) {
1028        return Some(TemplateScanScope::WholeDocument);
1029    }
1030    if is_markdown_source(source_path, source_language) {
1031        return Some(TemplateScanScope::Ranges(markdown_html_template_ranges(
1032            source,
1033        )));
1034    }
1035    None
1036}
1037
1038fn template_class_attribute_value_spans(
1039    source: &str,
1040    scan_ranges: Option<&[(usize, usize)]>,
1041    suppressed_ranges: &[(usize, usize)],
1042    allow_server_template_interpolation: bool,
1043) -> Vec<ParserByteSpanV0> {
1044    let lower = source.to_ascii_lowercase();
1045    let bytes = source.as_bytes();
1046    let mut cursor = 0usize;
1047    let mut spans = Vec::new();
1048
1049    while let Some(relative_start) = lower[cursor..].find("class") {
1050        let attr_start = cursor + relative_start;
1051        cursor = attr_start + "class".len();
1052        if !byte_in_optional_ranges(attr_start, scan_ranges)
1053            || byte_in_ranges(attr_start, suppressed_ranges)
1054            || !is_template_class_attribute_name(source, attr_start)
1055        {
1056            continue;
1057        }
1058
1059        let mut index = skip_ascii_whitespace_bytes(bytes, attr_start + "class".len());
1060        if bytes.get(index) != Some(&b'=') {
1061            continue;
1062        }
1063        index = skip_ascii_whitespace_bytes(bytes, index + 1);
1064        let Some((value_start, value_end)) = template_attribute_value_span(bytes, index) else {
1065            continue;
1066        };
1067        if value_start < value_end
1068            && (allow_server_template_interpolation
1069                || is_static_template_class_attribute_value(source, value_start, value_end))
1070        {
1071            spans.push(ParserByteSpanV0 {
1072                start: value_start,
1073                end: value_end,
1074            });
1075        }
1076        cursor = value_end;
1077    }
1078
1079    spans
1080}
1081
1082fn template_class_expression_spans(
1083    source: &str,
1084    scan_ranges: Option<&[(usize, usize)]>,
1085    suppressed_ranges: &[(usize, usize)],
1086) -> Vec<ParserByteSpanV0> {
1087    let lower = source.to_ascii_lowercase();
1088    let bytes = source.as_bytes();
1089    let mut cursor = 0usize;
1090    let mut spans = Vec::new();
1091
1092    while let Some(relative_start) = lower[cursor..].find("class") {
1093        let attr_start = cursor + relative_start;
1094        cursor = attr_start + "class".len();
1095        if !byte_in_optional_ranges(attr_start, scan_ranges)
1096            || byte_in_ranges(attr_start, suppressed_ranges)
1097        {
1098            continue;
1099        }
1100
1101        let is_dynamic_attr = is_template_dynamic_class_attribute_name(source, attr_start);
1102        let is_literal_attr = is_template_class_attribute_name(source, attr_start);
1103        if !is_dynamic_attr && !is_literal_attr {
1104            continue;
1105        }
1106
1107        let mut index = skip_ascii_whitespace_bytes(bytes, attr_start + "class".len());
1108        if bytes.get(index) != Some(&b'=') {
1109            continue;
1110        }
1111        index = skip_ascii_whitespace_bytes(bytes, index + 1);
1112        let Some((value_start, value_end)) = template_attribute_value_span(bytes, index) else {
1113            continue;
1114        };
1115        let expression_span = if is_dynamic_attr {
1116            ParserByteSpanV0 {
1117                start: value_start,
1118                end: value_end,
1119            }
1120        } else if value_start < value_end
1121            && bytes.get(value_start) == Some(&b'{')
1122            && bytes.get(value_end - 1) == Some(&b'}')
1123        {
1124            ParserByteSpanV0 {
1125                start: value_start + 1,
1126                end: value_end - 1,
1127            }
1128        } else {
1129            continue;
1130        };
1131        let (start, end) = trim_js_expression(source, expression_span.start, expression_span.end);
1132        if start < end {
1133            spans.push(ParserByteSpanV0 { start, end });
1134        }
1135        cursor = value_end;
1136    }
1137
1138    spans
1139}
1140
1141fn markdown_html_template_ranges(source: &str) -> Vec<(usize, usize)> {
1142    let mut ranges = Vec::new();
1143    let mut open_fence: Option<(char, usize)> = None;
1144    let mut open_html_start: Option<usize> = None;
1145    let mut offset = 0usize;
1146
1147    for line in source.split_inclusive('\n') {
1148        let line_start = offset;
1149        let line_end = offset + line.len();
1150        let line_without_newline = line.trim_end_matches(['\r', '\n']);
1151        let leading_spaces = line_without_newline
1152            .chars()
1153            .take_while(|ch| *ch == ' ')
1154            .count();
1155        let trimmed = line_without_newline.trim_start_matches(' ');
1156        if leading_spaces <= 3 {
1157            if let Some((fence_char, fence_len)) = open_fence {
1158                if markdown_fence_marker_for_template_scan(trimmed).is_some_and(
1159                    |(candidate_char, candidate_len)| {
1160                        candidate_char == fence_char && candidate_len >= fence_len
1161                    },
1162                ) {
1163                    open_fence = None;
1164                }
1165                offset = line_end;
1166                continue;
1167            }
1168            if let Some((fence_char, fence_len)) = markdown_fence_marker_for_template_scan(trimmed)
1169            {
1170                open_fence = Some((fence_char, fence_len));
1171                offset = line_end;
1172                continue;
1173            }
1174        }
1175
1176        if let Some(start) = open_html_start {
1177            if trimmed.contains('>') {
1178                ranges.push((start, line_end));
1179                open_html_start = None;
1180            }
1181            offset = line_end;
1182            continue;
1183        }
1184
1185        if leading_spaces >= 4 || trimmed.is_empty() {
1186            offset = line_end;
1187            continue;
1188        }
1189
1190        if markdown_line_starts_html_tag(trimmed) {
1191            if trimmed.contains('>') {
1192                ranges.push((line_start, line_end));
1193            } else {
1194                open_html_start = Some(line_start);
1195            }
1196        }
1197
1198        offset = line_end;
1199    }
1200
1201    if let Some(start) = open_html_start {
1202        ranges.push((start, source.len()));
1203    }
1204    ranges
1205}
1206
1207fn markdown_line_starts_html_tag(trimmed_line: &str) -> bool {
1208    let mut chars = trimmed_line.chars();
1209    if chars.next() != Some('<') {
1210        return false;
1211    }
1212    match chars.next() {
1213        Some('/') => chars.next().is_some_and(is_html_tag_name_start),
1214        Some('!') | Some('?') => false,
1215        Some(ch) => is_html_tag_name_start(ch),
1216        None => false,
1217    }
1218}
1219
1220fn is_html_tag_name_start(ch: char) -> bool {
1221    ch.is_ascii_alphabetic()
1222}
1223
1224fn markdown_fence_marker_for_template_scan(line: &str) -> Option<(char, usize)> {
1225    let mut chars = line.chars();
1226    let fence_char = chars.next()?;
1227    if fence_char != '`' && fence_char != '~' {
1228        return None;
1229    }
1230    let fence_len = 1 + chars.take_while(|ch| *ch == fence_char).count();
1231    if fence_len >= 3 {
1232        Some((fence_char, fence_len))
1233    } else {
1234        None
1235    }
1236}
1237
1238fn is_template_class_attribute_name(source: &str, attr_start: usize) -> bool {
1239    let bytes = source.as_bytes();
1240    let before = attr_start
1241        .checked_sub(1)
1242        .and_then(|index| bytes.get(index))
1243        .copied();
1244    if before.is_some_and(is_html_attribute_name_byte) {
1245        return false;
1246    }
1247    let after = attr_start + "class".len();
1248    bytes
1249        .get(after)
1250        .is_none_or(|byte| !is_html_attribute_name_byte(*byte))
1251}
1252
1253fn is_template_dynamic_class_attribute_name(source: &str, attr_start: usize) -> bool {
1254    let bytes = source.as_bytes();
1255    if attr_start >= 1
1256        && bytes.get(attr_start - 1) == Some(&b':')
1257        && attr_start
1258            .checked_sub(2)
1259            .and_then(|index| bytes.get(index))
1260            .is_none_or(|byte| !is_html_attribute_name_byte(*byte))
1261    {
1262        return true;
1263    }
1264    let prefix = "v-bind:";
1265    if attr_start < prefix.len() {
1266        return false;
1267    }
1268    let prefix_start = attr_start - prefix.len();
1269    source
1270        .get(prefix_start..attr_start)
1271        .is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
1272        && prefix_start
1273            .checked_sub(1)
1274            .and_then(|index| bytes.get(index))
1275            .is_none_or(|byte| !is_html_attribute_name_byte(*byte))
1276}
1277
1278fn is_html_attribute_name_byte(byte: u8) -> bool {
1279    byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'-' | b'_' | b'.')
1280}
1281
1282fn skip_ascii_whitespace_bytes(bytes: &[u8], mut index: usize) -> usize {
1283    while bytes.get(index).is_some_and(u8::is_ascii_whitespace) {
1284        index += 1;
1285    }
1286    index
1287}
1288
1289fn template_attribute_value_span(bytes: &[u8], value_start: usize) -> Option<(usize, usize)> {
1290    let quote = *bytes.get(value_start)?;
1291    if quote == b'\'' || quote == b'"' {
1292        let content_start = value_start + 1;
1293        let relative_end = bytes
1294            .get(content_start..)?
1295            .iter()
1296            .position(|byte| *byte == quote)?;
1297        return Some((content_start, content_start + relative_end));
1298    }
1299
1300    let relative_end = bytes
1301        .get(value_start..)?
1302        .iter()
1303        .position(|byte| byte.is_ascii_whitespace() || *byte == b'>')
1304        .unwrap_or_else(|| bytes.len().saturating_sub(value_start));
1305    Some((value_start, value_start + relative_end))
1306}
1307
1308fn is_static_template_class_attribute_value(source: &str, start: usize, end: usize) -> bool {
1309    source
1310        .get(start..end)
1311        .is_some_and(|value| !value.contains(['{', '}']))
1312}
1313
1314fn push_server_template_class_attribute_selector_references(
1315    source: &str,
1316    literal_span: ParserByteSpanV0,
1317    family: ServerTemplateDelimiterFamilyV0,
1318    references: &mut Vec<SourceSelectorReferenceFactV0>,
1319) {
1320    let interpolation_ranges =
1321        server_template_interpolation_ranges(source, literal_span.start, literal_span.end, family);
1322    if interpolation_ranges.is_empty() {
1323        if is_static_template_class_attribute_value(source, literal_span.start, literal_span.end) {
1324            push_string_literal_selector_references(source, literal_span, None, references);
1325        }
1326        return;
1327    }
1328
1329    for span in class_token_byte_spans(source, literal_span.start, literal_span.end) {
1330        if token_intersects_dynamic_template_segment(source, span, interpolation_ranges.as_slice())
1331        {
1332            continue;
1333        }
1334        references.push(SourceSelectorReferenceFactV0 {
1335            byte_span: span,
1336            selector_name: None,
1337            match_kind: SourceSelectorReferenceMatchKindV0::Exact,
1338            target_style_uri: None,
1339        });
1340    }
1341}
1342
1343fn server_template_interpolation_ranges(
1344    source: &str,
1345    value_start: usize,
1346    value_end: usize,
1347    family: ServerTemplateDelimiterFamilyV0,
1348) -> Vec<ParserByteSpanV0> {
1349    match family {
1350        ServerTemplateDelimiterFamilyV0::LiquidLike => collect_delimited_template_ranges(
1351            source,
1352            value_start,
1353            value_end,
1354            &[("{{", "}}"), ("{%", "%}"), ("{#", "#}")],
1355        ),
1356        ServerTemplateDelimiterFamilyV0::ErbLike => {
1357            collect_delimited_template_ranges(source, value_start, value_end, &[("<%", "%>")])
1358        }
1359        ServerTemplateDelimiterFamilyV0::Handlebars => collect_delimited_template_ranges(
1360            source,
1361            value_start,
1362            value_end,
1363            &[("{{{", "}}}"), ("{{", "}}")],
1364        ),
1365    }
1366}
1367
1368fn collect_delimited_template_ranges(
1369    source: &str,
1370    value_start: usize,
1371    value_end: usize,
1372    delimiters: &[(&'static str, &'static str)],
1373) -> Vec<ParserByteSpanV0> {
1374    let mut ranges = Vec::new();
1375    let mut cursor = value_start;
1376    while cursor < value_end {
1377        let Some((range_start, open, close)) =
1378            next_template_delimiter(source, cursor, value_end, delimiters)
1379        else {
1380            break;
1381        };
1382        let content_start = range_start + open.len();
1383        let Some(relative_end) = source
1384            .get(content_start..value_end)
1385            .and_then(|value| value.find(close))
1386        else {
1387            break;
1388        };
1389        let range_end = content_start + relative_end + close.len();
1390        ranges.push(ParserByteSpanV0 {
1391            start: range_start,
1392            end: range_end,
1393        });
1394        cursor = range_end;
1395    }
1396    ranges
1397}
1398
1399fn next_template_delimiter(
1400    source: &str,
1401    cursor: usize,
1402    limit: usize,
1403    delimiters: &[(&'static str, &'static str)],
1404) -> Option<(usize, &'static str, &'static str)> {
1405    let haystack = source.get(cursor..limit)?;
1406    delimiters
1407        .iter()
1408        .filter_map(|(open, close)| {
1409            haystack
1410                .find(open)
1411                .map(|relative_start| (cursor + relative_start, *open, *close))
1412        })
1413        .min_by(|(left_start, left_open, _), (right_start, right_open, _)| {
1414            left_start
1415                .cmp(right_start)
1416                .then_with(|| right_open.len().cmp(&left_open.len()))
1417        })
1418}
1419
1420fn token_intersects_dynamic_template_segment(
1421    source: &str,
1422    token_span: ParserByteSpanV0,
1423    interpolation_ranges: &[ParserByteSpanV0],
1424) -> bool {
1425    interpolation_ranges.iter().any(|range| {
1426        spans_overlap(token_span, *range)
1427            || adjacent_without_ascii_whitespace(source, token_span.end, range.start)
1428            || adjacent_without_ascii_whitespace(source, range.end, token_span.start)
1429    })
1430}
1431
1432fn spans_overlap(left: ParserByteSpanV0, right: ParserByteSpanV0) -> bool {
1433    left.start < right.end && right.start < left.end
1434}
1435
1436fn adjacent_without_ascii_whitespace(source: &str, left_end: usize, right_start: usize) -> bool {
1437    if left_end > right_start {
1438        return false;
1439    }
1440    source
1441        .get(left_end..right_start)
1442        .is_some_and(|between| !between.chars().any(|ch| ch.is_ascii_whitespace()))
1443}
1444
1445fn push_style_binding_selector_references_from_expression(
1446    source: &str,
1447    expression_span: ParserByteSpanV0,
1448    target: &SourceStyleBindingTarget,
1449    references: &mut Vec<SourceSelectorReferenceFactV0>,
1450) {
1451    let Some(expression) = source.get(expression_span.start..expression_span.end) else {
1452        return;
1453    };
1454    let mut cursor = 0usize;
1455    while let Some(relative_start) = expression[cursor..].find(target.binding.as_str()) {
1456        let binding_start = expression_span.start + cursor + relative_start;
1457        let binding_end = binding_start + target.binding.len();
1458        cursor += relative_start + target.binding.len();
1459        if !is_js_identifier_boundary(source, binding_start, binding_end) {
1460            continue;
1461        }
1462
1463        let access_start = skip_ascii_whitespace(source, binding_end);
1464        if source.as_bytes().get(access_start) == Some(&b'.') {
1465            let property_start = skip_ascii_whitespace(source, access_start + 1);
1466            if let Some((_, property_end)) = read_js_identifier(source, property_start) {
1467                let span = ParserByteSpanV0 {
1468                    start: property_start,
1469                    end: property_end,
1470                };
1471                if source[span.start..span.end]
1472                    .chars()
1473                    .all(is_css_identifier_continue)
1474                {
1475                    push_selector_reference(
1476                        span,
1477                        Some(source[span.start..span.end].to_string()),
1478                        SourceSelectorReferenceMatchKindV0::Exact,
1479                        target.target_style_uri.as_deref(),
1480                        references,
1481                    );
1482                }
1483            }
1484            continue;
1485        }
1486
1487        if let Some((literal_start, literal_end, _)) =
1488            bracket_string_literal_access(source, access_start)
1489        {
1490            push_selector_reference(
1491                ParserByteSpanV0 {
1492                    start: literal_start,
1493                    end: literal_end,
1494                },
1495                Some(source[literal_start..literal_end].to_string()),
1496                SourceSelectorReferenceMatchKindV0::Exact,
1497                target.target_style_uri.as_deref(),
1498                references,
1499            );
1500        }
1501    }
1502}
1503
1504fn is_js_identifier_boundary(source: &str, start: usize, end: usize) -> bool {
1505    let before = start
1506        .checked_sub(1)
1507        .and_then(|index| source.get(index..start))
1508        .and_then(|text| text.chars().next());
1509    let after = source.get(end..).and_then(|text| text.chars().next());
1510    before.is_none_or(|ch| !is_js_identifier_continue(ch))
1511        && after.is_none_or(|ch| !is_js_identifier_continue(ch))
1512}
1513
1514fn byte_in_ranges(byte_offset: usize, ranges: &[(usize, usize)]) -> bool {
1515    ranges
1516        .iter()
1517        .any(|(start, end)| byte_offset >= *start && byte_offset < *end)
1518}
1519
1520fn byte_in_optional_ranges(byte_offset: usize, ranges: Option<&[(usize, usize)]>) -> bool {
1521    ranges.is_none_or(|ranges| byte_in_ranges(byte_offset, ranges))
1522}
1523
1524fn imported_style_targets(
1525    bindings: &[SourceImportedStyleBindingV0],
1526) -> Vec<SourceStyleBindingTarget> {
1527    bindings
1528        .iter()
1529        .map(|binding| SourceStyleBindingTarget {
1530            binding: binding.binding.clone(),
1531            target_style_uri: Some(binding.style_uri.clone()),
1532            binding_symbol_id: None,
1533        })
1534        .collect()
1535}
1536
1537fn property_access_style_targets(
1538    bindings: &[SourceImportedStyleBindingV0],
1539) -> Vec<SourceStyleBindingTarget> {
1540    let imported = imported_style_targets(bindings);
1541    if imported.is_empty() {
1542        vec![SourceStyleBindingTarget {
1543            binding: "styles".to_string(),
1544            target_style_uri: None,
1545            binding_symbol_id: None,
1546        }]
1547    } else {
1548        imported
1549    }
1550}
1551
1552fn source_style_targets_with_symbols(
1553    targets: &[SourceStyleBindingTarget],
1554    program: &Program<'_>,
1555) -> Vec<SourceStyleBindingTarget> {
1556    let import_symbols = import_local_symbol_ids_by_name(program);
1557    let local_symbols = top_level_local_symbol_ids_by_name(program);
1558    targets
1559        .iter()
1560        .map(|target| SourceStyleBindingTarget {
1561            binding: target.binding.clone(),
1562            target_style_uri: target.target_style_uri.clone(),
1563            binding_symbol_id: import_symbols
1564                .get(target.binding.as_str())
1565                .or_else(|| local_symbols.get(target.binding.as_str()))
1566                .copied(),
1567        })
1568        .collect()
1569}
1570
1571fn classnames_bind_import_symbol_ids(
1572    program: &Program<'_>,
1573    classnames_bind_imports: &[String],
1574) -> BTreeSet<SymbolId> {
1575    let import_symbols = import_local_symbol_ids_by_name(program);
1576    classnames_bind_imports
1577        .iter()
1578        .filter_map(|binding| import_symbols.get(binding.as_str()).copied())
1579        .collect()
1580}
1581
1582fn import_local_symbol_ids_by_name(program: &Program<'_>) -> BTreeMap<String, SymbolId> {
1583    let mut symbols = BTreeMap::new();
1584    for statement in &program.body {
1585        let Statement::ImportDeclaration(import) = statement else {
1586            continue;
1587        };
1588        collect_import_local_symbol_ids(import, &mut symbols);
1589    }
1590    symbols
1591}
1592
1593fn top_level_local_symbol_ids_by_name(program: &Program<'_>) -> BTreeMap<String, SymbolId> {
1594    let mut symbols = BTreeMap::new();
1595    for statement in &program.body {
1596        collect_top_level_local_symbol_ids_from_statement(statement, &mut symbols);
1597    }
1598    symbols
1599}
1600
1601fn collect_top_level_local_symbol_ids_from_statement(
1602    statement: &Statement<'_>,
1603    symbols: &mut BTreeMap<String, SymbolId>,
1604) {
1605    match statement {
1606        Statement::VariableDeclaration(declaration) => {
1607            collect_top_level_local_symbol_ids_from_variable_declaration(declaration, symbols);
1608        }
1609        Statement::ExportNamedDeclaration(declaration) => {
1610            if let Some(Declaration::VariableDeclaration(declaration)) = &declaration.declaration {
1611                collect_top_level_local_symbol_ids_from_variable_declaration(declaration, symbols);
1612            }
1613        }
1614        _ => {}
1615    }
1616}
1617
1618fn collect_top_level_local_symbol_ids_from_variable_declaration(
1619    declaration: &oxc_ast::ast::VariableDeclaration<'_>,
1620    symbols: &mut BTreeMap<String, SymbolId>,
1621) {
1622    for declarator in &declaration.declarations {
1623        if let Some(identifier) = binding_pattern_identifier(&declarator.id)
1624            && let Some(symbol_id) = binding_identifier_symbol_id(identifier)
1625        {
1626            symbols.insert(identifier.name.as_str().to_string(), symbol_id);
1627        }
1628    }
1629}
1630
1631fn collect_import_local_symbol_ids(
1632    import: &ImportDeclaration<'_>,
1633    symbols: &mut BTreeMap<String, SymbolId>,
1634) {
1635    if import.import_kind != ImportOrExportKind::Value {
1636        return;
1637    }
1638    let Some(specifiers) = import.specifiers.as_ref() else {
1639        return;
1640    };
1641    for specifier in specifiers {
1642        let local = match specifier {
1643            ImportDeclarationSpecifier::ImportSpecifier(specifier) => &specifier.local,
1644            ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => &specifier.local,
1645            ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => &specifier.local,
1646        };
1647        if let Some(symbol_id) = binding_identifier_symbol_id(local) {
1648            symbols.insert(local.name.as_str().to_string(), symbol_id);
1649        }
1650    }
1651}
1652
1653fn reference_symbol_id(
1654    scoping: &Scoping,
1655    identifier: &IdentifierReference<'_>,
1656) -> Option<SymbolId> {
1657    identifier
1658        .reference_id
1659        .get()
1660        .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id())
1661}
1662
1663fn binding_identifier_symbol_id(identifier: &BindingIdentifier<'_>) -> Option<SymbolId> {
1664    identifier.symbol_id.get()
1665}
1666
1667struct SourceSyntaxAstFacts {
1668    binding_scopes: Vec<SourceBindingScopeFactV0>,
1669    scope_parent_edges: Vec<SourceScopeParentFactV0>,
1670    binding_decls: Vec<SourceBindingDeclFactV0>,
1671    scope_contains_decls: Vec<SourceScopeContainsDeclFactV0>,
1672    class_string_literals: Vec<ParserByteSpanV0>,
1673    style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
1674    inline_style_declarations: Vec<SourceInlineStyleDeclarationFactV0>,
1675    class_name_expression_spans: Vec<ParserByteSpanV0>,
1676    classnames_bind_utility_bindings: Vec<ClassnamesBindUtilityBinding>,
1677    classnames_bind_call_arguments: Vec<ClassnamesBindCallArgument>,
1678    symbol_ref_class_value_bindings: Vec<SymbolRefClassValueBinding>,
1679    module_specifiers: Vec<SourceModuleSpecifierFactV0>,
1680    class_value_universes: Vec<SourceClassValueUniverseEntryV0>,
1681    domain_class_references: Vec<SourceDomainClassReferenceFactV0>,
1682    source_elements: Vec<SourceElementFactV0>,
1683    element_parent_edges: Vec<SourceElementParentFactV0>,
1684}
1685
1686fn collect_source_syntax_ast_facts(
1687    source_path: &str,
1688    source: &str,
1689    source_type: SourceType,
1690    property_access_targets: &[SourceStyleBindingTarget],
1691    style_targets: &[SourceStyleBindingTarget],
1692    classnames_bind_imports: &[String],
1693) -> SourceSyntaxAstFacts {
1694    let allocator = Allocator::default();
1695    let ParserReturn {
1696        program, panicked, ..
1697    } = Parser::new(&allocator, source, source_type).parse();
1698    if panicked {
1699        return SourceSyntaxAstFacts {
1700            binding_scopes: Vec::new(),
1701            scope_parent_edges: Vec::new(),
1702            binding_decls: Vec::new(),
1703            scope_contains_decls: Vec::new(),
1704            class_string_literals: Vec::new(),
1705            style_property_accesses: Vec::new(),
1706            inline_style_declarations: Vec::new(),
1707            class_name_expression_spans: Vec::new(),
1708            classnames_bind_utility_bindings: Vec::new(),
1709            classnames_bind_call_arguments: Vec::new(),
1710            symbol_ref_class_value_bindings: Vec::new(),
1711            module_specifiers: Vec::new(),
1712            class_value_universes: Vec::new(),
1713            domain_class_references: Vec::new(),
1714            source_elements: Vec::new(),
1715            element_parent_edges: Vec::new(),
1716        };
1717    }
1718
1719    let semantic = SemanticBuilder::new().build(&program).semantic;
1720    let scoping = semantic.scoping();
1721    let property_access_targets =
1722        source_style_targets_with_symbols(property_access_targets, &program);
1723    let style_targets = source_style_targets_with_symbols(style_targets, &program);
1724    let classnames_bind_import_symbols =
1725        classnames_bind_import_symbol_ids(&program, classnames_bind_imports);
1726    let variant_recipe_bindings = collect_variant_recipe_bindings(source, &program, scoping);
1727    let mut collector = SourceSyntaxAstCollector {
1728        source_path,
1729        source,
1730        scoping,
1731        property_access_targets: property_access_targets.as_slice(),
1732        style_targets: style_targets.as_slice(),
1733        classnames_bind_import_symbols: &classnames_bind_import_symbols,
1734        variant_recipe_bindings: variant_recipe_bindings.as_slice(),
1735        binding_scopes: Vec::new(),
1736        scope_parent_edges: Vec::new(),
1737        binding_decls: Vec::new(),
1738        scope_contains_decls: Vec::new(),
1739        scope_stack: Vec::new(),
1740        class_string_literals: Vec::new(),
1741        style_property_accesses: Vec::new(),
1742        inline_style_declarations: Vec::new(),
1743        class_name_expression_spans: Vec::new(),
1744        classnames_bind_utility_bindings: Vec::new(),
1745        classnames_bind_call_arguments: Vec::new(),
1746        symbol_ref_class_value_bindings: Vec::new(),
1747        module_specifiers: Vec::new(),
1748        domain_class_references: Vec::new(),
1749        source_elements: Vec::new(),
1750        element_parent_edges: Vec::new(),
1751        element_stack: Vec::new(),
1752    };
1753    collector.collect_program(&program);
1754    collector.canonicalize();
1755    SourceSyntaxAstFacts {
1756        binding_scopes: collector.binding_scopes,
1757        scope_parent_edges: collector.scope_parent_edges,
1758        binding_decls: collector.binding_decls,
1759        scope_contains_decls: collector.scope_contains_decls,
1760        class_string_literals: collector.class_string_literals,
1761        style_property_accesses: collector.style_property_accesses,
1762        inline_style_declarations: collector.inline_style_declarations,
1763        class_name_expression_spans: collector.class_name_expression_spans,
1764        classnames_bind_utility_bindings: collector.classnames_bind_utility_bindings,
1765        classnames_bind_call_arguments: collector.classnames_bind_call_arguments,
1766        symbol_ref_class_value_bindings: collector.symbol_ref_class_value_bindings,
1767        module_specifiers: collector.module_specifiers,
1768        class_value_universes: variant_recipe_bindings
1769            .iter()
1770            .map(VariantRecipeBindingV0::to_universe_entry)
1771            .collect(),
1772        domain_class_references: collector.domain_class_references,
1773        source_elements: collector.source_elements,
1774        element_parent_edges: collector.element_parent_edges,
1775    }
1776}
1777
1778#[derive(Debug, Clone, PartialEq, Eq)]
1779struct VariantRecipeBindingV0 {
1780    plugin_id: &'static str,
1781    domain: &'static str,
1782    local_name: String,
1783    local_symbol_id: SymbolId,
1784    base_class_names: Vec<String>,
1785    variants: BTreeMap<String, BTreeMap<String, Vec<String>>>,
1786    compound_class_names: Vec<String>,
1787    byte_span: ParserByteSpanV0,
1788}
1789
1790impl VariantRecipeBindingV0 {
1791    fn to_universe_entry(&self) -> SourceClassValueUniverseEntryV0 {
1792        let mut class_names = self.base_class_names.clone();
1793        class_names.extend(
1794            self.variants
1795                .values()
1796                .flat_map(|options| options.values().flatten().cloned()),
1797        );
1798        class_names.extend(self.compound_class_names.iter().cloned());
1799        class_names.sort();
1800        class_names.dedup();
1801        SourceClassValueUniverseEntryV0 {
1802            plugin_id: self.plugin_id,
1803            domain: self.domain,
1804            owner_name: self.local_name.clone(),
1805            class_names,
1806            axes: self
1807                .variants
1808                .iter()
1809                .map(|(axis_name, options)| {
1810                    let mut values = options.keys().cloned().collect::<Vec<_>>();
1811                    values.sort();
1812                    SourceClassValueUniverseAxisV0 {
1813                        axis_name: axis_name.clone(),
1814                        values,
1815                    }
1816                })
1817                .collect(),
1818            patterns: Vec::new(),
1819            unresolved: Vec::new(),
1820            byte_span: self.byte_span,
1821        }
1822    }
1823}
1824
1825fn variant_recipe_configs() -> Vec<VariantRecipeConfigV0> {
1826    built_in_recipe_provider_configs()
1827}
1828
1829fn collect_variant_recipe_bindings(
1830    source: &str,
1831    program: &Program<'_>,
1832    scoping: &Scoping,
1833) -> Vec<VariantRecipeBindingV0> {
1834    let mut bindings = Vec::new();
1835    for config in variant_recipe_configs() {
1836        let imported_symbols = collect_variant_recipe_import_symbol_ids(program, config);
1837        if imported_symbols.is_empty() {
1838            continue;
1839        }
1840        for statement in &program.body {
1841            collect_variant_recipe_bindings_from_statement(
1842                source,
1843                statement,
1844                config,
1845                scoping,
1846                &imported_symbols,
1847                &mut bindings,
1848            );
1849        }
1850    }
1851    bindings.sort_by_key(|binding| {
1852        (
1853            binding.plugin_id,
1854            binding.local_name.clone(),
1855            binding.byte_span.start,
1856            binding.byte_span.end,
1857        )
1858    });
1859    bindings.dedup_by(|left, right| {
1860        left.plugin_id == right.plugin_id
1861            && left.local_name == right.local_name
1862            && left.byte_span == right.byte_span
1863    });
1864    bindings
1865}
1866
1867fn collect_variant_recipe_import_symbol_ids(
1868    program: &Program<'_>,
1869    config: VariantRecipeConfigV0,
1870) -> BTreeSet<SymbolId> {
1871    let mut symbols = BTreeSet::new();
1872    for statement in &program.body {
1873        let Statement::ImportDeclaration(import) = statement else {
1874            continue;
1875        };
1876        if import.import_kind != ImportOrExportKind::Value
1877            || !config
1878                .import_sources
1879                .contains(&import.source.value.as_str())
1880        {
1881            continue;
1882        }
1883        let Some(specifiers) = import.specifiers.as_ref() else {
1884            continue;
1885        };
1886        for specifier in specifiers {
1887            if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier {
1888                let imported_name = specifier.imported.name().as_str();
1889                if config.import_names.contains(&imported_name)
1890                    && let Some(symbol_id) = binding_identifier_symbol_id(&specifier.local)
1891                {
1892                    symbols.insert(symbol_id);
1893                }
1894            }
1895        }
1896    }
1897    symbols
1898}
1899
1900fn collect_variant_recipe_bindings_from_statement(
1901    source: &str,
1902    statement: &Statement<'_>,
1903    config: VariantRecipeConfigV0,
1904    scoping: &Scoping,
1905    imported_symbols: &BTreeSet<SymbolId>,
1906    out: &mut Vec<VariantRecipeBindingV0>,
1907) {
1908    match statement {
1909        Statement::VariableDeclaration(declaration) => {
1910            collect_variant_recipe_bindings_from_variable_declaration(
1911                source,
1912                declaration,
1913                config,
1914                scoping,
1915                imported_symbols,
1916                out,
1917            )
1918        }
1919        Statement::ExportNamedDeclaration(declaration) => {
1920            if let Some(Declaration::VariableDeclaration(declaration)) = &declaration.declaration {
1921                collect_variant_recipe_bindings_from_variable_declaration(
1922                    source,
1923                    declaration,
1924                    config,
1925                    scoping,
1926                    imported_symbols,
1927                    out,
1928                );
1929            }
1930        }
1931        _ => {}
1932    }
1933}
1934
1935fn collect_variant_recipe_bindings_from_variable_declaration(
1936    source: &str,
1937    declaration: &oxc_ast::ast::VariableDeclaration<'_>,
1938    config: VariantRecipeConfigV0,
1939    scoping: &Scoping,
1940    imported_symbols: &BTreeSet<SymbolId>,
1941    out: &mut Vec<VariantRecipeBindingV0>,
1942) {
1943    for declarator in &declaration.declarations {
1944        let Some(local_identifier) = binding_pattern_identifier(&declarator.id) else {
1945            continue;
1946        };
1947        let local_name = local_identifier.name.as_str();
1948        let Some(local_symbol_id) = binding_identifier_symbol_id(local_identifier) else {
1949            continue;
1950        };
1951        let Some(Expression::CallExpression(call)) = declarator
1952            .init
1953            .as_ref()
1954            .and_then(unwrap_transparent_expression)
1955        else {
1956            continue;
1957        };
1958        let Some(callee_identifier) = expression_identifier(&call.callee) else {
1959            continue;
1960        };
1961        let Some(callee_symbol_id) = reference_symbol_id(scoping, callee_identifier) else {
1962            continue;
1963        };
1964        if !imported_symbols.contains(&callee_symbol_id) {
1965            continue;
1966        }
1967        let Some(config_object) = variant_recipe_config_object(call, config.call_shape) else {
1968            continue;
1969        };
1970        let base_class_names = variant_recipe_base_class_names(
1971            source,
1972            local_name,
1973            call,
1974            config_object,
1975            config.call_shape,
1976        );
1977        let variants = variant_recipe_variants(source, local_name, config_object);
1978        let compound_class_names =
1979            variant_recipe_compound_class_names(source, local_name, config_object);
1980        if base_class_names.is_empty() && variants.is_empty() && compound_class_names.is_empty() {
1981            continue;
1982        }
1983        out.push(VariantRecipeBindingV0 {
1984            plugin_id: config.plugin_id,
1985            domain: config.domain,
1986            local_name: local_name.to_string(),
1987            local_symbol_id,
1988            base_class_names,
1989            variants,
1990            compound_class_names,
1991            byte_span: parser_byte_span(call.span()),
1992        });
1993    }
1994}
1995
1996fn variant_recipe_config_object<'a>(
1997    call: &'a CallExpression<'a>,
1998    call_shape: VariantRecipeCallShape,
1999) -> Option<&'a ObjectExpression<'a>> {
2000    let argument = match call_shape {
2001        VariantRecipeCallShape::BaseThenConfig => call.arguments.get(1),
2002        VariantRecipeCallShape::ObjectConfig => call.arguments.first(),
2003    }?;
2004    let expression = argument_expression(argument).and_then(unwrap_transparent_expression)?;
2005    match expression {
2006        Expression::ObjectExpression(object) => Some(object),
2007        _ => None,
2008    }
2009}
2010
2011fn variant_recipe_base_class_names(
2012    source: &str,
2013    local_name: &str,
2014    call: &CallExpression<'_>,
2015    config_object: &ObjectExpression<'_>,
2016    call_shape: VariantRecipeCallShape,
2017) -> Vec<String> {
2018    match call_shape {
2019        VariantRecipeCallShape::BaseThenConfig => call
2020            .arguments
2021            .first()
2022            .and_then(argument_expression)
2023            .map(|expression| class_names_from_expression(source, expression, Some(local_name)))
2024            .unwrap_or_else(|| vec![local_name.to_string()]),
2025        VariantRecipeCallShape::ObjectConfig => {
2026            object_property_expression(source, config_object, "base")
2027                .map(|expression| class_names_from_expression(source, expression, Some(local_name)))
2028                .unwrap_or_default()
2029        }
2030    }
2031}
2032
2033fn variant_recipe_variants(
2034    source: &str,
2035    recipe_name: &str,
2036    config_object: &ObjectExpression<'_>,
2037) -> BTreeMap<String, BTreeMap<String, Vec<String>>> {
2038    let Some(Expression::ObjectExpression(variants_object)) =
2039        object_property_expression(source, config_object, "variants")
2040            .and_then(unwrap_transparent_expression)
2041    else {
2042        return BTreeMap::new();
2043    };
2044    let mut variants = BTreeMap::new();
2045    for property in &variants_object.properties {
2046        let ObjectPropertyKind::ObjectProperty(property) = property else {
2047            continue;
2048        };
2049        if property.computed {
2050            continue;
2051        }
2052        let Some(axis_name) = property_key_text(source, &property.key) else {
2053            continue;
2054        };
2055        let Some(Expression::ObjectExpression(options_object)) =
2056            unwrap_transparent_expression(&property.value)
2057        else {
2058            continue;
2059        };
2060        let mut options = BTreeMap::new();
2061        for option in &options_object.properties {
2062            let ObjectPropertyKind::ObjectProperty(option) = option else {
2063                continue;
2064            };
2065            if option.computed {
2066                continue;
2067            }
2068            let Some(option_name) = property_key_text(source, &option.key) else {
2069                continue;
2070            };
2071            let fallback = format!("{recipe_name}.{axis_name}.{option_name}");
2072            options.insert(
2073                option_name,
2074                class_names_from_expression(source, &option.value, Some(fallback.as_str())),
2075            );
2076        }
2077        if !options.is_empty() {
2078            variants.insert(axis_name, options);
2079        }
2080    }
2081    variants
2082}
2083
2084fn variant_recipe_compound_class_names(
2085    source: &str,
2086    recipe_name: &str,
2087    config_object: &ObjectExpression<'_>,
2088) -> Vec<String> {
2089    let Some(Expression::ArrayExpression(compounds)) =
2090        object_property_expression(source, config_object, "compoundVariants")
2091            .and_then(unwrap_transparent_expression)
2092    else {
2093        return Vec::new();
2094    };
2095    let mut class_names = Vec::new();
2096    for element in &compounds.elements {
2097        let Some(Expression::ObjectExpression(compound)) =
2098            array_expression_element_expression(element).and_then(unwrap_transparent_expression)
2099        else {
2100            continue;
2101        };
2102        let fallback = format!("{recipe_name}.compound");
2103        for property_name in ["class", "className", "style"] {
2104            if let Some(expression) = object_property_expression(source, compound, property_name) {
2105                class_names.extend(class_names_from_expression(
2106                    source,
2107                    expression,
2108                    Some(fallback.as_str()),
2109                ));
2110            }
2111        }
2112    }
2113    class_names.sort();
2114    class_names.dedup();
2115    class_names
2116}
2117
2118fn collect_variant_recipe_call_references(
2119    source: &str,
2120    expression: &Expression<'_>,
2121    recipe: &VariantRecipeBindingV0,
2122    out: &mut Vec<SourceDomainClassReferenceFactV0>,
2123) {
2124    let Some(Expression::ObjectExpression(object)) = unwrap_transparent_expression(expression)
2125    else {
2126        return;
2127    };
2128    for property in &object.properties {
2129        let ObjectPropertyKind::ObjectProperty(property) = property else {
2130            continue;
2131        };
2132        if property.computed {
2133            continue;
2134        }
2135        let Some(axis_name) = property_key_text(source, &property.key) else {
2136            continue;
2137        };
2138        let Some(options) = recipe.variants.get(axis_name.as_str()) else {
2139            continue;
2140        };
2141        collect_variant_recipe_value_reference(
2142            source,
2143            recipe,
2144            axis_name.as_str(),
2145            options,
2146            &property.value,
2147            out,
2148        );
2149    }
2150}
2151
2152fn collect_variant_recipe_value_reference(
2153    source: &str,
2154    recipe: &VariantRecipeBindingV0,
2155    axis_name: &str,
2156    options: &BTreeMap<String, Vec<String>>,
2157    expression: &Expression<'_>,
2158    out: &mut Vec<SourceDomainClassReferenceFactV0>,
2159) {
2160    let Some(value) = unwrap_transparent_expression(expression) else {
2161        return;
2162    };
2163    if let Some((option_name, byte_span)) = string_expression_value_and_span(source, value) {
2164        out.push(SourceDomainClassReferenceFactV0 {
2165            byte_span,
2166            plugin_id: recipe.plugin_id,
2167            domain: recipe.domain,
2168            owner_name: recipe.local_name.clone(),
2169            axis_name: axis_name.to_string(),
2170            option_name: Some(option_name),
2171            prefix: None,
2172        });
2173        return;
2174    }
2175    if let Expression::ConditionalExpression(conditional) = value {
2176        collect_variant_recipe_value_reference(
2177            source,
2178            recipe,
2179            axis_name,
2180            options,
2181            &conditional.consequent,
2182            out,
2183        );
2184        collect_variant_recipe_value_reference(
2185            source,
2186            recipe,
2187            axis_name,
2188            options,
2189            &conditional.alternate,
2190            out,
2191        );
2192        return;
2193    }
2194    if let Expression::TemplateLiteral(template) = value
2195        && let Some(prefix) = source
2196            .get(template.span.start as usize + 1..template.span.end as usize)
2197            .and_then(|text| text.split("${").next())
2198            .filter(|prefix| !prefix.is_empty())
2199            .map(str::to_string)
2200        && options
2201            .keys()
2202            .any(|option| option.starts_with(prefix.as_str()))
2203    {
2204        out.push(SourceDomainClassReferenceFactV0 {
2205            byte_span: parser_byte_span(template.span),
2206            plugin_id: recipe.plugin_id,
2207            domain: recipe.domain,
2208            owner_name: recipe.local_name.clone(),
2209            axis_name: axis_name.to_string(),
2210            option_name: None,
2211            prefix: Some(prefix),
2212        });
2213    }
2214}
2215
2216fn collect_vue_use_css_module_import_names(program: &Program<'_>) -> BTreeSet<String> {
2217    let mut names = BTreeSet::new();
2218    for statement in &program.body {
2219        let Statement::ImportDeclaration(import) = statement else {
2220            continue;
2221        };
2222        if import.import_kind != ImportOrExportKind::Value || import.source.value.as_str() != "vue"
2223        {
2224            continue;
2225        }
2226        let Some(specifiers) = import.specifiers.as_ref() else {
2227            continue;
2228        };
2229        for specifier in specifiers {
2230            if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier {
2231                let imported_name = specifier.imported.name().as_str();
2232                if imported_name == "useCssModule" {
2233                    names.insert(specifier.local.name.as_str().to_string());
2234                }
2235            }
2236        }
2237    }
2238    names
2239}
2240
2241fn collect_vue_use_css_module_bindings(program: &Program<'_>) -> Vec<String> {
2242    let use_css_module_names = collect_vue_use_css_module_import_names(program);
2243    if use_css_module_names.is_empty() {
2244        return Vec::new();
2245    }
2246    let mut bindings = BTreeSet::new();
2247    for statement in &program.body {
2248        collect_vue_use_css_module_bindings_from_statement(
2249            statement,
2250            &use_css_module_names,
2251            &mut bindings,
2252        );
2253    }
2254    bindings.into_iter().collect()
2255}
2256
2257fn style_import_local_names_by_uri(
2258    bindings: &[SourceBindingStyleImportFactV0],
2259) -> BTreeMap<String, BTreeSet<String>> {
2260    let mut local_names_by_uri = BTreeMap::new();
2261    for binding in bindings {
2262        local_names_by_uri
2263            .entry(binding.style_uri.clone())
2264            .or_insert_with(BTreeSet::new)
2265            .insert(binding.local_name.clone());
2266    }
2267    local_names_by_uri
2268}
2269
2270fn single_btree_set_item(values: &BTreeSet<String>) -> Option<String> {
2271    if values.len() == 1 {
2272        values.iter().next().cloned()
2273    } else {
2274        None
2275    }
2276}
2277
2278fn collect_vue_use_css_module_bindings_from_statement(
2279    statement: &Statement<'_>,
2280    use_css_module_names: &BTreeSet<String>,
2281    bindings: &mut BTreeSet<String>,
2282) {
2283    match statement {
2284        Statement::VariableDeclaration(declaration) => {
2285            collect_vue_use_css_module_bindings_from_variable_declaration(
2286                declaration,
2287                use_css_module_names,
2288                bindings,
2289            );
2290        }
2291        Statement::ExportNamedDeclaration(declaration) => {
2292            if let Some(Declaration::VariableDeclaration(declaration)) = &declaration.declaration {
2293                collect_vue_use_css_module_bindings_from_variable_declaration(
2294                    declaration,
2295                    use_css_module_names,
2296                    bindings,
2297                );
2298            }
2299        }
2300        _ => {}
2301    }
2302}
2303
2304fn collect_vue_use_css_module_bindings_from_variable_declaration(
2305    declaration: &oxc_ast::ast::VariableDeclaration<'_>,
2306    use_css_module_names: &BTreeSet<String>,
2307    bindings: &mut BTreeSet<String>,
2308) {
2309    for declarator in &declaration.declarations {
2310        let Some(binding) = binding_pattern_identifier_name(&declarator.id) else {
2311            continue;
2312        };
2313        let Some(Expression::CallExpression(call)) = &declarator.init else {
2314            continue;
2315        };
2316        let Some(callee) = expression_identifier_name(&call.callee) else {
2317            continue;
2318        };
2319        if use_css_module_names.contains(callee) {
2320            bindings.insert(binding.to_string());
2321        }
2322    }
2323}
2324
2325struct SourceSyntaxAstCollector<'a, 'b, 's> {
2326    source_path: &'a str,
2327    source: &'a str,
2328    scoping: &'s Scoping,
2329    property_access_targets: &'a [SourceStyleBindingTarget],
2330    style_targets: &'a [SourceStyleBindingTarget],
2331    classnames_bind_import_symbols: &'a BTreeSet<SymbolId>,
2332    variant_recipe_bindings: &'b [VariantRecipeBindingV0],
2333    binding_scopes: Vec<SourceBindingScopeFactV0>,
2334    scope_parent_edges: Vec<SourceScopeParentFactV0>,
2335    binding_decls: Vec<SourceBindingDeclFactV0>,
2336    scope_contains_decls: Vec<SourceScopeContainsDeclFactV0>,
2337    scope_stack: Vec<SourceBindingScopeFactV0>,
2338    class_string_literals: Vec<ParserByteSpanV0>,
2339    style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
2340    inline_style_declarations: Vec<SourceInlineStyleDeclarationFactV0>,
2341    class_name_expression_spans: Vec<ParserByteSpanV0>,
2342    classnames_bind_utility_bindings: Vec<ClassnamesBindUtilityBinding>,
2343    classnames_bind_call_arguments: Vec<ClassnamesBindCallArgument>,
2344    symbol_ref_class_value_bindings: Vec<SymbolRefClassValueBinding>,
2345    module_specifiers: Vec<SourceModuleSpecifierFactV0>,
2346    domain_class_references: Vec<SourceDomainClassReferenceFactV0>,
2347    source_elements: Vec<SourceElementFactV0>,
2348    element_parent_edges: Vec<SourceElementParentFactV0>,
2349    element_stack: Vec<SourceElementIdentityFactV0>,
2350}
2351
2352impl<'a, 'b, 's> SourceSyntaxAstCollector<'a, 'b, 's> {
2353    fn collect_program(&mut self, program: &Program<'a>) {
2354        self.with_binding_scope("sourceFile", parser_byte_span(program.span), |collector| {
2355            for statement in &program.body {
2356                collector.collect_statement(statement);
2357            }
2358        });
2359    }
2360
2361    fn collect_statement(&mut self, statement: &Statement<'a>) {
2362        match statement {
2363            Statement::BlockStatement(statement) => {
2364                self.collect_block_statement(statement);
2365            }
2366            Statement::ExpressionStatement(statement) => {
2367                self.collect_expression(&statement.expression);
2368            }
2369            Statement::ReturnStatement(statement) => {
2370                if let Some(argument) = &statement.argument {
2371                    self.collect_expression(argument);
2372                }
2373            }
2374            Statement::IfStatement(statement) => {
2375                self.collect_expression(&statement.test);
2376                self.collect_statement(&statement.consequent);
2377                if let Some(alternate) = &statement.alternate {
2378                    self.collect_statement(alternate);
2379                }
2380            }
2381            Statement::ForStatement(statement) => {
2382                if let Some(init) = &statement.init {
2383                    self.collect_for_statement_init(init);
2384                }
2385                if let Some(test) = &statement.test {
2386                    self.collect_expression(test);
2387                }
2388                if let Some(update) = &statement.update {
2389                    self.collect_expression(update);
2390                }
2391                self.collect_statement(&statement.body);
2392            }
2393            Statement::ForInStatement(statement) => {
2394                self.collect_expression(&statement.right);
2395                self.collect_statement(&statement.body);
2396            }
2397            Statement::ForOfStatement(statement) => {
2398                self.collect_expression(&statement.right);
2399                self.collect_statement(&statement.body);
2400            }
2401            Statement::WhileStatement(statement) => {
2402                self.collect_expression(&statement.test);
2403                self.collect_statement(&statement.body);
2404            }
2405            Statement::DoWhileStatement(statement) => {
2406                self.collect_statement(&statement.body);
2407                self.collect_expression(&statement.test);
2408            }
2409            Statement::SwitchStatement(statement) => {
2410                self.collect_expression(&statement.discriminant);
2411                for switch_case in &statement.cases {
2412                    if let Some(test) = &switch_case.test {
2413                        self.collect_expression(test);
2414                    }
2415                    for consequent in &switch_case.consequent {
2416                        self.collect_statement(consequent);
2417                    }
2418                }
2419            }
2420            Statement::ThrowStatement(statement) => {
2421                self.collect_expression(&statement.argument);
2422            }
2423            Statement::TryStatement(statement) => {
2424                for statement in &statement.block.body {
2425                    self.collect_statement(statement);
2426                }
2427                if let Some(handler) = &statement.handler {
2428                    for statement in &handler.body.body {
2429                        self.collect_statement(statement);
2430                    }
2431                }
2432                if let Some(finalizer) = &statement.finalizer {
2433                    for statement in &finalizer.body {
2434                        self.collect_statement(statement);
2435                    }
2436                }
2437            }
2438            Statement::VariableDeclaration(declaration) => {
2439                self.collect_variable_declaration(declaration);
2440            }
2441            Statement::FunctionDeclaration(function) => {
2442                self.collect_function(function, true);
2443            }
2444            Statement::ClassDeclaration(class) => {
2445                self.collect_class(class);
2446            }
2447            Statement::ImportDeclaration(import) => {
2448                self.collect_import_declaration(import);
2449            }
2450            Statement::ExportNamedDeclaration(export) => {
2451                self.collect_export_named_module_specifier(export);
2452                if let Some(declaration) = &export.declaration {
2453                    self.collect_export_named_declaration(declaration, export.span);
2454                }
2455            }
2456            Statement::ExportAllDeclaration(export) => {
2457                self.collect_export_all_module_specifier(export);
2458            }
2459            Statement::TSImportEqualsDeclaration(declaration) => {
2460                self.collect_ts_import_equals_declaration(declaration);
2461            }
2462            Statement::ExportDefaultDeclaration(declaration) => {
2463                self.collect_export_default_declaration(&declaration.declaration, declaration.span);
2464            }
2465            Statement::TSExportAssignment(declaration) => {
2466                self.collect_expression(&declaration.expression);
2467            }
2468            _ => {}
2469        }
2470    }
2471
2472    fn collect_export_named_declaration(
2473        &mut self,
2474        declaration: &Declaration<'a>,
2475        export_span: Span,
2476    ) {
2477        match declaration {
2478            Declaration::VariableDeclaration(declaration) => {
2479                self.collect_variable_declaration(declaration);
2480            }
2481            Declaration::FunctionDeclaration(function) => {
2482                self.collect_function_with_scope_span(function, true, export_span);
2483            }
2484            Declaration::ClassDeclaration(class) => {
2485                self.collect_class(class);
2486            }
2487            _ => {}
2488        }
2489    }
2490
2491    fn collect_export_default_declaration(
2492        &mut self,
2493        declaration: &oxc_ast::ast::ExportDefaultDeclarationKind<'a>,
2494        export_span: Span,
2495    ) {
2496        match declaration {
2497            oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
2498                self.collect_function_with_scope_span(function, true, export_span);
2499            }
2500            oxc_ast::ast::ExportDefaultDeclarationKind::ClassDeclaration(class) => {
2501                self.collect_class(class);
2502            }
2503            // Every expression-kind default export (`export default <expr>`) delegates to
2504            // `collect_expression`, which already descends arrow/function/parenthesized bodies and
2505            // JSX. Previously only member/call kinds were matched and the catch-all silently
2506            // dropped `export default () => <JSX/>` (and parenthesized/JSX/conditional forms),
2507            // so their className usages were never collected -> unusedSelector false positives.
2508            // Non-expression kinds (`TSInterfaceDeclaration`) yield `None` and are correctly ignored.
2509            _ => {
2510                if let Some(expression) = declaration.as_expression() {
2511                    self.collect_expression(expression);
2512                }
2513            }
2514        }
2515    }
2516
2517    fn collect_for_statement_init(&mut self, init: &oxc_ast::ast::ForStatementInit<'a>) {
2518        match init {
2519            oxc_ast::ast::ForStatementInit::VariableDeclaration(declaration) => {
2520                self.collect_variable_declaration(declaration);
2521            }
2522            oxc_ast::ast::ForStatementInit::StaticMemberExpression(member) => {
2523                self.collect_static_member_expression(member);
2524            }
2525            oxc_ast::ast::ForStatementInit::ComputedMemberExpression(member) => {
2526                self.collect_computed_member_expression(member);
2527            }
2528            oxc_ast::ast::ForStatementInit::CallExpression(expression) => {
2529                self.collect_call_expression(expression);
2530            }
2531            _ => {}
2532        }
2533    }
2534
2535    fn collect_variable_declaration(
2536        &mut self,
2537        declaration: &oxc_ast::ast::VariableDeclaration<'a>,
2538    ) {
2539        for declarator in &declaration.declarations {
2540            self.collect_binding_pattern_decl_facts(&declarator.id, "localVar", None);
2541            if let Some(binding) = self.classnames_bind_utility_binding_from_declarator(declarator)
2542            {
2543                self.classnames_bind_utility_bindings.push(binding);
2544            }
2545            if let Some(init) = &declarator.init {
2546                self.collect_expression(init);
2547            }
2548        }
2549    }
2550
2551    fn collect_import_declaration(&mut self, import: &ImportDeclaration<'a>) {
2552        if import.import_kind != ImportOrExportKind::Value {
2553            return;
2554        }
2555        self.module_specifiers.push(SourceModuleSpecifierFactV0 {
2556            kind: "import",
2557            specifier: import.source.value.as_str().to_string(),
2558            byte_span: parser_byte_span(import.source.span),
2559        });
2560        let Some(specifiers) = import.specifiers.as_ref() else {
2561            return;
2562        };
2563        for specifier in specifiers {
2564            let local = match specifier {
2565                ImportDeclarationSpecifier::ImportSpecifier(specifier) => &specifier.local,
2566                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => &specifier.local,
2567                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => &specifier.local,
2568            };
2569            self.push_binding_identifier_decl_fact(
2570                local,
2571                "import",
2572                Some(import.source.value.as_str()),
2573            );
2574        }
2575    }
2576
2577    fn collect_export_named_module_specifier(
2578        &mut self,
2579        export: &oxc_ast::ast::ExportNamedDeclaration<'a>,
2580    ) {
2581        if export.export_kind != ImportOrExportKind::Value {
2582            return;
2583        }
2584        if let Some(source) = &export.source {
2585            self.module_specifiers.push(SourceModuleSpecifierFactV0 {
2586                kind: "export",
2587                specifier: source.value.as_str().to_string(),
2588                byte_span: parser_byte_span(source.span),
2589            });
2590        }
2591    }
2592
2593    fn collect_export_all_module_specifier(
2594        &mut self,
2595        export: &oxc_ast::ast::ExportAllDeclaration<'a>,
2596    ) {
2597        if export.export_kind != ImportOrExportKind::Value {
2598            return;
2599        }
2600        self.module_specifiers.push(SourceModuleSpecifierFactV0 {
2601            kind: "export",
2602            specifier: export.source.value.as_str().to_string(),
2603            byte_span: parser_byte_span(export.source.span),
2604        });
2605    }
2606
2607    fn collect_ts_import_equals_declaration(
2608        &mut self,
2609        declaration: &oxc_ast::ast::TSImportEqualsDeclaration<'a>,
2610    ) {
2611        if declaration.import_kind != ImportOrExportKind::Value {
2612            return;
2613        }
2614        if let TSModuleReference::ExternalModuleReference(reference) = &declaration.module_reference
2615        {
2616            self.module_specifiers.push(SourceModuleSpecifierFactV0 {
2617                kind: "importEquals",
2618                specifier: reference.expression.value.as_str().to_string(),
2619                byte_span: parser_byte_span(reference.expression.span),
2620            });
2621        }
2622    }
2623
2624    fn collect_function(&mut self, function: &oxc_ast::ast::Function<'a>, include_name: bool) {
2625        self.collect_function_with_scope_span(function, include_name, function.span);
2626    }
2627
2628    fn collect_function_with_scope_span(
2629        &mut self,
2630        function: &oxc_ast::ast::Function<'a>,
2631        include_name: bool,
2632        scope_span: Span,
2633    ) {
2634        if include_name && let Some(identifier) = &function.id {
2635            self.push_binding_identifier_decl_fact(identifier, "localVar", None);
2636        }
2637        self.with_binding_scope("function", parser_byte_span(scope_span), |collector| {
2638            collector.collect_function_parameters(&function.params);
2639            collector.collect_function_body(function.body.as_deref());
2640        });
2641    }
2642
2643    fn collect_arrow_function(&mut self, function: &oxc_ast::ast::ArrowFunctionExpression<'a>) {
2644        self.with_binding_scope("function", parser_byte_span(function.span), |collector| {
2645            collector.collect_function_parameters(&function.params);
2646            collector.collect_function_body(Some(&function.body));
2647        });
2648    }
2649
2650    fn collect_function_parameters(&mut self, params: &oxc_ast::ast::FormalParameters<'a>) {
2651        for parameter in &params.items {
2652            self.collect_binding_pattern_decl_facts(&parameter.pattern, "parameter", None);
2653        }
2654        if let Some(rest) = &params.rest {
2655            self.collect_binding_pattern_decl_facts(&rest.rest.argument, "parameter", None);
2656        }
2657    }
2658
2659    fn with_binding_scope(
2660        &mut self,
2661        kind: &'static str,
2662        byte_span: ParserByteSpanV0,
2663        collect: impl FnOnce(&mut Self),
2664    ) {
2665        let scope = SourceBindingScopeFactV0 { kind, byte_span };
2666        if let Some(parent) = self.scope_stack.last() {
2667            self.scope_parent_edges.push(SourceScopeParentFactV0 {
2668                child_kind: scope.kind,
2669                child_byte_span: scope.byte_span,
2670                parent_kind: parent.kind,
2671                parent_byte_span: parent.byte_span,
2672            });
2673        }
2674        self.binding_scopes.push(scope.clone());
2675        self.scope_stack.push(scope);
2676        collect(self);
2677        self.scope_stack.pop();
2678    }
2679
2680    fn collect_binding_pattern_decl_facts(
2681        &mut self,
2682        pattern: &BindingPattern<'_>,
2683        kind: &'static str,
2684        import_path: Option<&str>,
2685    ) {
2686        let mut facts = Vec::new();
2687        collect_binding_pattern_decl_facts(pattern, kind, import_path, &mut facts);
2688        for fact in facts {
2689            self.push_binding_decl_fact(fact);
2690        }
2691    }
2692
2693    fn push_binding_identifier_decl_fact(
2694        &mut self,
2695        identifier: &BindingIdentifier<'_>,
2696        kind: &'static str,
2697        import_path: Option<&str>,
2698    ) {
2699        let mut facts = Vec::new();
2700        push_binding_identifier_decl_fact(identifier, kind, import_path, &mut facts);
2701        for fact in facts {
2702            self.push_binding_decl_fact(fact);
2703        }
2704    }
2705
2706    fn push_binding_decl_fact(&mut self, fact: SourceBindingDeclFactV0) {
2707        if let Some(scope) = self.scope_stack.last() {
2708            self.scope_contains_decls
2709                .push(SourceScopeContainsDeclFactV0 {
2710                    scope_kind: scope.kind,
2711                    scope_byte_span: scope.byte_span,
2712                    decl_kind: fact.kind,
2713                    decl_name: fact.name.clone(),
2714                    decl_byte_span: fact.byte_span,
2715                    import_path: fact.import_path.clone(),
2716                });
2717        }
2718        self.binding_decls.push(fact);
2719    }
2720
2721    fn classnames_bind_utility_binding_from_declarator(
2722        &self,
2723        declarator: &VariableDeclarator<'a>,
2724    ) -> Option<ClassnamesBindUtilityBinding> {
2725        if self.style_targets.is_empty() || self.classnames_bind_import_symbols.is_empty() {
2726            return None;
2727        }
2728        let binding = binding_pattern_identifier(&declarator.id)?;
2729        let binding_symbol_id = binding_identifier_symbol_id(binding)?;
2730        let init = declarator.init.as_ref()?;
2731        let Expression::CallExpression(call) = init else {
2732            return None;
2733        };
2734        let Expression::StaticMemberExpression(callee) = &call.callee else {
2735            return None;
2736        };
2737        if callee.property.name.as_str() != "bind" {
2738            return None;
2739        }
2740        let callee_identifier = expression_identifier(&callee.object)?;
2741        let callee_symbol_id = reference_symbol_id(self.scoping, callee_identifier)?;
2742        if !self
2743            .classnames_bind_import_symbols
2744            .contains(&callee_symbol_id)
2745        {
2746            return None;
2747        }
2748        let style_identifier = call.arguments.first().and_then(argument_identifier)?;
2749        let style_symbol_id = reference_symbol_id(self.scoping, style_identifier)?;
2750        let style_uri = self
2751            .style_targets
2752            .iter()
2753            .find(|target| target.binding_symbol_id == Some(style_symbol_id))?
2754            .target_style_uri
2755            .clone()?;
2756
2757        Some(ClassnamesBindUtilityBinding {
2758            binding: binding.name.as_str().to_string(),
2759            binding_symbol_id,
2760            styles_binding: style_identifier.name.as_str().to_string(),
2761            style_uri,
2762            classnames_import_binding: callee_identifier.name.as_str().to_string(),
2763        })
2764    }
2765
2766    fn collect_function_body(&mut self, body: Option<&oxc_ast::ast::FunctionBody<'a>>) {
2767        let Some(body) = body else {
2768            return;
2769        };
2770        self.with_binding_scope("block", parser_byte_span(body.span), |collector| {
2771            for statement in &body.statements {
2772                collector.collect_statement(statement);
2773            }
2774        });
2775    }
2776
2777    fn collect_block_statement(&mut self, block: &oxc_ast::ast::BlockStatement<'a>) {
2778        self.with_binding_scope("block", parser_byte_span(block.span), |collector| {
2779            for statement in &block.body {
2780                collector.collect_statement(statement);
2781            }
2782        });
2783    }
2784
2785    fn collect_class(&mut self, class: &Class<'a>) {
2786        if let Some(super_class) = &class.super_class {
2787            self.collect_expression(super_class);
2788        }
2789        for element in &class.body.body {
2790            match element {
2791                ClassElement::MethodDefinition(method) => {
2792                    self.collect_function(&method.value, false);
2793                }
2794                ClassElement::PropertyDefinition(property) => {
2795                    if property.computed {
2796                        self.collect_property_key(&property.key);
2797                    }
2798                    if let Some(value) = &property.value {
2799                        self.collect_expression(value);
2800                    }
2801                }
2802                ClassElement::AccessorProperty(property) => {
2803                    if property.computed {
2804                        self.collect_property_key(&property.key);
2805                    }
2806                    if let Some(value) = &property.value {
2807                        self.collect_expression(value);
2808                    }
2809                }
2810                ClassElement::StaticBlock(block) => {
2811                    for statement in &block.body {
2812                        self.collect_statement(statement);
2813                    }
2814                }
2815                ClassElement::TSIndexSignature(_) => {}
2816            }
2817        }
2818    }
2819
2820    fn collect_expression(&mut self, expression: &Expression<'a>) {
2821        match expression {
2822            Expression::StaticMemberExpression(member) => {
2823                self.collect_static_member_expression(member);
2824            }
2825            Expression::ComputedMemberExpression(member) => {
2826                self.collect_computed_member_expression(member);
2827            }
2828            Expression::PrivateFieldExpression(member) => {
2829                self.collect_expression(&member.object);
2830            }
2831            Expression::ArrayExpression(expression) => {
2832                self.collect_array_expression(expression);
2833            }
2834            Expression::ObjectExpression(expression) => {
2835                self.collect_object_expression(expression);
2836            }
2837            Expression::CallExpression(expression) => {
2838                self.collect_call_expression(expression);
2839            }
2840            Expression::NewExpression(expression) => {
2841                self.collect_expression(&expression.callee);
2842                for argument in &expression.arguments {
2843                    self.collect_argument(argument);
2844                }
2845            }
2846            Expression::ChainExpression(expression) => {
2847                self.collect_chain_element(&expression.expression);
2848            }
2849            Expression::ConditionalExpression(expression) => {
2850                self.collect_conditional_expression(expression);
2851            }
2852            Expression::BinaryExpression(expression) => {
2853                self.collect_expression(&expression.left);
2854                self.collect_expression(&expression.right);
2855            }
2856            Expression::LogicalExpression(expression) => {
2857                self.collect_logical_expression(expression);
2858            }
2859            Expression::AssignmentExpression(expression) => {
2860                self.collect_expression(&expression.right);
2861            }
2862            Expression::SequenceExpression(expression) => {
2863                for expression in &expression.expressions {
2864                    self.collect_expression(expression);
2865                }
2866            }
2867            Expression::ParenthesizedExpression(expression) => {
2868                self.collect_parenthesized_expression(expression);
2869            }
2870            Expression::UnaryExpression(expression) => {
2871                self.collect_expression(&expression.argument);
2872            }
2873            Expression::AwaitExpression(expression) => {
2874                self.collect_expression(&expression.argument);
2875            }
2876            Expression::TemplateLiteral(expression) => {
2877                for expression in &expression.expressions {
2878                    self.collect_expression(expression);
2879                }
2880            }
2881            Expression::TaggedTemplateExpression(expression) => {
2882                self.collect_expression(&expression.tag);
2883                for expression in &expression.quasi.expressions {
2884                    self.collect_expression(expression);
2885                }
2886            }
2887            Expression::ArrowFunctionExpression(expression) => {
2888                self.collect_arrow_function(expression);
2889            }
2890            Expression::FunctionExpression(expression) => {
2891                self.collect_function(expression, false);
2892            }
2893            Expression::ClassExpression(class) => {
2894                self.collect_class(class);
2895            }
2896            Expression::ImportExpression(expression) => {
2897                self.collect_expression(&expression.source);
2898                if let Some(options) = &expression.options {
2899                    self.collect_expression(options);
2900                }
2901            }
2902            Expression::JSXElement(element) => {
2903                self.collect_jsx_element(element);
2904            }
2905            Expression::JSXFragment(fragment) => {
2906                for child in &fragment.children {
2907                    self.collect_jsx_child(child);
2908                }
2909            }
2910            Expression::TSAsExpression(expression) => {
2911                self.collect_ts_as_expression(expression);
2912            }
2913            Expression::TSSatisfiesExpression(expression) => {
2914                self.collect_ts_satisfies_expression(expression);
2915            }
2916            Expression::TSTypeAssertion(expression) => {
2917                self.collect_expression(&expression.expression);
2918            }
2919            Expression::TSNonNullExpression(expression) => {
2920                self.collect_ts_non_null_expression(expression);
2921            }
2922            Expression::TSInstantiationExpression(expression) => {
2923                self.collect_expression(&expression.expression);
2924            }
2925            _ => {}
2926        }
2927    }
2928
2929    fn collect_array_expression_element(&mut self, element: &ArrayExpressionElement<'a>) {
2930        match element {
2931            ArrayExpressionElement::SpreadElement(spread) => {
2932                self.collect_expression(&spread.argument);
2933            }
2934            ArrayExpressionElement::Elision(_) => {}
2935            _ => {
2936                if let Some(expression) = element.as_expression() {
2937                    self.collect_expression(expression);
2938                }
2939            }
2940        }
2941    }
2942
2943    fn collect_argument(&mut self, argument: &Argument<'a>) {
2944        match argument {
2945            Argument::SpreadElement(spread) => {
2946                self.collect_expression(&spread.argument);
2947            }
2948            _ => {
2949                if let Some(expression) = argument.as_expression() {
2950                    self.collect_expression(expression);
2951                }
2952            }
2953        }
2954    }
2955
2956    fn collect_chain_element(&mut self, element: &ChainElement<'a>) {
2957        match element {
2958            ChainElement::CallExpression(expression) => {
2959                self.collect_expression(&expression.callee);
2960                for argument in &expression.arguments {
2961                    self.collect_argument(argument);
2962                }
2963            }
2964            ChainElement::StaticMemberExpression(member) => {
2965                self.collect_static_member_expression(member);
2966            }
2967            ChainElement::ComputedMemberExpression(member) => {
2968                self.collect_computed_member_expression(member);
2969            }
2970            ChainElement::PrivateFieldExpression(member) => {
2971                self.collect_expression(&member.object);
2972            }
2973            ChainElement::TSNonNullExpression(expression) => {
2974                self.collect_expression(&expression.expression);
2975            }
2976        }
2977    }
2978
2979    fn collect_property_key(&mut self, key: &oxc_ast::ast::PropertyKey<'a>) {
2980        match key {
2981            oxc_ast::ast::PropertyKey::StaticIdentifier(_)
2982            | oxc_ast::ast::PropertyKey::PrivateIdentifier(_) => {}
2983            _ => {
2984                if let Some(expression) = key.as_expression() {
2985                    self.collect_expression(expression);
2986                }
2987            }
2988        }
2989    }
2990
2991    fn collect_jsx_element(&mut self, element: &oxc_ast::ast::JSXElement<'a>) {
2992        let identity = SourceElementIdentityFactV0 {
2993            source_path: self.source_path.to_string(),
2994            byte_span: parser_byte_span(element.span),
2995        };
2996        if let Some(parent) = self.element_stack.last() {
2997            self.element_parent_edges.push(SourceElementParentFactV0 {
2998                child: identity.clone(),
2999                parent: parent.clone(),
3000            });
3001        }
3002        let (static_class_names, classes_are_exact) = self.jsx_static_class_names(element);
3003        self.source_elements.push(SourceElementFactV0 {
3004            identity: identity.clone(),
3005            intrinsic_tag_name: self.jsx_intrinsic_tag_name(element),
3006            static_class_names,
3007            classes_are_exact,
3008        });
3009        self.element_stack.push(identity);
3010        for attribute in &element.opening_element.attributes {
3011            match attribute {
3012                oxc_ast::ast::JSXAttributeItem::Attribute(attribute) => {
3013                    if is_jsx_class_name_attribute(&attribute.name)
3014                        && let Some(value) = &attribute.value
3015                    {
3016                        self.collect_class_name_string_literal_attribute(value);
3017                        self.collect_class_name_expression_attribute(value);
3018                    }
3019                    if is_jsx_style_attribute(&attribute.name)
3020                        && let Some(value) = &attribute.value
3021                    {
3022                        self.collect_inline_style_attribute(value);
3023                    }
3024                    if let Some(value) = &attribute.value {
3025                        self.collect_jsx_attribute_value(value);
3026                    }
3027                }
3028                oxc_ast::ast::JSXAttributeItem::SpreadAttribute(attribute) => {
3029                    self.collect_expression(&attribute.argument);
3030                }
3031            }
3032        }
3033        for child in &element.children {
3034            self.collect_jsx_child(child);
3035        }
3036        self.element_stack.pop();
3037    }
3038
3039    fn jsx_intrinsic_tag_name(&self, element: &oxc_ast::ast::JSXElement<'a>) -> Option<String> {
3040        let span = parser_byte_span(element.opening_element.name.span());
3041        let name = self.source.get(span.start..span.end)?;
3042        let first = name.chars().next()?;
3043        (first.is_ascii_lowercase() && !name.contains(['.', ':']))
3044            .then(|| name.to_ascii_lowercase())
3045    }
3046
3047    fn jsx_static_class_names(
3048        &self,
3049        element: &oxc_ast::ast::JSXElement<'a>,
3050    ) -> (Vec<String>, bool) {
3051        let mut names = Vec::new();
3052        let mut exact = true;
3053        for attribute in &element.opening_element.attributes {
3054            match attribute {
3055                oxc_ast::ast::JSXAttributeItem::Attribute(attribute)
3056                    if is_jsx_class_name_attribute(&attribute.name) =>
3057                {
3058                    match attribute.value.as_ref() {
3059                        Some(JSXAttributeValue::StringLiteral(literal)) => {
3060                            names.extend(split_class_names(literal.value.as_str()));
3061                        }
3062                        _ => exact = false,
3063                    }
3064                }
3065                oxc_ast::ast::JSXAttributeItem::SpreadAttribute(_) => exact = false,
3066                _ => {}
3067            }
3068        }
3069        names.sort();
3070        names.dedup();
3071        (names, exact)
3072    }
3073
3074    fn collect_jsx_attribute_value(&mut self, value: &JSXAttributeValue<'a>) {
3075        match value {
3076            JSXAttributeValue::ExpressionContainer(container) => {
3077                self.collect_jsx_expression(&container.expression);
3078            }
3079            JSXAttributeValue::Element(element) => {
3080                self.collect_jsx_element(element);
3081            }
3082            JSXAttributeValue::Fragment(fragment) => {
3083                for child in &fragment.children {
3084                    self.collect_jsx_child(child);
3085                }
3086            }
3087            JSXAttributeValue::StringLiteral(_) => {}
3088        }
3089    }
3090
3091    fn collect_class_name_string_literal_attribute(&mut self, value: &JSXAttributeValue<'a>) {
3092        let JSXAttributeValue::StringLiteral(literal) = value else {
3093            return;
3094        };
3095        if let Some(span) = self.string_literal_content_span(literal.span) {
3096            self.class_string_literals.push(span);
3097        }
3098    }
3099
3100    fn collect_class_name_expression_attribute(&mut self, value: &JSXAttributeValue<'a>) {
3101        let JSXAttributeValue::ExpressionContainer(container) = value else {
3102            return;
3103        };
3104        if let Some(span) = jsx_expression_span(&container.expression) {
3105            self.class_name_expression_spans.push(span);
3106        }
3107    }
3108
3109    fn collect_inline_style_attribute(&mut self, value: &JSXAttributeValue<'a>) {
3110        let JSXAttributeValue::ExpressionContainer(container) = value else {
3111            return;
3112        };
3113        let JSXExpression::ObjectExpression(object) = &container.expression else {
3114            return;
3115        };
3116        let target_style_uri = self.single_imported_style_target_uri();
3117        for property in &object.properties {
3118            let ObjectPropertyKind::ObjectProperty(property) = property else {
3119                continue;
3120            };
3121            if property.computed {
3122                continue;
3123            }
3124            let Some((property_name, byte_span)) = self.inline_style_property_name(&property.key)
3125            else {
3126                continue;
3127            };
3128            let value_byte_span = Some(parser_byte_span(property.value.span()));
3129            self.inline_style_declarations
3130                .push(SourceInlineStyleDeclarationFactV0 {
3131                    byte_span,
3132                    value_byte_span,
3133                    property_name,
3134                    value: self.inline_style_static_value(&property.value),
3135                    target_style_uri: target_style_uri.clone(),
3136                    cascade_tier: "authorInlineStyle",
3137                    static_value: self.inline_style_value_is_static(&property.value),
3138                });
3139        }
3140    }
3141
3142    fn collect_jsx_child(&mut self, child: &JSXChild<'a>) {
3143        match child {
3144            JSXChild::Element(element) => {
3145                self.collect_jsx_element(element);
3146            }
3147            JSXChild::Fragment(fragment) => {
3148                for child in &fragment.children {
3149                    self.collect_jsx_child(child);
3150                }
3151            }
3152            JSXChild::ExpressionContainer(container) => {
3153                self.collect_jsx_expression(&container.expression);
3154            }
3155            JSXChild::Spread(spread) => {
3156                self.collect_expression(&spread.expression);
3157            }
3158            JSXChild::Text(_) => {}
3159        }
3160    }
3161
3162    fn collect_jsx_expression(&mut self, expression: &JSXExpression<'a>) {
3163        match expression {
3164            JSXExpression::StaticMemberExpression(member) => {
3165                self.collect_static_member_expression(member);
3166            }
3167            JSXExpression::ComputedMemberExpression(member) => {
3168                self.collect_computed_member_expression(member);
3169            }
3170            JSXExpression::CallExpression(expression) => {
3171                self.collect_call_expression(expression);
3172            }
3173            JSXExpression::ConditionalExpression(expression) => {
3174                self.collect_conditional_expression(expression);
3175            }
3176            JSXExpression::LogicalExpression(expression) => {
3177                self.collect_logical_expression(expression);
3178            }
3179            JSXExpression::ArrayExpression(expression) => {
3180                self.collect_array_expression(expression);
3181            }
3182            JSXExpression::ObjectExpression(expression) => {
3183                self.collect_object_expression(expression);
3184            }
3185            JSXExpression::ParenthesizedExpression(expression) => {
3186                self.collect_parenthesized_expression(expression);
3187            }
3188            JSXExpression::TSAsExpression(expression) => {
3189                self.collect_ts_as_expression(expression);
3190            }
3191            JSXExpression::TSSatisfiesExpression(expression) => {
3192                self.collect_ts_satisfies_expression(expression);
3193            }
3194            JSXExpression::TSNonNullExpression(expression) => {
3195                self.collect_ts_non_null_expression(expression);
3196            }
3197            JSXExpression::JSXElement(element) => {
3198                self.collect_jsx_element(element);
3199            }
3200            JSXExpression::JSXFragment(fragment) => {
3201                for child in &fragment.children {
3202                    self.collect_jsx_child(child);
3203                }
3204            }
3205            _ => {}
3206        }
3207    }
3208
3209    fn collect_array_expression(&mut self, expression: &ArrayExpression<'a>) {
3210        for element in &expression.elements {
3211            self.collect_array_expression_element(element);
3212        }
3213    }
3214
3215    fn collect_object_expression(&mut self, expression: &ObjectExpression<'a>) {
3216        for property in &expression.properties {
3217            match property {
3218                ObjectPropertyKind::ObjectProperty(property) => {
3219                    if property.computed {
3220                        self.collect_property_key(&property.key);
3221                    }
3222                    self.collect_expression(&property.value);
3223                }
3224                ObjectPropertyKind::SpreadProperty(spread) => {
3225                    self.collect_expression(&spread.argument);
3226                }
3227            }
3228        }
3229    }
3230
3231    fn single_imported_style_target_uri(&self) -> Option<String> {
3232        let targets = self
3233            .style_targets
3234            .iter()
3235            .filter_map(|target| target.target_style_uri.as_deref())
3236            .collect::<BTreeSet<_>>();
3237        if targets.len() == 1 {
3238            targets.into_iter().next().map(str::to_string)
3239        } else {
3240            None
3241        }
3242    }
3243
3244    fn inline_style_property_name(
3245        &self,
3246        key: &oxc_ast::ast::PropertyKey<'a>,
3247    ) -> Option<(String, ParserByteSpanV0)> {
3248        let byte_span = parser_byte_span(key.span());
3249        let raw = self.source.get(byte_span.start..byte_span.end)?.trim();
3250        let unquoted = raw
3251            .strip_prefix(['"', '\''])
3252            .and_then(|value| value.strip_suffix(['"', '\'']))
3253            .unwrap_or(raw);
3254        if unquoted.is_empty() || unquoted.contains(char::is_whitespace) {
3255            return None;
3256        }
3257        Some((normalize_inline_style_property_name(unquoted), byte_span))
3258    }
3259
3260    fn inline_style_static_value(&self, expression: &Expression<'a>) -> Option<String> {
3261        if !self.inline_style_value_is_static(expression) {
3262            return None;
3263        }
3264        let span = parser_byte_span(expression.span());
3265        Some(self.source.get(span.start..span.end)?.trim().to_string())
3266    }
3267
3268    fn inline_style_value_is_static(&self, expression: &Expression<'a>) -> bool {
3269        match expression {
3270            Expression::StringLiteral(_)
3271            | Expression::NumericLiteral(_)
3272            | Expression::BooleanLiteral(_)
3273            | Expression::NullLiteral(_) => true,
3274            Expression::TemplateLiteral(literal) => literal.expressions.is_empty(),
3275            Expression::ParenthesizedExpression(expression) => {
3276                self.inline_style_value_is_static(&expression.expression)
3277            }
3278            Expression::TSAsExpression(expression) => {
3279                self.inline_style_value_is_static(&expression.expression)
3280            }
3281            Expression::TSSatisfiesExpression(expression) => {
3282                self.inline_style_value_is_static(&expression.expression)
3283            }
3284            Expression::TSNonNullExpression(expression) => {
3285                self.inline_style_value_is_static(&expression.expression)
3286            }
3287            Expression::TSTypeAssertion(expression) => {
3288                self.inline_style_value_is_static(&expression.expression)
3289            }
3290            _ => false,
3291        }
3292    }
3293
3294    fn collect_call_expression(&mut self, expression: &CallExpression<'a>) {
3295        if let Some(callee_identifier) = expression_identifier(&expression.callee) {
3296            let callee_symbol_id = reference_symbol_id(self.scoping, callee_identifier);
3297            if let Some(recipe) = self
3298                .variant_recipe_bindings
3299                .iter()
3300                .find(|recipe| callee_symbol_id == Some(recipe.local_symbol_id))
3301                && let Some(argument) = expression.arguments.first().and_then(argument_expression)
3302            {
3303                collect_variant_recipe_call_references(
3304                    self.source,
3305                    argument,
3306                    recipe,
3307                    &mut self.domain_class_references,
3308                );
3309            }
3310            for argument in &expression.arguments {
3311                if let Some(binding_symbol_id) = callee_symbol_id
3312                    && let Some(byte_span) = argument_expression_span(argument)
3313                {
3314                    self.classnames_bind_call_arguments
3315                        .push(ClassnamesBindCallArgument {
3316                            binding: callee_identifier.name.as_str().to_string(),
3317                            binding_symbol_id,
3318                            byte_span,
3319                        });
3320                    if let Some(expression) = argument_expression(argument) {
3321                        self.collect_class_value_symbol_refs_from_expression(
3322                            expression,
3323                            binding_symbol_id,
3324                        );
3325                    }
3326                }
3327            }
3328        }
3329        self.collect_expression(&expression.callee);
3330        for argument in &expression.arguments {
3331            self.collect_argument(argument);
3332        }
3333    }
3334
3335    fn collect_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
3336        self.collect_expression(&expression.test);
3337        self.collect_expression(&expression.consequent);
3338        self.collect_expression(&expression.alternate);
3339    }
3340
3341    fn collect_logical_expression(&mut self, expression: &LogicalExpression<'a>) {
3342        self.collect_expression(&expression.left);
3343        self.collect_expression(&expression.right);
3344    }
3345
3346    fn collect_class_value_symbol_refs_from_expression<'expr>(
3347        &mut self,
3348        expression: &'expr Expression<'a>,
3349        classnames_binding_symbol_id: SymbolId,
3350    ) {
3351        let Some(expression) = unwrap_transparent_expression(expression) else {
3352            return;
3353        };
3354        if let Some(binding) = self.symbol_ref_class_value_binding_from_expression(
3355            expression,
3356            classnames_binding_symbol_id,
3357        ) {
3358            self.symbol_ref_class_value_bindings.push(binding);
3359            return;
3360        }
3361        match expression {
3362            Expression::ArrayExpression(expression) => {
3363                for element in &expression.elements {
3364                    if let Some(expression) = array_expression_element_expression(element) {
3365                        self.collect_class_value_symbol_refs_from_expression(
3366                            expression,
3367                            classnames_binding_symbol_id,
3368                        );
3369                    }
3370                }
3371            }
3372            Expression::ObjectExpression(expression) => {
3373                for property in &expression.properties {
3374                    let ObjectPropertyKind::ObjectProperty(property) = property else {
3375                        continue;
3376                    };
3377                    if property.computed
3378                        && let Some(expression) = property.key.as_expression()
3379                    {
3380                        self.collect_class_value_symbol_refs_from_expression(
3381                            expression,
3382                            classnames_binding_symbol_id,
3383                        );
3384                    }
3385                }
3386            }
3387            Expression::LogicalExpression(expression) if expression.operator.is_and() => {
3388                self.collect_class_value_symbol_refs_from_expression(
3389                    &expression.right,
3390                    classnames_binding_symbol_id,
3391                );
3392            }
3393            Expression::ConditionalExpression(expression) => {
3394                self.collect_class_value_symbol_refs_from_expression(
3395                    &expression.consequent,
3396                    classnames_binding_symbol_id,
3397                );
3398                self.collect_class_value_symbol_refs_from_expression(
3399                    &expression.alternate,
3400                    classnames_binding_symbol_id,
3401                );
3402            }
3403            _ => {}
3404        }
3405    }
3406
3407    fn symbol_ref_class_value_binding_from_expression<'expr>(
3408        &self,
3409        expression: &'expr Expression<'a>,
3410        classnames_binding_symbol_id: SymbolId,
3411    ) -> Option<SymbolRefClassValueBinding> {
3412        let root = root_identifier_for_symbol_ref_expression(expression)?;
3413        let root_symbol_id = reference_symbol_id(self.scoping, root)?;
3414        if self
3415            .property_access_targets
3416            .iter()
3417            .any(|target| target.binding_symbol_id == Some(root_symbol_id))
3418        {
3419            return None;
3420        }
3421        let byte_span = parser_byte_span(expression.span());
3422        let raw_reference = self.source.get(byte_span.start..byte_span.end)?.trim();
3423        Some(SymbolRefClassValueBinding {
3424            classnames_binding_symbol_id,
3425            byte_span,
3426            raw_reference: raw_reference.to_string(),
3427            root_name: root.name.as_str().to_string(),
3428            decl_name: self.scoping.symbol_name(root_symbol_id).to_string(),
3429        })
3430    }
3431
3432    fn collect_parenthesized_expression(&mut self, expression: &ParenthesizedExpression<'a>) {
3433        self.collect_expression(&expression.expression);
3434    }
3435
3436    fn collect_ts_as_expression(&mut self, expression: &TSAsExpression<'a>) {
3437        self.collect_expression(&expression.expression);
3438    }
3439
3440    fn collect_ts_satisfies_expression(&mut self, expression: &TSSatisfiesExpression<'a>) {
3441        self.collect_expression(&expression.expression);
3442    }
3443
3444    fn collect_ts_non_null_expression(&mut self, expression: &TSNonNullExpression<'a>) {
3445        self.collect_expression(&expression.expression);
3446    }
3447
3448    fn collect_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
3449        if let Some(target) = self.target_for_object(&member.object)
3450            && let Some(byte_span) = self.css_identifier_span(member.property.span)
3451        {
3452            self.style_property_accesses
3453                .push(SourceStylePropertyAccessFactV0 {
3454                    byte_span,
3455                    target_style_uri: target.target_style_uri.clone(),
3456                });
3457        }
3458        self.collect_expression(&member.object);
3459    }
3460
3461    fn collect_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
3462        if let Some(target) = self.target_for_object(&member.object)
3463            && let Some(byte_span) = self.static_string_expression_content_span(&member.expression)
3464        {
3465            self.style_property_accesses
3466                .push(SourceStylePropertyAccessFactV0 {
3467                    byte_span,
3468                    target_style_uri: target.target_style_uri.clone(),
3469                });
3470        }
3471        self.collect_expression(&member.object);
3472        self.collect_expression(&member.expression);
3473    }
3474
3475    fn target_for_object(&self, expression: &Expression<'a>) -> Option<&SourceStyleBindingTarget> {
3476        match expression {
3477            Expression::Identifier(identifier) => {
3478                let reference_symbol_id = reference_symbol_id(self.scoping, identifier)?;
3479                self.property_access_targets
3480                    .iter()
3481                    .find(|target| target.binding_symbol_id == Some(reference_symbol_id))
3482            }
3483            Expression::ParenthesizedExpression(expression) => {
3484                self.target_for_object(&expression.expression)
3485            }
3486            Expression::TSAsExpression(expression) => {
3487                self.target_for_object(&expression.expression)
3488            }
3489            Expression::TSSatisfiesExpression(expression) => {
3490                self.target_for_object(&expression.expression)
3491            }
3492            Expression::TSTypeAssertion(expression) => {
3493                self.target_for_object(&expression.expression)
3494            }
3495            Expression::TSNonNullExpression(expression) => {
3496                self.target_for_object(&expression.expression)
3497            }
3498            Expression::TSInstantiationExpression(expression) => {
3499                self.target_for_object(&expression.expression)
3500            }
3501            _ => None,
3502        }
3503    }
3504
3505    fn static_string_expression_content_span(
3506        &self,
3507        expression: &Expression<'a>,
3508    ) -> Option<ParserByteSpanV0> {
3509        match expression {
3510            Expression::StringLiteral(literal) => self.css_identifier_content_span(literal.span),
3511            Expression::TemplateLiteral(literal) if literal.expressions.is_empty() => {
3512                self.css_identifier_content_span(literal.span)
3513            }
3514            _ => None,
3515        }
3516    }
3517
3518    fn css_identifier_span(&self, span: Span) -> Option<ParserByteSpanV0> {
3519        let span = parser_byte_span(span);
3520        let text = self.source.get(span.start..span.end)?;
3521        (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(span)
3522    }
3523
3524    fn css_identifier_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
3525        let span = parser_byte_span(span);
3526        if span.end <= span.start + 1 {
3527            return None;
3528        }
3529        let content = ParserByteSpanV0 {
3530            start: span.start + 1,
3531            end: span.end - 1,
3532        };
3533        let text = self.source.get(content.start..content.end)?;
3534        (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(content)
3535    }
3536
3537    fn string_literal_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
3538        let span = parser_byte_span(span);
3539        if span.end <= span.start + 1 {
3540            return None;
3541        }
3542        let content = ParserByteSpanV0 {
3543            start: span.start + 1,
3544            end: span.end - 1,
3545        };
3546        self.source.get(content.start..content.end)?;
3547        Some(content)
3548    }
3549
3550    fn canonicalize(&mut self) {
3551        self.binding_scopes.sort();
3552        self.binding_scopes.dedup();
3553        self.scope_parent_edges.sort();
3554        self.scope_parent_edges.dedup();
3555        self.binding_decls.sort();
3556        self.binding_decls.dedup();
3557        self.scope_contains_decls.sort();
3558        self.scope_contains_decls.dedup();
3559        self.class_string_literals.sort_by(|left, right| {
3560            left.start
3561                .cmp(&right.start)
3562                .then_with(|| left.end.cmp(&right.end))
3563        });
3564        self.class_string_literals.dedup();
3565        self.style_property_accesses.sort_by(|left, right| {
3566            left.byte_span
3567                .start
3568                .cmp(&right.byte_span.start)
3569                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
3570                .then_with(|| left.target_style_uri.cmp(&right.target_style_uri))
3571        });
3572        self.style_property_accesses.dedup();
3573        self.inline_style_declarations.sort_by(|left, right| {
3574            left.byte_span
3575                .start
3576                .cmp(&right.byte_span.start)
3577                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
3578                .then_with(|| left.property_name.cmp(&right.property_name))
3579                .then_with(|| left.target_style_uri.cmp(&right.target_style_uri))
3580        });
3581        self.inline_style_declarations.dedup();
3582        self.source_elements.sort();
3583        self.source_elements.dedup();
3584        self.element_parent_edges.sort();
3585        self.element_parent_edges.dedup();
3586        self.classnames_bind_utility_bindings
3587            .sort_by(|left, right| {
3588                left.binding
3589                    .cmp(&right.binding)
3590                    .then_with(|| left.styles_binding.cmp(&right.styles_binding))
3591                    .then_with(|| left.style_uri.cmp(&right.style_uri))
3592                    .then_with(|| {
3593                        left.classnames_import_binding
3594                            .cmp(&right.classnames_import_binding)
3595                    })
3596            });
3597        self.classnames_bind_utility_bindings
3598            .dedup_by(|left, right| {
3599                left.binding == right.binding
3600                    && left.styles_binding == right.styles_binding
3601                    && left.style_uri == right.style_uri
3602                    && left.classnames_import_binding == right.classnames_import_binding
3603            });
3604        self.classnames_bind_call_arguments.sort_by(|left, right| {
3605            left.binding
3606                .cmp(&right.binding)
3607                .then_with(|| left.byte_span.start.cmp(&right.byte_span.start))
3608                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
3609        });
3610        self.classnames_bind_call_arguments.dedup_by(|left, right| {
3611            left.binding == right.binding && left.byte_span == right.byte_span
3612        });
3613        self.symbol_ref_class_value_bindings.sort_by(|left, right| {
3614            left.classnames_binding_symbol_id
3615                .cmp(&right.classnames_binding_symbol_id)
3616                .then_with(|| left.byte_span.start.cmp(&right.byte_span.start))
3617                .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
3618                .then_with(|| left.raw_reference.cmp(&right.raw_reference))
3619                .then_with(|| left.decl_name.cmp(&right.decl_name))
3620        });
3621        self.symbol_ref_class_value_bindings
3622            .dedup_by(|left, right| {
3623                left.classnames_binding_symbol_id == right.classnames_binding_symbol_id
3624                    && left.byte_span == right.byte_span
3625                    && left.raw_reference == right.raw_reference
3626                    && left.decl_name == right.decl_name
3627            });
3628    }
3629}
3630
3631fn parser_byte_span(span: Span) -> ParserByteSpanV0 {
3632    ParserByteSpanV0 {
3633        start: span.start as usize,
3634        end: span.end as usize,
3635    }
3636}
3637
3638fn is_jsx_class_name_attribute(name: &JSXAttributeName<'_>) -> bool {
3639    matches!(name, JSXAttributeName::Identifier(identifier) if identifier.name.as_str() == "className")
3640}
3641
3642fn is_jsx_style_attribute(name: &JSXAttributeName<'_>) -> bool {
3643    matches!(name, JSXAttributeName::Identifier(identifier) if identifier.name.as_str() == "style")
3644}
3645
3646fn normalize_inline_style_property_name(name: &str) -> String {
3647    if name.starts_with("--") {
3648        return name.to_string();
3649    }
3650    let mut normalized = String::new();
3651    for character in name.chars() {
3652        if character.is_ascii_uppercase() {
3653            if !normalized.is_empty() {
3654                normalized.push('-');
3655            }
3656            normalized.push(character.to_ascii_lowercase());
3657        } else {
3658            normalized.push(character);
3659        }
3660    }
3661    normalized
3662}
3663
3664fn jsx_expression_span(expression: &JSXExpression<'_>) -> Option<ParserByteSpanV0> {
3665    match expression {
3666        JSXExpression::EmptyExpression(_) => None,
3667        _ => Some(parser_byte_span(expression.span())),
3668    }
3669}
3670
3671fn argument_expression_span(argument: &Argument<'_>) -> Option<ParserByteSpanV0> {
3672    match argument {
3673        Argument::SpreadElement(spread) => Some(parser_byte_span(spread.argument.span())),
3674        _ => Some(parser_byte_span(argument.span())),
3675    }
3676}
3677
3678fn argument_expression<'arg, 'ast>(
3679    argument: &'arg Argument<'ast>,
3680) -> Option<&'arg Expression<'ast>> {
3681    match argument {
3682        Argument::SpreadElement(spread) => Some(&spread.argument),
3683        _ => argument.as_expression(),
3684    }
3685}
3686
3687fn array_expression_element_expression<'element, 'ast>(
3688    element: &'element ArrayExpressionElement<'ast>,
3689) -> Option<&'element Expression<'ast>> {
3690    match element {
3691        ArrayExpressionElement::SpreadElement(spread) => Some(&spread.argument),
3692        ArrayExpressionElement::Elision(_) => None,
3693        _ => element.as_expression(),
3694    }
3695}
3696
3697fn unwrap_transparent_expression<'expr, 'ast>(
3698    expression: &'expr Expression<'ast>,
3699) -> Option<&'expr Expression<'ast>> {
3700    match expression {
3701        Expression::ParenthesizedExpression(expression) => {
3702            unwrap_transparent_expression(&expression.expression)
3703        }
3704        Expression::TSAsExpression(expression) => {
3705            unwrap_transparent_expression(&expression.expression)
3706        }
3707        Expression::TSSatisfiesExpression(expression) => {
3708            unwrap_transparent_expression(&expression.expression)
3709        }
3710        Expression::TSTypeAssertion(expression) => {
3711            unwrap_transparent_expression(&expression.expression)
3712        }
3713        Expression::TSNonNullExpression(expression) => {
3714            unwrap_transparent_expression(&expression.expression)
3715        }
3716        Expression::TSInstantiationExpression(expression) => {
3717            unwrap_transparent_expression(&expression.expression)
3718        }
3719        _ => Some(expression),
3720    }
3721}
3722
3723fn root_identifier_for_symbol_ref_expression<'expr, 'ast>(
3724    expression: &'expr Expression<'ast>,
3725) -> Option<&'expr IdentifierReference<'ast>> {
3726    match expression {
3727        Expression::Identifier(identifier) => Some(identifier),
3728        Expression::StaticMemberExpression(member) => {
3729            root_identifier_for_symbol_ref_expression(&member.object)
3730        }
3731        Expression::ParenthesizedExpression(expression) => {
3732            root_identifier_for_symbol_ref_expression(&expression.expression)
3733        }
3734        Expression::TSAsExpression(expression) => {
3735            root_identifier_for_symbol_ref_expression(&expression.expression)
3736        }
3737        Expression::TSSatisfiesExpression(expression) => {
3738            root_identifier_for_symbol_ref_expression(&expression.expression)
3739        }
3740        Expression::TSTypeAssertion(expression) => {
3741            root_identifier_for_symbol_ref_expression(&expression.expression)
3742        }
3743        Expression::TSNonNullExpression(expression) => {
3744            root_identifier_for_symbol_ref_expression(&expression.expression)
3745        }
3746        Expression::TSInstantiationExpression(expression) => {
3747            root_identifier_for_symbol_ref_expression(&expression.expression)
3748        }
3749        _ => None,
3750    }
3751}
3752
3753fn object_property_expression<'a>(
3754    source: &str,
3755    object: &'a ObjectExpression<'a>,
3756    name: &str,
3757) -> Option<&'a Expression<'a>> {
3758    object.properties.iter().find_map(|property| {
3759        let ObjectPropertyKind::ObjectProperty(property) = property else {
3760            return None;
3761        };
3762        if property.computed || property_key_text(source, &property.key).as_deref() != Some(name) {
3763            return None;
3764        }
3765        Some(&property.value)
3766    })
3767}
3768
3769fn property_key_text(source: &str, key: &oxc_ast::ast::PropertyKey<'_>) -> Option<String> {
3770    let span = parser_byte_span(key.span());
3771    if source.is_empty() {
3772        match key {
3773            oxc_ast::ast::PropertyKey::StaticIdentifier(identifier) => {
3774                return Some(identifier.name.as_str().to_string());
3775            }
3776            oxc_ast::ast::PropertyKey::PrivateIdentifier(identifier) => {
3777                return Some(identifier.name.as_str().to_string());
3778            }
3779            _ => return None,
3780        }
3781    }
3782    object_property_name(source, span.start, span.end)
3783}
3784
3785fn class_names_from_expression(
3786    source: &str,
3787    expression: &Expression<'_>,
3788    fallback: Option<&str>,
3789) -> Vec<String> {
3790    let Some(value) = unwrap_transparent_expression(expression) else {
3791        return fallback.into_iter().map(str::to_string).collect();
3792    };
3793    if let Some((text, _)) = string_expression_value_and_span(source, value) {
3794        let class_names = split_class_names(text.as_str());
3795        return if class_names.is_empty() {
3796            fallback.into_iter().map(str::to_string).collect()
3797        } else {
3798            class_names
3799        };
3800    }
3801    if let Expression::ArrayExpression(array) = value {
3802        let mut values = array
3803            .elements
3804            .iter()
3805            .filter_map(array_expression_element_expression)
3806            .flat_map(|element| class_names_from_expression(source, element, None))
3807            .collect::<Vec<_>>();
3808        values.sort();
3809        values.dedup();
3810        return if values.is_empty() {
3811            fallback.into_iter().map(str::to_string).collect()
3812        } else {
3813            values
3814        };
3815    }
3816    fallback.into_iter().map(str::to_string).collect()
3817}
3818
3819fn string_expression_value_and_span(
3820    source: &str,
3821    expression: &Expression<'_>,
3822) -> Option<(String, ParserByteSpanV0)> {
3823    match expression {
3824        Expression::StringLiteral(_) => {}
3825        Expression::TemplateLiteral(template) if template.expressions.is_empty() => {}
3826        _ => return None,
3827    }
3828    let span = parser_byte_span(expression.span());
3829    let (start, end) = trim_js_expression(source, span.start, span.end);
3830    let (literal_start, literal_end, next_offset) = js_string_literal_span(source, start, end)?;
3831    if trim_js_expression(source, next_offset, end).0 < end {
3832        return None;
3833    }
3834    Some((
3835        source.get(literal_start..literal_end)?.to_string(),
3836        ParserByteSpanV0 {
3837            start: literal_start,
3838            end: literal_end,
3839        },
3840    ))
3841}
3842
3843fn split_class_names(value: &str) -> Vec<String> {
3844    let mut class_names = value
3845        .split_whitespace()
3846        .filter(|part| !part.is_empty())
3847        .map(str::to_string)
3848        .collect::<Vec<_>>();
3849    class_names.sort();
3850    class_names.dedup();
3851    class_names
3852}
3853
3854fn binding_pattern_identifier_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
3855    binding_pattern_identifier(pattern).map(|identifier| identifier.name.as_str())
3856}
3857
3858fn binding_pattern_identifier<'a>(
3859    pattern: &'a BindingPattern<'a>,
3860) -> Option<&'a BindingIdentifier<'a>> {
3861    match pattern {
3862        BindingPattern::BindingIdentifier(identifier) => Some(identifier),
3863        _ => None,
3864    }
3865}
3866
3867fn collect_binding_pattern_decl_facts(
3868    pattern: &BindingPattern<'_>,
3869    kind: &'static str,
3870    import_path: Option<&str>,
3871    out: &mut Vec<SourceBindingDeclFactV0>,
3872) {
3873    match pattern {
3874        BindingPattern::BindingIdentifier(identifier) => {
3875            push_binding_identifier_decl_fact(identifier, kind, import_path, out);
3876        }
3877        BindingPattern::ObjectPattern(pattern) => {
3878            for property in &pattern.properties {
3879                collect_binding_pattern_decl_facts(&property.value, kind, import_path, out);
3880            }
3881            if let Some(rest) = &pattern.rest {
3882                collect_binding_pattern_decl_facts(&rest.argument, kind, import_path, out);
3883            }
3884        }
3885        BindingPattern::ArrayPattern(pattern) => {
3886            for element in pattern.elements.iter().flatten() {
3887                collect_binding_pattern_decl_facts(element, kind, import_path, out);
3888            }
3889            if let Some(rest) = &pattern.rest {
3890                collect_binding_pattern_decl_facts(&rest.argument, kind, import_path, out);
3891            }
3892        }
3893        BindingPattern::AssignmentPattern(pattern) => {
3894            collect_binding_pattern_decl_facts(&pattern.left, kind, import_path, out);
3895        }
3896    }
3897}
3898
3899fn push_binding_identifier_decl_fact(
3900    identifier: &BindingIdentifier<'_>,
3901    kind: &'static str,
3902    import_path: Option<&str>,
3903    out: &mut Vec<SourceBindingDeclFactV0>,
3904) {
3905    out.push(SourceBindingDeclFactV0 {
3906        kind,
3907        name: identifier.name.as_str().to_string(),
3908        byte_span: parser_byte_span(identifier.span),
3909        import_path: import_path.map(str::to_string),
3910    });
3911}
3912
3913fn expression_identifier_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
3914    expression_identifier(expression).map(|identifier| identifier.name.as_str())
3915}
3916
3917fn expression_identifier<'a>(
3918    expression: &'a Expression<'a>,
3919) -> Option<&'a IdentifierReference<'a>> {
3920    match expression {
3921        Expression::Identifier(identifier) => Some(identifier),
3922        Expression::ParenthesizedExpression(expression) => {
3923            expression_identifier(&expression.expression)
3924        }
3925        Expression::TSAsExpression(expression) => expression_identifier(&expression.expression),
3926        Expression::TSSatisfiesExpression(expression) => {
3927            expression_identifier(&expression.expression)
3928        }
3929        Expression::TSTypeAssertion(expression) => expression_identifier(&expression.expression),
3930        Expression::TSNonNullExpression(expression) => {
3931            expression_identifier(&expression.expression)
3932        }
3933        Expression::TSInstantiationExpression(expression) => {
3934            expression_identifier(&expression.expression)
3935        }
3936        _ => None,
3937    }
3938}
3939
3940fn argument_identifier<'a>(argument: &'a Argument<'a>) -> Option<&'a IdentifierReference<'a>> {
3941    match argument {
3942        Argument::Identifier(identifier) => Some(identifier),
3943        Argument::ParenthesizedExpression(expression) => {
3944            expression_identifier(&expression.expression)
3945        }
3946        Argument::TSAsExpression(expression) => expression_identifier(&expression.expression),
3947        Argument::TSSatisfiesExpression(expression) => {
3948            expression_identifier(&expression.expression)
3949        }
3950        Argument::TSNonNullExpression(expression) => expression_identifier(&expression.expression),
3951        Argument::TSInstantiationExpression(expression) => {
3952            expression_identifier(&expression.expression)
3953        }
3954        _ => None,
3955    }
3956}
3957
3958fn collect_selector_references_from_js_expression(
3959    source: &str,
3960    start: usize,
3961    end: usize,
3962    target_style_uri: Option<&str>,
3963    local_class_values: &BTreeMap<String, SourceClassValue>,
3964    references: &mut Vec<SourceSelectorReferenceFactV0>,
3965    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
3966) {
3967    let (start, end) = trim_js_expression(source, start, end);
3968    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
3969    if start >= end {
3970        return;
3971    }
3972
3973    if let Some((literal_start, literal_end, next_offset)) =
3974        js_string_literal_span(source, start, end)
3975        && trim_js_expression(source, next_offset, end).0 >= end
3976    {
3977        push_js_literal_selector_references(
3978            source,
3979            literal_start,
3980            literal_end,
3981            source.as_bytes().get(start).copied() == Some(b'`'),
3982            target_style_uri,
3983            references,
3984        );
3985        if source.as_bytes().get(start).copied() == Some(b'`') {
3986            collect_template_type_fact_targets(
3987                source,
3988                literal_start,
3989                literal_end,
3990                target_style_uri,
3991                type_fact_targets,
3992            );
3993        }
3994        return;
3995    }
3996
3997    if source.as_bytes().get(start) == Some(&b'{')
3998        && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
3999    {
4000        collect_object_literal_selector_references(
4001            source,
4002            start,
4003            end,
4004            target_style_uri,
4005            local_class_values,
4006            references,
4007            type_fact_targets,
4008        );
4009        return;
4010    }
4011
4012    if source.as_bytes().get(start) == Some(&b'[')
4013        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
4014    {
4015        for (element_start, element_end) in
4016            split_top_level_js_segments(source, start + 1, end - 1, b',')
4017        {
4018            let element_start = skip_js_trivia_until(source, element_start, element_end);
4019            let element_start = if source[element_start..element_end].starts_with("...") {
4020                element_start + 3
4021            } else {
4022                element_start
4023            };
4024            collect_selector_references_from_js_expression(
4025                source,
4026                element_start,
4027                element_end,
4028                target_style_uri,
4029                local_class_values,
4030                references,
4031                type_fact_targets,
4032            );
4033        }
4034        return;
4035    }
4036
4037    if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
4038    {
4039        for (argument_start, argument_end) in
4040            split_top_level_js_segments(source, arguments_start, arguments_end, b',')
4041        {
4042            collect_selector_references_from_js_expression(
4043                source,
4044                argument_start,
4045                argument_end,
4046                target_style_uri,
4047                local_class_values,
4048                references,
4049                type_fact_targets,
4050            );
4051        }
4052        return;
4053    }
4054
4055    if let Some((_, true_start, true_end, false_start, false_end)) =
4056        top_level_conditional_parts(source, start, end)
4057    {
4058        collect_selector_references_from_js_expression(
4059            source,
4060            true_start,
4061            true_end,
4062            target_style_uri,
4063            local_class_values,
4064            references,
4065            type_fact_targets,
4066        );
4067        collect_selector_references_from_js_expression(
4068            source,
4069            false_start,
4070            false_end,
4071            target_style_uri,
4072            local_class_values,
4073            references,
4074            type_fact_targets,
4075        );
4076        return;
4077    }
4078
4079    if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
4080        .or_else(|| find_top_level_js_operator(source, start, end, "||"))
4081    {
4082        collect_selector_references_from_js_expression(
4083            source,
4084            operator_offset + 2,
4085            end,
4086            target_style_uri,
4087            local_class_values,
4088            references,
4089            type_fact_targets,
4090        );
4091        return;
4092    }
4093
4094    let expression_path = js_expression_path(source, start, end);
4095    if let Some(value) =
4096        source_class_value_from_js_expression(source, start, end, local_class_values)
4097        && !value.is_empty()
4098    {
4099        if let Some(path) = expression_path.as_deref() {
4100            push_source_type_fact_target(
4101                ParserByteSpanV0 { start, end },
4102                path,
4103                target_style_uri,
4104                "",
4105                "",
4106                type_fact_targets,
4107            );
4108        }
4109        push_source_class_value_reference(
4110            ParserByteSpanV0 { start, end },
4111            value,
4112            target_style_uri,
4113            references,
4114        );
4115        return;
4116    }
4117
4118    if let Some(prefix) =
4119        static_string_prefix_for_js_expression(source, start, end, local_class_values)
4120        && !prefix.is_empty()
4121    {
4122        push_selector_reference(
4123            ParserByteSpanV0 { start, end },
4124            Some(prefix),
4125            SourceSelectorReferenceMatchKindV0::Prefix,
4126            target_style_uri,
4127            references,
4128        );
4129        return;
4130    }
4131
4132    if let Some(path) = expression_path {
4133        push_source_type_fact_target(
4134            ParserByteSpanV0 { start, end },
4135            path.as_str(),
4136            target_style_uri,
4137            "",
4138            "",
4139            type_fact_targets,
4140        );
4141    }
4142}
4143
4144fn collect_local_class_value_bindings(source: &str) -> BTreeMap<String, SourceClassValue> {
4145    let mut values = BTreeMap::new();
4146    collect_local_class_value_declarations(source, &mut values);
4147    collect_local_class_value_reassignments(source, &mut values);
4148    values
4149}
4150
4151fn collect_local_class_value_declarations(
4152    source: &str,
4153    values: &mut BTreeMap<String, SourceClassValue>,
4154) {
4155    let mut cursor = 0usize;
4156    while let Some(keyword) = next_code_identifier(source, cursor) {
4157        cursor = keyword.end;
4158        if !matches!(keyword.text, "const" | "let" | "var") {
4159            continue;
4160        }
4161        let binding_start = skip_js_trivia(source, keyword.end);
4162        let Some((binding, binding_end)) = read_js_identifier(source, binding_start) else {
4163            continue;
4164        };
4165        let equals_offset = skip_js_trivia(source, binding_end);
4166        if source.as_bytes().get(equals_offset) != Some(&b'=') {
4167            continue;
4168        }
4169        let expression_start = skip_js_trivia(source, equals_offset + 1);
4170        let expression_end = js_statement_expression_end(source, expression_start);
4171        if let Some(value) =
4172            source_class_value_from_js_expression(source, expression_start, expression_end, values)
4173            && !value.is_empty()
4174        {
4175            values.insert(binding.to_string(), value);
4176        }
4177        let (_, property_values) = source_class_value_from_object_literal(
4178            source,
4179            expression_start,
4180            expression_end,
4181            values,
4182        );
4183        for (property, value) in property_values {
4184            if !value.is_empty() {
4185                values.insert(format!("{binding}.{property}"), value);
4186            }
4187        }
4188        cursor = expression_end.min(source.len());
4189    }
4190}
4191
4192fn collect_local_class_value_reassignments(
4193    source: &str,
4194    values: &mut BTreeMap<String, SourceClassValue>,
4195) {
4196    let mut cursor = 0usize;
4197    while let Some(identifier) = next_code_identifier(source, cursor) {
4198        cursor = identifier.end;
4199        if !values.contains_key(identifier.text) {
4200            continue;
4201        }
4202        let equals_offset = skip_js_trivia(source, identifier.end);
4203        if !is_simple_js_assignment_operator(source, equals_offset) {
4204            continue;
4205        }
4206        let expression_start = skip_js_trivia(source, equals_offset + 1);
4207        let expression_end = js_statement_expression_end(source, expression_start);
4208        if let Some(value) =
4209            source_class_value_from_js_expression(source, expression_start, expression_end, values)
4210            && !value.is_empty()
4211        {
4212            values
4213                .entry(identifier.text.to_string())
4214                .or_default()
4215                .merge(value);
4216        }
4217        cursor = expression_end.min(source.len());
4218    }
4219}
4220
4221fn is_simple_js_assignment_operator(source: &str, offset: usize) -> bool {
4222    if source.as_bytes().get(offset) != Some(&b'=') {
4223        return false;
4224    }
4225    let previous = offset
4226        .checked_sub(1)
4227        .and_then(|index| source.as_bytes().get(index).copied());
4228    let next = source.as_bytes().get(offset + 1).copied();
4229    !matches!(previous, Some(b'=' | b'!' | b'<' | b'>')) && !matches!(next, Some(b'=' | b'>'))
4230}
4231
4232fn source_class_value_from_js_expression(
4233    source: &str,
4234    start: usize,
4235    end: usize,
4236    local_class_values: &BTreeMap<String, SourceClassValue>,
4237) -> Option<SourceClassValue> {
4238    let (start, end) = trim_js_expression(source, start, end);
4239    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
4240    if start >= end {
4241        return None;
4242    }
4243
4244    if let Some((literal_start, literal_end, next_offset)) =
4245        js_string_literal_span(source, start, end)
4246        && trim_js_expression(source, next_offset, end).0 >= end
4247    {
4248        return Some(source_class_value_from_js_literal(
4249            source,
4250            literal_start,
4251            literal_end,
4252            source.as_bytes().get(start).copied() == Some(b'`'),
4253        ));
4254    }
4255
4256    if source.as_bytes().get(start) == Some(&b'{')
4257        && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
4258    {
4259        let (value, _) =
4260            source_class_value_from_object_literal(source, start, end, local_class_values);
4261        return Some(value);
4262    }
4263
4264    if source.as_bytes().get(start) == Some(&b'[')
4265        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
4266    {
4267        let mut value = SourceClassValue::default();
4268        for (element_start, element_end) in
4269            split_top_level_js_segments(source, start + 1, end - 1, b',')
4270        {
4271            let element_start = skip_js_trivia_until(source, element_start, element_end);
4272            let element_start = if source[element_start..element_end].starts_with("...") {
4273                element_start + 3
4274            } else {
4275                element_start
4276            };
4277            if let Some(element_value) = source_class_value_from_js_expression(
4278                source,
4279                element_start,
4280                element_end,
4281                local_class_values,
4282            ) {
4283                value.merge(element_value);
4284            }
4285        }
4286        return Some(value);
4287    }
4288
4289    if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
4290    {
4291        let mut value = SourceClassValue::default();
4292        for (argument_start, argument_end) in
4293            split_top_level_js_segments(source, arguments_start, arguments_end, b',')
4294        {
4295            if let Some(argument_value) = source_class_value_from_js_expression(
4296                source,
4297                argument_start,
4298                argument_end,
4299                local_class_values,
4300            ) {
4301                value.merge(argument_value);
4302            }
4303        }
4304        return Some(value);
4305    }
4306
4307    if let Some((_, true_start, true_end, false_start, false_end)) =
4308        top_level_conditional_parts(source, start, end)
4309    {
4310        let mut value = SourceClassValue::default();
4311        if let Some(true_value) =
4312            source_class_value_from_js_expression(source, true_start, true_end, local_class_values)
4313        {
4314            value.merge(true_value);
4315        }
4316        if let Some(false_value) = source_class_value_from_js_expression(
4317            source,
4318            false_start,
4319            false_end,
4320            local_class_values,
4321        ) {
4322            value.merge(false_value);
4323        }
4324        return Some(value);
4325    }
4326
4327    if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
4328        .or_else(|| find_top_level_js_operator(source, start, end, "||"))
4329    {
4330        return source_class_value_from_js_expression(
4331            source,
4332            operator_offset + 2,
4333            end,
4334            local_class_values,
4335        );
4336    }
4337
4338    if let Some(path) = js_expression_path(source, start, end)
4339        && let Some(value) = local_class_values.get(path.as_str())
4340    {
4341        return Some(value.clone());
4342    }
4343
4344    static_string_prefix_for_js_expression(source, start, end, local_class_values).map(|prefix| {
4345        let mut value = SourceClassValue::default();
4346        if !prefix.is_empty() {
4347            value.prefixes.push(prefix);
4348        }
4349        value
4350    })
4351}
4352
4353fn source_class_value_from_js_literal(
4354    source: &str,
4355    literal_start: usize,
4356    literal_end: usize,
4357    is_template: bool,
4358) -> SourceClassValue {
4359    let mut value = SourceClassValue::default();
4360    if is_template
4361        && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
4362    {
4363        let prefix_end = literal_start + relative_interpolation;
4364        push_template_prefix_value(source, literal_start, prefix_end, &mut value);
4365    } else {
4366        value
4367            .exact
4368            .extend(class_token_strings(source, literal_start, literal_end));
4369    }
4370    value.canonicalize();
4371    value
4372}
4373
4374fn source_class_value_from_object_literal(
4375    source: &str,
4376    start: usize,
4377    end: usize,
4378    local_class_values: &BTreeMap<String, SourceClassValue>,
4379) -> (SourceClassValue, BTreeMap<String, SourceClassValue>) {
4380    let (start, end) = trim_js_expression(source, start, end);
4381    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
4382    let mut object_value = SourceClassValue::default();
4383    let mut property_values = BTreeMap::new();
4384    if source.as_bytes().get(start) != Some(&b'{')
4385        || matching_js_block_end(source, start, b'{', b'}') != Some(end.saturating_sub(1))
4386    {
4387        return (object_value, property_values);
4388    }
4389
4390    for (property_start, property_end) in
4391        split_top_level_js_segments(source, start + 1, end - 1, b',')
4392    {
4393        let (property_start, property_end) =
4394            trim_js_expression(source, property_start, property_end);
4395        if property_start >= property_end {
4396            continue;
4397        }
4398        if source[property_start..property_end].starts_with("...") {
4399            if let Some(spread_value) = source_class_value_from_js_expression(
4400                source,
4401                property_start + 3,
4402                property_end,
4403                local_class_values,
4404            ) {
4405                object_value.merge(spread_value);
4406            }
4407            continue;
4408        }
4409        let colon = find_top_level_js_byte(source, property_start, property_end, b':');
4410        let key_end = colon.unwrap_or(property_end);
4411        let key_value =
4412            source_class_value_from_object_key(source, property_start, key_end, local_class_values);
4413        object_value.merge(key_value.clone());
4414        if let Some(property_name) = object_property_name(source, property_start, key_end)
4415            && let Some(property_value) = colon
4416                .and_then(|colon| {
4417                    source_class_value_from_js_expression(
4418                        source,
4419                        colon + 1,
4420                        property_end,
4421                        local_class_values,
4422                    )
4423                })
4424                .filter(|value| !value.is_empty())
4425        {
4426            property_values.insert(property_name, property_value);
4427        }
4428    }
4429    object_value.canonicalize();
4430    (object_value, property_values)
4431}
4432
4433fn collect_object_literal_selector_references(
4434    source: &str,
4435    start: usize,
4436    end: usize,
4437    target_style_uri: Option<&str>,
4438    local_class_values: &BTreeMap<String, SourceClassValue>,
4439    references: &mut Vec<SourceSelectorReferenceFactV0>,
4440    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
4441) {
4442    for (property_start, property_end) in
4443        split_top_level_js_segments(source, start + 1, end - 1, b',')
4444    {
4445        let (property_start, property_end) =
4446            trim_js_expression(source, property_start, property_end);
4447        if property_start >= property_end {
4448            continue;
4449        }
4450        if source[property_start..property_end].starts_with("...") {
4451            collect_selector_references_from_js_expression(
4452                source,
4453                property_start + 3,
4454                property_end,
4455                target_style_uri,
4456                local_class_values,
4457                references,
4458                type_fact_targets,
4459            );
4460            continue;
4461        }
4462        let colon = find_top_level_js_byte(source, property_start, property_end, b':');
4463        let key_end = colon.unwrap_or(property_end);
4464        collect_selector_references_from_object_key(
4465            source,
4466            property_start,
4467            key_end,
4468            target_style_uri,
4469            local_class_values,
4470            references,
4471            type_fact_targets,
4472        );
4473    }
4474}
4475
4476fn class_utility_call_arguments(source: &str, start: usize, end: usize) -> Option<(usize, usize)> {
4477    let (callee, callee_end) = read_js_identifier(source, start)?;
4478    if !is_class_utility_callee(callee) {
4479        return None;
4480    }
4481    let open_paren = skip_js_trivia_until(source, callee_end, end);
4482    if source.as_bytes().get(open_paren) != Some(&b'(') {
4483        return None;
4484    }
4485    let call_end = js_call_end(source, open_paren)?;
4486    if call_end > end || trim_js_expression(source, call_end + 1, end).0 < end {
4487        return None;
4488    }
4489    Some((open_paren + 1, call_end))
4490}
4491
4492fn is_class_utility_callee(callee: &str) -> bool {
4493    matches!(callee, "classnames" | "classNames" | "clsx" | "cn")
4494}
4495
4496fn is_class_utility_import_path(import_path: &str) -> bool {
4497    matches!(import_path, "clsx" | "clsx/lite" | "classnames")
4498}
4499
4500fn collect_selector_references_from_object_key(
4501    source: &str,
4502    start: usize,
4503    end: usize,
4504    target_style_uri: Option<&str>,
4505    local_class_values: &BTreeMap<String, SourceClassValue>,
4506    references: &mut Vec<SourceSelectorReferenceFactV0>,
4507    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
4508) {
4509    let (start, end) = trim_js_expression(source, start, end);
4510    if start >= end {
4511        return;
4512    }
4513    if source.as_bytes().get(start) == Some(&b'[')
4514        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
4515    {
4516        collect_selector_references_from_js_expression(
4517            source,
4518            start + 1,
4519            end - 1,
4520            target_style_uri,
4521            local_class_values,
4522            references,
4523            type_fact_targets,
4524        );
4525        return;
4526    }
4527    if let Some((literal_start, literal_end, next_offset)) =
4528        js_string_literal_span(source, start, end)
4529        && trim_js_expression(source, next_offset, end).0 >= end
4530    {
4531        push_js_literal_selector_references(
4532            source,
4533            literal_start,
4534            literal_end,
4535            source.as_bytes().get(start).copied() == Some(b'`'),
4536            target_style_uri,
4537            references,
4538        );
4539        if source.as_bytes().get(start).copied() == Some(b'`') {
4540            collect_template_type_fact_targets(
4541                source,
4542                literal_start,
4543                literal_end,
4544                target_style_uri,
4545                type_fact_targets,
4546            );
4547        }
4548        return;
4549    }
4550    if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
4551        && trim_js_expression(source, identifier_end, end).0 >= end
4552    {
4553        push_selector_reference(
4554            ParserByteSpanV0 { start, end },
4555            Some(identifier.to_string()),
4556            SourceSelectorReferenceMatchKindV0::Exact,
4557            target_style_uri,
4558            references,
4559        );
4560    }
4561}
4562
4563fn source_class_value_from_object_key(
4564    source: &str,
4565    start: usize,
4566    end: usize,
4567    local_class_values: &BTreeMap<String, SourceClassValue>,
4568) -> SourceClassValue {
4569    let (start, end) = trim_js_expression(source, start, end);
4570    if start >= end {
4571        return SourceClassValue::default();
4572    }
4573    if source.as_bytes().get(start) == Some(&b'[')
4574        && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
4575    {
4576        return source_class_value_from_js_expression(
4577            source,
4578            start + 1,
4579            end - 1,
4580            local_class_values,
4581        )
4582        .unwrap_or_default();
4583    }
4584    if let Some((literal_start, literal_end, next_offset)) =
4585        js_string_literal_span(source, start, end)
4586        && trim_js_expression(source, next_offset, end).0 >= end
4587    {
4588        return source_class_value_from_js_literal(
4589            source,
4590            literal_start,
4591            literal_end,
4592            source.as_bytes().get(start).copied() == Some(b'`'),
4593        );
4594    }
4595    if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
4596        && trim_js_expression(source, identifier_end, end).0 >= end
4597    {
4598        let mut value = SourceClassValue::default();
4599        value.exact.push(identifier.to_string());
4600        return value;
4601    }
4602    SourceClassValue::default()
4603}
4604
4605fn object_property_name(source: &str, start: usize, end: usize) -> Option<String> {
4606    let (start, end) = trim_js_expression(source, start, end);
4607    if let Some((literal_start, literal_end, next_offset)) =
4608        js_string_literal_span(source, start, end)
4609        && trim_js_expression(source, next_offset, end).0 >= end
4610    {
4611        return source.get(literal_start..literal_end).map(str::to_string);
4612    }
4613    let (identifier, identifier_end) = read_js_identifier(source, start)?;
4614    (trim_js_expression(source, identifier_end, end).0 >= end).then(|| identifier.to_string())
4615}
4616
4617fn push_source_class_value_reference(
4618    byte_span: ParserByteSpanV0,
4619    value: SourceClassValue,
4620    target_style_uri: Option<&str>,
4621    references: &mut Vec<SourceSelectorReferenceFactV0>,
4622) {
4623    for selector_name in value.exact {
4624        push_selector_reference(
4625            byte_span,
4626            Some(selector_name),
4627            SourceSelectorReferenceMatchKindV0::Exact,
4628            target_style_uri,
4629            references,
4630        );
4631    }
4632    for prefix in value.prefixes {
4633        push_selector_reference(
4634            byte_span,
4635            Some(prefix),
4636            SourceSelectorReferenceMatchKindV0::Prefix,
4637            target_style_uri,
4638            references,
4639        );
4640    }
4641}
4642
4643fn collect_template_type_fact_targets(
4644    source: &str,
4645    literal_start: usize,
4646    literal_end: usize,
4647    target_style_uri: Option<&str>,
4648    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
4649) {
4650    let Some((prefix, expression_span, suffix)) =
4651        single_template_interpolation_projection(source, literal_start, literal_end)
4652    else {
4653        return;
4654    };
4655    let Some(path) = js_expression_path(source, expression_span.start, expression_span.end) else {
4656        return;
4657    };
4658    push_source_type_fact_target(
4659        expression_span,
4660        path.as_str(),
4661        target_style_uri,
4662        prefix.as_str(),
4663        suffix.as_str(),
4664        type_fact_targets,
4665    );
4666}
4667
4668fn single_template_interpolation_projection(
4669    source: &str,
4670    literal_start: usize,
4671    literal_end: usize,
4672) -> Option<(String, ParserByteSpanV0, String)> {
4673    let relative_open = source.get(literal_start..literal_end)?.find("${")?;
4674    let open = literal_start + relative_open;
4675    if source.get(open + 2..literal_end)?.contains("${") {
4676        return None;
4677    }
4678    let expression_start = open + 2;
4679    let close = matching_js_block_end(source, open + 1, b'{', b'}')?;
4680    if close > literal_end {
4681        return None;
4682    }
4683    let (expression_start, expression_end) = trim_js_expression(source, expression_start, close);
4684    if expression_start >= expression_end {
4685        return None;
4686    }
4687    let prefix_start = template_token_start(source, literal_start, open);
4688    let suffix_end = template_token_end(source, close + 1, literal_end);
4689    let prefix = source.get(prefix_start..open)?.to_string();
4690    let suffix = source.get(close + 1..suffix_end)?.to_string();
4691    if !prefix.chars().all(is_css_identifier_continue)
4692        || !suffix.chars().all(is_css_identifier_continue)
4693    {
4694        return None;
4695    }
4696    Some((
4697        prefix,
4698        ParserByteSpanV0 {
4699            start: expression_start,
4700            end: expression_end,
4701        },
4702        suffix,
4703    ))
4704}
4705
4706fn template_token_start(source: &str, literal_start: usize, prefix_end: usize) -> usize {
4707    source
4708        .get(literal_start..prefix_end)
4709        .and_then(|value| {
4710            value
4711                .char_indices()
4712                .rev()
4713                .find(|(_, ch)| ch.is_ascii_whitespace())
4714                .map(|(index, ch)| literal_start + index + ch.len_utf8())
4715        })
4716        .unwrap_or(literal_start)
4717}
4718
4719fn template_token_end(source: &str, suffix_start: usize, literal_end: usize) -> usize {
4720    source
4721        .get(suffix_start..literal_end)
4722        .and_then(|value| {
4723            value
4724                .char_indices()
4725                .find(|(_, ch)| ch.is_ascii_whitespace())
4726                .map(|(index, _)| suffix_start + index)
4727        })
4728        .unwrap_or(literal_end)
4729}
4730
4731fn push_source_type_fact_target(
4732    byte_span: ParserByteSpanV0,
4733    expression_path: &str,
4734    target_style_uri: Option<&str>,
4735    prefix: &str,
4736    suffix: &str,
4737    type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
4738) {
4739    type_fact_targets.push(SourceTypeFactTargetV0 {
4740        byte_span,
4741        expression_id: source_type_fact_expression_id(expression_path, byte_span),
4742        target_style_uri: target_style_uri.map(ToString::to_string),
4743        prefix: prefix.to_string(),
4744        suffix: suffix.to_string(),
4745    });
4746}
4747
4748fn source_type_fact_expression_id(expression_path: &str, byte_span: ParserByteSpanV0) -> String {
4749    format!(
4750        "omena-bridge-source-type-fact:{expression_path}:{}:{}",
4751        byte_span.start, byte_span.end
4752    )
4753}
4754
4755fn push_selector_reference(
4756    byte_span: ParserByteSpanV0,
4757    selector_name: Option<String>,
4758    match_kind: SourceSelectorReferenceMatchKindV0,
4759    target_style_uri: Option<&str>,
4760    references: &mut Vec<SourceSelectorReferenceFactV0>,
4761) {
4762    references.push(SourceSelectorReferenceFactV0 {
4763        byte_span,
4764        selector_name,
4765        match_kind,
4766        target_style_uri: target_style_uri.map(ToString::to_string),
4767    });
4768}
4769
4770fn push_js_literal_selector_references(
4771    source: &str,
4772    literal_start: usize,
4773    literal_end: usize,
4774    is_template: bool,
4775    target_style_uri: Option<&str>,
4776    references: &mut Vec<SourceSelectorReferenceFactV0>,
4777) {
4778    if is_template
4779        && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
4780    {
4781        push_template_prefix_selector_references(
4782            source,
4783            literal_start,
4784            literal_start + relative_interpolation,
4785            target_style_uri,
4786            references,
4787        );
4788        return;
4789    }
4790
4791    push_string_literal_selector_references(
4792        source,
4793        ParserByteSpanV0 {
4794            start: literal_start,
4795            end: literal_end,
4796        },
4797        target_style_uri.map(ToString::to_string),
4798        references,
4799    );
4800}
4801
4802fn push_template_prefix_selector_references(
4803    source: &str,
4804    literal_start: usize,
4805    prefix_end: usize,
4806    target_style_uri: Option<&str>,
4807    references: &mut Vec<SourceSelectorReferenceFactV0>,
4808) {
4809    let spans = class_token_byte_spans(source, literal_start, prefix_end);
4810    let prefix_ends_with_space = source[..prefix_end]
4811        .chars()
4812        .last()
4813        .is_none_or(char::is_whitespace);
4814    for (index, span) in spans.iter().enumerate() {
4815        let is_open_prefix = index + 1 == spans.len() && !prefix_ends_with_space;
4816        push_selector_reference(
4817            *span,
4818            Some(source[span.start..span.end].to_string()),
4819            if is_open_prefix {
4820                SourceSelectorReferenceMatchKindV0::Prefix
4821            } else {
4822                SourceSelectorReferenceMatchKindV0::Exact
4823            },
4824            target_style_uri,
4825            references,
4826        );
4827    }
4828}
4829
4830fn push_template_prefix_value(
4831    source: &str,
4832    literal_start: usize,
4833    prefix_end: usize,
4834    value: &mut SourceClassValue,
4835) {
4836    let spans = class_token_byte_spans(source, literal_start, prefix_end);
4837    let prefix_ends_with_space = source[..prefix_end]
4838        .chars()
4839        .last()
4840        .is_none_or(char::is_whitespace);
4841    for (index, span) in spans.iter().enumerate() {
4842        let token = source[span.start..span.end].to_string();
4843        if index + 1 == spans.len() && !prefix_ends_with_space {
4844            value.prefixes.push(token);
4845        } else {
4846            value.exact.push(token);
4847        }
4848    }
4849}
4850
4851fn class_token_strings(source: &str, literal_start: usize, literal_end: usize) -> Vec<String> {
4852    class_token_byte_spans(source, literal_start, literal_end)
4853        .into_iter()
4854        .map(|span| source[span.start..span.end].to_string())
4855        .collect()
4856}
4857
4858fn push_string_literal_selector_references(
4859    source: &str,
4860    literal_span: ParserByteSpanV0,
4861    target_style_uri: Option<String>,
4862    references: &mut Vec<SourceSelectorReferenceFactV0>,
4863) {
4864    for span in class_token_byte_spans(source, literal_span.start, literal_span.end) {
4865        references.push(SourceSelectorReferenceFactV0 {
4866            byte_span: span,
4867            selector_name: None,
4868            match_kind: SourceSelectorReferenceMatchKindV0::Exact,
4869            target_style_uri: target_style_uri.clone(),
4870        });
4871    }
4872}
4873
4874fn trim_js_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
4875    let mut start = char_boundary_ceil(source, start);
4876    let mut end = char_boundary_floor(source, end);
4877    start = skip_js_trivia_until(source, start, end);
4878    while end > start
4879        && source
4880            .as_bytes()
4881            .get(end - 1)
4882            .is_some_and(u8::is_ascii_whitespace)
4883    {
4884        end -= 1;
4885    }
4886    (start, end)
4887}
4888
4889fn char_boundary_floor(source: &str, index: usize) -> usize {
4890    let mut index = index.min(source.len());
4891    while index > 0 && !source.is_char_boundary(index) {
4892        index -= 1;
4893    }
4894    index
4895}
4896
4897fn char_boundary_ceil(source: &str, index: usize) -> usize {
4898    let mut index = index.min(source.len());
4899    while index < source.len() && !source.is_char_boundary(index) {
4900        index += 1;
4901    }
4902    index
4903}
4904
4905fn advance_js_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
4906    let cursor = char_boundary_ceil(source, cursor);
4907    let limit = char_boundary_floor(source, limit);
4908    if cursor >= limit {
4909        return limit;
4910    }
4911    char_boundary_ceil(source, cursor + 1).min(limit)
4912}
4913
4914fn advance_js_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
4915    let after_slash = advance_js_scan_cursor(source, slash_offset, limit);
4916    advance_js_scan_cursor(source, after_slash, limit)
4917}
4918
4919fn unwrap_js_parenthesized_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
4920    let mut current_start = start;
4921    let mut current_end = end;
4922    loop {
4923        let (trimmed_start, trimmed_end) = trim_js_expression(source, current_start, current_end);
4924        if source.as_bytes().get(trimmed_start) == Some(&b'(')
4925            && matching_js_block_end(source, trimmed_start, b'(', b')')
4926                == Some(trimmed_end.saturating_sub(1))
4927        {
4928            current_start = trimmed_start + 1;
4929            current_end = trimmed_end - 1;
4930            continue;
4931        }
4932        return (trimmed_start, trimmed_end);
4933    }
4934}
4935
4936fn js_statement_expression_end(source: &str, start: usize) -> usize {
4937    let mut cursor = char_boundary_ceil(source, start);
4938    let mut depth = 0usize;
4939    while cursor < source.len() {
4940        match source.as_bytes().get(cursor).copied() {
4941            Some(b'\'' | b'"' | b'`') => {
4942                cursor =
4943                    skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
4944            }
4945            Some(b'(' | b'[' | b'{') => {
4946                depth += 1;
4947                cursor = advance_js_scan_cursor(source, cursor, source.len());
4948            }
4949            Some(b')' | b']' | b'}') => {
4950                depth = depth.saturating_sub(1);
4951                cursor = advance_js_scan_cursor(source, cursor, source.len());
4952            }
4953            Some(b';') if depth == 0 => return cursor,
4954            Some(b'\n') if depth == 0 => return cursor,
4955            Some(_) => cursor = advance_js_scan_cursor(source, cursor, source.len()),
4956            None => break,
4957        }
4958    }
4959    source.len()
4960}
4961
4962fn matching_js_block_end(source: &str, open_offset: usize, open: u8, close: u8) -> Option<usize> {
4963    if source.as_bytes().get(open_offset) != Some(&open) {
4964        return None;
4965    }
4966    let mut cursor = advance_js_scan_cursor(source, open_offset, source.len());
4967    let mut depth = 1usize;
4968    while cursor < source.len() {
4969        match source.as_bytes().get(cursor).copied()? {
4970            b'\'' | b'"' | b'`' => {
4971                cursor = skip_js_string_literal(source, cursor, source.len())?;
4972            }
4973            byte if byte == open => {
4974                depth += 1;
4975                cursor = advance_js_scan_cursor(source, cursor, source.len());
4976            }
4977            byte if byte == close => {
4978                depth -= 1;
4979                if depth == 0 {
4980                    return Some(cursor);
4981                }
4982                cursor = advance_js_scan_cursor(source, cursor, source.len());
4983            }
4984            _ => cursor = advance_js_scan_cursor(source, cursor, source.len()),
4985        }
4986    }
4987    None
4988}
4989
4990fn split_top_level_js_segments(
4991    source: &str,
4992    start: usize,
4993    end: usize,
4994    delimiter: u8,
4995) -> Vec<(usize, usize)> {
4996    let mut segments = Vec::new();
4997    let end = char_boundary_floor(source, end);
4998    let mut segment_start = char_boundary_ceil(source, start).min(end);
4999    let mut cursor = segment_start;
5000    let mut depth = 0usize;
5001    while cursor < end {
5002        match source.as_bytes().get(cursor).copied() {
5003            Some(b'\'' | b'"' | b'`') => {
5004                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
5005            }
5006            Some(b'(' | b'[' | b'{') => {
5007                depth += 1;
5008                cursor = advance_js_scan_cursor(source, cursor, end);
5009            }
5010            Some(b')' | b']' | b'}') => {
5011                depth = depth.saturating_sub(1);
5012                cursor = advance_js_scan_cursor(source, cursor, end);
5013            }
5014            Some(byte) if byte == delimiter && depth == 0 => {
5015                segments.push((segment_start, cursor));
5016                cursor = advance_js_scan_cursor(source, cursor, end);
5017                segment_start = cursor;
5018            }
5019            Some(_) => cursor = advance_js_scan_cursor(source, cursor, end),
5020            None => break,
5021        }
5022    }
5023    if segment_start <= end {
5024        segments.push((segment_start, end));
5025    }
5026    segments
5027}
5028
5029fn find_top_level_js_byte(source: &str, start: usize, end: usize, needle: u8) -> Option<usize> {
5030    let end = char_boundary_floor(source, end);
5031    let mut cursor = char_boundary_ceil(source, start).min(end);
5032    let mut depth = 0usize;
5033    while cursor < end {
5034        match source.as_bytes().get(cursor).copied()? {
5035            b'\'' | b'"' | b'`' => {
5036                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
5037            }
5038            b'(' | b'[' | b'{' => {
5039                depth += 1;
5040                cursor = advance_js_scan_cursor(source, cursor, end);
5041            }
5042            b')' | b']' | b'}' => {
5043                depth = depth.saturating_sub(1);
5044                cursor = advance_js_scan_cursor(source, cursor, end);
5045            }
5046            byte if byte == needle && depth == 0 => return Some(cursor),
5047            _ => cursor = advance_js_scan_cursor(source, cursor, end),
5048        }
5049    }
5050    None
5051}
5052
5053fn find_top_level_js_operator(
5054    source: &str,
5055    start: usize,
5056    end: usize,
5057    operator: &str,
5058) -> Option<usize> {
5059    let end = char_boundary_floor(source, end);
5060    let mut cursor = char_boundary_ceil(source, start).min(end);
5061    let mut depth = 0usize;
5062    while cursor < end {
5063        match source.as_bytes().get(cursor).copied()? {
5064            b'\'' | b'"' | b'`' => {
5065                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
5066            }
5067            b'(' | b'[' | b'{' => {
5068                depth += 1;
5069                cursor = advance_js_scan_cursor(source, cursor, end);
5070            }
5071            b')' | b']' | b'}' => {
5072                depth = depth.saturating_sub(1);
5073                cursor = advance_js_scan_cursor(source, cursor, end);
5074            }
5075            _ if depth == 0
5076                && source
5077                    .get(cursor..end)
5078                    .is_some_and(|rest| rest.starts_with(operator)) =>
5079            {
5080                return Some(cursor);
5081            }
5082            _ => cursor = advance_js_scan_cursor(source, cursor, end),
5083        }
5084    }
5085    None
5086}
5087
5088fn top_level_conditional_parts(
5089    source: &str,
5090    start: usize,
5091    end: usize,
5092) -> Option<(usize, usize, usize, usize, usize)> {
5093    let question = find_top_level_js_byte(source, start, end, b'?')?;
5094    let end = char_boundary_floor(source, end);
5095    let mut cursor = advance_js_scan_cursor(source, question, end);
5096    let mut depth = 0usize;
5097    let mut nested_conditional_depth = 0usize;
5098    while cursor < end {
5099        match source.as_bytes().get(cursor).copied()? {
5100            b'\'' | b'"' | b'`' => {
5101                cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
5102            }
5103            b'(' | b'[' | b'{' => {
5104                depth += 1;
5105                cursor = advance_js_scan_cursor(source, cursor, end);
5106            }
5107            b')' | b']' | b'}' => {
5108                depth = depth.saturating_sub(1);
5109                cursor = advance_js_scan_cursor(source, cursor, end);
5110            }
5111            b'?' if depth == 0 => {
5112                nested_conditional_depth += 1;
5113                cursor = advance_js_scan_cursor(source, cursor, end);
5114            }
5115            b':' if depth == 0 && nested_conditional_depth == 0 => {
5116                return Some((
5117                    question,
5118                    advance_js_scan_cursor(source, question, end),
5119                    cursor,
5120                    advance_js_scan_cursor(source, cursor, end),
5121                    end,
5122                ));
5123            }
5124            b':' if depth == 0 => {
5125                nested_conditional_depth = nested_conditional_depth.saturating_sub(1);
5126                cursor = advance_js_scan_cursor(source, cursor, end);
5127            }
5128            _ => cursor = advance_js_scan_cursor(source, cursor, end),
5129        }
5130    }
5131    None
5132}
5133
5134fn js_expression_path(source: &str, start: usize, end: usize) -> Option<String> {
5135    let (start, end) = trim_js_expression(source, start, end);
5136    let (first, mut cursor) = read_js_identifier(source, start)?;
5137    let mut path = vec![first.to_string()];
5138    loop {
5139        cursor = skip_js_trivia_until(source, cursor, end);
5140        match source.as_bytes().get(cursor).copied() {
5141            Some(b'.') => {
5142                let member_start = skip_js_trivia_until(source, cursor + 1, end);
5143                let (member, member_end) = read_js_identifier(source, member_start)?;
5144                path.push(member.to_string());
5145                cursor = member_end;
5146            }
5147            Some(b'[') => {
5148                if let Some((literal_start, literal_end, bracket_end)) =
5149                    bracket_string_literal_access(source, cursor)
5150                    && bracket_end <= end
5151                {
5152                    path.push(source[literal_start..literal_end].to_string());
5153                    cursor = bracket_end;
5154                } else {
5155                    return None;
5156                }
5157            }
5158            _ => break,
5159        }
5160    }
5161    (trim_js_expression(source, cursor, end).0 >= end).then(|| path.join("."))
5162}
5163
5164fn static_string_prefix_for_js_expression(
5165    source: &str,
5166    start: usize,
5167    end: usize,
5168    local_class_values: &BTreeMap<String, SourceClassValue>,
5169) -> Option<String> {
5170    let (start, end) = trim_js_expression(source, start, end);
5171    let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
5172    if let Some((literal_start, literal_end, next_offset)) =
5173        js_string_literal_span(source, start, end)
5174        && trim_js_expression(source, next_offset, end).0 >= end
5175    {
5176        if source.as_bytes().get(start).copied() == Some(b'`')
5177            && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
5178        {
5179            return Some(source[literal_start..literal_start + relative_interpolation].to_string());
5180        }
5181        return Some(source[literal_start..literal_end].to_string());
5182    }
5183    if let Some(path) = js_expression_path(source, start, end)
5184        && let Some(value) = local_class_values.get(path.as_str())
5185    {
5186        if value.exact.len() == 1 && value.prefixes.is_empty() {
5187            return value.exact.first().cloned();
5188        }
5189        if value.prefixes.len() == 1 && value.exact.is_empty() {
5190            return value.prefixes.first().cloned();
5191        }
5192    }
5193    if let Some(plus_offset) = find_top_level_js_operator(source, start, end, "+") {
5194        let left =
5195            static_string_prefix_for_js_expression(source, start, plus_offset, local_class_values)?;
5196        let right = static_string_prefix_for_js_expression(
5197            source,
5198            plus_offset + 1,
5199            end,
5200            local_class_values,
5201        )
5202        .unwrap_or_default();
5203        return Some(format!("{left}{right}"));
5204    }
5205    None
5206}
5207
5208fn js_call_end(source: &str, open_paren: usize) -> Option<usize> {
5209    if source.as_bytes().get(open_paren) != Some(&b'(') {
5210        return None;
5211    }
5212    let mut cursor = advance_js_scan_cursor(source, open_paren, source.len());
5213    let mut depth = 1usize;
5214    while cursor < source.len() {
5215        match source.as_bytes().get(cursor).copied()? {
5216            b'\'' | b'"' | b'`' => {
5217                cursor = skip_js_string_literal(source, cursor, source.len())?;
5218            }
5219            b'(' => {
5220                depth += 1;
5221                cursor = advance_js_scan_cursor(source, cursor, source.len());
5222            }
5223            b')' => {
5224                depth -= 1;
5225                if depth == 0 {
5226                    return Some(cursor);
5227                }
5228                cursor = advance_js_scan_cursor(source, cursor, source.len());
5229            }
5230            _ => {
5231                cursor = advance_js_scan_cursor(source, cursor, source.len());
5232            }
5233        }
5234    }
5235    None
5236}
5237
5238fn class_token_byte_spans(
5239    source: &str,
5240    literal_start: usize,
5241    literal_end: usize,
5242) -> Vec<ParserByteSpanV0> {
5243    let mut spans = Vec::new();
5244    let mut token_start: Option<usize> = None;
5245    for (relative_index, ch) in source[literal_start..literal_end].char_indices() {
5246        let index = literal_start + relative_index;
5247        if ch.is_ascii_whitespace() {
5248            if let Some(start) = token_start.take() {
5249                push_class_token_span(source, start, index, &mut spans);
5250            }
5251        } else if token_start.is_none() {
5252            token_start = Some(index);
5253        }
5254    }
5255    if let Some(start) = token_start {
5256        push_class_token_span(source, start, literal_end, &mut spans);
5257    }
5258    spans
5259}
5260
5261fn push_class_token_span(
5262    source: &str,
5263    start: usize,
5264    end: usize,
5265    spans: &mut Vec<ParserByteSpanV0>,
5266) {
5267    if start < end && source[start..end].chars().all(is_css_identifier_continue) {
5268        spans.push(ParserByteSpanV0 { start, end });
5269    }
5270}
5271
5272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5273struct CodeIdentifier<'a> {
5274    text: &'a str,
5275    end: usize,
5276}
5277
5278fn next_code_identifier(source: &str, mut cursor: usize) -> Option<CodeIdentifier<'_>> {
5279    while cursor < source.len() {
5280        cursor = skip_js_trivia(source, cursor);
5281        let byte = source.as_bytes().get(cursor).copied()?;
5282        if matches!(byte, b'\'' | b'"' | b'`') {
5283            cursor = skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
5284            continue;
5285        }
5286        if byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$') {
5287            let (text, end) = read_js_identifier(source, cursor)?;
5288            return Some(CodeIdentifier { text, end });
5289        }
5290        cursor = advance_js_scan_cursor(source, cursor, source.len());
5291    }
5292    None
5293}
5294
5295fn skip_js_trivia(source: &str, cursor: usize) -> usize {
5296    skip_js_trivia_until(source, cursor, source.len())
5297}
5298
5299fn skip_js_trivia_until(source: &str, mut cursor: usize, limit: usize) -> usize {
5300    loop {
5301        cursor = skip_ascii_whitespace_until(source, cursor, limit);
5302        if source.as_bytes().get(cursor) == Some(&b'/') {
5303            match source.as_bytes().get(cursor + 1).copied() {
5304                Some(b'/') => {
5305                    cursor = skip_js_line_comment(source, cursor + 2, limit);
5306                    continue;
5307                }
5308                Some(b'*') => {
5309                    cursor = skip_js_block_comment(source, cursor + 2, limit);
5310                    continue;
5311                }
5312                _ => {}
5313            }
5314        }
5315        return cursor;
5316    }
5317}
5318
5319fn skip_ascii_whitespace_until(source: &str, mut offset: usize, limit: usize) -> usize {
5320    while offset < limit
5321        && source
5322            .as_bytes()
5323            .get(offset)
5324            .is_some_and(u8::is_ascii_whitespace)
5325    {
5326        offset += 1;
5327    }
5328    offset
5329}
5330
5331fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
5332    while source
5333        .as_bytes()
5334        .get(offset)
5335        .is_some_and(u8::is_ascii_whitespace)
5336    {
5337        offset += 1;
5338    }
5339    offset
5340}
5341
5342fn skip_js_line_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
5343    let limit = char_boundary_floor(source, limit);
5344    while cursor < limit {
5345        if source.as_bytes().get(cursor) == Some(&b'\n') {
5346            return advance_js_scan_cursor(source, cursor, limit);
5347        }
5348        cursor = advance_js_scan_cursor(source, cursor, limit);
5349    }
5350    limit
5351}
5352
5353fn skip_js_block_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
5354    let limit = char_boundary_floor(source, limit);
5355    while cursor + 1 < limit {
5356        if source.as_bytes().get(cursor) == Some(&b'*')
5357            && source.as_bytes().get(cursor + 1) == Some(&b'/')
5358        {
5359            return cursor + 2;
5360        }
5361        cursor = advance_js_scan_cursor(source, cursor, limit);
5362    }
5363    limit
5364}
5365
5366fn js_string_literal_span(
5367    source: &str,
5368    quote_offset: usize,
5369    limit: usize,
5370) -> Option<(usize, usize, usize)> {
5371    let quote = source.as_bytes().get(quote_offset).copied()?;
5372    if !matches!(quote, b'\'' | b'"' | b'`') {
5373        return None;
5374    }
5375    let literal_start = quote_offset + 1;
5376    let next_offset = skip_js_string_literal(source, quote_offset, limit)?;
5377    Some((literal_start, next_offset - 1, next_offset))
5378}
5379
5380fn skip_js_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
5381    let quote = source.as_bytes().get(quote_offset).copied()?;
5382    let limit = char_boundary_floor(source, limit);
5383    let mut cursor = quote_offset + 1;
5384    while cursor < limit {
5385        let byte = source.as_bytes().get(cursor).copied()?;
5386        if byte == b'\\' {
5387            cursor = advance_js_escaped_char(source, cursor, limit);
5388            continue;
5389        }
5390        if byte == quote {
5391            return Some(cursor + 1);
5392        }
5393        cursor = advance_js_scan_cursor(source, cursor, limit);
5394    }
5395    None
5396}
5397
5398fn bracket_string_literal_access(
5399    source: &str,
5400    bracket_offset: usize,
5401) -> Option<(usize, usize, usize)> {
5402    if source.as_bytes().get(bracket_offset) != Some(&b'[') {
5403        return None;
5404    }
5405    let quote_offset = skip_ascii_whitespace(source, bracket_offset + 1);
5406    let quote = source.as_bytes().get(quote_offset).copied()?;
5407    if !matches!(quote, b'\'' | b'"') {
5408        return None;
5409    }
5410    let (literal_start, literal_end, literal_next) =
5411        js_string_literal_span(source, quote_offset, source.len())?;
5412    if literal_next > source.len() {
5413        return None;
5414    }
5415    let closing_bracket = skip_ascii_whitespace(source, literal_end + 1);
5416    if source.as_bytes().get(closing_bracket) != Some(&b']') {
5417        return None;
5418    }
5419    Some((literal_start, literal_end, closing_bracket + 1))
5420}
5421
5422fn read_js_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
5423    let start = char_boundary_ceil(source, start);
5424    let first = source.get(start..)?.chars().next()?;
5425    if !is_js_identifier_start(first) {
5426        return None;
5427    }
5428    let mut end = start + first.len_utf8();
5429    let scan_start = end;
5430    for (relative_index, ch) in source.get(scan_start..)?.char_indices() {
5431        if !is_js_identifier_continue(ch) {
5432            break;
5433        }
5434        end = scan_start + relative_index + ch.len_utf8();
5435    }
5436    Some((&source[start..end], end))
5437}
5438
5439fn is_js_identifier_start(ch: char) -> bool {
5440    ch.is_ascii_alphabetic() || matches!(ch, '_' | '$')
5441}
5442
5443fn is_js_identifier_continue(ch: char) -> bool {
5444    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')
5445}
5446
5447fn is_css_identifier_continue(ch: char) -> bool {
5448    ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
5449}
5450
5451#[cfg(test)]
5452mod tests;