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 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 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 _ => {
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 ¶ms.items {
3187 self.collect_binding_pattern_decl_facts(¶meter.pattern, "parameter", None);
3188 }
3189 if let Some(rest) = ¶ms.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;