Skip to main content

omena_bridge/
source_syntax.rs

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