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