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 _ => {
594 if let Some(expression) = declaration.as_expression() {
595 self.collect_expression(expression);
596 }
597 }
598 }
599 }
600
601 fn collect_for_statement_init(&mut self, init: &oxc_ast::ast::ForStatementInit<'a>) {
602 match init {
603 oxc_ast::ast::ForStatementInit::VariableDeclaration(declaration) => {
604 self.collect_variable_declaration(declaration);
605 }
606 oxc_ast::ast::ForStatementInit::StaticMemberExpression(member) => {
607 self.collect_static_member_expression(member);
608 }
609 oxc_ast::ast::ForStatementInit::ComputedMemberExpression(member) => {
610 self.collect_computed_member_expression(member);
611 }
612 oxc_ast::ast::ForStatementInit::CallExpression(expression) => {
613 self.collect_call_expression(expression);
614 }
615 _ => {}
616 }
617 }
618
619 fn collect_variable_declaration(
620 &mut self,
621 declaration: &oxc_ast::ast::VariableDeclaration<'a>,
622 ) {
623 for declarator in &declaration.declarations {
624 if let Some(binding) = self.classnames_bind_utility_binding_from_declarator(declarator)
625 {
626 self.classnames_bind_utility_bindings.push(binding);
627 }
628 if let Some(init) = &declarator.init {
629 self.collect_expression(init);
630 }
631 }
632 }
633
634 fn classnames_bind_utility_binding_from_declarator(
635 &self,
636 declarator: &VariableDeclarator<'a>,
637 ) -> Option<ClassnamesBindUtilityBinding> {
638 if self.style_targets.is_empty() || self.classnames_bind_imports.is_empty() {
639 return None;
640 }
641 let binding = binding_pattern_identifier_name(&declarator.id)?;
642 let init = declarator.init.as_ref()?;
643 let Expression::CallExpression(call) = init else {
644 return None;
645 };
646 let Expression::StaticMemberExpression(callee) = &call.callee else {
647 return None;
648 };
649 if callee.property.name.as_str() != "bind" {
650 return None;
651 }
652 let callee_binding = expression_identifier_name(&callee.object)?;
653 if !self
654 .classnames_bind_imports
655 .iter()
656 .any(|import_binding| import_binding == callee_binding)
657 {
658 return None;
659 }
660 let style_binding = call.arguments.first().and_then(argument_identifier_name)?;
661 let style_uri = self
662 .style_targets
663 .iter()
664 .find(|target| target.binding == style_binding)?
665 .target_style_uri
666 .clone()?;
667
668 Some(ClassnamesBindUtilityBinding {
669 binding: binding.to_string(),
670 style_uri,
671 })
672 }
673
674 fn collect_function_body(&mut self, body: Option<&oxc_ast::ast::FunctionBody<'a>>) {
675 let Some(body) = body else {
676 return;
677 };
678 for statement in &body.statements {
679 self.collect_statement(statement);
680 }
681 }
682
683 fn collect_class(&mut self, class: &Class<'a>) {
684 if let Some(super_class) = &class.super_class {
685 self.collect_expression(super_class);
686 }
687 for element in &class.body.body {
688 match element {
689 ClassElement::MethodDefinition(method) => {
690 self.collect_function_body(method.value.body.as_deref());
691 }
692 ClassElement::PropertyDefinition(property) => {
693 if property.computed {
694 self.collect_property_key(&property.key);
695 }
696 if let Some(value) = &property.value {
697 self.collect_expression(value);
698 }
699 }
700 ClassElement::AccessorProperty(property) => {
701 if property.computed {
702 self.collect_property_key(&property.key);
703 }
704 if let Some(value) = &property.value {
705 self.collect_expression(value);
706 }
707 }
708 ClassElement::StaticBlock(block) => {
709 for statement in &block.body {
710 self.collect_statement(statement);
711 }
712 }
713 ClassElement::TSIndexSignature(_) => {}
714 }
715 }
716 }
717
718 fn collect_expression(&mut self, expression: &Expression<'a>) {
719 match expression {
720 Expression::StaticMemberExpression(member) => {
721 self.collect_static_member_expression(member);
722 }
723 Expression::ComputedMemberExpression(member) => {
724 self.collect_computed_member_expression(member);
725 }
726 Expression::PrivateFieldExpression(member) => {
727 self.collect_expression(&member.object);
728 }
729 Expression::ArrayExpression(expression) => {
730 self.collect_array_expression(expression);
731 }
732 Expression::ObjectExpression(expression) => {
733 self.collect_object_expression(expression);
734 }
735 Expression::CallExpression(expression) => {
736 self.collect_call_expression(expression);
737 }
738 Expression::NewExpression(expression) => {
739 self.collect_expression(&expression.callee);
740 for argument in &expression.arguments {
741 self.collect_argument(argument);
742 }
743 }
744 Expression::ChainExpression(expression) => {
745 self.collect_chain_element(&expression.expression);
746 }
747 Expression::ConditionalExpression(expression) => {
748 self.collect_conditional_expression(expression);
749 }
750 Expression::BinaryExpression(expression) => {
751 self.collect_expression(&expression.left);
752 self.collect_expression(&expression.right);
753 }
754 Expression::LogicalExpression(expression) => {
755 self.collect_logical_expression(expression);
756 }
757 Expression::AssignmentExpression(expression) => {
758 self.collect_expression(&expression.right);
759 }
760 Expression::SequenceExpression(expression) => {
761 for expression in &expression.expressions {
762 self.collect_expression(expression);
763 }
764 }
765 Expression::ParenthesizedExpression(expression) => {
766 self.collect_parenthesized_expression(expression);
767 }
768 Expression::UnaryExpression(expression) => {
769 self.collect_expression(&expression.argument);
770 }
771 Expression::AwaitExpression(expression) => {
772 self.collect_expression(&expression.argument);
773 }
774 Expression::TemplateLiteral(expression) => {
775 for expression in &expression.expressions {
776 self.collect_expression(expression);
777 }
778 }
779 Expression::TaggedTemplateExpression(expression) => {
780 self.collect_expression(&expression.tag);
781 for expression in &expression.quasi.expressions {
782 self.collect_expression(expression);
783 }
784 }
785 Expression::ArrowFunctionExpression(expression) => {
786 self.collect_function_body(Some(&expression.body));
787 }
788 Expression::FunctionExpression(expression) => {
789 self.collect_function_body(expression.body.as_deref());
790 }
791 Expression::ClassExpression(class) => {
792 self.collect_class(class);
793 }
794 Expression::ImportExpression(expression) => {
795 self.collect_expression(&expression.source);
796 if let Some(options) = &expression.options {
797 self.collect_expression(options);
798 }
799 }
800 Expression::JSXElement(element) => {
801 self.collect_jsx_element(element);
802 }
803 Expression::JSXFragment(fragment) => {
804 for child in &fragment.children {
805 self.collect_jsx_child(child);
806 }
807 }
808 Expression::TSAsExpression(expression) => {
809 self.collect_ts_as_expression(expression);
810 }
811 Expression::TSSatisfiesExpression(expression) => {
812 self.collect_ts_satisfies_expression(expression);
813 }
814 Expression::TSTypeAssertion(expression) => {
815 self.collect_expression(&expression.expression);
816 }
817 Expression::TSNonNullExpression(expression) => {
818 self.collect_ts_non_null_expression(expression);
819 }
820 Expression::TSInstantiationExpression(expression) => {
821 self.collect_expression(&expression.expression);
822 }
823 _ => {}
824 }
825 }
826
827 fn collect_array_expression_element(&mut self, element: &ArrayExpressionElement<'a>) {
828 match element {
829 ArrayExpressionElement::SpreadElement(spread) => {
830 self.collect_expression(&spread.argument);
831 }
832 ArrayExpressionElement::Elision(_) => {}
833 _ => {
834 if let Some(expression) = element.as_expression() {
835 self.collect_expression(expression);
836 }
837 }
838 }
839 }
840
841 fn collect_argument(&mut self, argument: &Argument<'a>) {
842 match argument {
843 Argument::SpreadElement(spread) => {
844 self.collect_expression(&spread.argument);
845 }
846 _ => {
847 if let Some(expression) = argument.as_expression() {
848 self.collect_expression(expression);
849 }
850 }
851 }
852 }
853
854 fn collect_chain_element(&mut self, element: &ChainElement<'a>) {
855 match element {
856 ChainElement::CallExpression(expression) => {
857 self.collect_expression(&expression.callee);
858 for argument in &expression.arguments {
859 self.collect_argument(argument);
860 }
861 }
862 ChainElement::StaticMemberExpression(member) => {
863 self.collect_static_member_expression(member);
864 }
865 ChainElement::ComputedMemberExpression(member) => {
866 self.collect_computed_member_expression(member);
867 }
868 ChainElement::PrivateFieldExpression(member) => {
869 self.collect_expression(&member.object);
870 }
871 ChainElement::TSNonNullExpression(expression) => {
872 self.collect_expression(&expression.expression);
873 }
874 }
875 }
876
877 fn collect_property_key(&mut self, key: &oxc_ast::ast::PropertyKey<'a>) {
878 match key {
879 oxc_ast::ast::PropertyKey::StaticIdentifier(_)
880 | oxc_ast::ast::PropertyKey::PrivateIdentifier(_) => {}
881 _ => {
882 if let Some(expression) = key.as_expression() {
883 self.collect_expression(expression);
884 }
885 }
886 }
887 }
888
889 fn collect_jsx_element(&mut self, element: &oxc_ast::ast::JSXElement<'a>) {
890 for attribute in &element.opening_element.attributes {
891 match attribute {
892 oxc_ast::ast::JSXAttributeItem::Attribute(attribute) => {
893 if is_jsx_class_name_attribute(&attribute.name)
894 && let Some(value) = &attribute.value
895 {
896 self.collect_class_name_string_literal_attribute(value);
897 self.collect_class_name_expression_attribute(value);
898 }
899 if let Some(value) = &attribute.value {
900 self.collect_jsx_attribute_value(value);
901 }
902 }
903 oxc_ast::ast::JSXAttributeItem::SpreadAttribute(attribute) => {
904 self.collect_expression(&attribute.argument);
905 }
906 }
907 }
908 for child in &element.children {
909 self.collect_jsx_child(child);
910 }
911 }
912
913 fn collect_jsx_attribute_value(&mut self, value: &JSXAttributeValue<'a>) {
914 match value {
915 JSXAttributeValue::ExpressionContainer(container) => {
916 self.collect_jsx_expression(&container.expression);
917 }
918 JSXAttributeValue::Element(element) => {
919 self.collect_jsx_element(element);
920 }
921 JSXAttributeValue::Fragment(fragment) => {
922 for child in &fragment.children {
923 self.collect_jsx_child(child);
924 }
925 }
926 JSXAttributeValue::StringLiteral(_) => {}
927 }
928 }
929
930 fn collect_class_name_string_literal_attribute(&mut self, value: &JSXAttributeValue<'a>) {
931 let JSXAttributeValue::StringLiteral(literal) = value else {
932 return;
933 };
934 if let Some(span) = self.string_literal_content_span(literal.span) {
935 self.class_string_literals.push(span);
936 }
937 }
938
939 fn collect_class_name_expression_attribute(&mut self, value: &JSXAttributeValue<'a>) {
940 let JSXAttributeValue::ExpressionContainer(container) = value else {
941 return;
942 };
943 if let Some(span) = jsx_expression_span(&container.expression) {
944 self.class_name_expression_spans.push(span);
945 }
946 }
947
948 fn collect_jsx_child(&mut self, child: &JSXChild<'a>) {
949 match child {
950 JSXChild::Element(element) => {
951 self.collect_jsx_element(element);
952 }
953 JSXChild::Fragment(fragment) => {
954 for child in &fragment.children {
955 self.collect_jsx_child(child);
956 }
957 }
958 JSXChild::ExpressionContainer(container) => {
959 self.collect_jsx_expression(&container.expression);
960 }
961 JSXChild::Spread(spread) => {
962 self.collect_expression(&spread.expression);
963 }
964 JSXChild::Text(_) => {}
965 }
966 }
967
968 fn collect_jsx_expression(&mut self, expression: &JSXExpression<'a>) {
969 match expression {
970 JSXExpression::StaticMemberExpression(member) => {
971 self.collect_static_member_expression(member);
972 }
973 JSXExpression::ComputedMemberExpression(member) => {
974 self.collect_computed_member_expression(member);
975 }
976 JSXExpression::CallExpression(expression) => {
977 self.collect_call_expression(expression);
978 }
979 JSXExpression::ConditionalExpression(expression) => {
980 self.collect_conditional_expression(expression);
981 }
982 JSXExpression::LogicalExpression(expression) => {
983 self.collect_logical_expression(expression);
984 }
985 JSXExpression::ArrayExpression(expression) => {
986 self.collect_array_expression(expression);
987 }
988 JSXExpression::ObjectExpression(expression) => {
989 self.collect_object_expression(expression);
990 }
991 JSXExpression::ParenthesizedExpression(expression) => {
992 self.collect_parenthesized_expression(expression);
993 }
994 JSXExpression::TSAsExpression(expression) => {
995 self.collect_ts_as_expression(expression);
996 }
997 JSXExpression::TSSatisfiesExpression(expression) => {
998 self.collect_ts_satisfies_expression(expression);
999 }
1000 JSXExpression::TSNonNullExpression(expression) => {
1001 self.collect_ts_non_null_expression(expression);
1002 }
1003 JSXExpression::JSXElement(element) => {
1004 self.collect_jsx_element(element);
1005 }
1006 JSXExpression::JSXFragment(fragment) => {
1007 for child in &fragment.children {
1008 self.collect_jsx_child(child);
1009 }
1010 }
1011 _ => {}
1012 }
1013 }
1014
1015 fn collect_array_expression(&mut self, expression: &ArrayExpression<'a>) {
1016 for element in &expression.elements {
1017 self.collect_array_expression_element(element);
1018 }
1019 }
1020
1021 fn collect_object_expression(&mut self, expression: &ObjectExpression<'a>) {
1022 for property in &expression.properties {
1023 match property {
1024 ObjectPropertyKind::ObjectProperty(property) => {
1025 if property.computed {
1026 self.collect_property_key(&property.key);
1027 }
1028 self.collect_expression(&property.value);
1029 }
1030 ObjectPropertyKind::SpreadProperty(spread) => {
1031 self.collect_expression(&spread.argument);
1032 }
1033 }
1034 }
1035 }
1036
1037 fn collect_call_expression(&mut self, expression: &CallExpression<'a>) {
1038 if let Some(binding) = expression_identifier_name(&expression.callee) {
1039 for argument in &expression.arguments {
1040 if let Some(byte_span) = argument_expression_span(argument) {
1041 self.classnames_bind_call_arguments
1042 .push(ClassnamesBindCallArgument {
1043 binding: binding.to_string(),
1044 byte_span,
1045 });
1046 }
1047 }
1048 }
1049 self.collect_expression(&expression.callee);
1050 for argument in &expression.arguments {
1051 self.collect_argument(argument);
1052 }
1053 }
1054
1055 fn collect_conditional_expression(&mut self, expression: &ConditionalExpression<'a>) {
1056 self.collect_expression(&expression.test);
1057 self.collect_expression(&expression.consequent);
1058 self.collect_expression(&expression.alternate);
1059 }
1060
1061 fn collect_logical_expression(&mut self, expression: &LogicalExpression<'a>) {
1062 self.collect_expression(&expression.left);
1063 self.collect_expression(&expression.right);
1064 }
1065
1066 fn collect_parenthesized_expression(&mut self, expression: &ParenthesizedExpression<'a>) {
1067 self.collect_expression(&expression.expression);
1068 }
1069
1070 fn collect_ts_as_expression(&mut self, expression: &TSAsExpression<'a>) {
1071 self.collect_expression(&expression.expression);
1072 }
1073
1074 fn collect_ts_satisfies_expression(&mut self, expression: &TSSatisfiesExpression<'a>) {
1075 self.collect_expression(&expression.expression);
1076 }
1077
1078 fn collect_ts_non_null_expression(&mut self, expression: &TSNonNullExpression<'a>) {
1079 self.collect_expression(&expression.expression);
1080 }
1081
1082 fn collect_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
1083 if let Some(target) = self.target_for_object(&member.object)
1084 && let Some(byte_span) = self.css_identifier_span(member.property.span)
1085 {
1086 self.style_property_accesses
1087 .push(SourceStylePropertyAccessFactV0 {
1088 byte_span,
1089 target_style_uri: target.target_style_uri.clone(),
1090 });
1091 }
1092 self.collect_expression(&member.object);
1093 }
1094
1095 fn collect_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
1096 if let Some(target) = self.target_for_object(&member.object)
1097 && let Some(byte_span) = self.static_string_expression_content_span(&member.expression)
1098 {
1099 self.style_property_accesses
1100 .push(SourceStylePropertyAccessFactV0 {
1101 byte_span,
1102 target_style_uri: target.target_style_uri.clone(),
1103 });
1104 }
1105 self.collect_expression(&member.object);
1106 self.collect_expression(&member.expression);
1107 }
1108
1109 fn target_for_object(&self, expression: &Expression<'a>) -> Option<&SourceStyleBindingTarget> {
1110 match expression {
1111 Expression::Identifier(identifier) => self
1112 .property_access_targets
1113 .iter()
1114 .find(|target| target.binding == identifier.name.as_str()),
1115 Expression::ParenthesizedExpression(expression) => {
1116 self.target_for_object(&expression.expression)
1117 }
1118 Expression::TSAsExpression(expression) => {
1119 self.target_for_object(&expression.expression)
1120 }
1121 Expression::TSSatisfiesExpression(expression) => {
1122 self.target_for_object(&expression.expression)
1123 }
1124 Expression::TSTypeAssertion(expression) => {
1125 self.target_for_object(&expression.expression)
1126 }
1127 Expression::TSNonNullExpression(expression) => {
1128 self.target_for_object(&expression.expression)
1129 }
1130 Expression::TSInstantiationExpression(expression) => {
1131 self.target_for_object(&expression.expression)
1132 }
1133 _ => None,
1134 }
1135 }
1136
1137 fn static_string_expression_content_span(
1138 &self,
1139 expression: &Expression<'a>,
1140 ) -> Option<ParserByteSpanV0> {
1141 match expression {
1142 Expression::StringLiteral(literal) => self.css_identifier_content_span(literal.span),
1143 Expression::TemplateLiteral(literal) if literal.expressions.is_empty() => {
1144 self.css_identifier_content_span(literal.span)
1145 }
1146 _ => None,
1147 }
1148 }
1149
1150 fn css_identifier_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1151 let span = parser_byte_span(span);
1152 let text = self.source.get(span.start..span.end)?;
1153 (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(span)
1154 }
1155
1156 fn css_identifier_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1157 let span = parser_byte_span(span);
1158 if span.end <= span.start + 1 {
1159 return None;
1160 }
1161 let content = ParserByteSpanV0 {
1162 start: span.start + 1,
1163 end: span.end - 1,
1164 };
1165 let text = self.source.get(content.start..content.end)?;
1166 (!text.is_empty() && text.chars().all(is_css_identifier_continue)).then_some(content)
1167 }
1168
1169 fn string_literal_content_span(&self, span: Span) -> Option<ParserByteSpanV0> {
1170 let span = parser_byte_span(span);
1171 if span.end <= span.start + 1 {
1172 return None;
1173 }
1174 let content = ParserByteSpanV0 {
1175 start: span.start + 1,
1176 end: span.end - 1,
1177 };
1178 self.source.get(content.start..content.end)?;
1179 Some(content)
1180 }
1181
1182 fn canonicalize(&mut self) {
1183 self.class_string_literals.sort_by(|left, right| {
1184 left.start
1185 .cmp(&right.start)
1186 .then_with(|| left.end.cmp(&right.end))
1187 });
1188 self.class_string_literals.dedup();
1189 self.style_property_accesses.sort_by(|left, right| {
1190 left.byte_span
1191 .start
1192 .cmp(&right.byte_span.start)
1193 .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1194 .then_with(|| left.target_style_uri.cmp(&right.target_style_uri))
1195 });
1196 self.style_property_accesses.dedup();
1197 self.classnames_bind_utility_bindings
1198 .sort_by(|left, right| {
1199 left.binding
1200 .cmp(&right.binding)
1201 .then_with(|| left.style_uri.cmp(&right.style_uri))
1202 });
1203 self.classnames_bind_utility_bindings
1204 .dedup_by(|left, right| {
1205 left.binding == right.binding && left.style_uri == right.style_uri
1206 });
1207 self.classnames_bind_call_arguments.sort_by(|left, right| {
1208 left.binding
1209 .cmp(&right.binding)
1210 .then_with(|| left.byte_span.start.cmp(&right.byte_span.start))
1211 .then_with(|| left.byte_span.end.cmp(&right.byte_span.end))
1212 });
1213 self.classnames_bind_call_arguments.dedup_by(|left, right| {
1214 left.binding == right.binding && left.byte_span == right.byte_span
1215 });
1216 }
1217}
1218
1219fn parser_byte_span(span: Span) -> ParserByteSpanV0 {
1220 ParserByteSpanV0 {
1221 start: span.start as usize,
1222 end: span.end as usize,
1223 }
1224}
1225
1226fn is_jsx_class_name_attribute(name: &JSXAttributeName<'_>) -> bool {
1227 matches!(name, JSXAttributeName::Identifier(identifier) if identifier.name.as_str() == "className")
1228}
1229
1230fn jsx_expression_span(expression: &JSXExpression<'_>) -> Option<ParserByteSpanV0> {
1231 match expression {
1232 JSXExpression::EmptyExpression(_) => None,
1233 _ => Some(parser_byte_span(expression.span())),
1234 }
1235}
1236
1237fn argument_expression_span(argument: &Argument<'_>) -> Option<ParserByteSpanV0> {
1238 match argument {
1239 Argument::SpreadElement(spread) => Some(parser_byte_span(spread.argument.span())),
1240 _ => Some(parser_byte_span(argument.span())),
1241 }
1242}
1243
1244fn binding_pattern_identifier_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
1245 match pattern {
1246 BindingPattern::BindingIdentifier(identifier) => Some(identifier.name.as_str()),
1247 _ => None,
1248 }
1249}
1250
1251fn expression_identifier_name<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1252 match expression {
1253 Expression::Identifier(identifier) => Some(identifier.name.as_str()),
1254 Expression::ParenthesizedExpression(expression) => {
1255 expression_identifier_name(&expression.expression)
1256 }
1257 Expression::TSAsExpression(expression) => {
1258 expression_identifier_name(&expression.expression)
1259 }
1260 Expression::TSSatisfiesExpression(expression) => {
1261 expression_identifier_name(&expression.expression)
1262 }
1263 Expression::TSTypeAssertion(expression) => {
1264 expression_identifier_name(&expression.expression)
1265 }
1266 Expression::TSNonNullExpression(expression) => {
1267 expression_identifier_name(&expression.expression)
1268 }
1269 Expression::TSInstantiationExpression(expression) => {
1270 expression_identifier_name(&expression.expression)
1271 }
1272 _ => None,
1273 }
1274}
1275
1276fn argument_identifier_name<'a>(argument: &'a Argument<'a>) -> Option<&'a str> {
1277 match argument {
1278 Argument::Identifier(identifier) => Some(identifier.name.as_str()),
1279 Argument::ParenthesizedExpression(expression) => {
1280 expression_identifier_name(&expression.expression)
1281 }
1282 Argument::TSAsExpression(expression) => expression_identifier_name(&expression.expression),
1283 Argument::TSSatisfiesExpression(expression) => {
1284 expression_identifier_name(&expression.expression)
1285 }
1286 Argument::TSNonNullExpression(expression) => {
1287 expression_identifier_name(&expression.expression)
1288 }
1289 Argument::TSInstantiationExpression(expression) => {
1290 expression_identifier_name(&expression.expression)
1291 }
1292 _ => None,
1293 }
1294}
1295
1296fn collect_selector_references_from_js_expression(
1297 source: &str,
1298 start: usize,
1299 end: usize,
1300 target_style_uri: Option<&str>,
1301 local_class_values: &BTreeMap<String, SourceClassValue>,
1302 references: &mut Vec<SourceSelectorReferenceFactV0>,
1303 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1304) {
1305 let (start, end) = trim_js_expression(source, start, end);
1306 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1307 if start >= end {
1308 return;
1309 }
1310
1311 if let Some((literal_start, literal_end, next_offset)) =
1312 js_string_literal_span(source, start, end)
1313 && trim_js_expression(source, next_offset, end).0 >= end
1314 {
1315 push_js_literal_selector_references(
1316 source,
1317 literal_start,
1318 literal_end,
1319 source.as_bytes().get(start).copied() == Some(b'`'),
1320 target_style_uri,
1321 references,
1322 );
1323 if source.as_bytes().get(start).copied() == Some(b'`') {
1324 collect_template_type_fact_targets(
1325 source,
1326 literal_start,
1327 literal_end,
1328 target_style_uri,
1329 type_fact_targets,
1330 );
1331 }
1332 return;
1333 }
1334
1335 if source.as_bytes().get(start) == Some(&b'{')
1336 && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1337 {
1338 collect_object_literal_selector_references(
1339 source,
1340 start,
1341 end,
1342 target_style_uri,
1343 local_class_values,
1344 references,
1345 type_fact_targets,
1346 );
1347 return;
1348 }
1349
1350 if source.as_bytes().get(start) == Some(&b'[')
1351 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1352 {
1353 for (element_start, element_end) in
1354 split_top_level_js_segments(source, start + 1, end - 1, b',')
1355 {
1356 let element_start = skip_js_trivia_until(source, element_start, element_end);
1357 let element_start = if source[element_start..element_end].starts_with("...") {
1358 element_start + 3
1359 } else {
1360 element_start
1361 };
1362 collect_selector_references_from_js_expression(
1363 source,
1364 element_start,
1365 element_end,
1366 target_style_uri,
1367 local_class_values,
1368 references,
1369 type_fact_targets,
1370 );
1371 }
1372 return;
1373 }
1374
1375 if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1376 {
1377 for (argument_start, argument_end) in
1378 split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1379 {
1380 collect_selector_references_from_js_expression(
1381 source,
1382 argument_start,
1383 argument_end,
1384 target_style_uri,
1385 local_class_values,
1386 references,
1387 type_fact_targets,
1388 );
1389 }
1390 return;
1391 }
1392
1393 if let Some((_, true_start, true_end, false_start, false_end)) =
1394 top_level_conditional_parts(source, start, end)
1395 {
1396 collect_selector_references_from_js_expression(
1397 source,
1398 true_start,
1399 true_end,
1400 target_style_uri,
1401 local_class_values,
1402 references,
1403 type_fact_targets,
1404 );
1405 collect_selector_references_from_js_expression(
1406 source,
1407 false_start,
1408 false_end,
1409 target_style_uri,
1410 local_class_values,
1411 references,
1412 type_fact_targets,
1413 );
1414 return;
1415 }
1416
1417 if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1418 .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1419 {
1420 collect_selector_references_from_js_expression(
1421 source,
1422 operator_offset + 2,
1423 end,
1424 target_style_uri,
1425 local_class_values,
1426 references,
1427 type_fact_targets,
1428 );
1429 return;
1430 }
1431
1432 let expression_path = js_expression_path(source, start, end);
1433 if let Some(value) =
1434 source_class_value_from_js_expression(source, start, end, local_class_values)
1435 && !value.is_empty()
1436 {
1437 if let Some(path) = expression_path.as_deref() {
1438 push_source_type_fact_target(
1439 ParserByteSpanV0 { start, end },
1440 path,
1441 target_style_uri,
1442 "",
1443 "",
1444 type_fact_targets,
1445 );
1446 }
1447 push_source_class_value_reference(
1448 ParserByteSpanV0 { start, end },
1449 value,
1450 target_style_uri,
1451 references,
1452 );
1453 return;
1454 }
1455
1456 if let Some(prefix) =
1457 static_string_prefix_for_js_expression(source, start, end, local_class_values)
1458 && !prefix.is_empty()
1459 {
1460 push_selector_reference(
1461 ParserByteSpanV0 { start, end },
1462 Some(prefix),
1463 SourceSelectorReferenceMatchKindV0::Prefix,
1464 target_style_uri,
1465 references,
1466 );
1467 return;
1468 }
1469
1470 if let Some(path) = expression_path {
1471 push_source_type_fact_target(
1472 ParserByteSpanV0 { start, end },
1473 path.as_str(),
1474 target_style_uri,
1475 "",
1476 "",
1477 type_fact_targets,
1478 );
1479 }
1480}
1481
1482fn collect_local_class_value_bindings(source: &str) -> BTreeMap<String, SourceClassValue> {
1483 let mut values = BTreeMap::new();
1484 let mut cursor = 0usize;
1485 while let Some(keyword) = next_code_identifier(source, cursor) {
1486 cursor = keyword.end;
1487 if !matches!(keyword.text, "const" | "let" | "var") {
1488 continue;
1489 }
1490 let binding_start = skip_js_trivia(source, keyword.end);
1491 let Some((binding, binding_end)) = read_js_identifier(source, binding_start) else {
1492 continue;
1493 };
1494 let equals_offset = skip_js_trivia(source, binding_end);
1495 if source.as_bytes().get(equals_offset) != Some(&b'=') {
1496 continue;
1497 }
1498 let expression_start = skip_js_trivia(source, equals_offset + 1);
1499 let expression_end = js_statement_expression_end(source, expression_start);
1500 if let Some(value) =
1501 source_class_value_from_js_expression(source, expression_start, expression_end, &values)
1502 && !value.is_empty()
1503 {
1504 values.insert(binding.to_string(), value);
1505 }
1506 let (_, property_values) = source_class_value_from_object_literal(
1507 source,
1508 expression_start,
1509 expression_end,
1510 &values,
1511 );
1512 for (property, value) in property_values {
1513 if !value.is_empty() {
1514 values.insert(format!("{binding}.{property}"), value);
1515 }
1516 }
1517 cursor = expression_end.min(source.len());
1518 }
1519 values
1520}
1521
1522fn source_class_value_from_js_expression(
1523 source: &str,
1524 start: usize,
1525 end: usize,
1526 local_class_values: &BTreeMap<String, SourceClassValue>,
1527) -> Option<SourceClassValue> {
1528 let (start, end) = trim_js_expression(source, start, end);
1529 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1530 if start >= end {
1531 return None;
1532 }
1533
1534 if let Some((literal_start, literal_end, next_offset)) =
1535 js_string_literal_span(source, start, end)
1536 && trim_js_expression(source, next_offset, end).0 >= end
1537 {
1538 return Some(source_class_value_from_js_literal(
1539 source,
1540 literal_start,
1541 literal_end,
1542 source.as_bytes().get(start).copied() == Some(b'`'),
1543 ));
1544 }
1545
1546 if source.as_bytes().get(start) == Some(&b'{')
1547 && matching_js_block_end(source, start, b'{', b'}') == Some(end - 1)
1548 {
1549 let (value, _) =
1550 source_class_value_from_object_literal(source, start, end, local_class_values);
1551 return Some(value);
1552 }
1553
1554 if source.as_bytes().get(start) == Some(&b'[')
1555 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1556 {
1557 let mut value = SourceClassValue::default();
1558 for (element_start, element_end) in
1559 split_top_level_js_segments(source, start + 1, end - 1, b',')
1560 {
1561 let element_start = skip_js_trivia_until(source, element_start, element_end);
1562 let element_start = if source[element_start..element_end].starts_with("...") {
1563 element_start + 3
1564 } else {
1565 element_start
1566 };
1567 if let Some(element_value) = source_class_value_from_js_expression(
1568 source,
1569 element_start,
1570 element_end,
1571 local_class_values,
1572 ) {
1573 value.merge(element_value);
1574 }
1575 }
1576 return Some(value);
1577 }
1578
1579 if let Some((arguments_start, arguments_end)) = class_utility_call_arguments(source, start, end)
1580 {
1581 let mut value = SourceClassValue::default();
1582 for (argument_start, argument_end) in
1583 split_top_level_js_segments(source, arguments_start, arguments_end, b',')
1584 {
1585 if let Some(argument_value) = source_class_value_from_js_expression(
1586 source,
1587 argument_start,
1588 argument_end,
1589 local_class_values,
1590 ) {
1591 value.merge(argument_value);
1592 }
1593 }
1594 return Some(value);
1595 }
1596
1597 if let Some((_, true_start, true_end, false_start, false_end)) =
1598 top_level_conditional_parts(source, start, end)
1599 {
1600 let mut value = SourceClassValue::default();
1601 if let Some(true_value) =
1602 source_class_value_from_js_expression(source, true_start, true_end, local_class_values)
1603 {
1604 value.merge(true_value);
1605 }
1606 if let Some(false_value) = source_class_value_from_js_expression(
1607 source,
1608 false_start,
1609 false_end,
1610 local_class_values,
1611 ) {
1612 value.merge(false_value);
1613 }
1614 return Some(value);
1615 }
1616
1617 if let Some(operator_offset) = find_top_level_js_operator(source, start, end, "&&")
1618 .or_else(|| find_top_level_js_operator(source, start, end, "||"))
1619 {
1620 return source_class_value_from_js_expression(
1621 source,
1622 operator_offset + 2,
1623 end,
1624 local_class_values,
1625 );
1626 }
1627
1628 if let Some(path) = js_expression_path(source, start, end)
1629 && let Some(value) = local_class_values.get(path.as_str())
1630 {
1631 return Some(value.clone());
1632 }
1633
1634 static_string_prefix_for_js_expression(source, start, end, local_class_values).map(|prefix| {
1635 let mut value = SourceClassValue::default();
1636 if !prefix.is_empty() {
1637 value.prefixes.push(prefix);
1638 }
1639 value
1640 })
1641}
1642
1643fn source_class_value_from_js_literal(
1644 source: &str,
1645 literal_start: usize,
1646 literal_end: usize,
1647 is_template: bool,
1648) -> SourceClassValue {
1649 let mut value = SourceClassValue::default();
1650 if is_template
1651 && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
1652 {
1653 let prefix_end = literal_start + relative_interpolation;
1654 push_template_prefix_value(source, literal_start, prefix_end, &mut value);
1655 } else {
1656 value
1657 .exact
1658 .extend(class_token_strings(source, literal_start, literal_end));
1659 }
1660 value.canonicalize();
1661 value
1662}
1663
1664fn source_class_value_from_object_literal(
1665 source: &str,
1666 start: usize,
1667 end: usize,
1668 local_class_values: &BTreeMap<String, SourceClassValue>,
1669) -> (SourceClassValue, BTreeMap<String, SourceClassValue>) {
1670 let (start, end) = trim_js_expression(source, start, end);
1671 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
1672 let mut object_value = SourceClassValue::default();
1673 let mut property_values = BTreeMap::new();
1674 if source.as_bytes().get(start) != Some(&b'{')
1675 || matching_js_block_end(source, start, b'{', b'}') != Some(end.saturating_sub(1))
1676 {
1677 return (object_value, property_values);
1678 }
1679
1680 for (property_start, property_end) in
1681 split_top_level_js_segments(source, start + 1, end - 1, b',')
1682 {
1683 let (property_start, property_end) =
1684 trim_js_expression(source, property_start, property_end);
1685 if property_start >= property_end {
1686 continue;
1687 }
1688 if source[property_start..property_end].starts_with("...") {
1689 if let Some(spread_value) = source_class_value_from_js_expression(
1690 source,
1691 property_start + 3,
1692 property_end,
1693 local_class_values,
1694 ) {
1695 object_value.merge(spread_value);
1696 }
1697 continue;
1698 }
1699 let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1700 let key_end = colon.unwrap_or(property_end);
1701 let key_value =
1702 source_class_value_from_object_key(source, property_start, key_end, local_class_values);
1703 object_value.merge(key_value.clone());
1704 if let Some(property_name) = object_property_name(source, property_start, key_end)
1705 && let Some(property_value) = colon
1706 .and_then(|colon| {
1707 source_class_value_from_js_expression(
1708 source,
1709 colon + 1,
1710 property_end,
1711 local_class_values,
1712 )
1713 })
1714 .filter(|value| !value.is_empty())
1715 {
1716 property_values.insert(property_name, property_value);
1717 }
1718 }
1719 object_value.canonicalize();
1720 (object_value, property_values)
1721}
1722
1723fn collect_object_literal_selector_references(
1724 source: &str,
1725 start: usize,
1726 end: usize,
1727 target_style_uri: Option<&str>,
1728 local_class_values: &BTreeMap<String, SourceClassValue>,
1729 references: &mut Vec<SourceSelectorReferenceFactV0>,
1730 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1731) {
1732 for (property_start, property_end) in
1733 split_top_level_js_segments(source, start + 1, end - 1, b',')
1734 {
1735 let (property_start, property_end) =
1736 trim_js_expression(source, property_start, property_end);
1737 if property_start >= property_end {
1738 continue;
1739 }
1740 if source[property_start..property_end].starts_with("...") {
1741 collect_selector_references_from_js_expression(
1742 source,
1743 property_start + 3,
1744 property_end,
1745 target_style_uri,
1746 local_class_values,
1747 references,
1748 type_fact_targets,
1749 );
1750 continue;
1751 }
1752 let colon = find_top_level_js_byte(source, property_start, property_end, b':');
1753 let key_end = colon.unwrap_or(property_end);
1754 collect_selector_references_from_object_key(
1755 source,
1756 property_start,
1757 key_end,
1758 target_style_uri,
1759 local_class_values,
1760 references,
1761 type_fact_targets,
1762 );
1763 }
1764}
1765
1766fn class_utility_call_arguments(source: &str, start: usize, end: usize) -> Option<(usize, usize)> {
1767 let (callee, callee_end) = read_js_identifier(source, start)?;
1768 if !is_class_utility_callee(callee) {
1769 return None;
1770 }
1771 let open_paren = skip_js_trivia_until(source, callee_end, end);
1772 if source.as_bytes().get(open_paren) != Some(&b'(') {
1773 return None;
1774 }
1775 let call_end = js_call_end(source, open_paren)?;
1776 if call_end > end || trim_js_expression(source, call_end + 1, end).0 < end {
1777 return None;
1778 }
1779 Some((open_paren + 1, call_end))
1780}
1781
1782fn is_class_utility_callee(callee: &str) -> bool {
1783 matches!(callee, "classnames" | "classNames" | "clsx" | "cn")
1784}
1785
1786fn collect_selector_references_from_object_key(
1787 source: &str,
1788 start: usize,
1789 end: usize,
1790 target_style_uri: Option<&str>,
1791 local_class_values: &BTreeMap<String, SourceClassValue>,
1792 references: &mut Vec<SourceSelectorReferenceFactV0>,
1793 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1794) {
1795 let (start, end) = trim_js_expression(source, start, end);
1796 if start >= end {
1797 return;
1798 }
1799 if source.as_bytes().get(start) == Some(&b'[')
1800 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1801 {
1802 collect_selector_references_from_js_expression(
1803 source,
1804 start + 1,
1805 end - 1,
1806 target_style_uri,
1807 local_class_values,
1808 references,
1809 type_fact_targets,
1810 );
1811 return;
1812 }
1813 if let Some((literal_start, literal_end, next_offset)) =
1814 js_string_literal_span(source, start, end)
1815 && trim_js_expression(source, next_offset, end).0 >= end
1816 {
1817 push_js_literal_selector_references(
1818 source,
1819 literal_start,
1820 literal_end,
1821 source.as_bytes().get(start).copied() == Some(b'`'),
1822 target_style_uri,
1823 references,
1824 );
1825 if source.as_bytes().get(start).copied() == Some(b'`') {
1826 collect_template_type_fact_targets(
1827 source,
1828 literal_start,
1829 literal_end,
1830 target_style_uri,
1831 type_fact_targets,
1832 );
1833 }
1834 return;
1835 }
1836 if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1837 && trim_js_expression(source, identifier_end, end).0 >= end
1838 {
1839 push_selector_reference(
1840 ParserByteSpanV0 { start, end },
1841 Some(identifier.to_string()),
1842 SourceSelectorReferenceMatchKindV0::Exact,
1843 target_style_uri,
1844 references,
1845 );
1846 }
1847}
1848
1849fn source_class_value_from_object_key(
1850 source: &str,
1851 start: usize,
1852 end: usize,
1853 local_class_values: &BTreeMap<String, SourceClassValue>,
1854) -> SourceClassValue {
1855 let (start, end) = trim_js_expression(source, start, end);
1856 if start >= end {
1857 return SourceClassValue::default();
1858 }
1859 if source.as_bytes().get(start) == Some(&b'[')
1860 && matching_js_block_end(source, start, b'[', b']') == Some(end - 1)
1861 {
1862 return source_class_value_from_js_expression(
1863 source,
1864 start + 1,
1865 end - 1,
1866 local_class_values,
1867 )
1868 .unwrap_or_default();
1869 }
1870 if let Some((literal_start, literal_end, next_offset)) =
1871 js_string_literal_span(source, start, end)
1872 && trim_js_expression(source, next_offset, end).0 >= end
1873 {
1874 return source_class_value_from_js_literal(
1875 source,
1876 literal_start,
1877 literal_end,
1878 source.as_bytes().get(start).copied() == Some(b'`'),
1879 );
1880 }
1881 if let Some((identifier, identifier_end)) = read_js_identifier(source, start)
1882 && trim_js_expression(source, identifier_end, end).0 >= end
1883 {
1884 let mut value = SourceClassValue::default();
1885 value.exact.push(identifier.to_string());
1886 return value;
1887 }
1888 SourceClassValue::default()
1889}
1890
1891fn object_property_name(source: &str, start: usize, end: usize) -> Option<String> {
1892 let (start, end) = trim_js_expression(source, start, end);
1893 if let Some((literal_start, literal_end, next_offset)) =
1894 js_string_literal_span(source, start, end)
1895 && trim_js_expression(source, next_offset, end).0 >= end
1896 {
1897 return source.get(literal_start..literal_end).map(str::to_string);
1898 }
1899 let (identifier, identifier_end) = read_js_identifier(source, start)?;
1900 (trim_js_expression(source, identifier_end, end).0 >= end).then(|| identifier.to_string())
1901}
1902
1903fn push_source_class_value_reference(
1904 byte_span: ParserByteSpanV0,
1905 value: SourceClassValue,
1906 target_style_uri: Option<&str>,
1907 references: &mut Vec<SourceSelectorReferenceFactV0>,
1908) {
1909 for selector_name in value.exact {
1910 push_selector_reference(
1911 byte_span,
1912 Some(selector_name),
1913 SourceSelectorReferenceMatchKindV0::Exact,
1914 target_style_uri,
1915 references,
1916 );
1917 }
1918 for prefix in value.prefixes {
1919 push_selector_reference(
1920 byte_span,
1921 Some(prefix),
1922 SourceSelectorReferenceMatchKindV0::Prefix,
1923 target_style_uri,
1924 references,
1925 );
1926 }
1927}
1928
1929fn collect_template_type_fact_targets(
1930 source: &str,
1931 literal_start: usize,
1932 literal_end: usize,
1933 target_style_uri: Option<&str>,
1934 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
1935) {
1936 let Some((prefix, expression_span, suffix)) =
1937 single_template_interpolation_projection(source, literal_start, literal_end)
1938 else {
1939 return;
1940 };
1941 let Some(path) = js_expression_path(source, expression_span.start, expression_span.end) else {
1942 return;
1943 };
1944 push_source_type_fact_target(
1945 expression_span,
1946 path.as_str(),
1947 target_style_uri,
1948 prefix.as_str(),
1949 suffix.as_str(),
1950 type_fact_targets,
1951 );
1952}
1953
1954fn single_template_interpolation_projection(
1955 source: &str,
1956 literal_start: usize,
1957 literal_end: usize,
1958) -> Option<(String, ParserByteSpanV0, String)> {
1959 let relative_open = source.get(literal_start..literal_end)?.find("${")?;
1960 let open = literal_start + relative_open;
1961 if source.get(open + 2..literal_end)?.contains("${") {
1962 return None;
1963 }
1964 let expression_start = open + 2;
1965 let close = matching_js_block_end(source, open + 1, b'{', b'}')?;
1966 if close > literal_end {
1967 return None;
1968 }
1969 let (expression_start, expression_end) = trim_js_expression(source, expression_start, close);
1970 if expression_start >= expression_end {
1971 return None;
1972 }
1973 let prefix_start = template_token_start(source, literal_start, open);
1974 let suffix_end = template_token_end(source, close + 1, literal_end);
1975 let prefix = source.get(prefix_start..open)?.to_string();
1976 let suffix = source.get(close + 1..suffix_end)?.to_string();
1977 if !prefix.chars().all(is_css_identifier_continue)
1978 || !suffix.chars().all(is_css_identifier_continue)
1979 {
1980 return None;
1981 }
1982 Some((
1983 prefix,
1984 ParserByteSpanV0 {
1985 start: expression_start,
1986 end: expression_end,
1987 },
1988 suffix,
1989 ))
1990}
1991
1992fn template_token_start(source: &str, literal_start: usize, prefix_end: usize) -> usize {
1993 source
1994 .get(literal_start..prefix_end)
1995 .and_then(|value| {
1996 value
1997 .char_indices()
1998 .rev()
1999 .find(|(_, ch)| ch.is_ascii_whitespace())
2000 .map(|(index, ch)| literal_start + index + ch.len_utf8())
2001 })
2002 .unwrap_or(literal_start)
2003}
2004
2005fn template_token_end(source: &str, suffix_start: usize, literal_end: usize) -> usize {
2006 source
2007 .get(suffix_start..literal_end)
2008 .and_then(|value| {
2009 value
2010 .char_indices()
2011 .find(|(_, ch)| ch.is_ascii_whitespace())
2012 .map(|(index, _)| suffix_start + index)
2013 })
2014 .unwrap_or(literal_end)
2015}
2016
2017fn push_source_type_fact_target(
2018 byte_span: ParserByteSpanV0,
2019 expression_path: &str,
2020 target_style_uri: Option<&str>,
2021 prefix: &str,
2022 suffix: &str,
2023 type_fact_targets: &mut Vec<SourceTypeFactTargetV0>,
2024) {
2025 type_fact_targets.push(SourceTypeFactTargetV0 {
2026 byte_span,
2027 expression_id: source_type_fact_expression_id(expression_path, byte_span),
2028 target_style_uri: target_style_uri.map(ToString::to_string),
2029 prefix: prefix.to_string(),
2030 suffix: suffix.to_string(),
2031 });
2032}
2033
2034fn source_type_fact_expression_id(expression_path: &str, byte_span: ParserByteSpanV0) -> String {
2035 format!(
2036 "omena-bridge-source-type-fact:{expression_path}:{}:{}",
2037 byte_span.start, byte_span.end
2038 )
2039}
2040
2041fn push_selector_reference(
2042 byte_span: ParserByteSpanV0,
2043 selector_name: Option<String>,
2044 match_kind: SourceSelectorReferenceMatchKindV0,
2045 target_style_uri: Option<&str>,
2046 references: &mut Vec<SourceSelectorReferenceFactV0>,
2047) {
2048 references.push(SourceSelectorReferenceFactV0 {
2049 byte_span,
2050 selector_name,
2051 match_kind,
2052 target_style_uri: target_style_uri.map(ToString::to_string),
2053 });
2054}
2055
2056fn push_js_literal_selector_references(
2057 source: &str,
2058 literal_start: usize,
2059 literal_end: usize,
2060 is_template: bool,
2061 target_style_uri: Option<&str>,
2062 references: &mut Vec<SourceSelectorReferenceFactV0>,
2063) {
2064 if is_template
2065 && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2066 {
2067 push_template_prefix_selector_references(
2068 source,
2069 literal_start,
2070 literal_start + relative_interpolation,
2071 target_style_uri,
2072 references,
2073 );
2074 return;
2075 }
2076
2077 push_string_literal_selector_references(
2078 source,
2079 ParserByteSpanV0 {
2080 start: literal_start,
2081 end: literal_end,
2082 },
2083 target_style_uri.map(ToString::to_string),
2084 references,
2085 );
2086}
2087
2088fn push_template_prefix_selector_references(
2089 source: &str,
2090 literal_start: usize,
2091 prefix_end: usize,
2092 target_style_uri: Option<&str>,
2093 references: &mut Vec<SourceSelectorReferenceFactV0>,
2094) {
2095 let spans = class_token_byte_spans(source, literal_start, prefix_end);
2096 let prefix_ends_with_space = source[..prefix_end]
2097 .chars()
2098 .last()
2099 .is_none_or(char::is_whitespace);
2100 for (index, span) in spans.iter().enumerate() {
2101 let is_open_prefix = index + 1 == spans.len() && !prefix_ends_with_space;
2102 push_selector_reference(
2103 *span,
2104 Some(source[span.start..span.end].to_string()),
2105 if is_open_prefix {
2106 SourceSelectorReferenceMatchKindV0::Prefix
2107 } else {
2108 SourceSelectorReferenceMatchKindV0::Exact
2109 },
2110 target_style_uri,
2111 references,
2112 );
2113 }
2114}
2115
2116fn push_template_prefix_value(
2117 source: &str,
2118 literal_start: usize,
2119 prefix_end: usize,
2120 value: &mut SourceClassValue,
2121) {
2122 let spans = class_token_byte_spans(source, literal_start, prefix_end);
2123 let prefix_ends_with_space = source[..prefix_end]
2124 .chars()
2125 .last()
2126 .is_none_or(char::is_whitespace);
2127 for (index, span) in spans.iter().enumerate() {
2128 let token = source[span.start..span.end].to_string();
2129 if index + 1 == spans.len() && !prefix_ends_with_space {
2130 value.prefixes.push(token);
2131 } else {
2132 value.exact.push(token);
2133 }
2134 }
2135}
2136
2137fn class_token_strings(source: &str, literal_start: usize, literal_end: usize) -> Vec<String> {
2138 class_token_byte_spans(source, literal_start, literal_end)
2139 .into_iter()
2140 .map(|span| source[span.start..span.end].to_string())
2141 .collect()
2142}
2143
2144fn push_string_literal_selector_references(
2145 source: &str,
2146 literal_span: ParserByteSpanV0,
2147 target_style_uri: Option<String>,
2148 references: &mut Vec<SourceSelectorReferenceFactV0>,
2149) {
2150 for span in class_token_byte_spans(source, literal_span.start, literal_span.end) {
2151 references.push(SourceSelectorReferenceFactV0 {
2152 byte_span: span,
2153 selector_name: None,
2154 match_kind: SourceSelectorReferenceMatchKindV0::Exact,
2155 target_style_uri: target_style_uri.clone(),
2156 });
2157 }
2158}
2159
2160fn trim_js_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2161 let mut start = char_boundary_ceil(source, start);
2162 let mut end = char_boundary_floor(source, end);
2163 start = skip_js_trivia_until(source, start, end);
2164 while end > start
2165 && source
2166 .as_bytes()
2167 .get(end - 1)
2168 .is_some_and(u8::is_ascii_whitespace)
2169 {
2170 end -= 1;
2171 }
2172 (start, end)
2173}
2174
2175fn char_boundary_floor(source: &str, index: usize) -> usize {
2176 let mut index = index.min(source.len());
2177 while index > 0 && !source.is_char_boundary(index) {
2178 index -= 1;
2179 }
2180 index
2181}
2182
2183fn char_boundary_ceil(source: &str, index: usize) -> usize {
2184 let mut index = index.min(source.len());
2185 while index < source.len() && !source.is_char_boundary(index) {
2186 index += 1;
2187 }
2188 index
2189}
2190
2191fn advance_js_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
2192 let cursor = char_boundary_ceil(source, cursor);
2193 let limit = char_boundary_floor(source, limit);
2194 if cursor >= limit {
2195 return limit;
2196 }
2197 char_boundary_ceil(source, cursor + 1).min(limit)
2198}
2199
2200fn advance_js_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
2201 let after_slash = advance_js_scan_cursor(source, slash_offset, limit);
2202 advance_js_scan_cursor(source, after_slash, limit)
2203}
2204
2205fn unwrap_js_parenthesized_expression(source: &str, start: usize, end: usize) -> (usize, usize) {
2206 let mut current_start = start;
2207 let mut current_end = end;
2208 loop {
2209 let (trimmed_start, trimmed_end) = trim_js_expression(source, current_start, current_end);
2210 if source.as_bytes().get(trimmed_start) == Some(&b'(')
2211 && matching_js_block_end(source, trimmed_start, b'(', b')')
2212 == Some(trimmed_end.saturating_sub(1))
2213 {
2214 current_start = trimmed_start + 1;
2215 current_end = trimmed_end - 1;
2216 continue;
2217 }
2218 return (trimmed_start, trimmed_end);
2219 }
2220}
2221
2222fn js_statement_expression_end(source: &str, start: usize) -> usize {
2223 let mut cursor = char_boundary_ceil(source, start);
2224 let mut depth = 0usize;
2225 while cursor < source.len() {
2226 match source.as_bytes().get(cursor).copied() {
2227 Some(b'\'' | b'"' | b'`') => {
2228 cursor =
2229 skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2230 }
2231 Some(b'(' | b'[' | b'{') => {
2232 depth += 1;
2233 cursor = advance_js_scan_cursor(source, cursor, source.len());
2234 }
2235 Some(b')' | b']' | b'}') => {
2236 depth = depth.saturating_sub(1);
2237 cursor = advance_js_scan_cursor(source, cursor, source.len());
2238 }
2239 Some(b';') if depth == 0 => return cursor,
2240 Some(b'\n') if depth == 0 => return cursor,
2241 Some(_) => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2242 None => break,
2243 }
2244 }
2245 source.len()
2246}
2247
2248fn matching_js_block_end(source: &str, open_offset: usize, open: u8, close: u8) -> Option<usize> {
2249 if source.as_bytes().get(open_offset) != Some(&open) {
2250 return None;
2251 }
2252 let mut cursor = advance_js_scan_cursor(source, open_offset, source.len());
2253 let mut depth = 1usize;
2254 while cursor < source.len() {
2255 match source.as_bytes().get(cursor).copied()? {
2256 b'\'' | b'"' | b'`' => {
2257 cursor = skip_js_string_literal(source, cursor, source.len())?;
2258 }
2259 byte if byte == open => {
2260 depth += 1;
2261 cursor = advance_js_scan_cursor(source, cursor, source.len());
2262 }
2263 byte if byte == close => {
2264 depth -= 1;
2265 if depth == 0 {
2266 return Some(cursor);
2267 }
2268 cursor = advance_js_scan_cursor(source, cursor, source.len());
2269 }
2270 _ => cursor = advance_js_scan_cursor(source, cursor, source.len()),
2271 }
2272 }
2273 None
2274}
2275
2276fn split_top_level_js_segments(
2277 source: &str,
2278 start: usize,
2279 end: usize,
2280 delimiter: u8,
2281) -> Vec<(usize, usize)> {
2282 let mut segments = Vec::new();
2283 let end = char_boundary_floor(source, end);
2284 let mut segment_start = char_boundary_ceil(source, start).min(end);
2285 let mut cursor = segment_start;
2286 let mut depth = 0usize;
2287 while cursor < end {
2288 match source.as_bytes().get(cursor).copied() {
2289 Some(b'\'' | b'"' | b'`') => {
2290 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2291 }
2292 Some(b'(' | b'[' | b'{') => {
2293 depth += 1;
2294 cursor = advance_js_scan_cursor(source, cursor, end);
2295 }
2296 Some(b')' | b']' | b'}') => {
2297 depth = depth.saturating_sub(1);
2298 cursor = advance_js_scan_cursor(source, cursor, end);
2299 }
2300 Some(byte) if byte == delimiter && depth == 0 => {
2301 segments.push((segment_start, cursor));
2302 cursor = advance_js_scan_cursor(source, cursor, end);
2303 segment_start = cursor;
2304 }
2305 Some(_) => cursor = advance_js_scan_cursor(source, cursor, end),
2306 None => break,
2307 }
2308 }
2309 if segment_start <= end {
2310 segments.push((segment_start, end));
2311 }
2312 segments
2313}
2314
2315fn find_top_level_js_byte(source: &str, start: usize, end: usize, needle: u8) -> Option<usize> {
2316 let end = char_boundary_floor(source, end);
2317 let mut cursor = char_boundary_ceil(source, start).min(end);
2318 let mut depth = 0usize;
2319 while cursor < end {
2320 match source.as_bytes().get(cursor).copied()? {
2321 b'\'' | b'"' | b'`' => {
2322 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2323 }
2324 b'(' | b'[' | b'{' => {
2325 depth += 1;
2326 cursor = advance_js_scan_cursor(source, cursor, end);
2327 }
2328 b')' | b']' | b'}' => {
2329 depth = depth.saturating_sub(1);
2330 cursor = advance_js_scan_cursor(source, cursor, end);
2331 }
2332 byte if byte == needle && depth == 0 => return Some(cursor),
2333 _ => cursor = advance_js_scan_cursor(source, cursor, end),
2334 }
2335 }
2336 None
2337}
2338
2339fn find_top_level_js_operator(
2340 source: &str,
2341 start: usize,
2342 end: usize,
2343 operator: &str,
2344) -> Option<usize> {
2345 let end = char_boundary_floor(source, end);
2346 let mut cursor = char_boundary_ceil(source, start).min(end);
2347 let mut depth = 0usize;
2348 while cursor < end {
2349 match source.as_bytes().get(cursor).copied()? {
2350 b'\'' | b'"' | b'`' => {
2351 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2352 }
2353 b'(' | b'[' | b'{' => {
2354 depth += 1;
2355 cursor = advance_js_scan_cursor(source, cursor, end);
2356 }
2357 b')' | b']' | b'}' => {
2358 depth = depth.saturating_sub(1);
2359 cursor = advance_js_scan_cursor(source, cursor, end);
2360 }
2361 _ if depth == 0
2362 && source
2363 .get(cursor..end)
2364 .is_some_and(|rest| rest.starts_with(operator)) =>
2365 {
2366 return Some(cursor);
2367 }
2368 _ => cursor = advance_js_scan_cursor(source, cursor, end),
2369 }
2370 }
2371 None
2372}
2373
2374fn top_level_conditional_parts(
2375 source: &str,
2376 start: usize,
2377 end: usize,
2378) -> Option<(usize, usize, usize, usize, usize)> {
2379 let question = find_top_level_js_byte(source, start, end, b'?')?;
2380 let end = char_boundary_floor(source, end);
2381 let mut cursor = advance_js_scan_cursor(source, question, end);
2382 let mut depth = 0usize;
2383 let mut nested_conditional_depth = 0usize;
2384 while cursor < end {
2385 match source.as_bytes().get(cursor).copied()? {
2386 b'\'' | b'"' | b'`' => {
2387 cursor = skip_js_string_literal(source, cursor, end).unwrap_or(end);
2388 }
2389 b'(' | b'[' | b'{' => {
2390 depth += 1;
2391 cursor = advance_js_scan_cursor(source, cursor, end);
2392 }
2393 b')' | b']' | b'}' => {
2394 depth = depth.saturating_sub(1);
2395 cursor = advance_js_scan_cursor(source, cursor, end);
2396 }
2397 b'?' if depth == 0 => {
2398 nested_conditional_depth += 1;
2399 cursor = advance_js_scan_cursor(source, cursor, end);
2400 }
2401 b':' if depth == 0 && nested_conditional_depth == 0 => {
2402 return Some((
2403 question,
2404 advance_js_scan_cursor(source, question, end),
2405 cursor,
2406 advance_js_scan_cursor(source, cursor, end),
2407 end,
2408 ));
2409 }
2410 b':' if depth == 0 => {
2411 nested_conditional_depth = nested_conditional_depth.saturating_sub(1);
2412 cursor = advance_js_scan_cursor(source, cursor, end);
2413 }
2414 _ => cursor = advance_js_scan_cursor(source, cursor, end),
2415 }
2416 }
2417 None
2418}
2419
2420fn js_expression_path(source: &str, start: usize, end: usize) -> Option<String> {
2421 let (start, end) = trim_js_expression(source, start, end);
2422 let (first, mut cursor) = read_js_identifier(source, start)?;
2423 let mut path = vec![first.to_string()];
2424 loop {
2425 cursor = skip_js_trivia_until(source, cursor, end);
2426 match source.as_bytes().get(cursor).copied() {
2427 Some(b'.') => {
2428 let member_start = skip_js_trivia_until(source, cursor + 1, end);
2429 let (member, member_end) = read_js_identifier(source, member_start)?;
2430 path.push(member.to_string());
2431 cursor = member_end;
2432 }
2433 Some(b'[') => {
2434 if let Some((literal_start, literal_end, bracket_end)) =
2435 bracket_string_literal_access(source, cursor)
2436 && bracket_end <= end
2437 {
2438 path.push(source[literal_start..literal_end].to_string());
2439 cursor = bracket_end;
2440 } else {
2441 return None;
2442 }
2443 }
2444 _ => break,
2445 }
2446 }
2447 (trim_js_expression(source, cursor, end).0 >= end).then(|| path.join("."))
2448}
2449
2450fn static_string_prefix_for_js_expression(
2451 source: &str,
2452 start: usize,
2453 end: usize,
2454 local_class_values: &BTreeMap<String, SourceClassValue>,
2455) -> Option<String> {
2456 let (start, end) = trim_js_expression(source, start, end);
2457 let (start, end) = unwrap_js_parenthesized_expression(source, start, end);
2458 if let Some((literal_start, literal_end, next_offset)) =
2459 js_string_literal_span(source, start, end)
2460 && trim_js_expression(source, next_offset, end).0 >= end
2461 {
2462 if source.as_bytes().get(start).copied() == Some(b'`')
2463 && let Some(relative_interpolation) = source[literal_start..literal_end].find("${")
2464 {
2465 return Some(source[literal_start..literal_start + relative_interpolation].to_string());
2466 }
2467 return Some(source[literal_start..literal_end].to_string());
2468 }
2469 if let Some(path) = js_expression_path(source, start, end)
2470 && let Some(value) = local_class_values.get(path.as_str())
2471 {
2472 if value.exact.len() == 1 && value.prefixes.is_empty() {
2473 return value.exact.first().cloned();
2474 }
2475 if value.prefixes.len() == 1 && value.exact.is_empty() {
2476 return value.prefixes.first().cloned();
2477 }
2478 }
2479 if let Some(plus_offset) = find_top_level_js_operator(source, start, end, "+") {
2480 let left =
2481 static_string_prefix_for_js_expression(source, start, plus_offset, local_class_values)?;
2482 let right = static_string_prefix_for_js_expression(
2483 source,
2484 plus_offset + 1,
2485 end,
2486 local_class_values,
2487 )
2488 .unwrap_or_default();
2489 return Some(format!("{left}{right}"));
2490 }
2491 None
2492}
2493
2494fn js_call_end(source: &str, open_paren: usize) -> Option<usize> {
2495 if source.as_bytes().get(open_paren) != Some(&b'(') {
2496 return None;
2497 }
2498 let mut cursor = advance_js_scan_cursor(source, open_paren, source.len());
2499 let mut depth = 1usize;
2500 while cursor < source.len() {
2501 match source.as_bytes().get(cursor).copied()? {
2502 b'\'' | b'"' | b'`' => {
2503 cursor = skip_js_string_literal(source, cursor, source.len())?;
2504 }
2505 b'(' => {
2506 depth += 1;
2507 cursor = advance_js_scan_cursor(source, cursor, source.len());
2508 }
2509 b')' => {
2510 depth -= 1;
2511 if depth == 0 {
2512 return Some(cursor);
2513 }
2514 cursor = advance_js_scan_cursor(source, cursor, source.len());
2515 }
2516 _ => {
2517 cursor = advance_js_scan_cursor(source, cursor, source.len());
2518 }
2519 }
2520 }
2521 None
2522}
2523
2524fn class_token_byte_spans(
2525 source: &str,
2526 literal_start: usize,
2527 literal_end: usize,
2528) -> Vec<ParserByteSpanV0> {
2529 let mut spans = Vec::new();
2530 let mut token_start: Option<usize> = None;
2531 for (relative_index, ch) in source[literal_start..literal_end].char_indices() {
2532 let index = literal_start + relative_index;
2533 if ch.is_ascii_whitespace() {
2534 if let Some(start) = token_start.take() {
2535 push_class_token_span(source, start, index, &mut spans);
2536 }
2537 } else if token_start.is_none() {
2538 token_start = Some(index);
2539 }
2540 }
2541 if let Some(start) = token_start {
2542 push_class_token_span(source, start, literal_end, &mut spans);
2543 }
2544 spans
2545}
2546
2547fn push_class_token_span(
2548 source: &str,
2549 start: usize,
2550 end: usize,
2551 spans: &mut Vec<ParserByteSpanV0>,
2552) {
2553 if start < end && source[start..end].chars().all(is_css_identifier_continue) {
2554 spans.push(ParserByteSpanV0 { start, end });
2555 }
2556}
2557
2558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2559struct CodeIdentifier<'a> {
2560 text: &'a str,
2561 end: usize,
2562}
2563
2564fn next_code_identifier(source: &str, mut cursor: usize) -> Option<CodeIdentifier<'_>> {
2565 while cursor < source.len() {
2566 cursor = skip_js_trivia(source, cursor);
2567 let byte = source.as_bytes().get(cursor).copied()?;
2568 if matches!(byte, b'\'' | b'"' | b'`') {
2569 cursor = skip_js_string_literal(source, cursor, source.len()).unwrap_or(source.len());
2570 continue;
2571 }
2572 if byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$') {
2573 let (text, end) = read_js_identifier(source, cursor)?;
2574 return Some(CodeIdentifier { text, end });
2575 }
2576 cursor = advance_js_scan_cursor(source, cursor, source.len());
2577 }
2578 None
2579}
2580
2581fn skip_js_trivia(source: &str, cursor: usize) -> usize {
2582 skip_js_trivia_until(source, cursor, source.len())
2583}
2584
2585fn skip_js_trivia_until(source: &str, mut cursor: usize, limit: usize) -> usize {
2586 loop {
2587 cursor = skip_ascii_whitespace_until(source, cursor, limit);
2588 if source.as_bytes().get(cursor) == Some(&b'/') {
2589 match source.as_bytes().get(cursor + 1).copied() {
2590 Some(b'/') => {
2591 cursor = skip_js_line_comment(source, cursor + 2, limit);
2592 continue;
2593 }
2594 Some(b'*') => {
2595 cursor = skip_js_block_comment(source, cursor + 2, limit);
2596 continue;
2597 }
2598 _ => {}
2599 }
2600 }
2601 return cursor;
2602 }
2603}
2604
2605fn skip_ascii_whitespace_until(source: &str, mut offset: usize, limit: usize) -> usize {
2606 while offset < limit
2607 && source
2608 .as_bytes()
2609 .get(offset)
2610 .is_some_and(u8::is_ascii_whitespace)
2611 {
2612 offset += 1;
2613 }
2614 offset
2615}
2616
2617fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
2618 while source
2619 .as_bytes()
2620 .get(offset)
2621 .is_some_and(u8::is_ascii_whitespace)
2622 {
2623 offset += 1;
2624 }
2625 offset
2626}
2627
2628fn skip_js_line_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2629 let limit = char_boundary_floor(source, limit);
2630 while cursor < limit {
2631 if source.as_bytes().get(cursor) == Some(&b'\n') {
2632 return advance_js_scan_cursor(source, cursor, limit);
2633 }
2634 cursor = advance_js_scan_cursor(source, cursor, limit);
2635 }
2636 limit
2637}
2638
2639fn skip_js_block_comment(source: &str, mut cursor: usize, limit: usize) -> usize {
2640 let limit = char_boundary_floor(source, limit);
2641 while cursor + 1 < limit {
2642 if source.as_bytes().get(cursor) == Some(&b'*')
2643 && source.as_bytes().get(cursor + 1) == Some(&b'/')
2644 {
2645 return cursor + 2;
2646 }
2647 cursor = advance_js_scan_cursor(source, cursor, limit);
2648 }
2649 limit
2650}
2651
2652fn js_string_literal_span(
2653 source: &str,
2654 quote_offset: usize,
2655 limit: usize,
2656) -> Option<(usize, usize, usize)> {
2657 let quote = source.as_bytes().get(quote_offset).copied()?;
2658 if !matches!(quote, b'\'' | b'"' | b'`') {
2659 return None;
2660 }
2661 let literal_start = quote_offset + 1;
2662 let next_offset = skip_js_string_literal(source, quote_offset, limit)?;
2663 Some((literal_start, next_offset - 1, next_offset))
2664}
2665
2666fn skip_js_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
2667 let quote = source.as_bytes().get(quote_offset).copied()?;
2668 let limit = char_boundary_floor(source, limit);
2669 let mut cursor = quote_offset + 1;
2670 while cursor < limit {
2671 let byte = source.as_bytes().get(cursor).copied()?;
2672 if byte == b'\\' {
2673 cursor = advance_js_escaped_char(source, cursor, limit);
2674 continue;
2675 }
2676 if byte == quote {
2677 return Some(cursor + 1);
2678 }
2679 cursor = advance_js_scan_cursor(source, cursor, limit);
2680 }
2681 None
2682}
2683
2684fn bracket_string_literal_access(
2685 source: &str,
2686 bracket_offset: usize,
2687) -> Option<(usize, usize, usize)> {
2688 if source.as_bytes().get(bracket_offset) != Some(&b'[') {
2689 return None;
2690 }
2691 let quote_offset = skip_ascii_whitespace(source, bracket_offset + 1);
2692 let quote = source.as_bytes().get(quote_offset).copied()?;
2693 if !matches!(quote, b'\'' | b'"') {
2694 return None;
2695 }
2696 let (literal_start, literal_end, literal_next) =
2697 js_string_literal_span(source, quote_offset, source.len())?;
2698 if literal_next > source.len() {
2699 return None;
2700 }
2701 let closing_bracket = skip_ascii_whitespace(source, literal_end + 1);
2702 if source.as_bytes().get(closing_bracket) != Some(&b']') {
2703 return None;
2704 }
2705 Some((literal_start, literal_end, closing_bracket + 1))
2706}
2707
2708fn read_js_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
2709 let start = char_boundary_ceil(source, start);
2710 let first = source.get(start..)?.chars().next()?;
2711 if !is_js_identifier_start(first) {
2712 return None;
2713 }
2714 let mut end = start + first.len_utf8();
2715 let scan_start = end;
2716 for (relative_index, ch) in source.get(scan_start..)?.char_indices() {
2717 if !is_js_identifier_continue(ch) {
2718 break;
2719 }
2720 end = scan_start + relative_index + ch.len_utf8();
2721 }
2722 Some((&source[start..end], end))
2723}
2724
2725fn is_js_identifier_start(ch: char) -> bool {
2726 ch.is_ascii_alphabetic() || matches!(ch, '_' | '$')
2727}
2728
2729fn is_js_identifier_continue(ch: char) -> bool {
2730 ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')
2731}
2732
2733fn is_css_identifier_continue(ch: char) -> bool {
2734 ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
2735}
2736
2737#[cfg(test)]
2738mod tests;