1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use oxc_allocator::Allocator;
5use oxc_ast::ast::{
6 Argument, AssignmentTarget, BindingPatternKind, ChainElement, ClassElement, Declaration,
7 ExportDefaultDeclarationKind, Expression, ImportDeclarationSpecifier, JSXAttributeItem,
8 JSXAttributeName, JSXAttributeValue, JSXChild, JSXElementName, JSXExpression, JSXFragment,
9 JSXMemberExpression, JSXMemberExpressionObject, ModuleExportName, ObjectPropertyKind, Program,
10 PropertyKey, PropertyKind, SimpleAssignmentTarget, Statement,
11};
12use oxc_ast_visit::{walk, Visit};
13use oxc_diagnostics::Severity as OxcSeverity;
14use oxc_estree::{CompactTSSerializer, ESTree};
15use oxc_parser::Parser;
16use oxc_span::{GetSpan, SourceType, Span};
17
18use crate::model::{
19 ParseDiagnostic, ParseLabel, ParseSeverity, ParsedArithmeticExpression,
20 ParsedArithmeticExpressionKind, ParsedArithmeticOperator, ParsedCallArgument,
21 ParsedCallExpression, ParsedClass, ParsedClassHeritage, ParsedComparisonOperator,
22 ParsedComputedExpression, ParsedComputedExpressionKind, ParsedConstantExpression,
23 ParsedConstantExpressionKind, ParsedDecorator, ParsedEffectBody, ParsedEffectCleanup,
24 ParsedEffectExpression, ParsedEffectExpressionKind, ParsedEffectStatement,
25 ParsedEffectStatementKind, ParsedEventHandler, ParsedExport, ParsedExportKind,
26 ParsedExportSpecifier, ParsedFile, ParsedImport, ParsedImportSpecifier, ParsedInitializerCall,
27 ParsedInlineHandler, ParsedJsxAttribute, ParsedJsxAttributeValue, ParsedJsxChild,
28 ParsedJsxConditional, ParsedJsxElement, ParsedJsxFragment, ParsedJsxList, ParsedJsxNode,
29 ParsedLocalVariable, ParsedLogicalOperator, ParsedMethod, ParsedMethodCall,
30 ParsedMethodParameter, ParsedProperty, ParsedSerializableValue, ParsedSourceAst,
31 ParsedStateOperation, ParsedStateUpdate, ParsedStaticMemberDesignator,
32 ParsedThisMemberDesignator, ParsedTypeAlias, ParsedTypeAnnotation, ParsedUnaryOperator,
33 ParsedUnsupportedEffectStatementKind, ParsedValidationRuleArgument,
34 ParsedValidationRuleArgumentKind, ParsedValidationRuleExpression,
35 ParsedValidationRuleExpressionKind, SourceSpan,
36};
37
38pub fn parse_file(path: impl AsRef<Path>, source: &str) -> ParsedFile {
39 let path = path.as_ref();
40 let source_type = SourceType::from_path(path)
41 .unwrap_or_default()
42 .with_typescript(true)
43 .with_jsx(true);
44
45 let allocator = Allocator::default();
46 let ret = Parser::new(&allocator, source, source_type).parse();
47 let syntax = parse_source_ast(&ret.program, source);
48 let call_expressions = collect_call_expressions(&ret.program, source);
49
50 let ParsedProgramFacts {
51 classes,
52 type_aliases,
53 local_type_bindings,
54 local_value_bindings,
55 imports,
56 exports,
57 } = parse_program(&ret.program, source);
58 let diagnostics = ret
59 .errors
60 .iter()
61 .map(|diagnostic| parse_diagnostic(source, diagnostic))
62 .collect::<Vec<_>>();
63
64 ParsedFile {
65 path: PathBuf::from(path),
66 syntax,
67 classes,
68 type_aliases,
69 local_type_bindings,
70 local_value_bindings,
71 imports,
72 exports,
73 call_expressions,
74 diagnostics,
75 }
76}
77
78fn collect_call_expressions(program: &Program<'_>, source: &str) -> Vec<ParsedCallExpression> {
79 struct Collector<'a> {
80 source: &'a str,
81 calls: Vec<ParsedCallExpression>,
82 }
83
84 impl<'a> Visit<'a> for Collector<'a> {
85 fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
86 let (member_object_span, member_property_span) = match &call.callee {
87 Expression::StaticMemberExpression(member) => (
88 Some(source_span(self.source, member.object.span())),
89 Some(source_span(self.source, member.property.span)),
90 ),
91 _ => (None, None),
92 };
93 self.calls.push(ParsedCallExpression {
94 callee_span: source_span(self.source, call.callee.span()),
95 member_object_span,
96 member_property_span,
97 span: source_span(self.source, call.span),
98 arguments: call
99 .arguments
100 .iter()
101 .map(|argument| match argument {
102 Argument::StringLiteral(value) => ParsedCallArgument::StringLiteral {
103 value: value.value.to_string(),
104 span: source_span(self.source, value.span),
105 },
106 _ => ParsedCallArgument::Other {
107 span: source_span(self.source, argument.span()),
108 },
109 })
110 .collect(),
111 });
112 walk::walk_call_expression(self, call);
113 }
114 }
115
116 let mut collector = Collector {
117 source,
118 calls: Vec::new(),
119 };
120 collector.visit_program(program);
121 collector
122 .calls
123 .sort_by_key(|call| (call.span.start, call.span.end));
124 collector.calls
125}
126
127fn parse_source_ast(program: &Program<'_>, source: &str) -> ParsedSourceAst {
128 let mut serializer = CompactTSSerializer::new(true);
129 program.serialize(&mut serializer);
130 ParsedSourceAst {
131 source: source.to_string(),
132 estree_json: serializer.into_string(),
133 span: source_span(source, program.span),
134 }
135}
136
137struct ParsedProgramFacts {
138 classes: Vec<ParsedClass>,
139 type_aliases: Vec<ParsedTypeAlias>,
140 local_type_bindings: Vec<String>,
141 local_value_bindings: Vec<String>,
142 imports: Vec<ParsedImport>,
143 exports: Vec<ParsedExport>,
144}
145
146fn parse_program(program: &Program<'_>, source: &str) -> ParsedProgramFacts {
147 let mut classes = Vec::new();
148 let mut type_aliases = Vec::new();
149 let mut local_type_bindings = Vec::new();
150 let mut local_value_bindings = Vec::new();
151 let mut imports = Vec::new();
152 let mut exports = Vec::new();
153
154 for statement in &program.body {
155 match statement {
156 Statement::ImportDeclaration(declaration) => {
157 imports.push(parse_import_declaration(declaration, source));
158 continue;
159 }
160 Statement::ExportNamedDeclaration(declaration) => {
161 exports.push(parse_named_export_declaration(declaration, source));
162 if let Some(declaration) = &declaration.declaration {
163 retain_local_type_binding(declaration, &mut local_type_bindings);
164 retain_local_value_binding(declaration, &mut local_value_bindings);
165 if let Some(class) = parse_declaration(declaration, source) {
166 classes.push(class);
167 }
168 if let Some(alias) = parse_type_alias_declaration(declaration, source) {
169 type_aliases.push(alias);
170 }
171 }
172 continue;
173 }
174 Statement::ExportDefaultDeclaration(declaration) => {
175 exports.push(parse_default_export_declaration(declaration, source));
176 if let ExportDefaultDeclarationKind::ClassDeclaration(class) =
177 &declaration.declaration
178 {
179 if let Some(class) = parse_class(class, source) {
180 local_type_bindings.push(class.name.clone());
181 classes.push(class);
182 }
183 }
184 continue;
185 }
186 Statement::ExportAllDeclaration(declaration) => {
187 exports.push(ParsedExport {
188 kind: ParsedExportKind::All,
189 source: Some(declaration.source.value.to_string()),
190 specifiers: declaration
191 .exported
192 .as_ref()
193 .map(|exported| {
194 vec![ParsedExportSpecifier {
195 local: None,
196 exported: module_export_name(exported),
197 }]
198 })
199 .unwrap_or_default(),
200 span: source_span(source, declaration.span),
201 });
202 continue;
203 }
204 _ => {}
205 }
206
207 if let Some(declaration) = statement.as_declaration() {
208 retain_local_type_binding(declaration, &mut local_type_bindings);
209 retain_local_value_binding(declaration, &mut local_value_bindings);
210 if let Some(class) = parse_declaration(declaration, source) {
211 classes.push(class);
212 }
213 if let Some(alias) = parse_type_alias_declaration(declaration, source) {
214 type_aliases.push(alias);
215 }
216 }
217 }
218
219 local_type_bindings.sort();
220 local_type_bindings.dedup();
221 local_value_bindings.sort();
222 local_value_bindings.dedup();
223 ParsedProgramFacts {
224 classes,
225 type_aliases,
226 local_type_bindings,
227 local_value_bindings,
228 imports,
229 exports,
230 }
231}
232
233fn retain_local_value_binding(declaration: &Declaration<'_>, bindings: &mut Vec<String>) {
234 if let Declaration::FunctionDeclaration(function) = declaration {
235 if let Some(id) = &function.id {
236 bindings.push(id.name.to_string());
237 }
238 }
239}
240
241fn retain_local_type_binding(declaration: &Declaration<'_>, bindings: &mut Vec<String>) {
242 let name = match declaration {
243 Declaration::ClassDeclaration(class) => class.id.as_ref().map(|id| id.name.as_str()),
244 Declaration::TSTypeAliasDeclaration(alias) => Some(alias.id.name.as_str()),
245 Declaration::TSInterfaceDeclaration(interface) => Some(interface.id.name.as_str()),
246 Declaration::TSEnumDeclaration(r#enum) => Some(r#enum.id.name.as_str()),
247 Declaration::TSImportEqualsDeclaration(import) => Some(import.id.name.as_str()),
248 Declaration::VariableDeclaration(_)
249 | Declaration::FunctionDeclaration(_)
250 | Declaration::TSModuleDeclaration(_) => None,
251 };
252 if let Some(name) = name {
253 bindings.push(name.to_string());
254 }
255}
256
257fn parse_type_alias_declaration(
258 declaration: &Declaration<'_>,
259 source: &str,
260) -> Option<ParsedTypeAlias> {
261 let Declaration::TSTypeAliasDeclaration(alias) = declaration else {
262 return None;
263 };
264 let type_span = source_span(source, alias.type_annotation.span());
265
266 Some(ParsedTypeAlias {
267 name: alias.id.name.to_string(),
268 type_text: source[type_span.start..type_span.end].trim().to_string(),
269 span: source_span(source, alias.span),
270 type_span,
271 })
272}
273
274fn parse_import_declaration(
275 declaration: &oxc_ast::ast::ImportDeclaration<'_>,
276 source: &str,
277) -> ParsedImport {
278 let specifiers = declaration
279 .specifiers
280 .as_ref()
281 .map(|specifiers| {
282 specifiers
283 .iter()
284 .map(|specifier| match specifier {
285 ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
286 ParsedImportSpecifier {
287 imported: module_export_name(&specifier.imported),
288 local: specifier.local.name.to_string(),
289 local_span: source_span(source, specifier.local.span),
290 }
291 }
292 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
293 ParsedImportSpecifier {
294 imported: "default".to_string(),
295 local: specifier.local.name.to_string(),
296 local_span: source_span(source, specifier.local.span),
297 }
298 }
299 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
300 ParsedImportSpecifier {
301 imported: "*".to_string(),
302 local: specifier.local.name.to_string(),
303 local_span: source_span(source, specifier.local.span),
304 }
305 }
306 })
307 .collect()
308 })
309 .unwrap_or_default();
310
311 ParsedImport {
312 source: declaration.source.value.to_string(),
313 specifiers,
314 span: source_span(source, declaration.span),
315 }
316}
317
318fn parse_named_export_declaration(
319 declaration: &oxc_ast::ast::ExportNamedDeclaration<'_>,
320 source: &str,
321) -> ParsedExport {
322 let mut specifiers = declaration
323 .specifiers
324 .iter()
325 .map(|specifier| ParsedExportSpecifier {
326 local: Some(module_export_name(&specifier.local)),
327 exported: module_export_name(&specifier.exported),
328 })
329 .collect::<Vec<_>>();
330
331 if let Some(declaration) = &declaration.declaration {
332 specifiers.extend(named_declaration_exports(declaration));
333 }
334
335 ParsedExport {
336 kind: ParsedExportKind::Named,
337 source: declaration
338 .source
339 .as_ref()
340 .map(|source| source.value.to_string()),
341 specifiers,
342 span: source_span(source, declaration.span),
343 }
344}
345
346fn parse_default_export_declaration(
347 declaration: &oxc_ast::ast::ExportDefaultDeclaration<'_>,
348 source: &str,
349) -> ParsedExport {
350 ParsedExport {
351 kind: ParsedExportKind::Default,
352 source: None,
353 specifiers: vec![ParsedExportSpecifier {
354 local: default_declaration_name(&declaration.declaration),
355 exported: "default".to_string(),
356 }],
357 span: source_span(source, declaration.span),
358 }
359}
360
361fn named_declaration_exports(declaration: &Declaration<'_>) -> Vec<ParsedExportSpecifier> {
362 let name = match declaration {
363 Declaration::ClassDeclaration(class) => class.id.as_ref().map(|id| id.name.to_string()),
364 Declaration::FunctionDeclaration(function) => {
365 function.id.as_ref().map(|id| id.name.to_string())
366 }
367 Declaration::TSTypeAliasDeclaration(alias) => Some(alias.id.name.to_string()),
368 Declaration::VariableDeclaration(declaration) => {
369 let names = declaration
370 .declarations
371 .iter()
372 .filter_map(|declarator| binding_identifier_name(&declarator.id.kind))
373 .collect::<Vec<_>>();
374
375 return names
376 .into_iter()
377 .map(|name| ParsedExportSpecifier {
378 local: Some(name.clone()),
379 exported: name,
380 })
381 .collect();
382 }
383 _ => None,
384 };
385
386 name.map(|name| {
387 vec![ParsedExportSpecifier {
388 local: Some(name.clone()),
389 exported: name,
390 }]
391 })
392 .unwrap_or_default()
393}
394
395fn default_declaration_name(declaration: &ExportDefaultDeclarationKind<'_>) -> Option<String> {
396 match declaration {
397 ExportDefaultDeclarationKind::ClassDeclaration(class) => {
398 class.id.as_ref().map(|id| id.name.to_string())
399 }
400 ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
401 function.id.as_ref().map(|id| id.name.to_string())
402 }
403 _ => None,
404 }
405}
406
407fn module_export_name(name: &ModuleExportName<'_>) -> String {
408 match name {
409 ModuleExportName::IdentifierName(name) => name.name.to_string(),
410 ModuleExportName::IdentifierReference(name) => name.name.to_string(),
411 ModuleExportName::StringLiteral(name) => name.value.to_string(),
412 }
413}
414
415fn parse_declaration(declaration: &Declaration<'_>, source: &str) -> Option<ParsedClass> {
416 let Declaration::ClassDeclaration(class) = declaration else {
417 return None;
418 };
419
420 parse_class(class, source)
421}
422
423fn parse_class(class: &oxc_ast::ast::Class<'_>, source: &str) -> Option<ParsedClass> {
424 let name = class
425 .id
426 .as_ref()
427 .map(|id| id.name.to_string())
428 .unwrap_or_else(|| "<anonymous>".to_string());
429
430 let decorators = normalized_decorators(
431 class
432 .decorators
433 .iter()
434 .filter_map(|decorator| parse_decorator(decorator, source))
435 .collect::<Vec<_>>(),
436 );
437
438 let mut properties = Vec::new();
439 let mut methods = Vec::new();
440
441 for element in &class.body.body {
442 match element {
443 ClassElement::PropertyDefinition(property) => {
444 if let Some(property) = parse_property(property, source) {
445 properties.push(property);
446 }
447 }
448 ClassElement::MethodDefinition(method) => {
449 if let Some(method) = parse_method(method, source) {
450 methods.push(method);
451 }
452 }
453 _ => {}
454 }
455 }
456
457 Some(ParsedClass {
458 name,
459 span: source_span(source, class.span),
460 heritage: class.super_class.as_ref().map(|base| {
461 let span = base.span();
462 ParsedClassHeritage {
463 base: source
464 .get(span.start as usize..span.end as usize)
465 .unwrap_or_default()
466 .trim()
467 .to_string(),
468 span: source_span(source, span),
469 }
470 }),
471 decorators,
472 properties,
473 methods,
474 })
475}
476
477fn parse_decorator(
478 decorator: &oxc_ast::ast::Decorator<'_>,
479 source: &str,
480) -> Option<ParsedDecorator> {
481 match &decorator.expression {
482 Expression::CallExpression(call) => {
483 let Expression::Identifier(callee) = &call.callee else {
484 return None;
485 };
486 Some(ParsedDecorator {
487 name: callee.name.to_string(),
488 is_invoked: true,
489 arguments: call.arguments.iter().map(argument_string_value).collect(),
490 argument: call.arguments.first().and_then(argument_string_value),
491 argument_count: call.arguments.len(),
492 argument_spans: call
493 .arguments
494 .iter()
495 .map(|argument| source_span(source, argument.span()))
496 .collect(),
497 static_member_argument: call
498 .arguments
499 .first()
500 .and_then(|argument| parsed_static_member_designator(argument, source)),
501 this_member_argument: call
502 .arguments
503 .first()
504 .and_then(|argument| parsed_this_member_designator(argument, source)),
505 validation_rule_expression: (callee.name == "validate")
506 .then(|| call.arguments.first()?.as_expression())
507 .flatten()
508 .map(|expression| parsed_validation_rule_expression(expression, source)),
509 span: source_span(source, decorator.span),
510 })
511 }
512 Expression::Identifier(identifier) => Some(ParsedDecorator {
513 name: identifier.name.to_string(),
514 is_invoked: false,
515 arguments: Vec::new(),
516 argument: None,
517 argument_count: 0,
518 argument_spans: Vec::new(),
519 static_member_argument: None,
520 this_member_argument: None,
521 validation_rule_expression: None,
522 span: source_span(source, decorator.span),
523 }),
524 _ => None,
525 }
526}
527
528fn normalized_decorators(mut decorators: Vec<ParsedDecorator>) -> Vec<ParsedDecorator> {
529 if !decorators.iter().any(|decorator| {
530 matches!(
531 decorator.name.as_str(),
532 "form" | "field" | "validate" | "submit" | "serialize"
533 )
534 }) {
535 decorators.retain(|decorator| decorator.is_invoked);
536 }
537 decorators
538}
539
540fn parsed_validation_rule_expression(
541 expression: &Expression<'_>,
542 source: &str,
543) -> ParsedValidationRuleExpression {
544 let kind = match expression {
545 Expression::CallExpression(call) => ParsedValidationRuleExpressionKind::Call {
546 callee: match &call.callee {
547 Expression::Identifier(identifier) => Some(identifier.name.to_string()),
548 _ => None,
549 },
550 arguments: call
551 .arguments
552 .iter()
553 .map(|argument| parsed_validation_rule_argument(argument, source))
554 .collect(),
555 },
556 Expression::Identifier(identifier) => {
557 ParsedValidationRuleExpressionKind::Identifier(identifier.name.to_string())
558 }
559 _ => ParsedValidationRuleExpressionKind::Unsupported,
560 };
561 ParsedValidationRuleExpression {
562 kind,
563 span: source_span(source, expression.span()),
564 }
565}
566
567fn parsed_validation_rule_argument(
568 argument: &Argument<'_>,
569 source: &str,
570) -> ParsedValidationRuleArgument {
571 let span = source_span(source, argument.span());
572 let kind = argument.as_expression().map_or(
573 ParsedValidationRuleArgumentKind::Unsupported,
574 |expression| {
575 if let Expression::StringLiteral(literal) = expression {
576 return ParsedValidationRuleArgumentKind::StringLiteral(literal.value.to_string());
577 }
578 if let Some(designator) = parsed_this_member_expression(expression, source) {
579 return ParsedValidationRuleArgumentKind::ThisMember(designator);
580 }
581 parsed_constant_expression(expression, source).map_or(
582 ParsedValidationRuleArgumentKind::Unsupported,
583 ParsedValidationRuleArgumentKind::Constant,
584 )
585 },
586 );
587 ParsedValidationRuleArgument { kind, span }
588}
589
590fn parsed_this_member_designator(
591 argument: &Argument<'_>,
592 source: &str,
593) -> Option<ParsedThisMemberDesignator> {
594 parsed_this_member_expression(argument.as_expression()?, source)
595}
596
597fn parsed_this_member_expression(
598 expression: &Expression<'_>,
599 source: &str,
600) -> Option<ParsedThisMemberDesignator> {
601 let Expression::StaticMemberExpression(member) = expression else {
602 return None;
603 };
604 let Expression::ThisExpression(this) = &member.object else {
605 return None;
606 };
607 Some(ParsedThisMemberDesignator {
608 member: member.property.name.to_string(),
609 span: source_span(source, member.span),
610 this_span: source_span(source, this.span),
611 member_span: source_span(source, member.property.span),
612 })
613}
614
615fn parsed_static_member_designator(
616 argument: &Argument<'_>,
617 source: &str,
618) -> Option<ParsedStaticMemberDesignator> {
619 let Expression::StaticMemberExpression(member) = argument.as_expression()? else {
620 return None;
621 };
622 let Expression::Identifier(object) = &member.object else {
623 return None;
624 };
625 Some(ParsedStaticMemberDesignator {
626 object: object.name.to_string(),
627 member: member.property.name.to_string(),
628 span: source_span(source, member.span),
629 object_span: source_span(source, object.span),
630 member_span: source_span(source, member.property.span),
631 })
632}
633
634fn argument_string_value(argument: &Argument<'_>) -> Option<String> {
635 match argument {
636 Argument::StringLiteral(literal) => Some(literal.value.to_string()),
637 _ => None,
638 }
639}
640
641fn parse_property(
642 property: &oxc_ast::ast::PropertyDefinition<'_>,
643 source: &str,
644) -> Option<ParsedProperty> {
645 let decorators = normalized_decorators(
646 property
647 .decorators
648 .iter()
649 .filter_map(|decorator| parse_decorator(decorator, source))
650 .collect::<Vec<_>>(),
651 );
652 let (name, is_identifier_name) = match &property.key {
653 PropertyKey::StaticIdentifier(identifier) => (identifier.name.to_string(), true),
654 key => match property_key_name(key) {
655 Some(name) => (name, false),
656 None if decorators
657 .iter()
658 .any(|decorator| matches!(decorator.name.as_str(), "form" | "field")) =>
659 {
660 (
661 format!("<unsupported:{}>", property.key.span().start),
662 false,
663 )
664 }
665 None => return None,
666 },
667 };
668
669 let initializer = property.value.as_ref().and_then(expression_summary);
670 let initializer_call = property.value.as_ref().and_then(|expression| {
671 let Expression::CallExpression(call) = expression else {
672 return None;
673 };
674 Some(ParsedInitializerCall {
675 callee_span: source_span(source, call.callee.span()),
676 span: source_span(source, call.span),
677 argument_count: call.arguments.len(),
678 inline_handler: parsed_inline_handler(call, source),
679 })
680 });
681 let initializer_literal = property
682 .value
683 .as_ref()
684 .and_then(serializable_value_from_expression);
685 let initializer_expression = property
686 .value
687 .as_ref()
688 .and_then(|expression| parsed_computed_expression(expression, source));
689 let initializer_constant_expression = property
690 .value
691 .as_ref()
692 .and_then(|expression| parsed_constant_expression(expression, source));
693 let initializer_span = property
694 .value
695 .as_ref()
696 .map(|value| source_span(source, value.span()));
697
698 let state_initial_value = property.value.as_ref().and_then(state_initial_value);
699 let state_initial_expression = property
700 .value
701 .as_ref()
702 .and_then(|expression| state_initial_constant_expression(expression, source));
703 let type_annotation = property
704 .type_annotation
705 .as_ref()
706 .map(|annotation| parsed_type_annotation(annotation.span, source));
707 let state_type_annotation = (initializer.as_deref() == Some("state(...)"))
708 .then_some(type_annotation.clone())
709 .flatten();
710 let declaration_start = decorators
711 .first()
712 .map_or(property.span.start as usize, |decorator| {
713 decorator.span.start
714 });
715
716 Some(ParsedProperty {
717 name,
718 is_identifier_name,
719 decorators,
720 initializer_call,
721 initializer,
722 initializer_literal,
723 initializer_expression,
724 initializer_constant_expression,
725 initializer_span,
726 state_initial_value,
727 state_initial_expression,
728 state_type_annotation,
729 type_annotation,
730 name_span: source_span(source, property.key.span()),
731 is_static: property.r#static,
732 is_definite_assignment: property.definite,
733 is_declare: property.declare,
734 span: source_span_from_offsets(source, declaration_start, property.span.end as usize),
735 })
736}
737
738fn parsed_inline_handler(
741 call: &oxc_ast::ast::CallExpression<'_>,
742 source: &str,
743) -> Option<ParsedInlineHandler> {
744 let [argument] = call.arguments.as_slice() else {
745 return None;
746 };
747 match argument {
748 Argument::ArrowFunctionExpression(handler) => Some(parsed_inline_handler_body(
749 handler.span,
750 &handler.body,
751 handler.r#async,
752 handler.expression,
753 parsed_inline_handler_parameters(&handler.params, source),
754 source,
755 )),
756 Argument::FunctionExpression(handler) => Some(parsed_inline_handler_body(
757 handler.span,
758 handler.body.as_deref()?,
759 handler.r#async,
760 false,
761 parsed_inline_handler_parameters(&handler.params, source),
762 source,
763 )),
764 _ => None,
765 }
766}
767
768fn parsed_inline_handler_body(
769 span: Span,
770 body: &oxc_ast::ast::FunctionBody<'_>,
771 is_async: bool,
772 is_expression_body: bool,
773 parameters: Vec<ParsedMethodParameter>,
774 source: &str,
775) -> ParsedInlineHandler {
776 let mut local_variables = Vec::new();
777 let mut state_updates = Vec::new();
778 let mut unsupported_statement_spans = Vec::new();
779 for statement in &body.statements {
780 if let Some(update) = parsed_state_update(statement, source) {
781 state_updates.push(update);
782 } else if let locals @ [.., _] = parsed_local_variables(statement, source).as_slice() {
783 local_variables.extend_from_slice(locals);
784 } else if !matches!(statement, Statement::EmptyStatement(_)) {
785 unsupported_statement_spans.push(source_span(source, statement.span()));
786 }
787 }
788 ParsedInlineHandler {
789 span: source_span(source, span),
790 body_span: source_span(source, body.span),
791 is_async,
792 is_expression_body,
793 parameters,
794 local_variables,
795 state_updates,
796 unsupported_statement_spans,
797 effect_body: (!is_expression_body).then(|| parsed_inline_effect_body(body, source)),
798 }
799}
800
801fn parsed_inline_handler_parameters(
802 parameters: &oxc_ast::ast::FormalParameters<'_>,
803 source: &str,
804) -> Vec<ParsedMethodParameter> {
805 parameters
806 .items
807 .iter()
808 .filter_map(|parameter| {
809 Some(ParsedMethodParameter {
810 name: binding_identifier_name(¶meter.pattern.kind)?,
811 decorators: normalized_decorators(
812 parameter
813 .decorators
814 .iter()
815 .filter_map(|decorator| parse_decorator(decorator, source))
816 .collect(),
817 ),
818 span: source_span(source, parameter.span),
819 type_annotation: parameter
820 .pattern
821 .type_annotation
822 .as_ref()
823 .map(|annotation| parsed_type_annotation(annotation.span, source)),
824 })
825 })
826 .collect()
827}
828
829fn parsed_type_annotation(span: Span, source: &str) -> ParsedTypeAnnotation {
830 let span = source_span(source, span);
831 let text = source[span.start..span.end]
832 .strip_prefix(':')
833 .expect("TypeScript annotation span should start with a colon")
834 .trim()
835 .to_string();
836
837 ParsedTypeAnnotation { text, span }
838}
839
840fn parse_method(method: &oxc_ast::ast::MethodDefinition<'_>, source: &str) -> Option<ParsedMethod> {
841 let name = property_key_name(&method.key)?;
842 let decorators = normalized_decorators(
843 method
844 .decorators
845 .iter()
846 .filter_map(|decorator| parse_decorator(decorator, source))
847 .collect::<Vec<_>>(),
848 );
849
850 let mut jsx_roots = Vec::new();
851 let mut bindings = Vec::new();
852 let mut state_updates = Vec::new();
853 let mut local_variables = Vec::new();
854 let mut return_values = Vec::new();
855 let mut calls = Vec::new();
856 let parameters = method
857 .value
858 .params
859 .items
860 .iter()
861 .filter_map(|parameter| {
862 Some(ParsedMethodParameter {
863 name: binding_identifier_name(¶meter.pattern.kind)?,
864 decorators: normalized_decorators(
865 parameter
866 .decorators
867 .iter()
868 .filter_map(|decorator| parse_decorator(decorator, source))
869 .collect(),
870 ),
871 span: source_span(source, parameter.span),
872 type_annotation: parameter
873 .pattern
874 .type_annotation
875 .as_ref()
876 .map(|annotation| parsed_type_annotation(annotation.span, source)),
877 })
878 })
879 .collect();
880
881 if let Some(body) = &method.value.body {
882 for statement in &body.statements {
883 parse_statement_for_jsx(statement, source, &mut jsx_roots, &mut bindings);
884 if let Some(update) = parsed_state_update(statement, source) {
885 state_updates.push(update);
886 }
887 local_variables.extend(parsed_local_variables(statement, source));
888 if let Some(value) = parsed_return_value(statement) {
889 return_values.push(value);
890 }
891 collect_method_calls(statement, source, &mut calls);
892 }
893 }
894
895 let computed_expression = (method.kind == oxc_ast::ast::MethodDefinitionKind::Get)
899 .then(|| {
900 let body = method.value.body.as_ref()?;
901 let [Statement::ReturnStatement(return_statement)] = body.statements.as_slice() else {
902 return None;
903 };
904 parsed_computed_expression(return_statement.argument.as_ref()?, source)
905 })
906 .flatten();
907 let effect_body = decorators
908 .iter()
909 .any(|decorator| decorator.name == "effect")
910 .then(|| {
911 method
912 .value
913 .body
914 .as_ref()
915 .map(|body| parsed_effect_body(body, source))
916 })
917 .flatten();
918
919 Some(ParsedMethod {
920 name,
921 span: source_span(source, method.span),
922 decorators,
923 is_getter: method.kind == oxc_ast::ast::MethodDefinitionKind::Get,
924 is_setter: method.kind == oxc_ast::ast::MethodDefinitionKind::Set,
925 is_async: method.value.r#async,
926 is_static: method.r#static,
927 jsx_roots,
928 bindings,
929 state_updates,
930 local_variables,
931 parameters,
932 return_type_annotation: method
933 .value
934 .return_type
935 .as_ref()
936 .map(|annotation| parsed_type_annotation(annotation.span, source)),
937 return_values,
938 computed_expression,
939 effect_body,
940 calls,
941 })
942}
943
944fn parsed_effect_body(body: &oxc_ast::ast::FunctionBody<'_>, source: &str) -> ParsedEffectBody {
945 parsed_effect_body_with_cleanup(body, source, false)
946}
947
948fn parsed_inline_effect_body(
949 body: &oxc_ast::ast::FunctionBody<'_>,
950 source: &str,
951) -> ParsedEffectBody {
952 parsed_effect_body_with_cleanup(body, source, true)
953}
954
955fn parsed_effect_body_with_cleanup(
956 body: &oxc_ast::ast::FunctionBody<'_>,
957 source: &str,
958 allow_cleanup: bool,
959) -> ParsedEffectBody {
960 let final_statement = body.statements.len().saturating_sub(1);
961 let cleanup = allow_cleanup
962 .then(|| body.statements.last())
963 .flatten()
964 .and_then(|statement| parsed_effect_cleanup(statement, source));
965 ParsedEffectBody {
966 statements: body
967 .statements
968 .iter()
969 .enumerate()
970 .filter(|(index, _)| cleanup.is_none() || *index != final_statement)
971 .map(|(index, statement)| {
972 parsed_effect_statement(statement, index == final_statement, source)
973 })
974 .collect(),
975 cleanup,
976 }
977}
978
979fn parsed_effect_cleanup(statement: &Statement<'_>, source: &str) -> Option<ParsedEffectCleanup> {
980 let Statement::ReturnStatement(return_statement) = statement else {
981 return None;
982 };
983 let expression = return_statement.argument.as_ref()?;
984 let (span, is_async, body) = match expression {
985 Expression::ArrowFunctionExpression(handler) if !handler.expression => {
986 (handler.span, handler.r#async, handler.body.as_ref())
987 }
988 Expression::FunctionExpression(handler) => {
989 (handler.span, handler.r#async, handler.body.as_deref()?)
990 }
991 _ => return None,
992 };
993 Some(ParsedEffectCleanup {
994 span: source_span(source, span),
995 is_async,
996 body: Box::new(parsed_effect_body(body, source)),
997 })
998}
999
1000fn parsed_effect_statement(
1001 statement: &Statement<'_>,
1002 is_final: bool,
1003 source: &str,
1004) -> ParsedEffectStatement {
1005 let span = source_span(source, statement.span());
1006 let kind = match statement {
1007 Statement::EmptyStatement(_) => ParsedEffectStatementKind::Empty,
1008 Statement::ExpressionStatement(statement) => match &statement.expression {
1009 Expression::AssignmentExpression(assignment) if assignment.operator.as_str() == "=" => {
1010 match (
1011 parsed_effect_assignment_target(&assignment.left, source),
1012 parsed_effect_expression(&assignment.right, source),
1013 ) {
1014 (Some(target), Some(value)) => {
1015 ParsedEffectStatementKind::StaticMemberAssignment { target, value }
1016 }
1017 _ => ParsedEffectStatementKind::Unsupported(
1018 ParsedUnsupportedEffectStatementKind::UnsupportedExpression,
1019 ),
1020 }
1021 }
1022 Expression::AssignmentExpression(_) | Expression::UpdateExpression(_) => {
1023 ParsedEffectStatementKind::Unsupported(
1024 ParsedUnsupportedEffectStatementKind::CompoundAssignment,
1025 )
1026 }
1027 Expression::CallExpression(call) => match (
1028 parsed_effect_expression(&call.callee, source),
1029 call.arguments
1030 .iter()
1031 .map(|argument| {
1032 argument
1033 .as_expression()
1034 .and_then(|expression| parsed_effect_expression(expression, source))
1035 })
1036 .collect::<Option<Vec<_>>>(),
1037 ) {
1038 (Some(callee), Some(arguments)) => {
1039 ParsedEffectStatementKind::CapabilityCall { callee, arguments }
1040 }
1041 _ => ParsedEffectStatementKind::Unsupported(
1042 ParsedUnsupportedEffectStatementKind::UnsupportedExpression,
1043 ),
1044 },
1045 _ => ParsedEffectStatementKind::Unsupported(
1046 ParsedUnsupportedEffectStatementKind::UnsupportedExpression,
1047 ),
1048 },
1049 Statement::ReturnStatement(statement) => match &statement.argument {
1050 None if is_final => ParsedEffectStatementKind::EffectReturn { value: None },
1051 Some(value) => parsed_effect_expression(value, source).map_or_else(
1052 || {
1053 ParsedEffectStatementKind::Unsupported(
1054 ParsedUnsupportedEffectStatementKind::CleanupReturnCandidate,
1055 )
1056 },
1057 |value| ParsedEffectStatementKind::EffectReturn { value: Some(value) },
1058 ),
1059 None => ParsedEffectStatementKind::Unsupported(
1060 ParsedUnsupportedEffectStatementKind::UnsupportedExpression,
1061 ),
1062 },
1063 statement if statement.as_declaration().is_some() => {
1064 ParsedEffectStatementKind::Unsupported(
1065 ParsedUnsupportedEffectStatementKind::LocalDeclaration,
1066 )
1067 }
1068 Statement::IfStatement(_) | Statement::SwitchStatement(_) => {
1069 ParsedEffectStatementKind::Unsupported(ParsedUnsupportedEffectStatementKind::Branch)
1070 }
1071 Statement::ForInStatement(_)
1072 | Statement::ForOfStatement(_)
1073 | Statement::ForStatement(_)
1074 | Statement::WhileStatement(_)
1075 | Statement::DoWhileStatement(_) => {
1076 ParsedEffectStatementKind::Unsupported(ParsedUnsupportedEffectStatementKind::Loop)
1077 }
1078 Statement::BlockStatement(_) => ParsedEffectStatementKind::Unsupported(
1079 ParsedUnsupportedEffectStatementKind::NestedBlock,
1080 ),
1081 Statement::TryStatement(_) | Statement::ThrowStatement(_) => {
1082 ParsedEffectStatementKind::Unsupported(
1083 ParsedUnsupportedEffectStatementKind::ExceptionHandling,
1084 )
1085 }
1086 _ => ParsedEffectStatementKind::Unsupported(
1087 ParsedUnsupportedEffectStatementKind::UnsupportedExpression,
1088 ),
1089 };
1090 ParsedEffectStatement { kind, span }
1091}
1092
1093fn parsed_effect_assignment_target(
1094 target: &AssignmentTarget<'_>,
1095 source: &str,
1096) -> Option<ParsedEffectExpression> {
1097 let AssignmentTarget::StaticMemberExpression(member) = target else {
1098 return None;
1099 };
1100 parsed_effect_static_member(
1101 &member.object,
1102 member.property.name.as_str(),
1103 member.span,
1104 source,
1105 )
1106}
1107
1108fn parsed_effect_expression(
1109 expression: &Expression<'_>,
1110 source: &str,
1111) -> Option<ParsedEffectExpression> {
1112 if let Expression::ParenthesizedExpression(parenthesized) = expression {
1113 return parsed_effect_expression(&parenthesized.expression, source);
1114 }
1115 if let Some(value) = serializable_value_from_expression(expression) {
1116 return Some(ParsedEffectExpression {
1117 kind: ParsedEffectExpressionKind::Literal(value),
1118 span: source_span(source, expression.span()),
1119 });
1120 }
1121 match expression {
1122 Expression::Identifier(identifier) => Some(ParsedEffectExpression {
1123 kind: ParsedEffectExpressionKind::Identifier(identifier.name.to_string()),
1124 span: source_span(source, identifier.span),
1125 }),
1126 Expression::ThisExpression(this) => Some(ParsedEffectExpression {
1127 kind: ParsedEffectExpressionKind::Identifier("this".to_string()),
1128 span: source_span(source, this.span),
1129 }),
1130 Expression::StaticMemberExpression(member) => parsed_effect_static_member(
1131 &member.object,
1132 member.property.name.as_str(),
1133 member.span,
1134 source,
1135 ),
1136 Expression::BinaryExpression(binary) => {
1137 let kind = match binary.operator.as_str() {
1138 "+" => ParsedEffectExpressionKind::Arithmetic {
1139 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1140 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1141 operator: ParsedArithmeticOperator::Add,
1142 },
1143 "-" => ParsedEffectExpressionKind::Arithmetic {
1144 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1145 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1146 operator: ParsedArithmeticOperator::Subtract,
1147 },
1148 "*" => ParsedEffectExpressionKind::Arithmetic {
1149 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1150 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1151 operator: ParsedArithmeticOperator::Multiply,
1152 },
1153 "/" => ParsedEffectExpressionKind::Arithmetic {
1154 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1155 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1156 operator: ParsedArithmeticOperator::Divide,
1157 },
1158 "%" => ParsedEffectExpressionKind::Arithmetic {
1159 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1160 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1161 operator: ParsedArithmeticOperator::Remainder,
1162 },
1163 "===" => ParsedEffectExpressionKind::Comparison {
1164 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1165 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1166 operator: ParsedComparisonOperator::Equal,
1167 },
1168 "!==" => ParsedEffectExpressionKind::Comparison {
1169 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1170 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1171 operator: ParsedComparisonOperator::NotEqual,
1172 },
1173 "<" => ParsedEffectExpressionKind::Comparison {
1174 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1175 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1176 operator: ParsedComparisonOperator::LessThan,
1177 },
1178 "<=" => ParsedEffectExpressionKind::Comparison {
1179 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1180 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1181 operator: ParsedComparisonOperator::LessThanOrEqual,
1182 },
1183 ">" => ParsedEffectExpressionKind::Comparison {
1184 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1185 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1186 operator: ParsedComparisonOperator::GreaterThan,
1187 },
1188 ">=" => ParsedEffectExpressionKind::Comparison {
1189 left: Box::new(parsed_effect_expression(&binary.left, source)?),
1190 right: Box::new(parsed_effect_expression(&binary.right, source)?),
1191 operator: ParsedComparisonOperator::GreaterThanOrEqual,
1192 },
1193 _ => return None,
1194 };
1195 Some(ParsedEffectExpression {
1196 kind,
1197 span: source_span(source, binary.span),
1198 })
1199 }
1200 Expression::LogicalExpression(logical) => {
1201 let kind = match logical.operator.as_str() {
1202 "&&" => ParsedEffectExpressionKind::Logical {
1203 left: Box::new(parsed_effect_expression(&logical.left, source)?),
1204 right: Box::new(parsed_effect_expression(&logical.right, source)?),
1205 operator: ParsedLogicalOperator::And,
1206 },
1207 "||" => ParsedEffectExpressionKind::Logical {
1208 left: Box::new(parsed_effect_expression(&logical.left, source)?),
1209 right: Box::new(parsed_effect_expression(&logical.right, source)?),
1210 operator: ParsedLogicalOperator::Or,
1211 },
1212 "??" => ParsedEffectExpressionKind::NullishCoalescing {
1213 left: Box::new(parsed_effect_expression(&logical.left, source)?),
1214 right: Box::new(parsed_effect_expression(&logical.right, source)?),
1215 },
1216 _ => return None,
1217 };
1218 Some(ParsedEffectExpression {
1219 kind,
1220 span: source_span(source, logical.span),
1221 })
1222 }
1223 Expression::UnaryExpression(unary) => {
1224 let operator = match unary.operator.as_str() {
1225 "!" => ParsedUnaryOperator::Not,
1226 "+" => ParsedUnaryOperator::Plus,
1227 "-" => ParsedUnaryOperator::Minus,
1228 _ => return None,
1229 };
1230 Some(ParsedEffectExpression {
1231 kind: ParsedEffectExpressionKind::Unary {
1232 operand: Box::new(parsed_effect_expression(&unary.argument, source)?),
1233 operator,
1234 },
1235 span: source_span(source, unary.span),
1236 })
1237 }
1238 _ => None,
1239 }
1240}
1241
1242fn parsed_effect_static_member(
1243 object: &Expression<'_>,
1244 property: &str,
1245 span: Span,
1246 source: &str,
1247) -> Option<ParsedEffectExpression> {
1248 let object = parsed_effect_expression(object, source)?;
1249 let kind = if matches!(&object.kind, ParsedEffectExpressionKind::Identifier(name) if name == "this")
1250 {
1251 ParsedEffectExpressionKind::ThisMember(property.to_string())
1252 } else {
1253 ParsedEffectExpressionKind::MemberAccess {
1254 object: Box::new(object),
1255 property: property.to_string(),
1256 }
1257 };
1258 Some(ParsedEffectExpression {
1259 kind,
1260 span: source_span(source, span),
1261 })
1262}
1263
1264fn collect_method_calls(
1265 statement: &Statement<'_>,
1266 source: &str,
1267 calls: &mut Vec<ParsedMethodCall>,
1268) {
1269 let expression = match statement {
1270 Statement::ExpressionStatement(statement) => Some(&statement.expression),
1271 Statement::ReturnStatement(statement) => statement.argument.as_ref(),
1272 _ => None,
1273 };
1274 if let Some(expression) = expression {
1275 collect_calls_from_expression(expression, source, calls);
1276 }
1277}
1278
1279fn collect_calls_from_expression(
1280 expression: &Expression<'_>,
1281 source: &str,
1282 calls: &mut Vec<ParsedMethodCall>,
1283) {
1284 match expression {
1285 Expression::ParenthesizedExpression(parenthesized) => {
1286 collect_calls_from_expression(&parenthesized.expression, source, calls);
1287 }
1288 Expression::CallExpression(call) => {
1289 if let Some(callee) = expression_summary(&call.callee) {
1290 calls.push(ParsedMethodCall {
1291 callee,
1292 span: source_span(source, call.span),
1293 });
1294 }
1295 collect_calls_from_expression(&call.callee, source, calls);
1296 for argument in &call.arguments {
1297 if let Some(expression) = argument.as_expression() {
1298 collect_calls_from_expression(expression, source, calls);
1299 }
1300 }
1301 }
1302 Expression::BinaryExpression(binary) => {
1303 collect_calls_from_expression(&binary.left, source, calls);
1304 collect_calls_from_expression(&binary.right, source, calls);
1305 }
1306 Expression::LogicalExpression(logical) => {
1307 collect_calls_from_expression(&logical.left, source, calls);
1308 collect_calls_from_expression(&logical.right, source, calls);
1309 }
1310 Expression::UnaryExpression(unary) => {
1311 collect_calls_from_expression(&unary.argument, source, calls);
1312 }
1313 _ => {}
1314 }
1315}
1316
1317fn parsed_computed_expression(
1318 expression: &Expression<'_>,
1319 source: &str,
1320) -> Option<ParsedComputedExpression> {
1321 if let Expression::ParenthesizedExpression(parenthesized) = expression {
1322 return parsed_computed_expression(&parenthesized.expression, source);
1323 }
1324
1325 if let Some(value) = serializable_value_from_expression(expression) {
1326 return Some(ParsedComputedExpression {
1327 kind: ParsedComputedExpressionKind::Literal(value),
1328 span: source_span(source, expression.span()),
1329 });
1330 }
1331
1332 match expression {
1333 Expression::TemplateLiteral(template) => Some(ParsedComputedExpression {
1334 kind: ParsedComputedExpressionKind::Template {
1335 quasis: template
1336 .quasis
1337 .iter()
1338 .map(|quasi| quasi.value.cooked.as_ref().map(ToString::to_string))
1339 .collect::<Option<Vec<_>>>()?,
1340 expressions: template
1341 .expressions
1342 .iter()
1343 .map(|expression| parsed_computed_expression(expression, source))
1344 .collect::<Option<Vec<_>>>()?,
1345 },
1346 span: source_span(source, template.span),
1347 }),
1348 Expression::CallExpression(call) => {
1349 let callee = expression_summary(&call.callee)?;
1350 let arguments = call
1351 .arguments
1352 .iter()
1353 .map(|argument| parsed_computed_expression(argument.as_expression()?, source))
1354 .collect::<Option<Vec<_>>>()?;
1355 Some(ParsedComputedExpression {
1356 kind: ParsedComputedExpressionKind::Call { callee, arguments },
1357 span: source_span(source, call.span),
1358 })
1359 }
1360 Expression::StaticMemberExpression(member) => {
1361 let property = member.property.name.to_string();
1362 let kind = if matches!(&member.object, Expression::ThisExpression(_)) {
1363 ParsedComputedExpressionKind::ThisMember(property)
1364 } else {
1365 ParsedComputedExpressionKind::MemberAccess {
1366 object: Box::new(parsed_computed_expression(&member.object, source)?),
1367 property,
1368 optional: member.optional,
1369 }
1370 };
1371 Some(ParsedComputedExpression {
1372 kind,
1373 span: source_span(source, member.span),
1374 })
1375 }
1376 Expression::ChainExpression(chain) => match &chain.expression {
1377 ChainElement::StaticMemberExpression(member) => Some(ParsedComputedExpression {
1378 kind: ParsedComputedExpressionKind::MemberAccess {
1379 object: Box::new(parsed_computed_expression(&member.object, source)?),
1380 property: member.property.name.to_string(),
1381 optional: true,
1382 },
1383 span: source_span(source, chain.span),
1384 }),
1385 _ => None,
1386 },
1387 Expression::ComputedMemberExpression(member) => {
1388 let index = parsed_computed_expression(&member.expression, source)?;
1389 let supported_index = match &index.kind {
1390 ParsedComputedExpressionKind::Literal(ParsedSerializableValue::String(_)) => true,
1391 ParsedComputedExpressionKind::Literal(ParsedSerializableValue::Number(value)) => {
1392 value.parse::<u64>().is_ok()
1393 }
1394 _ => false,
1395 };
1396 if !supported_index {
1397 return None;
1398 }
1399 Some(ParsedComputedExpression {
1400 kind: ParsedComputedExpressionKind::IndexAccess {
1401 object: Box::new(parsed_computed_expression(&member.object, source)?),
1402 index: Box::new(index),
1403 },
1404 span: source_span(source, member.span),
1405 })
1406 }
1407 Expression::ConditionalExpression(conditional) => Some(ParsedComputedExpression {
1408 kind: ParsedComputedExpressionKind::Conditional {
1409 condition: Box::new(parsed_computed_expression(&conditional.test, source)?),
1410 when_true: Box::new(parsed_computed_expression(&conditional.consequent, source)?),
1411 when_false: Box::new(parsed_computed_expression(&conditional.alternate, source)?),
1412 },
1413 span: source_span(source, conditional.span),
1414 }),
1415 Expression::BinaryExpression(binary) => {
1416 let operator = match binary.operator.as_str() {
1417 "+" => ParsedComputedExpressionKind::Arithmetic {
1418 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1419 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1420 operator: ParsedArithmeticOperator::Add,
1421 },
1422 "-" => ParsedComputedExpressionKind::Arithmetic {
1423 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1424 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1425 operator: ParsedArithmeticOperator::Subtract,
1426 },
1427 "*" => ParsedComputedExpressionKind::Arithmetic {
1428 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1429 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1430 operator: ParsedArithmeticOperator::Multiply,
1431 },
1432 "/" => ParsedComputedExpressionKind::Arithmetic {
1433 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1434 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1435 operator: ParsedArithmeticOperator::Divide,
1436 },
1437 "%" => ParsedComputedExpressionKind::Arithmetic {
1438 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1439 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1440 operator: ParsedArithmeticOperator::Remainder,
1441 },
1442 "===" => ParsedComputedExpressionKind::Comparison {
1443 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1444 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1445 operator: ParsedComparisonOperator::Equal,
1446 },
1447 "!==" => ParsedComputedExpressionKind::Comparison {
1448 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1449 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1450 operator: ParsedComparisonOperator::NotEqual,
1451 },
1452 "<" => ParsedComputedExpressionKind::Comparison {
1453 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1454 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1455 operator: ParsedComparisonOperator::LessThan,
1456 },
1457 "<=" => ParsedComputedExpressionKind::Comparison {
1458 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1459 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1460 operator: ParsedComparisonOperator::LessThanOrEqual,
1461 },
1462 ">" => ParsedComputedExpressionKind::Comparison {
1463 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1464 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1465 operator: ParsedComparisonOperator::GreaterThan,
1466 },
1467 ">=" => ParsedComputedExpressionKind::Comparison {
1468 left: Box::new(parsed_computed_expression(&binary.left, source)?),
1469 right: Box::new(parsed_computed_expression(&binary.right, source)?),
1470 operator: ParsedComparisonOperator::GreaterThanOrEqual,
1471 },
1472 _ => return None,
1473 };
1474 Some(ParsedComputedExpression {
1475 kind: operator,
1476 span: source_span(source, binary.span),
1477 })
1478 }
1479 Expression::LogicalExpression(logical) => {
1480 let kind = match logical.operator.as_str() {
1481 "&&" => ParsedComputedExpressionKind::Logical {
1482 left: Box::new(parsed_computed_expression(&logical.left, source)?),
1483 right: Box::new(parsed_computed_expression(&logical.right, source)?),
1484 operator: ParsedLogicalOperator::And,
1485 },
1486 "||" => ParsedComputedExpressionKind::Logical {
1487 left: Box::new(parsed_computed_expression(&logical.left, source)?),
1488 right: Box::new(parsed_computed_expression(&logical.right, source)?),
1489 operator: ParsedLogicalOperator::Or,
1490 },
1491 "??" => ParsedComputedExpressionKind::NullishCoalescing {
1492 left: Box::new(parsed_computed_expression(&logical.left, source)?),
1493 right: Box::new(parsed_computed_expression(&logical.right, source)?),
1494 },
1495 _ => return None,
1496 };
1497 Some(ParsedComputedExpression {
1498 kind,
1499 span: source_span(source, logical.span),
1500 })
1501 }
1502 Expression::UnaryExpression(unary) => {
1503 let operator = match unary.operator.as_str() {
1504 "!" => ParsedUnaryOperator::Not,
1505 "+" => ParsedUnaryOperator::Plus,
1506 "-" => ParsedUnaryOperator::Minus,
1507 _ => return None,
1508 };
1509 Some(ParsedComputedExpression {
1510 kind: ParsedComputedExpressionKind::Unary {
1511 operand: Box::new(parsed_computed_expression(&unary.argument, source)?),
1512 operator,
1513 },
1514 span: source_span(source, unary.span),
1515 })
1516 }
1517 _ => None,
1518 }
1519}
1520
1521fn parsed_return_value(statement: &Statement<'_>) -> Option<ParsedSerializableValue> {
1522 let Statement::ReturnStatement(return_statement) = statement else {
1523 return None;
1524 };
1525 serializable_value_from_expression(return_statement.argument.as_ref()?)
1526}
1527
1528fn parsed_local_variables(statement: &Statement<'_>, source: &str) -> Vec<ParsedLocalVariable> {
1529 let Some(Declaration::VariableDeclaration(declaration)) = statement.as_declaration() else {
1530 return Vec::new();
1531 };
1532 declaration
1533 .declarations
1534 .iter()
1535 .filter_map(|declarator| {
1536 let name = binding_identifier_name(&declarator.id.kind)?;
1537 let value = serializable_value_from_expression(declarator.init.as_ref()?)?;
1538 Some(ParsedLocalVariable {
1539 name,
1540 value,
1541 span: source_span(source, declarator.span),
1542 })
1543 })
1544 .collect()
1545}
1546
1547fn parsed_state_update(statement: &Statement<'_>, source: &str) -> Option<ParsedStateUpdate> {
1548 let Statement::ExpressionStatement(statement) = statement else {
1549 return None;
1550 };
1551
1552 match &statement.expression {
1553 Expression::UpdateExpression(update) => parsed_update_state_update(update, source),
1554 Expression::AssignmentExpression(assignment) => {
1555 parsed_assignment_state_update(assignment, source)
1556 }
1557 _ => None,
1558 }
1559}
1560
1561fn parsed_update_state_update(
1562 update: &oxc_ast::ast::UpdateExpression<'_>,
1563 source: &str,
1564) -> Option<ParsedStateUpdate> {
1565 let operation = match update.operator.as_str() {
1566 "++" => ParsedStateOperation::Increment,
1567 "--" => ParsedStateOperation::Decrement,
1568 _ => return None,
1569 };
1570
1571 let field = this_assignment_target_field(&update.argument)?;
1572
1573 Some(ParsedStateUpdate {
1574 field,
1575 operation,
1576 span: source_span(source, update.span),
1577 })
1578}
1579
1580fn parsed_assignment_state_update(
1581 assignment: &oxc_ast::ast::AssignmentExpression<'_>,
1582 source: &str,
1583) -> Option<ParsedStateUpdate> {
1584 let field = this_assignment_target_field_from_assignment_target(&assignment.left)?;
1585
1586 let operation = match assignment.operator.as_str() {
1587 "+=" => {
1588 ParsedStateOperation::AddAssign(serializable_value_from_expression(&assignment.right)?)
1589 }
1590 "-=" => ParsedStateOperation::SubtractAssign(serializable_value_from_expression(
1591 &assignment.right,
1592 )?),
1593 "=" if toggled_this_field(&assignment.right).as_deref() == Some(field.as_str()) => {
1594 ParsedStateOperation::Toggle
1595 }
1596 "=" => match &assignment.right {
1597 Expression::Identifier(identifier) => {
1598 ParsedStateOperation::AssignParameter(identifier.name.to_string())
1599 }
1600 expression => {
1601 ParsedStateOperation::Assign(serializable_value_from_expression(expression)?)
1602 }
1603 },
1604 _ => return None,
1605 };
1606
1607 Some(ParsedStateUpdate {
1608 field,
1609 operation,
1610 span: source_span(source, assignment.span),
1611 })
1612}
1613
1614fn this_assignment_target_field(target: &SimpleAssignmentTarget<'_>) -> Option<String> {
1615 let SimpleAssignmentTarget::StaticMemberExpression(member) = target else {
1616 return None;
1617 };
1618
1619 let Expression::ThisExpression(_) = &member.object else {
1620 return None;
1621 };
1622
1623 Some(member.property.name.to_string())
1624}
1625
1626fn this_assignment_target_field_from_assignment_target(
1627 target: &AssignmentTarget<'_>,
1628) -> Option<String> {
1629 let AssignmentTarget::StaticMemberExpression(member) = target else {
1630 return None;
1631 };
1632
1633 let Expression::ThisExpression(_) = &member.object else {
1634 return None;
1635 };
1636
1637 Some(member.property.name.to_string())
1638}
1639
1640fn parse_statement_for_jsx(
1641 statement: &Statement<'_>,
1642 source: &str,
1643 jsx_roots: &mut Vec<ParsedJsxNode>,
1644 bindings: &mut Vec<String>,
1645) {
1646 if let Statement::ReturnStatement(return_statement) = statement {
1647 if let Some(argument) = &return_statement.argument {
1648 parse_expression_for_jsx(argument, source, jsx_roots, bindings);
1649 }
1650 }
1651}
1652
1653fn parse_expression_for_jsx(
1654 expression: &Expression<'_>,
1655 source: &str,
1656 jsx_roots: &mut Vec<ParsedJsxNode>,
1657 bindings: &mut Vec<String>,
1658) {
1659 match expression {
1660 Expression::ParenthesizedExpression(parenthesized) => {
1661 parse_expression_for_jsx(&parenthesized.expression, source, jsx_roots, bindings);
1662 }
1663 Expression::JSXElement(element) => {
1664 for child in &element.children {
1665 parse_jsx_child(child, bindings);
1666 }
1667
1668 if let Some(element) = parsed_jsx_element(element, source) {
1669 jsx_roots.push(ParsedJsxNode::Element(element));
1670 }
1671 }
1672 Expression::JSXFragment(fragment) => {
1673 for child in &fragment.children {
1674 parse_jsx_child(child, bindings);
1675 }
1676
1677 jsx_roots.push(ParsedJsxNode::Fragment(parsed_jsx_fragment(
1678 fragment, source,
1679 )));
1680 }
1681 _ => {}
1682 }
1683}
1684
1685fn parse_jsx_child(child: &JSXChild<'_>, bindings: &mut Vec<String>) {
1686 match child {
1687 JSXChild::ExpressionContainer(container) => {
1688 if let Some(binding) = jsx_expression_binding_summary(&container.expression) {
1689 bindings.push(binding);
1690 }
1691 }
1692 JSXChild::Element(element) => {
1693 for child in &element.children {
1694 parse_jsx_child(child, bindings);
1695 }
1696 }
1697 JSXChild::Fragment(fragment) => {
1698 for child in &fragment.children {
1699 parse_jsx_child(child, bindings);
1700 }
1701 }
1702 _ => {}
1703 }
1704}
1705
1706fn parsed_jsx_child(child: &JSXChild<'_>, source: &str) -> Option<ParsedJsxChild> {
1707 match child {
1708 JSXChild::Text(text) => {
1709 let normalized = normalize_jsx_text(&text.value);
1710
1711 if normalized.is_empty() {
1712 None
1713 } else {
1714 Some(ParsedJsxChild::Text {
1715 value: normalized,
1716 span: jsx_text_value_span(source, text.value.as_str(), text.span),
1717 })
1718 }
1719 }
1720 JSXChild::ExpressionContainer(container) => parsed_jsx_expression_child(
1721 &container.expression,
1722 source,
1723 source_span(source, container.span),
1724 ),
1725 JSXChild::Element(element) => {
1726 parsed_jsx_element(element, source).map(ParsedJsxChild::Element)
1727 }
1728 JSXChild::Fragment(fragment) => Some(ParsedJsxChild::Fragment(parsed_jsx_fragment(
1729 fragment, source,
1730 ))),
1731 _ => None,
1732 }
1733}
1734
1735fn parsed_jsx_expression_child(
1736 expression: &JSXExpression<'_>,
1737 source: &str,
1738 span: SourceSpan,
1739) -> Option<ParsedJsxChild> {
1740 let expression = expression.as_expression()?;
1741
1742 if let Expression::CallExpression(call) = expression {
1743 if let Some(list) = parsed_jsx_list(call, source) {
1744 return Some(ParsedJsxChild::List(list));
1745 }
1746 }
1747
1748 if let Expression::ConditionalExpression(conditional) = expression {
1749 return parsed_jsx_conditional(conditional, source).map(ParsedJsxChild::Conditional);
1750 }
1751
1752 if let Expression::LogicalExpression(logical) = expression {
1753 return parsed_jsx_logical_and(logical, source).map(ParsedJsxChild::Conditional);
1754 }
1755
1756 expression_summary(expression).map(|expression| ParsedJsxChild::Binding { expression, span })
1757}
1758
1759fn parsed_jsx_list(call: &oxc_ast::ast::CallExpression<'_>, source: &str) -> Option<ParsedJsxList> {
1760 let Expression::StaticMemberExpression(member) = &call.callee else {
1761 return None;
1762 };
1763
1764 if member.property.name != "map" || call.arguments.len() != 1 {
1765 return None;
1766 }
1767
1768 let iterable = expression_summary(&member.object)?;
1769 let Argument::ArrowFunctionExpression(callback) = &call.arguments[0] else {
1770 return None;
1771 };
1772
1773 if callback.params.rest.is_some() || !(1..=2).contains(&callback.params.items.len()) {
1774 return None;
1775 }
1776
1777 let item_variable = binding_identifier_name(&callback.params.items[0].pattern.kind)?;
1778 let index_variable = match callback.params.items.get(1) {
1779 Some(parameter) => Some(binding_identifier_name(¶meter.pattern.kind)?),
1780 None => None,
1781 };
1782 let item_template = parsed_jsx_node_from_expression(callback.get_expression()?, source)?;
1783 let key_expression = key_expression_from_jsx_node(&item_template).unwrap_or_default();
1784
1785 Some(ParsedJsxList {
1786 iterable,
1787 item_variable,
1788 index_variable,
1789 key_expression,
1790 span: source_span(source, call.span),
1791 item_template,
1792 })
1793}
1794
1795fn binding_identifier_name(pattern: &BindingPatternKind<'_>) -> Option<String> {
1796 let BindingPatternKind::BindingIdentifier(identifier) = pattern else {
1797 return None;
1798 };
1799
1800 Some(identifier.name.to_string())
1801}
1802
1803fn key_expression_from_jsx_node(node: &ParsedJsxNode) -> Option<String> {
1804 let ParsedJsxNode::Element(element) = node else {
1805 return None;
1806 };
1807
1808 element.attributes.iter().find_map(|attribute| {
1809 if attribute.name != "key" {
1810 return None;
1811 }
1812
1813 let ParsedJsxAttributeValue::Expression(Some(expression)) = &attribute.value else {
1814 return None;
1815 };
1816
1817 Some(expression.clone())
1818 })
1819}
1820
1821fn parsed_jsx_conditional(
1822 conditional: &oxc_ast::ast::ConditionalExpression<'_>,
1823 source: &str,
1824) -> Option<ParsedJsxConditional> {
1825 let condition = expression_summary(&conditional.test)?;
1826 let when_true = parsed_jsx_node_from_expression(&conditional.consequent, source)?;
1827 let when_false = parsed_jsx_node_from_expression(&conditional.alternate, source)?;
1828
1829 Some(ParsedJsxConditional {
1830 condition,
1831 span: source_span(source, conditional.span),
1832 when_true,
1833 when_false: Some(when_false),
1834 })
1835}
1836
1837fn parsed_jsx_logical_and(
1838 logical: &oxc_ast::ast::LogicalExpression<'_>,
1839 source: &str,
1840) -> Option<ParsedJsxConditional> {
1841 if logical.operator.as_str() != "&&" {
1842 return None;
1843 }
1844
1845 let condition = expression_summary(&logical.left)?;
1846 let when_true = parsed_jsx_node_from_expression(&logical.right, source)?;
1847
1848 Some(ParsedJsxConditional {
1849 condition,
1850 span: source_span(source, logical.span),
1851 when_true,
1852 when_false: None,
1853 })
1854}
1855
1856fn parsed_jsx_node_from_expression(
1857 expression: &Expression<'_>,
1858 source: &str,
1859) -> Option<ParsedJsxNode> {
1860 match expression {
1861 Expression::ParenthesizedExpression(parenthesized) => {
1862 parsed_jsx_node_from_expression(&parenthesized.expression, source)
1863 }
1864 Expression::JSXElement(element) => {
1865 parsed_jsx_element(element, source).map(ParsedJsxNode::Element)
1866 }
1867 Expression::JSXFragment(fragment) => Some(ParsedJsxNode::Fragment(parsed_jsx_fragment(
1868 fragment, source,
1869 ))),
1870 _ => None,
1871 }
1872}
1873
1874fn parsed_jsx_fragment(fragment: &JSXFragment<'_>, source: &str) -> ParsedJsxFragment {
1875 ParsedJsxFragment {
1876 span: source_span(source, fragment.span),
1877 children: fragment
1878 .children
1879 .iter()
1880 .filter_map(|child| parsed_jsx_child(child, source))
1881 .collect(),
1882 }
1883}
1884
1885fn parsed_jsx_element(
1886 element: &oxc_ast::ast::JSXElement<'_>,
1887 source: &str,
1888) -> Option<ParsedJsxElement> {
1889 let name =
1890 jsx_element_name(&element.opening_element.name).unwrap_or_else(|| "<unknown>".to_string());
1891
1892 let attributes = element
1893 .opening_element
1894 .attributes
1895 .iter()
1896 .map(|attribute| parsed_jsx_attribute(attribute, source))
1897 .collect::<Vec<_>>();
1898
1899 let event_handlers = element
1900 .opening_element
1901 .attributes
1902 .iter()
1903 .filter_map(|attribute| jsx_event_handler(attribute, source))
1904 .collect::<Vec<_>>();
1905
1906 let children = element
1907 .children
1908 .iter()
1909 .filter_map(|child| parsed_jsx_child(child, source))
1910 .collect::<Vec<_>>();
1911
1912 Some(ParsedJsxElement {
1913 name,
1914 name_span: source_span(source, element.opening_element.name.span()),
1915 span: source_span(source, element.span),
1916 attributes,
1917 event_handlers,
1918 children,
1919 })
1920}
1921
1922fn normalize_jsx_text(value: &str) -> String {
1923 value.split_whitespace().collect::<Vec<_>>().join(" ")
1924}
1925
1926fn jsx_text_value_span(source: &str, value: &str, span: Span) -> SourceSpan {
1927 let leading_whitespace = value
1928 .char_indices()
1929 .find(|(_, character)| !character.is_whitespace())
1930 .map_or(value.len(), |(index, _)| index);
1931 let trailing_whitespace = value
1932 .char_indices()
1933 .rev()
1934 .find(|(_, character)| !character.is_whitespace())
1935 .map_or(value.len(), |(index, character)| {
1936 index + character.len_utf8()
1937 });
1938
1939 source_span_from_offsets(
1940 source,
1941 span.start as usize + leading_whitespace,
1942 span.start as usize + trailing_whitespace,
1943 )
1944}
1945
1946fn property_key_name(key: &PropertyKey<'_>) -> Option<String> {
1947 match key {
1948 PropertyKey::StaticIdentifier(identifier) => Some(identifier.name.to_string()),
1949 PropertyKey::PrivateIdentifier(identifier) => Some(format!("#{}", identifier.name)),
1950 PropertyKey::StringLiteral(literal) => Some(literal.value.to_string()),
1951 PropertyKey::NumericLiteral(literal) => Some(literal.raw.as_ref()?.to_string()),
1952 _ => None,
1953 }
1954}
1955
1956fn jsx_expression_summary(expression: &JSXExpression<'_>) -> Option<String> {
1957 if let Some(expression) = expression.as_expression() {
1958 return expression_summary(expression);
1959 }
1960
1961 None
1962}
1963
1964fn jsx_expression_binding_summary(expression: &JSXExpression<'_>) -> Option<String> {
1965 let expression = expression.as_expression()?;
1966
1967 if let Expression::ConditionalExpression(conditional) = expression {
1968 return expression_summary(&conditional.test);
1969 }
1970
1971 if let Expression::LogicalExpression(logical) = expression {
1972 if logical.operator.as_str() == "&&" {
1973 return expression_summary(&logical.left);
1974 }
1975 }
1976
1977 if let Expression::CallExpression(call) = expression {
1978 if let Some(iterable) = list_iterable_dependency(call) {
1979 return Some(iterable);
1980 }
1981 }
1982
1983 expression_summary(expression)
1984}
1985
1986fn list_iterable_dependency(call: &oxc_ast::ast::CallExpression<'_>) -> Option<String> {
1987 let Expression::StaticMemberExpression(member) = &call.callee else {
1988 return None;
1989 };
1990
1991 if member.property.name != "map" {
1992 return None;
1993 }
1994
1995 expression_summary(&member.object)
1996}
1997
1998fn expression_summary(expression: &Expression<'_>) -> Option<String> {
1999 match expression {
2000 Expression::CallExpression(call) => {
2001 let callee = expression_summary(&call.callee)?;
2002 Some(format!("{callee}(...)"))
2003 }
2004 Expression::Identifier(identifier) => Some(identifier.name.to_string()),
2005 Expression::ThisExpression(_) => Some("this".to_string()),
2006 Expression::StaticMemberExpression(member) => {
2007 let object = expression_summary(&member.object)?;
2008 Some(format!("{object}.{}", member.property.name))
2009 }
2010 Expression::NumericLiteral(literal) => Some(literal.raw.as_ref()?.to_string()),
2011 Expression::StringLiteral(literal) => Some(format!("{:?}", literal.value.as_str())),
2012 _ => None,
2013 }
2014}
2015
2016fn state_initial_value(expression: &Expression<'_>) -> Option<ParsedSerializableValue> {
2017 let Expression::CallExpression(call) = expression else {
2018 return None;
2019 };
2020
2021 let Expression::Identifier(callee) = &call.callee else {
2022 return None;
2023 };
2024
2025 if callee.name != "state" {
2026 return None;
2027 }
2028
2029 call.arguments.first().and_then(state_argument_literal)
2030}
2031
2032fn state_initial_constant_expression(
2033 expression: &Expression<'_>,
2034 source: &str,
2035) -> Option<ParsedConstantExpression> {
2036 let Expression::CallExpression(call) = expression else {
2037 return None;
2038 };
2039 let Expression::Identifier(callee) = &call.callee else {
2040 return None;
2041 };
2042 if callee.name != "state" || call.arguments.len() != 1 {
2043 return None;
2044 }
2045
2046 let expression = parsed_constant_expression(call.arguments[0].as_expression()?, source)?;
2047 (!matches!(
2048 expression.kind,
2049 ParsedConstantExpressionKind::Primitive(_) | ParsedConstantExpressionKind::Boolean(_)
2050 ))
2051 .then_some(expression)
2052}
2053
2054fn parsed_constant_expression(
2055 expression: &Expression<'_>,
2056 source: &str,
2057) -> Option<ParsedConstantExpression> {
2058 if let Expression::ParenthesizedExpression(parenthesized) = expression {
2059 return parsed_constant_expression(&parenthesized.expression, source);
2060 }
2061 if let Expression::BooleanLiteral(literal) = expression {
2062 return Some(ParsedConstantExpression {
2063 kind: ParsedConstantExpressionKind::Boolean(literal.value),
2064 span: source_span(source, literal.span),
2065 });
2066 }
2067 if let Some(primitive) = parsed_primitive_constant_expression(expression, source) {
2068 return Some(primitive);
2069 }
2070 if let Some(unary) = parsed_unary_constant_expression(expression, source) {
2071 return Some(unary);
2072 }
2073 if let Some(logical) = parsed_logical_expression(expression, source) {
2074 return Some(logical);
2075 }
2076 if let Some(comparison) = parsed_comparison_expression(expression, source) {
2077 return Some(comparison);
2078 }
2079 if let Some(nullish) = parsed_nullish_coalescing_expression(expression, source) {
2080 return Some(nullish);
2081 }
2082
2083 let arithmetic = parsed_arithmetic_expression(expression, source)?;
2084 matches!(
2085 arithmetic.kind,
2086 ParsedArithmeticExpressionKind::Binary { .. }
2087 )
2088 .then_some(ParsedConstantExpression {
2089 span: arithmetic.span,
2090 kind: ParsedConstantExpressionKind::Arithmetic(arithmetic),
2091 })
2092}
2093
2094fn parsed_unary_constant_expression(
2095 expression: &Expression<'_>,
2096 source: &str,
2097) -> Option<ParsedConstantExpression> {
2098 let Expression::UnaryExpression(unary) = expression else {
2099 return None;
2100 };
2101 let operator = match unary.operator.as_str() {
2102 "!" => ParsedUnaryOperator::Not,
2103 "+" => ParsedUnaryOperator::Plus,
2104 "-" => ParsedUnaryOperator::Minus,
2105 _ => return None,
2106 };
2107 let operand = parsed_constant_expression(&unary.argument, source)?;
2108 let valid = match operator {
2109 ParsedUnaryOperator::Not => matches!(
2110 operand.kind,
2111 ParsedConstantExpressionKind::Boolean(_)
2112 | ParsedConstantExpressionKind::Comparison { .. }
2113 | ParsedConstantExpressionKind::Logical { .. }
2114 ),
2115 ParsedUnaryOperator::Plus | ParsedUnaryOperator::Minus => matches!(
2116 operand.kind,
2117 ParsedConstantExpressionKind::Primitive(ParsedSerializableValue::Number(_))
2118 | ParsedConstantExpressionKind::Arithmetic(_)
2119 ),
2120 };
2121 valid.then_some(ParsedConstantExpression {
2122 kind: ParsedConstantExpressionKind::Unary {
2123 operator,
2124 operand: Box::new(operand),
2125 },
2126 span: source_span(source, unary.span),
2127 })
2128}
2129
2130fn parsed_primitive_constant_expression(
2131 expression: &Expression<'_>,
2132 source: &str,
2133) -> Option<ParsedConstantExpression> {
2134 let (value, span) = match expression {
2135 Expression::NullLiteral(literal) => (ParsedSerializableValue::Null, literal.span),
2136 Expression::NumericLiteral(literal) => (
2137 ParsedSerializableValue::Number(literal.raw.as_ref()?.to_string()),
2138 literal.span,
2139 ),
2140 Expression::StringLiteral(literal) => (
2141 ParsedSerializableValue::String(literal.value.to_string()),
2142 literal.span,
2143 ),
2144 _ => return None,
2145 };
2146
2147 Some(ParsedConstantExpression {
2148 kind: ParsedConstantExpressionKind::Primitive(value),
2149 span: source_span(source, span),
2150 })
2151}
2152
2153fn parsed_logical_expression(
2154 expression: &Expression<'_>,
2155 source: &str,
2156) -> Option<ParsedConstantExpression> {
2157 let Expression::LogicalExpression(logical) = expression else {
2158 return None;
2159 };
2160 let operator = match logical.operator.as_str() {
2161 "&&" => ParsedLogicalOperator::And,
2162 "||" => ParsedLogicalOperator::Or,
2163 _ => return None,
2164 };
2165
2166 Some(ParsedConstantExpression {
2167 kind: ParsedConstantExpressionKind::Logical {
2168 operator,
2169 left: Box::new(parsed_boolean_constant_expression(&logical.left, source)?),
2170 right: Box::new(parsed_boolean_constant_expression(&logical.right, source)?),
2171 },
2172 span: source_span(source, logical.span),
2173 })
2174}
2175
2176fn parsed_nullish_coalescing_expression(
2177 expression: &Expression<'_>,
2178 source: &str,
2179) -> Option<ParsedConstantExpression> {
2180 let Expression::LogicalExpression(logical) = expression else {
2181 return None;
2182 };
2183 if logical.operator.as_str() != "??" {
2184 return None;
2185 }
2186
2187 Some(ParsedConstantExpression {
2188 kind: ParsedConstantExpressionKind::NullishCoalescing {
2189 left: Box::new(parsed_nullish_constant_expression(&logical.left, source)?),
2190 right: Box::new(parsed_nullish_constant_expression(&logical.right, source)?),
2191 },
2192 span: source_span(source, logical.span),
2193 })
2194}
2195
2196fn parsed_boolean_constant_expression(
2197 expression: &Expression<'_>,
2198 source: &str,
2199) -> Option<ParsedConstantExpression> {
2200 let expression = parsed_constant_expression(expression, source)?;
2201 matches!(
2202 expression.kind,
2203 ParsedConstantExpressionKind::Boolean(_)
2204 | ParsedConstantExpressionKind::Comparison { .. }
2205 | ParsedConstantExpressionKind::Logical { .. }
2206 )
2207 .then_some(expression)
2208}
2209
2210fn parsed_nullish_constant_expression(
2211 expression: &Expression<'_>,
2212 source: &str,
2213) -> Option<ParsedConstantExpression> {
2214 let expression = parsed_constant_expression(expression, source)?;
2215 matches!(
2216 expression.kind,
2217 ParsedConstantExpressionKind::Primitive(_)
2218 | ParsedConstantExpressionKind::Boolean(_)
2219 | ParsedConstantExpressionKind::Arithmetic(_)
2220 | ParsedConstantExpressionKind::Comparison { .. }
2221 | ParsedConstantExpressionKind::Logical { .. }
2222 | ParsedConstantExpressionKind::NullishCoalescing { .. }
2223 )
2224 .then_some(expression)
2225}
2226
2227fn parsed_comparison_expression(
2228 expression: &Expression<'_>,
2229 source: &str,
2230) -> Option<ParsedConstantExpression> {
2231 if let Expression::ParenthesizedExpression(parenthesized) = expression {
2232 return parsed_comparison_expression(&parenthesized.expression, source);
2233 }
2234 let Expression::BinaryExpression(binary) = expression else {
2235 return None;
2236 };
2237 let operator = match binary.operator.as_str() {
2238 "===" => ParsedComparisonOperator::Equal,
2239 "!==" => ParsedComparisonOperator::NotEqual,
2240 "<" => ParsedComparisonOperator::LessThan,
2241 "<=" => ParsedComparisonOperator::LessThanOrEqual,
2242 ">" => ParsedComparisonOperator::GreaterThan,
2243 ">=" => ParsedComparisonOperator::GreaterThanOrEqual,
2244 _ => return None,
2245 };
2246
2247 Some(ParsedConstantExpression {
2248 kind: ParsedConstantExpressionKind::Comparison {
2249 operator,
2250 left: parsed_arithmetic_expression(&binary.left, source)?,
2251 right: parsed_arithmetic_expression(&binary.right, source)?,
2252 },
2253 span: source_span(source, binary.span),
2254 })
2255}
2256
2257fn parsed_arithmetic_expression(
2258 expression: &Expression<'_>,
2259 source: &str,
2260) -> Option<ParsedArithmeticExpression> {
2261 match expression {
2262 Expression::ParenthesizedExpression(parenthesized) => {
2263 parsed_arithmetic_expression(&parenthesized.expression, source)
2264 }
2265 Expression::NumericLiteral(literal) => Some(ParsedArithmeticExpression {
2266 kind: ParsedArithmeticExpressionKind::Number(literal.raw.as_ref()?.to_string()),
2267 span: source_span(source, literal.span),
2268 }),
2269 Expression::BinaryExpression(binary) => {
2270 let operator = match binary.operator.as_str() {
2271 "+" => ParsedArithmeticOperator::Add,
2272 "-" => ParsedArithmeticOperator::Subtract,
2273 "*" => ParsedArithmeticOperator::Multiply,
2274 "/" => ParsedArithmeticOperator::Divide,
2275 "%" => ParsedArithmeticOperator::Remainder,
2276 _ => return None,
2277 };
2278 let left = parsed_arithmetic_expression(&binary.left, source)?;
2279 let right = parsed_arithmetic_expression(&binary.right, source)?;
2280
2281 Some(ParsedArithmeticExpression {
2282 kind: ParsedArithmeticExpressionKind::Binary {
2283 operator,
2284 left: Box::new(left),
2285 right: Box::new(right),
2286 },
2287 span: source_span(source, binary.span),
2288 })
2289 }
2290 _ => None,
2291 }
2292}
2293
2294fn state_argument_literal(argument: &Argument<'_>) -> Option<ParsedSerializableValue> {
2295 serializable_value_from_expression(argument.as_expression()?)
2296}
2297
2298fn serializable_value_from_expression(
2299 expression: &Expression<'_>,
2300) -> Option<ParsedSerializableValue> {
2301 match expression {
2302 Expression::NullLiteral(_) => Some(ParsedSerializableValue::Null),
2303 Expression::NumericLiteral(literal) => literal
2304 .raw
2305 .as_ref()
2306 .map(ToString::to_string)
2307 .map(ParsedSerializableValue::Number),
2308 Expression::StringLiteral(literal) => {
2309 Some(ParsedSerializableValue::String(literal.value.to_string()))
2310 }
2311 Expression::BooleanLiteral(literal) => {
2312 Some(ParsedSerializableValue::Boolean(literal.value))
2313 }
2314 Expression::ArrayExpression(array) => array
2315 .elements
2316 .iter()
2317 .map(|element| serializable_value_from_expression(element.as_expression()?))
2318 .collect::<Option<Vec<_>>>()
2319 .map(ParsedSerializableValue::Array),
2320 Expression::ObjectExpression(object) => object
2321 .properties
2322 .iter()
2323 .map(|property| {
2324 let ObjectPropertyKind::ObjectProperty(property) = property else {
2325 return None;
2326 };
2327
2328 if property.computed || property.method || property.kind != PropertyKind::Init {
2329 return None;
2330 }
2331
2332 let key = object_property_key_name(&property.key)?;
2333 let value = serializable_value_from_expression(&property.value)?;
2334 Some((key, value))
2335 })
2336 .collect::<Option<BTreeMap<_, _>>>()
2337 .map(ParsedSerializableValue::Object),
2338 _ => None,
2339 }
2340}
2341
2342fn object_property_key_name(key: &PropertyKey<'_>) -> Option<String> {
2343 match key {
2344 PropertyKey::StaticIdentifier(_)
2345 | PropertyKey::StringLiteral(_)
2346 | PropertyKey::NumericLiteral(_) => property_key_name(key),
2347 PropertyKey::PrivateIdentifier(_) => None,
2348 _ => None,
2349 }
2350}
2351
2352fn toggled_this_field(expression: &Expression<'_>) -> Option<String> {
2353 let Expression::UnaryExpression(unary) = expression else {
2354 return None;
2355 };
2356
2357 if unary.operator.as_str() != "!" {
2358 return None;
2359 }
2360
2361 this_member_expression_field(&unary.argument)
2362}
2363
2364fn this_member_expression_field(expression: &Expression<'_>) -> Option<String> {
2365 let Expression::StaticMemberExpression(member) = expression else {
2366 return None;
2367 };
2368
2369 let Expression::ThisExpression(_) = &member.object else {
2370 return None;
2371 };
2372
2373 Some(member.property.name.to_string())
2374}
2375
2376fn jsx_element_name(name: &JSXElementName<'_>) -> Option<String> {
2377 match name {
2378 JSXElementName::Identifier(identifier) => Some(identifier.name.to_string()),
2379 JSXElementName::IdentifierReference(identifier) => Some(identifier.name.to_string()),
2380 JSXElementName::NamespacedName(namespaced) => Some(format!(
2381 "{}:{}",
2382 namespaced.namespace.name, namespaced.name.name
2383 )),
2384 JSXElementName::MemberExpression(member) => Some(jsx_member_expression_name(member)),
2385 JSXElementName::ThisExpression(_) => Some("this".to_string()),
2386 }
2387}
2388
2389fn jsx_member_expression_name(member: &JSXMemberExpression<'_>) -> String {
2390 let object = match &member.object {
2391 JSXMemberExpressionObject::IdentifierReference(identifier) => identifier.name.to_string(),
2392 JSXMemberExpressionObject::MemberExpression(member) => jsx_member_expression_name(member),
2393 JSXMemberExpressionObject::ThisExpression(_) => "this".to_string(),
2394 };
2395 format!("{object}.{}", member.property.name)
2396}
2397
2398fn parsed_jsx_attribute(attribute: &JSXAttributeItem<'_>, source: &str) -> ParsedJsxAttribute {
2399 match attribute {
2400 JSXAttributeItem::Attribute(attribute) => {
2401 let name = jsx_attribute_name(&attribute.name);
2402 let expression = attribute.value.as_ref().and_then(|value| match value {
2403 JSXAttributeValue::ExpressionContainer(container) => {
2404 container.expression.as_expression()
2405 }
2406 _ => None,
2407 });
2408 let value = match &attribute.value {
2409 Some(JSXAttributeValue::StringLiteral(literal)) => {
2410 ParsedJsxAttributeValue::Static(literal.value.to_string())
2411 }
2412 Some(JSXAttributeValue::ExpressionContainer(container)) => {
2413 ParsedJsxAttributeValue::Expression(jsx_expression_summary(
2414 &container.expression,
2415 ))
2416 }
2417 Some(JSXAttributeValue::Element(_)) | Some(JSXAttributeValue::Fragment(_)) => {
2418 ParsedJsxAttributeValue::Unsupported
2419 }
2420 None => ParsedJsxAttributeValue::Boolean,
2421 };
2422
2423 ParsedJsxAttribute {
2424 name,
2425 value,
2426 name_span: source_span(source, attribute.name.span()),
2427 value_span: attribute
2428 .value
2429 .as_ref()
2430 .map(|value| source_span(source, value.span())),
2431 expression_span: expression
2432 .map(|expression| source_span(source, expression.span())),
2433 this_member: expression
2434 .and_then(|expression| parsed_this_member_expression(expression, source)),
2435 constant_value: match &attribute.value {
2436 Some(JSXAttributeValue::StringLiteral(literal)) => {
2437 Some(ParsedSerializableValue::String(literal.value.to_string()))
2438 }
2439 Some(JSXAttributeValue::ExpressionContainer(container)) => container
2440 .expression
2441 .as_expression()
2442 .and_then(serializable_value_from_expression),
2443 _ => None,
2444 },
2445 span: source_span(source, attribute.span),
2446 }
2447 }
2448 JSXAttributeItem::SpreadAttribute(spread) => ParsedJsxAttribute {
2449 name: "{...}".to_string(),
2450 value: ParsedJsxAttributeValue::Spread(expression_summary(&spread.argument)),
2451 name_span: source_span(source, spread.span),
2452 value_span: Some(source_span(source, spread.argument.span())),
2453 expression_span: Some(source_span(source, spread.argument.span())),
2454 this_member: parsed_this_member_expression(&spread.argument, source),
2455 constant_value: serializable_value_from_expression(&spread.argument),
2456 span: source_span(source, spread.span),
2457 },
2458 }
2459}
2460
2461fn jsx_attribute_name(name: &JSXAttributeName<'_>) -> String {
2462 match name {
2463 JSXAttributeName::Identifier(identifier) => identifier.name.to_string(),
2464 JSXAttributeName::NamespacedName(namespaced) => {
2465 format!("{}:{}", namespaced.namespace.name, namespaced.name.name)
2466 }
2467 }
2468}
2469
2470fn jsx_event_handler(attribute: &JSXAttributeItem<'_>, source: &str) -> Option<ParsedEventHandler> {
2471 let JSXAttributeItem::Attribute(attribute) = attribute else {
2472 return None;
2473 };
2474
2475 let attribute_name = match &attribute.name {
2476 JSXAttributeName::Identifier(identifier) => identifier.name.as_str(),
2477 JSXAttributeName::NamespacedName(_) => return None,
2478 };
2479
2480 let event = jsx_event_type(attribute_name)?;
2481
2482 let Some(JSXAttributeValue::ExpressionContainer(container)) = &attribute.value else {
2483 return None;
2484 };
2485
2486 let (handler, arguments) = jsx_expression_event_handler_ref(&container.expression)?;
2487
2488 Some(ParsedEventHandler {
2489 event,
2490 handler,
2491 arguments,
2492 span: source_span(source, attribute.span),
2493 })
2494}
2495
2496fn jsx_event_type(attribute_name: &str) -> Option<String> {
2497 let name = attribute_name.strip_prefix("on")?;
2498
2499 if !name.chars().next().is_some_and(char::is_uppercase) {
2500 return None;
2501 }
2502
2503 let mut chars = name.chars();
2504 let first = chars.next()?.to_ascii_lowercase();
2505 let rest = chars.collect::<String>();
2506
2507 Some(format!("{first}{rest}"))
2508}
2509
2510fn jsx_expression_event_handler_ref(
2511 expression: &JSXExpression<'_>,
2512) -> Option<(String, Vec<ParsedSerializableValue>)> {
2513 let expression = expression.as_expression()?;
2514 expression_event_handler_ref(expression)
2515}
2516
2517fn expression_event_handler_ref(
2518 expression: &Expression<'_>,
2519) -> Option<(String, Vec<ParsedSerializableValue>)> {
2520 match expression {
2521 Expression::ArrowFunctionExpression(arrow) => {
2522 for statement in &arrow.body.statements {
2523 if let Statement::ExpressionStatement(statement) = statement {
2524 if let Some(reference) = expression_event_handler_ref(&statement.expression) {
2525 return Some(reference);
2526 }
2527 }
2528 }
2529
2530 None
2531 }
2532 Expression::CallExpression(call) => Some((
2533 expression_summary(&call.callee)?,
2534 call.arguments
2535 .iter()
2536 .map(|argument| serializable_value_from_expression(argument.as_expression()?))
2537 .collect::<Option<Vec<_>>>()?,
2538 )),
2539 Expression::StaticMemberExpression(_) => {
2540 Some((expression_summary(expression)?, Vec::new()))
2541 }
2542 _ => None,
2543 }
2544}
2545
2546fn parse_diagnostic(source: &str, diagnostic: &oxc_diagnostics::OxcDiagnostic) -> ParseDiagnostic {
2547 let severity = match diagnostic.severity {
2548 OxcSeverity::Advice => ParseSeverity::Info,
2549 OxcSeverity::Warning => ParseSeverity::Warning,
2550 OxcSeverity::Error => ParseSeverity::Error,
2551 };
2552
2553 let labels = diagnostic
2554 .labels
2555 .as_ref()
2556 .map(|labels| {
2557 labels
2558 .iter()
2559 .map(|label| {
2560 let start = label.offset();
2561 let end = start + label.len();
2562
2563 ParseLabel {
2564 span: source_span_from_offsets(source, start, end),
2565 }
2566 })
2567 .collect::<Vec<_>>()
2568 })
2569 .unwrap_or_default();
2570
2571 ParseDiagnostic {
2572 message: diagnostic.message.to_string(),
2573 severity,
2574 labels,
2575 }
2576}
2577
2578fn source_span(source: &str, span: Span) -> SourceSpan {
2579 source_span_from_offsets(source, span.start as usize, span.end as usize)
2580}
2581
2582fn source_span_from_offsets(source: &str, start: usize, end: usize) -> SourceSpan {
2583 let (line, column) = line_column_at(source, start);
2584
2585 SourceSpan {
2586 start,
2587 end,
2588 line,
2589 column,
2590 }
2591}
2592
2593fn line_column_at(source: &str, offset: usize) -> (usize, usize) {
2594 let clamped = offset.min(source.len());
2595 let prefix = &source[..clamped];
2596
2597 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
2598
2599 let column = prefix
2600 .rsplit_once('\n')
2601 .map_or(prefix.len() + 1, |(_, tail)| tail.len() + 1);
2602
2603 (line, column)
2604}