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