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