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