1use omena_parser::ParserByteSpanV0;
2use oxc_allocator::Allocator;
3use oxc_ast::ast::{
4 Argument, ArrayExpression, ArrayExpressionElement, BindingPattern, CallExpression,
5 ChainElement, Class, ClassElement, ComputedMemberExpression, ConditionalExpression,
6 Declaration, Expression, ImportDeclarationSpecifier, ImportOrExportKind, JSXAttributeName,
7 JSXAttributeValue, JSXChild, JSXExpression, LogicalExpression, ObjectExpression,
8 ObjectPropertyKind, ParenthesizedExpression, Program, Statement, StaticMemberExpression,
9 TSAsExpression, TSNonNullExpression, TSSatisfiesExpression, VariableDeclarator,
10};
11use oxc_parser::{Parser, ParserReturn};
12use oxc_span::{GetSpan, SourceType, Span};
13use serde::Serialize;
14use std::collections::{BTreeMap, BTreeSet};
15
16use crate::source_language::{project_source_for_language, source_type_for_language};
17
18#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SourceSyntaxIndexV0 {
21 pub schema_version: &'static str,
22 pub product: &'static str,
23 pub imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
24 pub class_string_literals: Vec<ParserByteSpanV0>,
25 pub style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
26 pub selector_references: Vec<SourceSelectorReferenceFactV0>,
27 pub type_fact_targets: Vec<SourceTypeFactTargetV0>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct SourceImportedStyleBindingV0 {
33 pub binding: String,
34 pub style_uri: String,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct SourceStylePropertyAccessFactV0 {
40 pub byte_span: ParserByteSpanV0,
41 pub target_style_uri: Option<String>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct SourceSelectorReferenceFactV0 {
47 pub byte_span: ParserByteSpanV0,
48 pub selector_name: Option<String>,
49 pub match_kind: SourceSelectorReferenceMatchKindV0,
50 pub target_style_uri: Option<String>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct SourceTypeFactTargetV0 {
56 pub byte_span: ParserByteSpanV0,
57 pub expression_id: String,
58 pub target_style_uri: Option<String>,
59 pub prefix: String,
60 pub suffix: String,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub enum SourceSelectorReferenceMatchKindV0 {
66 Exact,
67 Prefix,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71struct SourceStyleBindingTarget {
72 binding: String,
73 target_style_uri: Option<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77struct ClassnamesBindUtilityBinding {
78 binding: String,
79 style_uri: String,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83struct ClassnamesBindCallArgument {
84 binding: String,
85 byte_span: ParserByteSpanV0,
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Eq)]
89struct SourceClassValue {
90 exact: Vec<String>,
91 prefixes: Vec<String>,
92}
93
94impl SourceClassValue {
95 fn is_empty(&self) -> bool {
96 self.exact.is_empty() && self.prefixes.is_empty()
97 }
98
99 fn merge(&mut self, other: SourceClassValue) {
100 self.exact.extend(other.exact);
101 self.prefixes.extend(other.prefixes);
102 self.canonicalize();
103 }
104
105 fn canonicalize(&mut self) {
106 self.exact.sort();
107 self.exact.dedup();
108 self.prefixes.sort();
109 self.prefixes.dedup();
110 }
111}
112
113type SourceReferenceDedupeKey = (
114 usize,
115 usize,
116 Option<String>,
117 SourceSelectorReferenceMatchKindV0,
118);
119type SourceReferenceTargetMap = BTreeMap<SourceReferenceDedupeKey, BTreeSet<Option<String>>>;
120
121pub fn summarize_omena_bridge_source_syntax_index(
122 source: &str,
123 imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
124 classnames_bind_bindings: Vec<String>,
125) -> SourceSyntaxIndexV0 {
126 summarize_omena_bridge_source_syntax_index_for_source_language(
127 "source.tsx",
128 source,
129 None,
130 imported_style_bindings,
131 classnames_bind_bindings,
132 )
133}
134
135pub fn summarize_omena_bridge_source_syntax_index_for_source_language(
136 source_path: &str,
137 source: &str,
138 source_language: Option<&str>,
139 imported_style_bindings: Vec<SourceImportedStyleBindingV0>,
140 classnames_bind_bindings: Vec<String>,
141) -> SourceSyntaxIndexV0 {
142 let projected_source = project_source_for_language(source_path, source, source_language);
143 let imported_style_targets = imported_style_targets(imported_style_bindings.as_slice());
144 let property_access_targets = property_access_style_targets(imported_style_bindings.as_slice());
145 let ast_facts = collect_source_syntax_ast_facts(
146 projected_source.as_ref(),
147 source_type_for_language(source_path, source_language),
148 property_access_targets.as_slice(),
149 imported_style_targets.as_slice(),
150 classnames_bind_bindings.as_slice(),
151 );
152 let class_string_literals = ast_facts.class_string_literals;
153 let style_property_accesses = ast_facts.style_property_accesses;
154 let class_name_expression_spans = ast_facts.class_name_expression_spans;
155 let classnames_bind_targets = ast_facts.classnames_bind_utility_bindings;
156 let classnames_bind_call_arguments = ast_facts.classnames_bind_call_arguments;
157 let local_class_values = collect_local_class_value_bindings(projected_source.as_ref());
158
159 let mut index = SourceSyntaxIndexV0 {
160 schema_version: "0",
161 product: "omena-bridge.source-syntax-index",
162 imported_style_bindings,
163 class_string_literals,
164 style_property_accesses,
165 selector_references: Vec::new(),
166 type_fact_targets: Vec::new(),
167 };
168
169 for span in &index.class_string_literals {
170 push_string_literal_selector_references(
171 source,
172 *span,
173 None,
174 &mut index.selector_references,
175 );
176 }
177 for span in class_name_expression_spans {
178 collect_selector_references_from_js_expression(
179 source,
180 span.start,
181 span.end,
182 None,
183 &local_class_values,
184 &mut index.selector_references,
185 &mut index.type_fact_targets,
186 );
187 }
188 for access in &index.style_property_accesses {
189 index
190 .selector_references
191 .push(SourceSelectorReferenceFactV0 {
192 byte_span: access.byte_span,
193 selector_name: None,
194 match_kind: SourceSelectorReferenceMatchKindV0::Exact,
195 target_style_uri: access.target_style_uri.clone(),
196 });
197 }
198 for argument in classnames_bind_call_arguments {
199 if let Some(binding) = classnames_bind_targets
200 .iter()
201 .find(|binding| binding.binding == argument.binding)
202 {
203 collect_selector_references_from_js_expression(
204 source,
205 argument.byte_span.start,
206 argument.byte_span.end,
207 Some(binding.style_uri.as_str()),
208 &local_class_values,
209 &mut index.selector_references,
210 &mut index.type_fact_targets,
211 );
212 }
213 }
214 canonicalize_source_selector_references(&mut index.selector_references);
215
216 index
217}
218
219pub fn collect_omena_bridge_vue_style_module_bindings(
220 source_path: &str,
221 source: &str,
222 source_language: Option<&str>,
223) -> Vec<String> {
224 let projected_source = project_source_for_language(source_path, source, source_language);
225 let allocator = Allocator::default();
226 let ParserReturn {
227 program, panicked, ..
228 } = Parser::new(
229 &allocator,
230 projected_source.as_ref(),
231 source_type_for_language(source_path, source_language),
232 )
233 .parse();
234 if panicked {
235 return Vec::new();
236 }
237 collect_vue_use_css_module_bindings(&program)
238}
239
240pub fn canonicalize_source_selector_references(
241 references: &mut Vec<SourceSelectorReferenceFactV0>,
242) {
243 let mut targets_by_reference: SourceReferenceTargetMap = BTreeMap::new();
244 for reference in references.iter() {
245 targets_by_reference
246 .entry((
247 reference.byte_span.start,
248 reference.byte_span.end,
249 reference.selector_name.clone(),
250 reference.match_kind,
251 ))
252 .or_default()
253 .insert(reference.target_style_uri.clone());
254 }
255
256 let mut canonical = Vec::new();
257 for ((start, end, selector_name, match_kind), targets) in targets_by_reference {
258 let has_targeted_reference = targets.iter().any(Option::is_some);
259 for target_style_uri in targets {
260 if has_targeted_reference && target_style_uri.is_none() {
261 continue;
262 }
263 canonical.push(SourceSelectorReferenceFactV0 {
264 byte_span: ParserByteSpanV0 { start, end },
265 selector_name: selector_name.clone(),
266 match_kind,
267 target_style_uri,
268 });
269 }
270 }
271 *references = canonical;
272}
273
274fn imported_style_targets(
275 bindings: &[SourceImportedStyleBindingV0],
276) -> Vec<SourceStyleBindingTarget> {
277 bindings
278 .iter()
279 .map(|binding| SourceStyleBindingTarget {
280 binding: binding.binding.clone(),
281 target_style_uri: Some(binding.style_uri.clone()),
282 })
283 .collect()
284}
285
286fn property_access_style_targets(
287 bindings: &[SourceImportedStyleBindingV0],
288) -> Vec<SourceStyleBindingTarget> {
289 let imported = imported_style_targets(bindings);
290 if imported.is_empty() {
291 vec![SourceStyleBindingTarget {
292 binding: "styles".to_string(),
293 target_style_uri: None,
294 }]
295 } else {
296 imported
297 }
298}
299
300struct SourceSyntaxAstFacts {
301 class_string_literals: Vec<ParserByteSpanV0>,
302 style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
303 class_name_expression_spans: Vec<ParserByteSpanV0>,
304 classnames_bind_utility_bindings: Vec<ClassnamesBindUtilityBinding>,
305 classnames_bind_call_arguments: Vec<ClassnamesBindCallArgument>,
306}
307
308fn collect_source_syntax_ast_facts(
309 source: &str,
310 source_type: SourceType,
311 property_access_targets: &[SourceStyleBindingTarget],
312 style_targets: &[SourceStyleBindingTarget],
313 classnames_bind_imports: &[String],
314) -> SourceSyntaxAstFacts {
315 let allocator = Allocator::default();
316 let ParserReturn {
317 program, panicked, ..
318 } = Parser::new(&allocator, source, source_type).parse();
319 if panicked {
320 return SourceSyntaxAstFacts {
321 class_string_literals: Vec::new(),
322 style_property_accesses: Vec::new(),
323 class_name_expression_spans: Vec::new(),
324 classnames_bind_utility_bindings: Vec::new(),
325 classnames_bind_call_arguments: Vec::new(),
326 };
327 }
328
329 let mut collector = SourceSyntaxAstCollector {
330 source,
331 property_access_targets,
332 style_targets,
333 classnames_bind_imports,
334 class_string_literals: Vec::new(),
335 style_property_accesses: Vec::new(),
336 class_name_expression_spans: Vec::new(),
337 classnames_bind_utility_bindings: Vec::new(),
338 classnames_bind_call_arguments: Vec::new(),
339 };
340 collector.collect_program(&program);
341 collector.canonicalize();
342 SourceSyntaxAstFacts {
343 class_string_literals: collector.class_string_literals,
344 style_property_accesses: collector.style_property_accesses,
345 class_name_expression_spans: collector.class_name_expression_spans,
346 classnames_bind_utility_bindings: collector.classnames_bind_utility_bindings,
347 classnames_bind_call_arguments: collector.classnames_bind_call_arguments,
348 }
349}
350
351fn collect_vue_use_css_module_import_names(program: &Program<'_>) -> BTreeSet<String> {
352 let mut names = BTreeSet::new();
353 for statement in &program.body {
354 let Statement::ImportDeclaration(import) = statement else {
355 continue;
356 };
357 if import.import_kind != ImportOrExportKind::Value || import.source.value.as_str() != "vue"
358 {
359 continue;
360 }
361 let Some(specifiers) = import.specifiers.as_ref() else {
362 continue;
363 };
364 for specifier in specifiers {
365 if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = specifier {
366 let imported_name = specifier.imported.name().as_str();
367 if imported_name == "useCssModule" {
368 names.insert(specifier.local.name.as_str().to_string());
369 }
370 }
371 }
372 }
373 names
374}
375
376fn collect_vue_use_css_module_bindings(program: &Program<'_>) -> Vec<String> {
377 let use_css_module_names = collect_vue_use_css_module_import_names(program);
378 if use_css_module_names.is_empty() {
379 return Vec::new();
380 }
381 let mut bindings = BTreeSet::new();
382 for statement in &program.body {
383 collect_vue_use_css_module_bindings_from_statement(
384 statement,
385 &use_css_module_names,
386 &mut bindings,
387 );
388 }
389 bindings.into_iter().collect()
390}
391
392fn collect_vue_use_css_module_bindings_from_statement(
393 statement: &Statement<'_>,
394 use_css_module_names: &BTreeSet<String>,
395 bindings: &mut BTreeSet<String>,
396) {
397 match statement {
398 Statement::VariableDeclaration(declaration) => {
399 collect_vue_use_css_module_bindings_from_variable_declaration(
400 declaration,
401 use_css_module_names,
402 bindings,
403 );
404 }
405 Statement::ExportNamedDeclaration(declaration) => {
406 if let Some(Declaration::VariableDeclaration(declaration)) = &declaration.declaration {
407 collect_vue_use_css_module_bindings_from_variable_declaration(
408 declaration,
409 use_css_module_names,
410 bindings,
411 );
412 }
413 }
414 _ => {}
415 }
416}
417
418fn collect_vue_use_css_module_bindings_from_variable_declaration(
419 declaration: &oxc_ast::ast::VariableDeclaration<'_>,
420 use_css_module_names: &BTreeSet<String>,
421 bindings: &mut BTreeSet<String>,
422) {
423 for declarator in &declaration.declarations {
424 let Some(binding) = binding_pattern_identifier_name(&declarator.id) else {
425 continue;
426 };
427 let Some(Expression::CallExpression(call)) = &declarator.init else {
428 continue;
429 };
430 let Some(callee) = expression_identifier_name(&call.callee) else {
431 continue;
432 };
433 if use_css_module_names.contains(callee) {
434 bindings.insert(binding.to_string());
435 }
436 }
437}
438
439struct SourceSyntaxAstCollector<'a> {
440 source: &'a str,
441 property_access_targets: &'a [SourceStyleBindingTarget],
442 style_targets: &'a [SourceStyleBindingTarget],
443 classnames_bind_imports: &'a [String],
444 class_string_literals: Vec<ParserByteSpanV0>,
445 style_property_accesses: Vec<SourceStylePropertyAccessFactV0>,
446 class_name_expression_spans: Vec<ParserByteSpanV0>,
447 classnames_bind_utility_bindings: Vec<ClassnamesBindUtilityBinding>,
448 classnames_bind_call_arguments: Vec<ClassnamesBindCallArgument>,
449}
450
451impl<'a> SourceSyntaxAstCollector<'a> {
452 fn collect_program(&mut self, program: &Program<'a>) {
453 for statement in &program.body {
454 self.collect_statement(statement);
455 }
456 }
457
458 fn collect_statement(&mut self, statement: &Statement<'a>) {
459 match statement {
460 Statement::BlockStatement(statement) => {
461 for statement in &statement.body {
462 self.collect_statement(statement);
463 }
464 }
465 Statement::ExpressionStatement(statement) => {
466 self.collect_expression(&statement.expression);
467 }
468 Statement::ReturnStatement(statement) => {
469 if let Some(argument) = &statement.argument {
470 self.collect_expression(argument);
471 }
472 }
473 Statement::IfStatement(statement) => {
474 self.collect_expression(&statement.test);
475 self.collect_statement(&statement.consequent);
476 if let Some(alternate) = &statement.alternate {
477 self.collect_statement(alternate);
478 }
479 }
480 Statement::ForStatement(statement) => {
481 if let Some(init) = &statement.init {
482 self.collect_for_statement_init(init);
483 }
484 if let Some(test) = &statement.test {
485 self.collect_expression(test);
486 }
487 if let Some(update) = &statement.update {
488 self.collect_expression(update);
489 }
490 self.collect_statement(&statement.body);
491 }
492 Statement::ForInStatement(statement) => {
493 self.collect_expression(&statement.right);
494 self.collect_statement(&statement.body);
495 }
496 Statement::ForOfStatement(statement) => {
497 self.collect_expression(&statement.right);
498 self.collect_statement(&statement.body);
499 }
500 Statement::WhileStatement(statement) => {
501 self.collect_expression(&statement.test);
502 self.collect_statement(&statement.body);
503 }
504 Statement::DoWhileStatement(statement) => {
505 self.collect_statement(&statement.body);
506 self.collect_expression(&statement.test);
507 }
508 Statement::SwitchStatement(statement) => {
509 self.collect_expression(&statement.discriminant);
510 for switch_case in &statement.cases {
511 if let Some(test) = &switch_case.test {
512 self.collect_expression(test);
513 }
514 for consequent in &switch_case.consequent {
515 self.collect_statement(consequent);
516 }
517 }
518 }
519 Statement::ThrowStatement(statement) => {
520 self.collect_expression(&statement.argument);
521 }
522 Statement::TryStatement(statement) => {
523 for statement in &statement.block.body {
524 self.collect_statement(statement);
525 }
526 if let Some(handler) = &statement.handler {
527 for statement in &handler.body.body {
528 self.collect_statement(statement);
529 }
530 }
531 if let Some(finalizer) = &statement.finalizer {
532 for statement in &finalizer.body {
533 self.collect_statement(statement);
534 }
535 }
536 }
537 Statement::VariableDeclaration(declaration) => {
538 self.collect_variable_declaration(declaration);
539 }
540 Statement::FunctionDeclaration(function) => {
541 self.collect_function_body(function.body.as_deref());
542 }
543 Statement::ClassDeclaration(class) => {
544 self.collect_class(class);
545 }
546 Statement::ExportNamedDeclaration(declaration) => {
547 if let Some(declaration) = &declaration.declaration {
548 self.collect_declaration(declaration);
549 }
550 }
551 Statement::ExportDefaultDeclaration(declaration) => {
552 self.collect_export_default_declaration(&declaration.declaration);
553 }
554 Statement::TSExportAssignment(declaration) => {
555 self.collect_expression(&declaration.expression);
556 }
557 _ => {}
558 }
559 }
560
561 fn collect_declaration(&mut self, declaration: &Declaration<'a>) {
562 match declaration {
563 Declaration::VariableDeclaration(declaration) => {
564 self.collect_variable_declaration(declaration);
565 }
566 Declaration::FunctionDeclaration(function) => {
567 self.collect_function_body(function.body.as_deref());
568 }
569 Declaration::ClassDeclaration(class) => {
570 self.collect_class(class);
571 }
572 _ => {}
573 }
574 }
575
576 fn collect_export_default_declaration(
577 &mut self,
578 declaration: &oxc_ast::ast::ExportDefaultDeclarationKind<'a>,
579 ) {
580 match declaration {
581 oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
582 self.collect_function_body(function.body.as_deref());
583 }
584 oxc_ast::ast::ExportDefaultDeclarationKind::ClassDeclaration(class) => {
585 self.collect_class(class);
586 }
587 oxc_ast::ast::ExportDefaultDeclarationKind::StaticMemberExpression(member) => {
588 self.collect_static_member_expression(member);
589 }
590 oxc_ast::ast::ExportDefaultDeclarationKind::ComputedMemberExpression(member) => {
591 self.collect_computed_member_expression(member);
592 }
593 oxc_ast::ast::ExportDefaultDeclarationKind::CallExpression(expression) => {
594 self.collect_call_expression(expression);
595 }
596 _ => {}
597 }
598 }
599
600 fn collect_for_statement_init(&mut self, init: &oxc_ast::ast::ForStatementInit<'a>) {
601 match init {
602 oxc_ast::ast::ForStatementInit::VariableDeclaration(declaration) => {
603 self.collect_variable_declaration(declaration);
604 }
605 oxc_ast::ast::ForStatementInit::StaticMemberExpression(member) => {
606 self.collect_static_member_expression(member);
607 }
608 oxc_ast::ast::ForStatementInit::ComputedMemberExpression(member) => {
609 self.collect_computed_member_expression(member);
610 }
611 oxc_ast::ast::ForStatementInit::CallExpression(expression) => {
612 self.collect_call_expression(expression);
613 }
614 _ => {}
615 }
616 }
617
618 fn collect_variable_declaration(
619 &mut self,
620 declaration: &oxc_ast::ast::VariableDeclaration<'a>,
621 ) {
622 for declarator in &declaration.declarations {
623 if let Some(binding) = self.classnames_bind_utility_binding_from_declarator(declarator)
624 {
625 self.classnames_bind_utility_bindings.push(binding);
626 }
627 if let Some(init) = &declarator.init {
628 self.collect_expression(init);
629 }
630 }
631 }
632
633 fn classnames_bind_utility_binding_from_declarator(
634 &self,
635 declarator: &VariableDeclarator<'a>,
636 ) -> Option<ClassnamesBindUtilityBinding> {
637 if self.style_targets.is_empty() || self.classnames_bind_imports.is_empty() {
638 return None;
639 }
640 let binding = binding_pattern_identifier_name(&declarator.id)?;
641 let init = declarator.init.as_ref()?;
642 let Expression::CallExpression(call) = init else {
643 return None;
644 };
645 let Expression::StaticMemberExpression(callee) = &call.callee else {
646 return None;
647 };
648 if callee.property.name.as_str() != "bind" {
649 return None;
650 }
651 let callee_binding = expression_identifier_name(&callee.object)?;
652 if !self
653 .classnames_bind_imports
654 .iter()
655 .any(|import_binding| import_binding == callee_binding)
656 {
657 return None;
658 }
659 let style_binding = call.arguments.first().and_then(argument_identifier_name)?;
660 let style_uri = self
661 .style_targets
662 .iter()
663 .find(|target| target.binding == style_binding)?
664 .target_style_uri
665 .clone()?;
666
667 Some(ClassnamesBindUtilityBinding {
668 binding: binding.to_string(),
669 style_uri,
670 })
671 }
672
673 fn collect_function_body(&mut self, body: Option<&oxc_ast::ast::FunctionBody<'a>>) {
674 let Some(body) = body else {
675 return;
676 };
677 for statement in &body.statements {
678 self.collect_statement(statement);
679 }
680 }
681
682 fn collect_class(&mut self, class: &Class<'a>) {
683 if let Some(super_class) = &class.super_class {
684 self.collect_expression(super_class);
685 }
686 for element in &class.body.body {
687 match element {
688 ClassElement::MethodDefinition(method) => {
689 self.collect_function_body(method.value.body.as_deref());
690 }
691 ClassElement::PropertyDefinition(property) => {
692 if property.computed {
693 self.collect_property_key(&property.key);
694 }
695 if let Some(value) = &property.value {
696 self.collect_expression(value);
697 }
698 }
699 ClassElement::AccessorProperty(property) => {
700 if property.computed {
701 self.collect_property_key(&property.key);
702 }
703 if let Some(value) = &property.value {
704 self.collect_expression(value);
705 }
706 }
707 ClassElement::StaticBlock(block) => {
708 for statement in &block.body {
709 self.collect_statement(statement);
710 }
711 }
712 ClassElement::TSIndexSignature(_) => {}
713 }
714 }
715 }
716
717 fn collect_expression(&mut self, expression: &Expression<'a>) {
718 match expression {
719 Expression::StaticMemberExpression(member) => {
720 self.collect_static_member_expression(member);
721 }
722 Expression::ComputedMemberExpression(member) => {
723 self.collect_computed_member_expression(member);
724 }
725 Expression::PrivateFieldExpression(member) => {
726 self.collect_expression(&member.object);
727 }
728 Expression::ArrayExpression(expression) => {
729 self.collect_array_expression(expression);
730 }
731 Expression::ObjectExpression(expression) => {
732 self.collect_object_expression(expression);
733 }
734 Expression::CallExpression(expression) => {
735 self.collect_call_expression(expression);
736 }
737 Expression::NewExpression(expression) => {
738 self.collect_expression(&expression.callee);
739 for argument in &expression.arguments {
740 self.collect_argument(argument);
741 }
742 }
743 Expression::ChainExpression(expression) => {
744 self.collect_chain_element(&expression.expression);
745 }
746 Expression::ConditionalExpression(expression) => {
747 self.collect_conditional_expression(expression);
748 }
749 Expression::BinaryExpression(expression) => {
750 self.collect_expression(&expression.left);
751 self.collect_expression(&expression.right);
752 }
753 Expression::LogicalExpression(expression) => {
754 self.collect_logical_expression(expression);
755 }
756 Expression::AssignmentExpression(expression) => {
757 self.collect_expression(&expression.right);
758 }
759 Expression::SequenceExpression(expression) => {
760 for expression in &expression.expressions {
761 self.collect_expression(expression);
762 }
763 }
764 Expression::ParenthesizedExpression(expression) => {
765 self.collect_parenthesized_expression(expression);
766 }
767 Expression::UnaryExpression(expression) => {
768 self.collect_expression(&expression.argument);
769 }
770 Expression::AwaitExpression(expression) => {
771 self.collect_expression(&expression.argument);
772 }
773 Expression::TemplateLiteral(expression) => {
774 for expression in &expression.expressions {
775 self.collect_expression(expression);
776 }
777 }
778 Expression::TaggedTemplateExpression(expression) => {
779 self.collect_expression(&expression.tag);
780 for expression in &expression.quasi.expressions {
781 self.collect_expression(expression);
782 }
783 }
784 Expression::ArrowFunctionExpression(expression) => {
785 self.collect_function_body(Some(&expression.body));
786 }
787 Expression::FunctionExpression(expression) => {
788 self.collect_function_body(expression.body.as_deref());
789 }
790 Expression::ClassExpression(class) => {
791 self.collect_class(class);
792 }
793 Expression::ImportExpression(expression) => {
794 self.collect_expression(&expression.source);
795 if let Some(options) = &expression.options {
796 self.collect_expression(options);
797 }
798 }
799 Expression::JSXElement(element) => {
800 self.collect_jsx_element(element);
801 }
802 Expression::JSXFragment(fragment) => {
803 for child in &fragment.children {
804 self.collect_jsx_child(child);
805 }
806 }
807 Expression::TSAsExpression(expression) => {
808 self.collect_ts_as_expression(expression);
809 }
810 Expression::TSSatisfiesExpression(expression) => {
811 self.collect_ts_satisfies_expression(expression);
812 }
813 Expression::TSTypeAssertion(expression) => {
814 self.collect_expression(&expression.expression);
815 }
816 Expression::TSNonNullExpression(expression) => {
817 self.collect_ts_non_null_expression(expression);
818 }
819 Expression::TSInstantiationExpression(expression) => {
820 self.collect_expression(&expression.expression);
821 }
822 _ => {}
823 }
824 }
825
826 fn collect_array_expression_element(&mut self, element: &ArrayExpressionElement<'a>) {
827 match element {
828 ArrayExpressionElement::SpreadElement(spread) => {
829 self.collect_expression(&spread.argument);
830 }
831 ArrayExpressionElement::Elision(_) => {}
832 _ => {
833 if let Some(expression) = element.as_expression() {
834 self.collect_expression(expression);
835 }
836 }
837 }
838 }
839
840 fn collect_argument(&mut self, argument: &Argument<'a>) {
841 match argument {
842 Argument::SpreadElement(spread) => {
843 self.collect_expression(&spread.argument);
844 }
845 _ => {
846 if let Some(expression) = argument.as_expression() {
847 self.collect_expression(expression);
848 }
849 }
850 }
851 }
852
853 fn collect_chain_element(&mut self, element: &ChainElement<'a>) {
854 match element {
855 ChainElement::CallExpression(expression) => {
856 self.collect_expression(&expression.callee);
857 for argument in &expression.arguments {
858 self.collect_argument(argument);
859 }
860 }
861 ChainElement::StaticMemberExpression(member) => {
862 self.collect_static_member_expression(member);
863 }
864 ChainElement::ComputedMemberExpression(member) => {
865 self.collect_computed_member_expression(member);
866 }
867 ChainElement::PrivateFieldExpression(member) => {
868 self.collect_expression(&member.object);
869 }
870 ChainElement::TSNonNullExpression(expression) => {
871 self.collect_expression(&expression.expression);
872 }
873 }
874 }
875
876 fn collect_property_key(&mut self, key: &oxc_ast::ast::PropertyKey<'a>) {
877 match key {
878 oxc_ast::ast::PropertyKey::StaticIdentifier(_)
879 | oxc_ast::ast::PropertyKey::PrivateIdentifier(_) => {}
880 _ => {
881 if let Some(expression) = key.as_expression() {
882 self.collect_expression(expression);
883 }
884 }
885 }
886 }
887
888 fn collect_jsx_element(&mut self, element: &oxc_ast::ast::JSXElement<'a>) {
889 for attribute in &element.opening_element.attributes {
890 match attribute {
891 oxc_ast::ast::JSXAttributeItem::Attribute(attribute) => {
892 if is_jsx_class_name_attribute(&attribute.name)
893 && let Some(value) = &attribute.value
894 {
895 self.collect_class_name_string_literal_attribute(value);
896 self.collect_class_name_expression_attribute(value);
897 }
898 if let Some(value) = &attribute.value {
899 self.collect_jsx_attribute_value(value);
900 }
901 }
902 oxc_ast::ast::JSXAttributeItem::SpreadAttribute(attribute) => {
903 self.collect_expression(&attribute.argument);
904 }
905 }
906 }
907 for child in &element.children {
908 self.collect_jsx_child(child);
909 }
910 }
911
912 fn collect_jsx_attribute_value(&mut self, value: &JSXAttributeValue<'a>) {
913 match value {
914 JSXAttributeValue::ExpressionContainer(container) => {
915 self.collect_jsx_expression(&container.expression);
916 }
917 JSXAttributeValue::Element(element) => {
918 self.collect_jsx_element(element);
919 }
920 JSXAttributeValue::Fragment(fragment) => {
921 for child in &fragment.children {
922 self.collect_jsx_child(child);
923 }
924 }
925 JSXAttributeValue::StringLiteral(_) => {}
926 }
927 }
928
929 fn collect_class_name_string_literal_attribute(&mut self, value: &JSXAttributeValue<'a>) {
930 let JSXAttributeValue::StringLiteral(literal) = value else {
931 return;
932 };
933 if let Some(span) = self.string_literal_content_span(literal.span) {
934 self.class_string_literals.push(span);
935 }
936 }
937
938 fn collect_class_name_expression_attribute(&mut self, value: &JSXAttributeValue<'a>) {
939 let JSXAttributeValue::ExpressionContainer(container) = value else {
940 return;
941 };
942 if let Some(span) = jsx_expression_span(&container.expression) {
943 self.class_name_expression_spans.push(span);
944 }
945 }
946
947 fn collect_jsx_child(&mut self, child: &JSXChild<'a>) {
948 match child {
949 JSXChild::Element(element) => {
950 self.collect_jsx_element(element);
951 }
952 JSXChild::Fragment(fragment) => {
953 for child in &fragment.children {
954 self.collect_jsx_child(child);
955 }
956 }
957 JSXChild::ExpressionContainer(container) => {
958 self.collect_jsx_expression(&container.expression);
959 }
960 JSXChild::Spread(spread) => {
961 self.collect_expression(&spread.expression);
962 }
963 JSXChild::Text(_) => {}
964 }
965 }
966
967 fn collect_jsx_expression(&mut self, expression: &JSXExpression<'a>) {
968 match expression {
969 JSXExpression::StaticMemberExpression(member) => {
970 self.collect_static_member_expression(member);
971 }
972 JSXExpression::ComputedMemberExpression(member) => {
973 self.collect_computed_member_expression(member);
974 }
975 JSXExpression::CallExpression(expression) => {
976 self.collect_call_expression(expression);
977 }
978 JSXExpression::ConditionalExpression(expression) => {
979 self.collect_conditional_expression(expression);
980 }
981 JSXExpression::LogicalExpression(expression) => {
982 self.collect_logical_expression(expression);
983 }
984 JSXExpression::ArrayExpression(expression) => {
985 self.collect_array_expression(expression);
986 }
987 JSXExpression::ObjectExpression(expression) => {
988 self.collect_object_expression(expression);
989 }
990 JSXExpression::ParenthesizedExpression(expression) => {
991 self.collect_parenthesized_expression(expression);
992 }
993 JSXExpression::TSAsExpression(expression) => {
994 self.collect_ts_as_expression(expression);
995 }
996 JSXExpression::TSSatisfiesExpression(expression) => {
997 self.collect_ts_satisfies_expression(expression);
998 }
999 JSXExpression::TSNonNullExpression(expression) => {
1000 self.collect_ts_non_null_expression(expression);
1001 }
1002 JSXExpression::JSXElement(element) => {
1003 self.collect_jsx_element(element);
1004 }
1005 JSXExpression::JSXFragment(fragment) => {
1006 for child in &fragment.children {
1007 self.collect_jsx_child(child);
1008 }
1009 }
1010 _ => {}
1011 }
1012 }
1013
1014 fn collect_array_expression(&mut self, expression: &ArrayExpression<'a>) {
1015 for element in &expression.elements {
1016 self.collect_array_expression_element(element);
1017 }
1018 }
1019
1020 fn collect_object_expression(&mut self, expression: &ObjectExpression<'a>) {
1021 for property in &expression.properties {
1022 match property {
1023 ObjectPropertyKind::ObjectProperty(property) => {
1024 if property.computed {
1025 self.collect_property_key(&property.key);
1026 }
1027 self.collect_expression(&property.value);
1028 }
1029 ObjectPropertyKind::SpreadProperty(spread) => {
1030 self.collect_expression(&spread.argument);
1031 }
1032 }
1033 }
1034 }
1035
1036 fn collect_call_expression(&mut self, expression: &CallExpression<'a>) {
1037 if let Some(binding) = expression_identifier_name(&expression.callee) {
1038 for argument in &expression.arguments {
1039 if let Some(byte_span) = argument_expression_span(argument) {
1040 self.classnames_bind_call_arguments
1041 .push(ClassnamesBindCallArgument {
1042 binding: binding.to_string(),
1043 byte_span,
1044 });
1045 }
1046 }
1047 }
1048 self.collect_expression(&expression.callee);
1049 for argument in &expression.arguments {
1050 self.collect_argument(argument);
1051 }
1052 }
1053
1054 fn collect_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
1055 self.collect_expression(&expression.test);
1056 self.collect_expression(&expression.consequent);
1057 self.collect_expression(&expression.alternate);
1058 }
1059
1060 fn collect_logical_expression(&mut self, expression: &LogicalExpression<'a>) {
1061 self.collect_expression(&expression.left);
1062 self.collect_expression(&expression.right);
1063 }
1064
1065 fn collect_parenthesized_expression(&mut self, expression: &ParenthesizedExpression<'a>) {
1066 self.collect_expression(&expression.expression);
1067 }
1068
1069 fn collect_ts_as_expression(&mut self, expression: &TSAsExpression<'a>) {
1070 self.collect_expression(&expression.expression);
1071 }
1072
1073 fn collect_ts_satisfies_expression(&mut self, expression: &TSSatisfiesExpression<'a>) {
1074 self.collect_expression(&expression.expression);
1075 }
1076
1077 fn collect_ts_non_null_expression(&mut self, expression: &TSNonNullExpression<'a>) {
1078 self.collect_expression(&expression.expression);
1079 }
1080
1081 fn collect_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
1082 if let Some(target) = self.target_for_object(&member.object)
1083 && let Some(byte_span) = self.css_identifier_span(member.property.span)
1084 {
1085 self.style_property_accesses
1086 .push(SourceStylePropertyAccessFactV0 {
1087 byte_span,
1088 target_style_uri: target.target_style_uri.clone(),
1089 });
1090 }
1091 self.collect_expression(&member.object);
1092 }
1093
1094 fn collect_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
1095 if let Some(target) = self.target_for_object(&member.object)
1096 && let Some(byte_span) = self.static_string_expression_content_span(&member.expression)
1097 {
1098 self.style_property_accesses
1099 .push(SourceStylePropertyAccessFactV0 {
1100 byte_span,
1101 target_style_uri: target.target_style_uri.clone(),
1102 });
1103 }
1104 self.collect_expression(&member.object);
1105 self.collect_expression(&member.expression);
1106 }
1107
1108 fn target_for_object(&self, expression: &Expression<'a>) -> Option<&SourceStyleBindingTarget> {
1109 match expression {
1110 Expression::Identifier(identifier) => self
1111 .property_access_targets
1112 .iter()
1113 .find(|target| target.binding == identifier.name.as_str()),
1114 Expression::ParenthesizedExpression(expression) => {
1115 self.target_for_object(&expression.expression)
1116 }
1117 Expression::TSAsExpression(expression) => {
1118 self.target_for_object(&expression.expression)
1119 }
1120 Expression::TSSatisfiesExpression(expression) => {
1121 self.target_for_object(&expression.expression)
1122 }
1123 Expression::TSTypeAssertion(expression) => {
1124 self.target_for_object(&expression.expression)
1125 }
1126 Expression::TSNonNullExpression(expression) => {
1127 self.target_for_object(&expression.expression)
1128 }
1129 Expression::TSInstantiationExpression(expression) => {
1130 self.target_for_object(&expression.expression)
1131 }
1132 _ => None,
1133 }
1134 }
1135
1136 fn static_string_expression_content_span(
1137 &self,
1138 expression: &Expression<'a>,
1139 ) -> Option<ParserByteSpanV0> {
1140 match expression {
1141 Expression::StringLiteral(literal) => self.css_identifier_content_span(literal.span),
1142 Expression::TemplateLiteral(literal) if literal.expressions.is_empty() => {
1143 self.css_identifier_content_span(literal.span)
1144 }
1145 _ => None,
1146 }
1147 }
1148
1149 fn css_identifier_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1150 let span = parser_byte_span(span);
1151 let text = self.source.get(span.start..span.end)?;
1152 (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(span)
1153 }
1154
1155 fn css_identifier_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1156 let span = parser_byte_span(span);
1157 if span.end <= span.start + 1 {
1158 return None;
1159 }
1160 let content = ParserByteSpanV0 {
1161 start: span.start + 1,
1162 end: span.end - 1,
1163 };
1164 let text = self.source.get(content.start..content.end)?;
1165 (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(content)
1166 }
1167
1168 fn string_literal_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1169 let span = parser_byte_span(span);
1170 if span.end <= span.start + 1 {
1171 return None;
1172 }
1173 let content = ParserByteSpanV0 {
1174 start: span.start + 1,
1175 end: span.end - 1,
1176 };
1177 self.source.get(content.start..content.end)?;
1178 Some(content)
1179 }
1180
1181 fn canonicalize(&mut self) {
1182 self.class_string_literals.sort_by(|left, right| {
1183 left.start
1184 .cmp(&right.start)
1185 .then_with(|| left.end.cmp(&right.end))
1186 });
1187 self.class_string_literals.dedup();
1188 self.style_property_accesses.sort_by(|left, right| {
1189 left.byte_span
1190 .start
1191 .cmp(&right.byte_span.start)
1192 .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1193 .then_with(|| left.target_style_uri.cmp(&right.target_style_uri))
1194 });
1195 self.style_property_accesses.dedup();
1196 self.classnames_bind_utility_bindings
1197 .sort_by(|left, right| {
1198 left.binding
1199 .cmp(&right.binding)
1200 .then_with(|| left.style_uri.cmp(&right.style_uri))
1201 });
1202 self.classnames_bind_utility_bindings
1203 .dedup_by(|left, right| {
1204 left.binding == right.binding && left.style_uri == right.style_uri
1205 });
1206 self.classnames_bind_call_arguments.sort_by(|left, right| {
1207 left.binding
1208 .cmp(&right.binding)
1209 .then_with(|| left.byte_span.start.cmp(&right.byte_span.start))
1210 .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1211 });
1212 self.classnames_bind_call_arguments.dedup_by(|left, right| {
1213 left.binding == right.binding && left.byte_span == right.byte_span
1214 });
1215 }
1216}
1217
1218fn parser_byte_span(span: Span) -> ParserByteSpanV0 {
1219 ParserByteSpanV0 {
1220 start: span.start as usize,
1221 end: span.end as usize,
1222 }
1223}
1224
1225fn is_jsx_class_name_attribute(name: &JSXAttributeName<'_>) -> bool {
1226 matches!(name, JSXAttributeName::Identifier(identifier) if identifier.name.as_str() == "className")
1227}
1228
1229fn jsx_expression_span(expression: &JSXExpression<'_>) -> Option<ParserByteSpanV0> {
1230 match expression {
1231 JSXExpression::EmptyExpression(_) => None,
1232 _ => Some(parser_byte_span(expression.span())),
1233 }
1234}
1235
1236fn argument_expression_span(argument: &Argument<'_>) -> Option<ParserByteSpanV0> {
1237 match argument {
1238 Argument::SpreadElement(spread) => Some(parser_byte_span(spread.argument.span())),
1239 _ => Some(parser_byte_span(argument.span())),
1240 }
1241}
1242
1243fn binding_pattern_identifier_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
1244 match pattern {
1245 BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.as_str()),
1246 _ => None,
1247 }
1248}
1249
1250fn expression_identifier_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1251 match expression {
1252 Expression::Identifier(identifier) => Some(identifier.name.as_str()),
1253 Expression::ParenthesizedExpression(expression) => {
1254 expression_identifier_name(&expression.expression)
1255 }
1256 Expression::TSAsExpression(expression) => {
1257 expression_identifier_name(&expression.expression)
1258 }
1259 Expression::TSSatisfiesExpression(expression) => {
1260 expression_identifier_name(&expression.expression)
1261 }
1262 Expression::TSTypeAssertion(expression) => {
1263 expression_identifier_name(&expression.expression)
1264 }
1265 Expression::TSNonNullExpression(expression) => {
1266 expression_identifier_name(&expression.expression)
1267 }
1268 Expression::TSInstantiationExpression(expression) => {
1269 expression_identifier_name(&expression.expression)
1270 }
1271 _ => None,
1272 }
1273}
1274
1275fn argument_identifier_name<'a>(argument: &'a Argument<'a>) -> Option<&'a str> {
1276 match argument {
1277 Argument::Identifier(identifier) => Some(identifier.name.as_str()),
1278 Argument::ParenthesizedExpression(expression) => {
1279 expression_identifier_name(&expression.expression)
1280 }
1281 Argument::TSAsExpression(expression) => expression_identifier_name(&expression.expression),
1282 Argument::TSSatisfiesExpression(expression) => {
1283 expression_identifier_name(&expression.expression)
1284 }
1285 Argument::TSNonNullExpression(expression) => {
1286 expression_identifier_name(&expression.expression)
1287 }
1288 Argument::TSInstantiationExpression(expression) => {
1289 expression_identifier_name(&expression.expression)
1290 }
1291 _ => None,
1292 }
1293}
1294
1295fn collect_selector_references_from_js_expression(
1296 source: &str,
1297 start: usize,
1298 end: usize,
1299 target_style_uri: Option<&str>,
1300 local_class_values: &BTreeMap<String, SourceClassValue>,
1301 references: &mut Vec<SourceSelectorReferenceFactV0>,
1302 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1303) {
1304 let (start, end) = trim_js_expression(source, start, end);
1305 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1306 if start >= end {
1307 return;
1308 }
1309
1310 if let Some((literal_start, literal_end, next_offset)) =
1311 js_string_literal_span(source, start, end)
1312 && trim_js_expression(source, next_offset, end).0 >= end
1313 {
1314 push_js_literal_selector_references(
1315 source,
1316 literal_start,
1317 literal_end,
1318 source.as_bytes().get(start).copied() == Some(b'`'),
1319 target_style_uri,
1320 references,
1321 );
1322 if source.as_bytes().get(start).copied() == Some(b'`') {
1323 collect_template_type_fact_targets(
1324 source,
1325 literal_start,
1326 literal_end,
1327 target_style_uri,
1328 type_fact_targets,
1329 );
1330 }
1331 return;
1332 }
1333
1334 if source.as_bytes().get(start) == Some(&b'{')
1335 && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1336 {
1337 collect_object_literal_selector_references(
1338 source,
1339 start,
1340 end,
1341 target_style_uri,
1342 local_class_values,
1343 references,
1344 type_fact_targets,
1345 );
1346 return;
1347 }
1348
1349 if source.as_bytes().get(start) == Some(&b'[')
1350 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1351 {
1352 for (element_start, element_end) in
1353 split_top_level_js_segments(source, start + 1, end - 1, b',')
1354 {
1355 let element_start = skip_js_trivia_until(source, element_start, element_end);
1356 let element_start = if source[element_start..element_end].starts_with("...") {
1357 element_start + 3
1358 } else {
1359 element_start
1360 };
1361 collect_selector_references_from_js_expression(
1362 source,
1363 element_start,
1364 element_end,
1365 target_style_uri,
1366 local_class_values,
1367 references,
1368 type_fact_targets,
1369 );
1370 }
1371 return;
1372 }
1373
1374 if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1375 {
1376 for (argument_start, argument_end) in
1377 split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1378 {
1379 collect_selector_references_from_js_expression(
1380 source,
1381 argument_start,
1382 argument_end,
1383 target_style_uri,
1384 local_class_values,
1385 references,
1386 type_fact_targets,
1387 );
1388 }
1389 return;
1390 }
1391
1392 if let Some((_, true_start, true_end, false_start, false_end)) =
1393 top_level_conditional_parts(source, start, end)
1394 {
1395 collect_selector_references_from_js_expression(
1396 source,
1397 true_start,
1398 true_end,
1399 target_style_uri,
1400 local_class_values,
1401 references,
1402 type_fact_targets,
1403 );
1404 collect_selector_references_from_js_expression(
1405 source,
1406 false_start,
1407 false_end,
1408 target_style_uri,
1409 local_class_values,
1410 references,
1411 type_fact_targets,
1412 );
1413 return;
1414 }
1415
1416 if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1417 .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1418 {
1419 collect_selector_references_from_js_expression(
1420 source,
1421 operator_offset + 2,
1422 end,
1423 target_style_uri,
1424 local_class_values,
1425 references,
1426 type_fact_targets,
1427 );
1428 return;
1429 }
1430
1431 let expression_path = js_expression_path(source, start, end);
1432 if let Some(value) =
1433 source_class_value_from_js_expression(source, start, end, local_class_values)
1434 && !value.is_empty()
1435 {
1436 if let Some(path) = expression_path.as_deref() {
1437 push_source_type_fact_target(
1438 ParserByteSpanV0 { start, end },
1439 path,
1440 target_style_uri,
1441 "",
1442 "",
1443 type_fact_targets,
1444 );
1445 }
1446 push_source_class_value_reference(
1447 ParserByteSpanV0 { start, end },
1448 value,
1449 target_style_uri,
1450 references,
1451 );
1452 return;
1453 }
1454
1455 if let Some(prefix) =
1456 static_string_prefix_for_js_expression(source, start, end, local_class_values)
1457 && !prefix.is_empty()
1458 {
1459 push_selector_reference(
1460 ParserByteSpanV0 { start, end },
1461 Some(prefix),
1462 SourceSelectorReferenceMatchKindV0::Prefix,
1463 target_style_uri,
1464 references,
1465 );
1466 return;
1467 }
1468
1469 if let Some(path) = expression_path {
1470 push_source_type_fact_target(
1471 ParserByteSpanV0 { start, end },
1472 path.as_str(),
1473 target_style_uri,
1474 "",
1475 "",
1476 type_fact_targets,
1477 );
1478 }
1479}
1480
1481fn collect_local_class_value_bindings(source: &str) -> BTreeMap<String, SourceClassValue> {
1482 let mut values = BTreeMap::new();
1483 let mut cursor = 0usize;
1484 while let Some(keyword) = next_code_identifier(source, cursor) {
1485 cursor = keyword.end;
1486 if !matches!(keyword.text, "const" | "let" | "var") {
1487 continue;
1488 }
1489 let binding_start = skip_js_trivia(source, keyword.end);
1490 let Some((binding, binding_end)) = read_js_identifier(source, binding_start) else {
1491 continue;
1492 };
1493 let equals_offset = skip_js_trivia(source, binding_end);
1494 if source.as_bytes().get(equals_offset) != Some(&b'=') {
1495 continue;
1496 }
1497 let expression_start = skip_js_trivia(source, equals_offset + 1);
1498 let expression_end = js_statement_expression_end(source, expression_start);
1499 if let Some(value) =
1500 source_class_value_from_js_expression(source, expression_start, expression_end, &values)
1501 && !value.is_empty()
1502 {
1503 values.insert(binding.to_string(), value);
1504 }
1505 let (_, property_values) = source_class_value_from_object_literal(
1506 source,
1507 expression_start,
1508 expression_end,
1509 &values,
1510 );
1511 for (property, value) in property_values {
1512 if !value.is_empty() {
1513 values.insert(format!("{binding}.{property}"), value);
1514 }
1515 }
1516 cursor = expression_end.min(source.len());
1517 }
1518 values
1519}
1520
1521fn source_class_value_from_js_expression(
1522 source: &str,
1523 start: usize,
1524 end: usize,
1525 local_class_values: &BTreeMap<String, SourceClassValue>,
1526) -> Option<SourceClassValue> {
1527 let (start, end) = trim_js_expression(source, start, end);
1528 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1529 if start >= end {
1530 return None;
1531 }
1532
1533 if let Some((literal_start, literal_end, next_offset)) =
1534 js_string_literal_span(source, start, end)
1535 && trim_js_expression(source, next_offset, end).0 >= end
1536 {
1537 return Some(source_class_value_from_js_literal(
1538 source,
1539 literal_start,
1540 literal_end,
1541 source.as_bytes().get(start).copied() == Some(b'`'),
1542 ));
1543 }
1544
1545 if source.as_bytes().get(start) == Some(&b'{')
1546 && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1547 {
1548 let (value, _) =
1549 source_class_value_from_object_literal(source, start, end, local_class_values);
1550 return Some(value);
1551 }
1552
1553 if source.as_bytes().get(start) == Some(&b'[')
1554 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1555 {
1556 let mut value = SourceClassValue::default();
1557 for (element_start, element_end) in
1558 split_top_level_js_segments(source, start + 1, end - 1, b',')
1559 {
1560 let element_start = skip_js_trivia_until(source, element_start, element_end);
1561 let element_start = if source[element_start..element_end].starts_with("...") {
1562 element_start + 3
1563 } else {
1564 element_start
1565 };
1566 if let Some(element_value) = source_class_value_from_js_expression(
1567 source,
1568 element_start,
1569 element_end,
1570 local_class_values,
1571 ) {
1572 value.merge(element_value);
1573 }
1574 }
1575 return Some(value);
1576 }
1577
1578 if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1579 {
1580 let mut value = SourceClassValue::default();
1581 for (argument_start, argument_end) in
1582 split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1583 {
1584 if let Some(argument_value) = source_class_value_from_js_expression(
1585 source,
1586 argument_start,
1587 argument_end,
1588 local_class_values,
1589 ) {
1590 value.merge(argument_value);
1591 }
1592 }
1593 return Some(value);
1594 }
1595
1596 if let Some((_, true_start, true_end, false_start, false_end)) =
1597 top_level_conditional_parts(source, start, end)
1598 {
1599 let mut value = SourceClassValue::default();
1600 if let Some(true_value) =
1601 source_class_value_from_js_expression(source, true_start, true_end, local_class_values)
1602 {
1603 value.merge(true_value);
1604 }
1605 if let Some(false_value) = source_class_value_from_js_expression(
1606 source,
1607 false_start,
1608 false_end,
1609 local_class_values,
1610 ) {
1611 value.merge(false_value);
1612 }
1613 return Some(value);
1614 }
1615
1616 if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1617 .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1618 {
1619 return source_class_value_from_js_expression(
1620 source,
1621 operator_offset + 2,
1622 end,
1623 local_class_values,
1624 );
1625 }
1626
1627 if let Some(path) = js_expression_path(source, start, end)
1628 && let Some(value) = local_class_values.get(path.as_str())
1629 {
1630 return Some(value.clone());
1631 }
1632
1633 static_string_prefix_for_js_expression(source, start, end, local_class_values).map(|prefix| {
1634 let mut value = SourceClassValue::default();
1635 if !prefix.is_empty() {
1636 value.prefixes.push(prefix);
1637 }
1638 value
1639 })
1640}
1641
1642fn source_class_value_from_js_literal(
1643 source: &str,
1644 literal_start: usize,
1645 literal_end: usize,
1646 is_template: bool,
1647) -> SourceClassValue {
1648 let mut value = SourceClassValue::default();
1649 if is_template
1650 && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
1651 {
1652 let prefix_end = literal_start + relative_interpolation;
1653 push_template_prefix_value(source, literal_start, prefix_end, &mut value);
1654 } else {
1655 value
1656 .exact
1657 .extend(class_token_strings(source, literal_start, literal_end));
1658 }
1659 value.canonicalize();
1660 value
1661}
1662
1663fn source_class_value_from_object_literal(
1664 source: &str,
1665 start: usize,
1666 end: usize,
1667 local_class_values: &BTreeMap<String, SourceClassValue>,
1668) -> (SourceClassValue, BTreeMap<String, SourceClassValue>) {
1669 let (start, end) = trim_js_expression(source, start, end);
1670 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1671 let mut object_value = SourceClassValue::default();
1672 let mut property_values = BTreeMap::new();
1673 if source.as_bytes().get(start) != Some(&b'{')
1674 || matching_js_block_end(source, start, b'{', b'}') != Some(end.saturating_sub(1))
1675 {
1676 return (object_value, property_values);
1677 }
1678
1679 for (property_start, property_end) in
1680 split_top_level_js_segments(source, start + 1, end - 1, b',')
1681 {
1682 let (property_start, property_end) =
1683 trim_js_expression(source, property_start, property_end);
1684 if property_start >= property_end {
1685 continue;
1686 }
1687 if source[property_start..property_end].starts_with("...") {
1688 if let Some(spread_value) = source_class_value_from_js_expression(
1689 source,
1690 property_start + 3,
1691 property_end,
1692 local_class_values,
1693 ) {
1694 object_value.merge(spread_value);
1695 }
1696 continue;
1697 }
1698 let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1699 let key_end = colon.unwrap_or(property_end);
1700 let key_value =
1701 source_class_value_from_object_key(source, property_start, key_end, local_class_values);
1702 object_value.merge(key_value.clone());
1703 if let Some(property_name) = object_property_name(source, property_start, key_end)
1704 && let Some(property_value) = colon
1705 .and_then(|colon| {
1706 source_class_value_from_js_expression(
1707 source,
1708 colon + 1,
1709 property_end,
1710 local_class_values,
1711 )
1712 })
1713 .filter(|value| !value.is_empty())
1714 {
1715 property_values.insert(property_name, property_value);
1716 }
1717 }
1718 object_value.canonicalize();
1719 (object_value, property_values)
1720}
1721
1722fn collect_object_literal_selector_references(
1723 source: &str,
1724 start: usize,
1725 end: usize,
1726 target_style_uri: Option<&str>,
1727 local_class_values: &BTreeMap<String, SourceClassValue>,
1728 references: &mut Vec<SourceSelectorReferenceFactV0>,
1729 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1730) {
1731 for (property_start, property_end) in
1732 split_top_level_js_segments(source, start + 1, end - 1, b',')
1733 {
1734 let (property_start, property_end) =
1735 trim_js_expression(source, property_start, property_end);
1736 if property_start >= property_end {
1737 continue;
1738 }
1739 if source[property_start..property_end].starts_with("...") {
1740 collect_selector_references_from_js_expression(
1741 source,
1742 property_start + 3,
1743 property_end,
1744 target_style_uri,
1745 local_class_values,
1746 references,
1747 type_fact_targets,
1748 );
1749 continue;
1750 }
1751 let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1752 let key_end = colon.unwrap_or(property_end);
1753 collect_selector_references_from_object_key(
1754 source,
1755 property_start,
1756 key_end,
1757 target_style_uri,
1758 local_class_values,
1759 references,
1760 type_fact_targets,
1761 );
1762 }
1763}
1764
1765fn class_utility_call_arguments(source: &str, start: usize, end: usize) -> Option<(usize, usize)> {
1766 let (callee, callee_end) = read_js_identifier(source, start)?;
1767 if !is_class_utility_callee(callee) {
1768 return None;
1769 }
1770 let open_paren = skip_js_trivia_until(source, callee_end, end);
1771 if source.as_bytes().get(open_paren) != Some(&b'(') {
1772 return None;
1773 }
1774 let call_end = js_call_end(source, open_paren)?;
1775 if call_end > end || trim_js_expression(source, call_end + 1, end).0 < end {
1776 return None;
1777 }
1778 Some((open_paren + 1, call_end))
1779}
1780
1781fn is_class_utility_callee(callee: &str) -> bool {
1782 matches!(callee, "classnames" | "classNames" | "clsx" | "cn")
1783}
1784
1785fn collect_selector_references_from_object_key(
1786 source: &str,
1787 start: usize,
1788 end: usize,
1789 target_style_uri: Option<&str>,
1790 local_class_values: &BTreeMap<String, SourceClassValue>,
1791 references: &mut Vec<SourceSelectorReferenceFactV0>,
1792 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1793) {
1794 let (start, end) = trim_js_expression(source, start, end);
1795 if start >= end {
1796 return;
1797 }
1798 if source.as_bytes().get(start) == Some(&b'[')
1799 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1800 {
1801 collect_selector_references_from_js_expression(
1802 source,
1803 start + 1,
1804 end - 1,
1805 target_style_uri,
1806 local_class_values,
1807 references,
1808 type_fact_targets,
1809 );
1810 return;
1811 }
1812 if let Some((literal_start, literal_end, next_offset)) =
1813 js_string_literal_span(source, start, end)
1814 && trim_js_expression(source, next_offset, end).0 >= end
1815 {
1816 push_js_literal_selector_references(
1817 source,
1818 literal_start,
1819 literal_end,
1820 source.as_bytes().get(start).copied() == Some(b'`'),
1821 target_style_uri,
1822 references,
1823 );
1824 if source.as_bytes().get(start).copied() == Some(b'`') {
1825 collect_template_type_fact_targets(
1826 source,
1827 literal_start,
1828 literal_end,
1829 target_style_uri,
1830 type_fact_targets,
1831 );
1832 }
1833 return;
1834 }
1835 if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1836 && trim_js_expression(source, identifier_end, end).0 >= end
1837 {
1838 push_selector_reference(
1839 ParserByteSpanV0 { start, end },
1840 Some(identifier.to_string()),
1841 SourceSelectorReferenceMatchKindV0::Exact,
1842 target_style_uri,
1843 references,
1844 );
1845 }
1846}
1847
1848fn source_class_value_from_object_key(
1849 source: &str,
1850 start: usize,
1851 end: usize,
1852 local_class_values: &BTreeMap<String, SourceClassValue>,
1853) -> SourceClassValue {
1854 let (start, end) = trim_js_expression(source, start, end);
1855 if start >= end {
1856 return SourceClassValue::default();
1857 }
1858 if source.as_bytes().get(start) == Some(&b'[')
1859 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1860 {
1861 return source_class_value_from_js_expression(
1862 source,
1863 start + 1,
1864 end - 1,
1865 local_class_values,
1866 )
1867 .unwrap_or_default();
1868 }
1869 if let Some((literal_start, literal_end, next_offset)) =
1870 js_string_literal_span(source, start, end)
1871 && trim_js_expression(source, next_offset, end).0 >= end
1872 {
1873 return source_class_value_from_js_literal(
1874 source,
1875 literal_start,
1876 literal_end,
1877 source.as_bytes().get(start).copied() == Some(b'`'),
1878 );
1879 }
1880 if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1881 && trim_js_expression(source, identifier_end, end).0 >= end
1882 {
1883 let mut value = SourceClassValue::default();
1884 value.exact.push(identifier.to_string());
1885 return value;
1886 }
1887 SourceClassValue::default()
1888}
1889
1890fn object_property_name(source: &str, start: usize, end: usize) -> Option<String> {
1891 let (start, end) = trim_js_expression(source, start, end);
1892 if let Some((literal_start, literal_end, next_offset)) =
1893 js_string_literal_span(source, start, end)
1894 && trim_js_expression(source, next_offset, end).0 >= end
1895 {
1896 return source.get(literal_start..literal_end).map(str::to_string);
1897 }
1898 let (identifier, identifier_end) = read_js_identifier(source, start)?;
1899 (trim_js_expression(source, identifier_end, end).0 >= end).then(|| identifier.to_string())
1900}
1901
1902fn push_source_class_value_reference(
1903 byte_span: ParserByteSpanV0,
1904 value: SourceClassValue,
1905 target_style_uri: Option<&str>,
1906 references: &mut Vec<SourceSelectorReferenceFactV0>,
1907) {
1908 for selector_name in value.exact {
1909 push_selector_reference(
1910 byte_span,
1911 Some(selector_name),
1912 SourceSelectorReferenceMatchKindV0::Exact,
1913 target_style_uri,
1914 references,
1915 );
1916 }
1917 for prefix in value.prefixes {
1918 push_selector_reference(
1919 byte_span,
1920 Some(prefix),
1921 SourceSelectorReferenceMatchKindV0::Prefix,
1922 target_style_uri,
1923 references,
1924 );
1925 }
1926}
1927
1928fn collect_template_type_fact_targets(
1929 source: &str,
1930 literal_start: usize,
1931 literal_end: usize,
1932 target_style_uri: Option<&str>,
1933 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1934) {
1935 let Some((prefix, expression_span, suffix)) =
1936 single_template_interpolation_projection(source, literal_start, literal_end)
1937 else {
1938 return;
1939 };
1940 let Some(path) = js_expression_path(source, expression_span.start, expression_span.end) else {
1941 return;
1942 };
1943 push_source_type_fact_target(
1944 expression_span,
1945 path.as_str(),
1946 target_style_uri,
1947 prefix.as_str(),
1948 suffix.as_str(),
1949 type_fact_targets,
1950 );
1951}
1952
1953fn single_template_interpolation_projection(
1954 source: &str,
1955 literal_start: usize,
1956 literal_end: usize,
1957) -> Option<(String, ParserByteSpanV0, String)> {
1958 let relative_open = source.get(literal_start..literal_end)?.find("${")?;
1959 let open = literal_start + relative_open;
1960 if source.get(open + 2..literal_end)?.contains("${") {
1961 return None;
1962 }
1963 let expression_start = open + 2;
1964 let close = matching_js_block_end(source, open + 1, b'{', b'}')?;
1965 if close > literal_end {
1966 return None;
1967 }
1968 let (expression_start, expression_end) = trim_js_expression(source, expression_start, close);
1969 if expression_start >= expression_end {
1970 return None;
1971 }
1972 let prefix_start = template_token_start(source, literal_start, open);
1973 let suffix_end = template_token_end(source, close + 1, literal_end);
1974 let prefix = source.get(prefix_start..open)?.to_string();
1975 let suffix = source.get(close + 1..suffix_end)?.to_string();
1976 if !prefix.chars().all(is_css_identifier_continue)
1977 || !suffix.chars().all(is_css_identifier_continue)
1978 {
1979 return None;
1980 }
1981 Some((
1982 prefix,
1983 ParserByteSpanV0 {
1984 start: expression_start,
1985 end: expression_end,
1986 },
1987 suffix,
1988 ))
1989}
1990
1991fn template_token_start(source: &str, literal_start: usize, prefix_end: usize) -> usize {
1992 source
1993 .get(literal_start..prefix_end)
1994 .and_then(|value| {
1995 value
1996 .char_indices()
1997 .rev()
1998 .find(|(_, ch)| ch.is_ascii_whitespace())
1999 .map(|(index, ch)| literal_start + index + ch.len_utf8())
2000 })
2001 .unwrap_or(literal_start)
2002}
2003
2004fn template_token_end(source: &str, suffix_start: usize, literal_end: usize) -> usize {
2005 source
2006 .get(suffix_start..literal_end)
2007 .and_then(|value| {
2008 value
2009 .char_indices()
2010 .find(|(_, ch)| ch.is_ascii_whitespace())
2011 .map(|(index, _)| suffix_start + index)
2012 })
2013 .unwrap_or(literal_end)
2014}
2015
2016fn push_source_type_fact_target(
2017 byte_span: ParserByteSpanV0,
2018 expression_path: &str,
2019 target_style_uri: Option<&str>,
2020 prefix: &str,
2021 suffix: &str,
2022 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
2023) {
2024 type_fact_targets.push(SourceTypeFactTargetV0 {
2025 byte_span,
2026 expression_id: source_type_fact_expression_id(expression_path, byte_span),
2027 target_style_uri: target_style_uri.map(ToString::to_string),
2028 prefix: prefix.to_string(),
2029 suffix: suffix.to_string(),
2030 });
2031}
2032
2033fn source_type_fact_expression_id(expression_path: &str, byte_span: ParserByteSpanV0) -> String {
2034 format!(
2035 "omena-bridge-source-type-fact:{expression_path}:{}:{}",
2036 byte_span.start, byte_span.end
2037 )
2038}
2039
2040fn push_selector_reference(
2041 byte_span: ParserByteSpanV0,
2042 selector_name: Option<String>,
2043 match_kind: SourceSelectorReferenceMatchKindV0,
2044 target_style_uri: Option<&str>,
2045 references: &mut Vec<SourceSelectorReferenceFactV0>,
2046) {
2047 references.push(SourceSelectorReferenceFactV0 {
2048 byte_span,
2049 selector_name,
2050 match_kind,
2051 target_style_uri: target_style_uri.map(ToString::to_string),
2052 });
2053}
2054
2055fn push_js_literal_selector_references(
2056 source: &str,
2057 literal_start: usize,
2058 literal_end: usize,
2059 is_template: bool,
2060 target_style_uri: Option<&str>,
2061 references: &mut Vec<SourceSelectorReferenceFactV0>,
2062) {
2063 if is_template
2064 && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2065 {
2066 push_template_prefix_selector_references(
2067 source,
2068 literal_start,
2069 literal_start + relative_interpolation,
2070 target_style_uri,
2071 references,
2072 );
2073 return;
2074 }
2075
2076 push_string_literal_selector_references(
2077 source,
2078 ParserByteSpanV0 {
2079 start: literal_start,
2080 end: literal_end,
2081 },
2082 target_style_uri.map(ToString::to_string),
2083 references,
2084 );
2085}
2086
2087fn push_template_prefix_selector_references(
2088 source: &str,
2089 literal_start: usize,
2090 prefix_end: usize,
2091 target_style_uri: Option<&str>,
2092 references: &mut Vec<SourceSelectorReferenceFactV0>,
2093) {
2094 let spans = class_token_byte_spans(source, literal_start, prefix_end);
2095 let prefix_ends_with_space = source[..prefix_end]
2096 .chars()
2097 .last()
2098 .is_none_or(char::is_whitespace);
2099 for (index, span) in spans.iter().enumerate() {
2100 let is_open_prefix = index + 1 == spans.len() && !prefix_ends_with_space;
2101 push_selector_reference(
2102 *span,
2103 Some(source[span.start..span.end].to_string()),
2104 if is_open_prefix {
2105 SourceSelectorReferenceMatchKindV0::Prefix
2106 } else {
2107 SourceSelectorReferenceMatchKindV0::Exact
2108 },
2109 target_style_uri,
2110 references,
2111 );
2112 }
2113}
2114
2115fn push_template_prefix_value(
2116 source: &str,
2117 literal_start: usize,
2118 prefix_end: usize,
2119 value: &mut SourceClassValue,
2120) {
2121 let spans = class_token_byte_spans(source, literal_start, prefix_end);
2122 let prefix_ends_with_space = source[..prefix_end]
2123 .chars()
2124 .last()
2125 .is_none_or(char::is_whitespace);
2126 for (index, span) in spans.iter().enumerate() {
2127 let token = source[span.start..span.end].to_string();
2128 if index + 1 == spans.len() && !prefix_ends_with_space {
2129 value.prefixes.push(token);
2130 } else {
2131 value.exact.push(token);
2132 }
2133 }
2134}
2135
2136fn class_token_strings(source: &str, literal_start: usize, literal_end: usize) -> Vec<String> {
2137 class_token_byte_spans(source, literal_start, literal_end)
2138 .into_iter()
2139 .map(|span| source[span.start..span.end].to_string())
2140 .collect()
2141}
2142
2143fn push_string_literal_selector_references(
2144 source: &str,
2145 literal_span: ParserByteSpanV0,
2146 target_style_uri: Option<String>,
2147 references: &mut Vec<SourceSelectorReferenceFactV0>,
2148) {
2149 for span in class_token_byte_spans(source, literal_span.start, literal_span.end) {
2150 references.push(SourceSelectorReferenceFactV0 {
2151 byte_span: span,
2152 selector_name: None,
2153 match_kind: SourceSelectorReferenceMatchKindV0::Exact,
2154 target_style_uri: target_style_uri.clone(),
2155 });
2156 }
2157}
2158
2159fn trim_js_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2160 let mut start = char_boundary_ceil(source, start);
2161 let mut end = char_boundary_floor(source, end);
2162 start = skip_js_trivia_until(source, start, end);
2163 while end > start
2164 && source
2165 .as_bytes()
2166 .get(end - 1)
2167 .is_some_and(u8::is_ascii_whitespace)
2168 {
2169 end -= 1;
2170 }
2171 (start, end)
2172}
2173
2174fn char_boundary_floor(source: &str, index: usize) -> usize {
2175 let mut index = index.min(source.len());
2176 while index > 0 && !source.is_char_boundary(index) {
2177 index -= 1;
2178 }
2179 index
2180}
2181
2182fn char_boundary_ceil(source: &str, index: usize) -> usize {
2183 let mut index = index.min(source.len());
2184 while index < source.len() && !source.is_char_boundary(index) {
2185 index += 1;
2186 }
2187 index
2188}
2189
2190fn advance_js_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
2191 let cursor = char_boundary_ceil(source, cursor);
2192 let limit = char_boundary_floor(source, limit);
2193 if cursor >= limit {
2194 return limit;
2195 }
2196 char_boundary_ceil(source, cursor + 1).min(limit)
2197}
2198
2199fn advance_js_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
2200 let after_slash = advance_js_scan_cursor(source, slash_offset, limit);
2201 advance_js_scan_cursor(source, after_slash, limit)
2202}
2203
2204fn unwrap_js_parenthesized_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2205 let mut current_start = start;
2206 let mut current_end = end;
2207 loop {
2208 let (trimmed_start, trimmed_end) = trim_js_expression(source, current_start, current_end);
2209 if source.as_bytes().get(trimmed_start) == Some(&b'(')
2210 && matching_js_block_end(source, trimmed_start, b'(', b')')
2211 == Some(trimmed_end.saturating_sub(1))
2212 {
2213 current_start = trimmed_start + 1;
2214 current_end = trimmed_end - 1;
2215 continue;
2216 }
2217 return (trimmed_start, trimmed_end);
2218 }
2219}
2220
2221fn js_statement_expression_end(source: &str, start: usize) -> usize {
2222 let mut cursor = char_boundary_ceil(source, start);
2223 let mut depth = 0usize;
2224 while cursor < source.len() {
2225 match source.as_bytes().get(cursor).copied() {
2226 Some(b'\'' | b'"' | b'`') => {
2227 cursor =
2228 skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2229 }
2230 Some(b'(' | b'[' | b'{') => {
2231 depth += 1;
2232 cursor = advance_js_scan_cursor(source, cursor, source.len());
2233 }
2234 Some(b')' | b']' | b'}') => {
2235 depth = depth.saturating_sub(1);
2236 cursor = advance_js_scan_cursor(source, cursor, source.len());
2237 }
2238 Some(b';') if depth == 0 => return cursor,
2239 Some(b'\n') if depth == 0 => return cursor,
2240 Some(_) => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2241 None => break,
2242 }
2243 }
2244 source.len()
2245}
2246
2247fn matching_js_block_end(source: &str, open_offset: usize, open: u8, close: u8) -> Option<usize> {
2248 if source.as_bytes().get(open_offset) != Some(&open) {
2249 return None;
2250 }
2251 let mut cursor = advance_js_scan_cursor(source, open_offset, source.len());
2252 let mut depth = 1usize;
2253 while cursor < source.len() {
2254 match source.as_bytes().get(cursor).copied()? {
2255 b'\'' | b'"' | b'`' => {
2256 cursor = skip_js_string_literal(source, cursor, source.len())?;
2257 }
2258 byte if byte == open => {
2259 depth += 1;
2260 cursor = advance_js_scan_cursor(source, cursor, source.len());
2261 }
2262 byte if byte == close => {
2263 depth -= 1;
2264 if depth == 0 {
2265 return Some(cursor);
2266 }
2267 cursor = advance_js_scan_cursor(source, cursor, source.len());
2268 }
2269 _ => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2270 }
2271 }
2272 None
2273}
2274
2275fn split_top_level_js_segments(
2276 source: &str,
2277 start: usize,
2278 end: usize,
2279 delimiter: u8,
2280) -> Vec<(usize, usize)> {
2281 let mut segments = Vec::new();
2282 let end = char_boundary_floor(source, end);
2283 let mut segment_start = char_boundary_ceil(source, start).min(end);
2284 let mut cursor = segment_start;
2285 let mut depth = 0usize;
2286 while cursor < end {
2287 match source.as_bytes().get(cursor).copied() {
2288 Some(b'\'' | b'"' | b'`') => {
2289 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2290 }
2291 Some(b'(' | b'[' | b'{') => {
2292 depth += 1;
2293 cursor = advance_js_scan_cursor(source, cursor, end);
2294 }
2295 Some(b')' | b']' | b'}') => {
2296 depth = depth.saturating_sub(1);
2297 cursor = advance_js_scan_cursor(source, cursor, end);
2298 }
2299 Some(byte) if byte == delimiter && depth == 0 => {
2300 segments.push((segment_start, cursor));
2301 cursor = advance_js_scan_cursor(source, cursor, end);
2302 segment_start = cursor;
2303 }
2304 Some(_) => cursor = advance_js_scan_cursor(source, cursor, end),
2305 None => break,
2306 }
2307 }
2308 if segment_start <= end {
2309 segments.push((segment_start, end));
2310 }
2311 segments
2312}
2313
2314fn find_top_level_js_byte(source: &str, start: usize, end: usize, needle: u8) -> Option<usize> {
2315 let end = char_boundary_floor(source, end);
2316 let mut cursor = char_boundary_ceil(source, start).min(end);
2317 let mut depth = 0usize;
2318 while cursor < end {
2319 match source.as_bytes().get(cursor).copied()? {
2320 b'\'' | b'"' | b'`' => {
2321 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2322 }
2323 b'(' | b'[' | b'{' => {
2324 depth += 1;
2325 cursor = advance_js_scan_cursor(source, cursor, end);
2326 }
2327 b')' | b']' | b'}' => {
2328 depth = depth.saturating_sub(1);
2329 cursor = advance_js_scan_cursor(source, cursor, end);
2330 }
2331 byte if byte == needle && depth == 0 => return Some(cursor),
2332 _ => cursor = advance_js_scan_cursor(source, cursor, end),
2333 }
2334 }
2335 None
2336}
2337
2338fn find_top_level_js_operator(
2339 source: &str,
2340 start: usize,
2341 end: usize,
2342 operator: &str,
2343) -> Option<usize> {
2344 let end = char_boundary_floor(source, end);
2345 let mut cursor = char_boundary_ceil(source, start).min(end);
2346 let mut depth = 0usize;
2347 while cursor < end {
2348 match source.as_bytes().get(cursor).copied()? {
2349 b'\'' | b'"' | b'`' => {
2350 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2351 }
2352 b'(' | b'[' | b'{' => {
2353 depth += 1;
2354 cursor = advance_js_scan_cursor(source, cursor, end);
2355 }
2356 b')' | b']' | b'}' => {
2357 depth = depth.saturating_sub(1);
2358 cursor = advance_js_scan_cursor(source, cursor, end);
2359 }
2360 _ if depth == 0
2361 && source
2362 .get(cursor..end)
2363 .is_some_and(|rest| rest.starts_with(operator)) =>
2364 {
2365 return Some(cursor);
2366 }
2367 _ => cursor = advance_js_scan_cursor(source, cursor, end),
2368 }
2369 }
2370 None
2371}
2372
2373fn top_level_conditional_parts(
2374 source: &str,
2375 start: usize,
2376 end: usize,
2377) -> Option<(usize, usize, usize, usize, usize)> {
2378 let question = find_top_level_js_byte(source, start, end, b'?')?;
2379 let end = char_boundary_floor(source, end);
2380 let mut cursor = advance_js_scan_cursor(source, question, end);
2381 let mut depth = 0usize;
2382 let mut nested_conditional_depth = 0usize;
2383 while cursor < end {
2384 match source.as_bytes().get(cursor).copied()? {
2385 b'\'' | b'"' | b'`' => {
2386 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2387 }
2388 b'(' | b'[' | b'{' => {
2389 depth += 1;
2390 cursor = advance_js_scan_cursor(source, cursor, end);
2391 }
2392 b')' | b']' | b'}' => {
2393 depth = depth.saturating_sub(1);
2394 cursor = advance_js_scan_cursor(source, cursor, end);
2395 }
2396 b'?' if depth == 0 => {
2397 nested_conditional_depth += 1;
2398 cursor = advance_js_scan_cursor(source, cursor, end);
2399 }
2400 b':' if depth == 0 && nested_conditional_depth == 0 => {
2401 return Some((
2402 question,
2403 advance_js_scan_cursor(source, question, end),
2404 cursor,
2405 advance_js_scan_cursor(source, cursor, end),
2406 end,
2407 ));
2408 }
2409 b':' if depth == 0 => {
2410 nested_conditional_depth = nested_conditional_depth.saturating_sub(1);
2411 cursor = advance_js_scan_cursor(source, cursor, end);
2412 }
2413 _ => cursor = advance_js_scan_cursor(source, cursor, end),
2414 }
2415 }
2416 None
2417}
2418
2419fn js_expression_path(source: &str, start: usize, end: usize) -> Option<String> {
2420 let (start, end) = trim_js_expression(source, start, end);
2421 let (first, mut cursor) = read_js_identifier(source, start)?;
2422 let mut path = vec![first.to_string()];
2423 loop {
2424 cursor = skip_js_trivia_until(source, cursor, end);
2425 match source.as_bytes().get(cursor).copied() {
2426 Some(b'.') => {
2427 let member_start = skip_js_trivia_until(source, cursor + 1, end);
2428 let (member, member_end) = read_js_identifier(source, member_start)?;
2429 path.push(member.to_string());
2430 cursor = member_end;
2431 }
2432 Some(b'[') => {
2433 if let Some((literal_start, literal_end, bracket_end)) =
2434 bracket_string_literal_access(source, cursor)
2435 && bracket_end <= end
2436 {
2437 path.push(source[literal_start..literal_end].to_string());
2438 cursor = bracket_end;
2439 } else {
2440 return None;
2441 }
2442 }
2443 _ => break,
2444 }
2445 }
2446 (trim_js_expression(source, cursor, end).0 >= end).then(|| path.join("."))
2447}
2448
2449fn static_string_prefix_for_js_expression(
2450 source: &str,
2451 start: usize,
2452 end: usize,
2453 local_class_values: &BTreeMap<String, SourceClassValue>,
2454) -> Option<String> {
2455 let (start, end) = trim_js_expression(source, start, end);
2456 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
2457 if let Some((literal_start, literal_end, next_offset)) =
2458 js_string_literal_span(source, start, end)
2459 && trim_js_expression(source, next_offset, end).0 >= end
2460 {
2461 if source.as_bytes().get(start).copied() == Some(b'`')
2462 && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2463 {
2464 return Some(source[literal_start..literal_start + relative_interpolation].to_string());
2465 }
2466 return Some(source[literal_start..literal_end].to_string());
2467 }
2468 if let Some(path) = js_expression_path(source, start, end)
2469 && let Some(value) = local_class_values.get(path.as_str())
2470 {
2471 if value.exact.len() == 1 && value.prefixes.is_empty() {
2472 return value.exact.first().cloned();
2473 }
2474 if value.prefixes.len() == 1 && value.exact.is_empty() {
2475 return value.prefixes.first().cloned();
2476 }
2477 }
2478 if let Some(plus_offset) = find_top_level_js_operator(source, start, end, "+") {
2479 let left =
2480 static_string_prefix_for_js_expression(source, start, plus_offset, local_class_values)?;
2481 let right = static_string_prefix_for_js_expression(
2482 source,
2483 plus_offset + 1,
2484 end,
2485 local_class_values,
2486 )
2487 .unwrap_or_default();
2488 return Some(format!("{left}{right}"));
2489 }
2490 None
2491}
2492
2493fn js_call_end(source: &str, open_paren: usize) -> Option<usize> {
2494 if source.as_bytes().get(open_paren) != Some(&b'(') {
2495 return None;
2496 }
2497 let mut cursor = advance_js_scan_cursor(source, open_paren, source.len());
2498 let mut depth = 1usize;
2499 while cursor < source.len() {
2500 match source.as_bytes().get(cursor).copied()? {
2501 b'\'' | b'"' | b'`' => {
2502 cursor = skip_js_string_literal(source, cursor, source.len())?;
2503 }
2504 b'(' => {
2505 depth += 1;
2506 cursor = advance_js_scan_cursor(source, cursor, source.len());
2507 }
2508 b')' => {
2509 depth -= 1;
2510 if depth == 0 {
2511 return Some(cursor);
2512 }
2513 cursor = advance_js_scan_cursor(source, cursor, source.len());
2514 }
2515 _ => {
2516 cursor = advance_js_scan_cursor(source, cursor, source.len());
2517 }
2518 }
2519 }
2520 None
2521}
2522
2523fn class_token_byte_spans(
2524 source: &str,
2525 literal_start: usize,
2526 literal_end: usize,
2527) -> Vec<ParserByteSpanV0> {
2528 let mut spans = Vec::new();
2529 let mut token_start: Option<usize> = None;
2530 for (relative_index, ch) in source[literal_start..literal_end].char_indices() {
2531 let index = literal_start + relative_index;
2532 if ch.is_ascii_whitespace() {
2533 if let Some(start) = token_start.take() {
2534 push_class_token_span(source, start, index, &mut spans);
2535 }
2536 } else if token_start.is_none() {
2537 token_start = Some(index);
2538 }
2539 }
2540 if let Some(start) = token_start {
2541 push_class_token_span(source, start, literal_end, &mut spans);
2542 }
2543 spans
2544}
2545
2546fn push_class_token_span(
2547 source: &str,
2548 start: usize,
2549 end: usize,
2550 spans: &mut Vec<ParserByteSpanV0>,
2551) {
2552 if start < end && source[start..end].chars().all(is_css_identifier_continue) {
2553 spans.push(ParserByteSpanV0 { start, end });
2554 }
2555}
2556
2557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2558struct CodeIdentifier<'a> {
2559 text: &'a str,
2560 end: usize,
2561}
2562
2563fn next_code_identifier(source: &str, mut cursor: usize) -> Option<CodeIdentifier<'_>> {
2564 while cursor < source.len() {
2565 cursor = skip_js_trivia(source, cursor);
2566 let byte = source.as_bytes().get(cursor).copied()?;
2567 if matches!(byte, b'\'' | b'"' | b'`') {
2568 cursor = skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2569 continue;
2570 }
2571 if byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$') {
2572 let (text, end) = read_js_identifier(source, cursor)?;
2573 return Some(CodeIdentifier { text, end });
2574 }
2575 cursor = advance_js_scan_cursor(source, cursor, source.len());
2576 }
2577 None
2578}
2579
2580fn skip_js_trivia(source: &str, cursor: usize) -> usize {
2581 skip_js_trivia_until(source, cursor, source.len())
2582}
2583
2584fn skip_js_trivia_until(source: &str, mut cursor: usize, limit: usize) -> usize {
2585 loop {
2586 cursor = skip_ascii_whitespace_until(source, cursor, limit);
2587 if source.as_bytes().get(cursor) == Some(&b'/') {
2588 match source.as_bytes().get(cursor + 1).copied() {
2589 Some(b'/') => {
2590 cursor = skip_js_line_comment(source, cursor + 2, limit);
2591 continue;
2592 }
2593 Some(b'*') => {
2594 cursor = skip_js_block_comment(source, cursor + 2, limit);
2595 continue;
2596 }
2597 _ => {}
2598 }
2599 }
2600 return cursor;
2601 }
2602}
2603
2604fn skip_ascii_whitespace_until(source: &str, mut offset: usize, limit: usize) -> usize {
2605 while offset < limit
2606 && source
2607 .as_bytes()
2608 .get(offset)
2609 .is_some_and(u8::is_ascii_whitespace)
2610 {
2611 offset += 1;
2612 }
2613 offset
2614}
2615
2616fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
2617 while source
2618 .as_bytes()
2619 .get(offset)
2620 .is_some_and(u8::is_ascii_whitespace)
2621 {
2622 offset += 1;
2623 }
2624 offset
2625}
2626
2627fn skip_js_line_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2628 let limit = char_boundary_floor(source, limit);
2629 while cursor < limit {
2630 if source.as_bytes().get(cursor) == Some(&b'\n') {
2631 return advance_js_scan_cursor(source, cursor, limit);
2632 }
2633 cursor = advance_js_scan_cursor(source, cursor, limit);
2634 }
2635 limit
2636}
2637
2638fn skip_js_block_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2639 let limit = char_boundary_floor(source, limit);
2640 while cursor + 1 < limit {
2641 if source.as_bytes().get(cursor) == Some(&b'*')
2642 && source.as_bytes().get(cursor + 1) == Some(&b'/')
2643 {
2644 return cursor + 2;
2645 }
2646 cursor = advance_js_scan_cursor(source, cursor, limit);
2647 }
2648 limit
2649}
2650
2651fn js_string_literal_span(
2652 source: &str,
2653 quote_offset: usize,
2654 limit: usize,
2655) -> Option<(usize, usize, usize)> {
2656 let quote = source.as_bytes().get(quote_offset).copied()?;
2657 if !matches!(quote, b'\'' | b'"' | b'`') {
2658 return None;
2659 }
2660 let literal_start = quote_offset + 1;
2661 let next_offset = skip_js_string_literal(source, quote_offset, limit)?;
2662 Some((literal_start, next_offset - 1, next_offset))
2663}
2664
2665fn skip_js_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
2666 let quote = source.as_bytes().get(quote_offset).copied()?;
2667 let limit = char_boundary_floor(source, limit);
2668 let mut cursor = quote_offset + 1;
2669 while cursor < limit {
2670 let byte = source.as_bytes().get(cursor).copied()?;
2671 if byte == b'\\' {
2672 cursor = advance_js_escaped_char(source, cursor, limit);
2673 continue;
2674 }
2675 if byte == quote {
2676 return Some(cursor + 1);
2677 }
2678 cursor = advance_js_scan_cursor(source, cursor, limit);
2679 }
2680 None
2681}
2682
2683fn bracket_string_literal_access(
2684 source: &str,
2685 bracket_offset: usize,
2686) -> Option<(usize, usize, usize)> {
2687 if source.as_bytes().get(bracket_offset) != Some(&b'[') {
2688 return None;
2689 }
2690 let quote_offset = skip_ascii_whitespace(source, bracket_offset + 1);
2691 let quote = source.as_bytes().get(quote_offset).copied()?;
2692 if !matches!(quote, b'\'' | b'"') {
2693 return None;
2694 }
2695 let (literal_start, literal_end, literal_next) =
2696 js_string_literal_span(source, quote_offset, source.len())?;
2697 if literal_next > source.len() {
2698 return None;
2699 }
2700 let closing_bracket = skip_ascii_whitespace(source, literal_end + 1);
2701 if source.as_bytes().get(closing_bracket) != Some(&b']') {
2702 return None;
2703 }
2704 Some((literal_start, literal_end, closing_bracket + 1))
2705}
2706
2707fn read_js_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
2708 let start = char_boundary_ceil(source, start);
2709 let first = source.get(start..)?.chars().next()?;
2710 if !is_js_identifier_start(first) {
2711 return None;
2712 }
2713 let mut end = start + first.len_utf8();
2714 let scan_start = end;
2715 for (relative_index, ch) in source.get(scan_start..)?.char_indices() {
2716 if !is_js_identifier_continue(ch) {
2717 break;
2718 }
2719 end = scan_start + relative_index + ch.len_utf8();
2720 }
2721 Some((&source[start..end], end))
2722}
2723
2724fn is_js_identifier_start(ch: char) -> bool {
2725 ch.is_ascii_alphabetic() || matches!(ch, '_' | '$')
2726}
2727
2728fn is_js_identifier_continue(ch: char) -> bool {
2729 ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')
2730}
2731
2732fn is_css_identifier_continue(ch: char) -> bool {
2733 ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
2734}
2735
2736#[cfg(test)]
2737mod tests;