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 let decoded = html_escape::decode_html_entities(value);
1924 let normalized = decoded.split_whitespace().collect::<Vec<_>>().join(" ");
1925 if normalized.is_empty() {
1926 return if decoded.is_empty() || decoded.contains(['\n', '\r']) {
1931 String::new()
1932 } else {
1933 " ".to_owned()
1934 };
1935 }
1936 if decoded.contains(['\n', '\r']) {
1937 return normalized;
1938 }
1939
1940 let leading = decoded.chars().next().is_some_and(char::is_whitespace);
1941 let trailing = decoded.chars().next_back().is_some_and(char::is_whitespace);
1942 format!(
1943 "{}{}{}",
1944 if leading { " " } else { "" },
1945 normalized,
1946 if trailing { " " } else { "" },
1947 )
1948}
1949
1950fn jsx_text_value_span(source: &str, value: &str, span: Span) -> SourceSpan {
1951 let leading_whitespace = value
1952 .char_indices()
1953 .find(|(_, character)| !character.is_whitespace())
1954 .map_or(value.len(), |(index, _)| index);
1955 let trailing_whitespace = value
1956 .char_indices()
1957 .rev()
1958 .find(|(_, character)| !character.is_whitespace())
1959 .map_or(value.len(), |(index, character)| {
1960 index + character.len_utf8()
1961 });
1962
1963 source_span_from_offsets(
1964 source,
1965 span.start as usize + leading_whitespace,
1966 span.start as usize + trailing_whitespace,
1967 )
1968}
1969
1970fn property_key_name(key: &PropertyKey<'_>) -> Option<String> {
1971 match key {
1972 PropertyKey::StaticIdentifier(identifier) => Some(identifier.name.to_string()),
1973 PropertyKey::PrivateIdentifier(identifier) => Some(format!("#{}", identifier.name)),
1974 PropertyKey::StringLiteral(literal) => Some(literal.value.to_string()),
1975 PropertyKey::NumericLiteral(literal) => Some(literal.raw.as_ref()?.to_string()),
1976 _ => None,
1977 }
1978}
1979
1980fn jsx_expression_summary(expression: &JSXExpression<'_>) -> Option<String> {
1981 if let Some(expression) = expression.as_expression() {
1982 return expression_summary(expression);
1983 }
1984
1985 None
1986}
1987
1988fn jsx_expression_binding_summary(expression: &JSXExpression<'_>) -> Option<String> {
1989 let expression = expression.as_expression()?;
1990
1991 if let Expression::ConditionalExpression(conditional) = expression {
1992 return expression_summary(&conditional.test);
1993 }
1994
1995 if let Expression::LogicalExpression(logical) = expression {
1996 if logical.operator.as_str() == "&&" {
1997 return expression_summary(&logical.left);
1998 }
1999 }
2000
2001 if let Expression::CallExpression(call) = expression {
2002 if let Some(iterable) = list_iterable_dependency(call) {
2003 return Some(iterable);
2004 }
2005 }
2006
2007 expression_summary(expression)
2008}
2009
2010fn list_iterable_dependency(call: &oxc_ast::ast::CallExpression<'_>) -> Option<String> {
2011 let Expression::StaticMemberExpression(member) = &call.callee else {
2012 return None;
2013 };
2014
2015 if member.property.name != "map" {
2016 return None;
2017 }
2018
2019 expression_summary(&member.object)
2020}
2021
2022fn expression_summary(expression: &Expression<'_>) -> Option<String> {
2023 match expression {
2024 Expression::CallExpression(call) => {
2025 let callee = expression_summary(&call.callee)?;
2026 Some(format!("{callee}(...)"))
2027 }
2028 Expression::Identifier(identifier) => Some(identifier.name.to_string()),
2029 Expression::ThisExpression(_) => Some("this".to_string()),
2030 Expression::StaticMemberExpression(member) => {
2031 let object = expression_summary(&member.object)?;
2032 Some(format!("{object}.{}", member.property.name))
2033 }
2034 Expression::NumericLiteral(literal) => Some(literal.raw.as_ref()?.to_string()),
2035 Expression::StringLiteral(literal) => Some(format!("{:?}", literal.value.as_str())),
2036 _ => None,
2037 }
2038}
2039
2040fn state_initial_value(expression: &Expression<'_>) -> Option<ParsedSerializableValue> {
2041 let Expression::CallExpression(call) = expression else {
2042 return None;
2043 };
2044
2045 let Expression::Identifier(callee) = &call.callee else {
2046 return None;
2047 };
2048
2049 if callee.name != "state" {
2050 return None;
2051 }
2052
2053 call.arguments.first().and_then(state_argument_literal)
2054}
2055
2056fn state_initial_constant_expression(
2057 expression: &Expression<'_>,
2058 source: &str,
2059) -> Option<ParsedConstantExpression> {
2060 let Expression::CallExpression(call) = expression else {
2061 return None;
2062 };
2063 let Expression::Identifier(callee) = &call.callee else {
2064 return None;
2065 };
2066 if callee.name != "state" || call.arguments.len() != 1 {
2067 return None;
2068 }
2069
2070 let expression = parsed_constant_expression(call.arguments[0].as_expression()?, source)?;
2071 (!matches!(
2072 expression.kind,
2073 ParsedConstantExpressionKind::Primitive(_) | ParsedConstantExpressionKind::Boolean(_)
2074 ))
2075 .then_some(expression)
2076}
2077
2078fn parsed_constant_expression(
2079 expression: &Expression<'_>,
2080 source: &str,
2081) -> Option<ParsedConstantExpression> {
2082 if let Expression::ParenthesizedExpression(parenthesized) = expression {
2083 return parsed_constant_expression(&parenthesized.expression, source);
2084 }
2085 if let Expression::BooleanLiteral(literal) = expression {
2086 return Some(ParsedConstantExpression {
2087 kind: ParsedConstantExpressionKind::Boolean(literal.value),
2088 span: source_span(source, literal.span),
2089 });
2090 }
2091 if let Some(primitive) = parsed_primitive_constant_expression(expression, source) {
2092 return Some(primitive);
2093 }
2094 if let Some(unary) = parsed_unary_constant_expression(expression, source) {
2095 return Some(unary);
2096 }
2097 if let Some(logical) = parsed_logical_expression(expression, source) {
2098 return Some(logical);
2099 }
2100 if let Some(comparison) = parsed_comparison_expression(expression, source) {
2101 return Some(comparison);
2102 }
2103 if let Some(nullish) = parsed_nullish_coalescing_expression(expression, source) {
2104 return Some(nullish);
2105 }
2106
2107 let arithmetic = parsed_arithmetic_expression(expression, source)?;
2108 matches!(
2109 arithmetic.kind,
2110 ParsedArithmeticExpressionKind::Binary { .. }
2111 )
2112 .then_some(ParsedConstantExpression {
2113 span: arithmetic.span,
2114 kind: ParsedConstantExpressionKind::Arithmetic(arithmetic),
2115 })
2116}
2117
2118fn parsed_unary_constant_expression(
2119 expression: &Expression<'_>,
2120 source: &str,
2121) -> Option<ParsedConstantExpression> {
2122 let Expression::UnaryExpression(unary) = expression else {
2123 return None;
2124 };
2125 let operator = match unary.operator.as_str() {
2126 "!" => ParsedUnaryOperator::Not,
2127 "+" => ParsedUnaryOperator::Plus,
2128 "-" => ParsedUnaryOperator::Minus,
2129 _ => return None,
2130 };
2131 let operand = parsed_constant_expression(&unary.argument, source)?;
2132 let valid = match operator {
2133 ParsedUnaryOperator::Not => matches!(
2134 operand.kind,
2135 ParsedConstantExpressionKind::Boolean(_)
2136 | ParsedConstantExpressionKind::Comparison { .. }
2137 | ParsedConstantExpressionKind::Logical { .. }
2138 ),
2139 ParsedUnaryOperator::Plus | ParsedUnaryOperator::Minus => matches!(
2140 operand.kind,
2141 ParsedConstantExpressionKind::Primitive(ParsedSerializableValue::Number(_))
2142 | ParsedConstantExpressionKind::Arithmetic(_)
2143 ),
2144 };
2145 valid.then_some(ParsedConstantExpression {
2146 kind: ParsedConstantExpressionKind::Unary {
2147 operator,
2148 operand: Box::new(operand),
2149 },
2150 span: source_span(source, unary.span),
2151 })
2152}
2153
2154fn parsed_primitive_constant_expression(
2155 expression: &Expression<'_>,
2156 source: &str,
2157) -> Option<ParsedConstantExpression> {
2158 let (value, span) = match expression {
2159 Expression::NullLiteral(literal) => (ParsedSerializableValue::Null, literal.span),
2160 Expression::NumericLiteral(literal) => (
2161 ParsedSerializableValue::Number(literal.raw.as_ref()?.to_string()),
2162 literal.span,
2163 ),
2164 Expression::StringLiteral(literal) => (
2165 ParsedSerializableValue::String(literal.value.to_string()),
2166 literal.span,
2167 ),
2168 _ => return None,
2169 };
2170
2171 Some(ParsedConstantExpression {
2172 kind: ParsedConstantExpressionKind::Primitive(value),
2173 span: source_span(source, span),
2174 })
2175}
2176
2177fn parsed_logical_expression(
2178 expression: &Expression<'_>,
2179 source: &str,
2180) -> Option<ParsedConstantExpression> {
2181 let Expression::LogicalExpression(logical) = expression else {
2182 return None;
2183 };
2184 let operator = match logical.operator.as_str() {
2185 "&&" => ParsedLogicalOperator::And,
2186 "||" => ParsedLogicalOperator::Or,
2187 _ => return None,
2188 };
2189
2190 Some(ParsedConstantExpression {
2191 kind: ParsedConstantExpressionKind::Logical {
2192 operator,
2193 left: Box::new(parsed_boolean_constant_expression(&logical.left, source)?),
2194 right: Box::new(parsed_boolean_constant_expression(&logical.right, source)?),
2195 },
2196 span: source_span(source, logical.span),
2197 })
2198}
2199
2200fn parsed_nullish_coalescing_expression(
2201 expression: &Expression<'_>,
2202 source: &str,
2203) -> Option<ParsedConstantExpression> {
2204 let Expression::LogicalExpression(logical) = expression else {
2205 return None;
2206 };
2207 if logical.operator.as_str() != "??" {
2208 return None;
2209 }
2210
2211 Some(ParsedConstantExpression {
2212 kind: ParsedConstantExpressionKind::NullishCoalescing {
2213 left: Box::new(parsed_nullish_constant_expression(&logical.left, source)?),
2214 right: Box::new(parsed_nullish_constant_expression(&logical.right, source)?),
2215 },
2216 span: source_span(source, logical.span),
2217 })
2218}
2219
2220fn parsed_boolean_constant_expression(
2221 expression: &Expression<'_>,
2222 source: &str,
2223) -> Option<ParsedConstantExpression> {
2224 let expression = parsed_constant_expression(expression, source)?;
2225 matches!(
2226 expression.kind,
2227 ParsedConstantExpressionKind::Boolean(_)
2228 | ParsedConstantExpressionKind::Comparison { .. }
2229 | ParsedConstantExpressionKind::Logical { .. }
2230 )
2231 .then_some(expression)
2232}
2233
2234fn parsed_nullish_constant_expression(
2235 expression: &Expression<'_>,
2236 source: &str,
2237) -> Option<ParsedConstantExpression> {
2238 let expression = parsed_constant_expression(expression, source)?;
2239 matches!(
2240 expression.kind,
2241 ParsedConstantExpressionKind::Primitive(_)
2242 | ParsedConstantExpressionKind::Boolean(_)
2243 | ParsedConstantExpressionKind::Arithmetic(_)
2244 | ParsedConstantExpressionKind::Comparison { .. }
2245 | ParsedConstantExpressionKind::Logical { .. }
2246 | ParsedConstantExpressionKind::NullishCoalescing { .. }
2247 )
2248 .then_some(expression)
2249}
2250
2251fn parsed_comparison_expression(
2252 expression: &Expression<'_>,
2253 source: &str,
2254) -> Option<ParsedConstantExpression> {
2255 if let Expression::ParenthesizedExpression(parenthesized) = expression {
2256 return parsed_comparison_expression(&parenthesized.expression, source);
2257 }
2258 let Expression::BinaryExpression(binary) = expression else {
2259 return None;
2260 };
2261 let operator = match binary.operator.as_str() {
2262 "===" => ParsedComparisonOperator::Equal,
2263 "!==" => ParsedComparisonOperator::NotEqual,
2264 "<" => ParsedComparisonOperator::LessThan,
2265 "<=" => ParsedComparisonOperator::LessThanOrEqual,
2266 ">" => ParsedComparisonOperator::GreaterThan,
2267 ">=" => ParsedComparisonOperator::GreaterThanOrEqual,
2268 _ => return None,
2269 };
2270
2271 Some(ParsedConstantExpression {
2272 kind: ParsedConstantExpressionKind::Comparison {
2273 operator,
2274 left: parsed_arithmetic_expression(&binary.left, source)?,
2275 right: parsed_arithmetic_expression(&binary.right, source)?,
2276 },
2277 span: source_span(source, binary.span),
2278 })
2279}
2280
2281fn parsed_arithmetic_expression(
2282 expression: &Expression<'_>,
2283 source: &str,
2284) -> Option<ParsedArithmeticExpression> {
2285 match expression {
2286 Expression::ParenthesizedExpression(parenthesized) => {
2287 parsed_arithmetic_expression(&parenthesized.expression, source)
2288 }
2289 Expression::NumericLiteral(literal) => Some(ParsedArithmeticExpression {
2290 kind: ParsedArithmeticExpressionKind::Number(literal.raw.as_ref()?.to_string()),
2291 span: source_span(source, literal.span),
2292 }),
2293 Expression::BinaryExpression(binary) => {
2294 let operator = match binary.operator.as_str() {
2295 "+" => ParsedArithmeticOperator::Add,
2296 "-" => ParsedArithmeticOperator::Subtract,
2297 "*" => ParsedArithmeticOperator::Multiply,
2298 "/" => ParsedArithmeticOperator::Divide,
2299 "%" => ParsedArithmeticOperator::Remainder,
2300 _ => return None,
2301 };
2302 let left = parsed_arithmetic_expression(&binary.left, source)?;
2303 let right = parsed_arithmetic_expression(&binary.right, source)?;
2304
2305 Some(ParsedArithmeticExpression {
2306 kind: ParsedArithmeticExpressionKind::Binary {
2307 operator,
2308 left: Box::new(left),
2309 right: Box::new(right),
2310 },
2311 span: source_span(source, binary.span),
2312 })
2313 }
2314 _ => None,
2315 }
2316}
2317
2318fn state_argument_literal(argument: &Argument<'_>) -> Option<ParsedSerializableValue> {
2319 serializable_value_from_expression(argument.as_expression()?)
2320}
2321
2322fn serializable_value_from_expression(
2323 expression: &Expression<'_>,
2324) -> Option<ParsedSerializableValue> {
2325 match expression {
2326 Expression::NullLiteral(_) => Some(ParsedSerializableValue::Null),
2327 Expression::NumericLiteral(literal) => literal
2328 .raw
2329 .as_ref()
2330 .map(ToString::to_string)
2331 .map(ParsedSerializableValue::Number),
2332 Expression::StringLiteral(literal) => {
2333 Some(ParsedSerializableValue::String(literal.value.to_string()))
2334 }
2335 Expression::BooleanLiteral(literal) => {
2336 Some(ParsedSerializableValue::Boolean(literal.value))
2337 }
2338 Expression::ArrayExpression(array) => array
2339 .elements
2340 .iter()
2341 .map(|element| serializable_value_from_expression(element.as_expression()?))
2342 .collect::<Option<Vec<_>>>()
2343 .map(ParsedSerializableValue::Array),
2344 Expression::ObjectExpression(object) => object
2345 .properties
2346 .iter()
2347 .map(|property| {
2348 let ObjectPropertyKind::ObjectProperty(property) = property else {
2349 return None;
2350 };
2351
2352 if property.computed || property.method || property.kind != PropertyKind::Init {
2353 return None;
2354 }
2355
2356 let key = object_property_key_name(&property.key)?;
2357 let value = serializable_value_from_expression(&property.value)?;
2358 Some((key, value))
2359 })
2360 .collect::<Option<BTreeMap<_, _>>>()
2361 .map(ParsedSerializableValue::Object),
2362 _ => None,
2363 }
2364}
2365
2366fn object_property_key_name(key: &PropertyKey<'_>) -> Option<String> {
2367 match key {
2368 PropertyKey::StaticIdentifier(_)
2369 | PropertyKey::StringLiteral(_)
2370 | PropertyKey::NumericLiteral(_) => property_key_name(key),
2371 PropertyKey::PrivateIdentifier(_) => None,
2372 _ => None,
2373 }
2374}
2375
2376fn toggled_this_field(expression: &Expression<'_>) -> Option<String> {
2377 let Expression::UnaryExpression(unary) = expression else {
2378 return None;
2379 };
2380
2381 if unary.operator.as_str() != "!" {
2382 return None;
2383 }
2384
2385 this_member_expression_field(&unary.argument)
2386}
2387
2388fn this_member_expression_field(expression: &Expression<'_>) -> Option<String> {
2389 let Expression::StaticMemberExpression(member) = expression else {
2390 return None;
2391 };
2392
2393 let Expression::ThisExpression(_) = &member.object else {
2394 return None;
2395 };
2396
2397 Some(member.property.name.to_string())
2398}
2399
2400fn jsx_element_name(name: &JSXElementName<'_>) -> Option<String> {
2401 match name {
2402 JSXElementName::Identifier(identifier) => Some(identifier.name.to_string()),
2403 JSXElementName::IdentifierReference(identifier) => Some(identifier.name.to_string()),
2404 JSXElementName::NamespacedName(namespaced) => Some(format!(
2405 "{}:{}",
2406 namespaced.namespace.name, namespaced.name.name
2407 )),
2408 JSXElementName::MemberExpression(member) => Some(jsx_member_expression_name(member)),
2409 JSXElementName::ThisExpression(_) => Some("this".to_string()),
2410 }
2411}
2412
2413fn jsx_member_expression_name(member: &JSXMemberExpression<'_>) -> String {
2414 let object = match &member.object {
2415 JSXMemberExpressionObject::IdentifierReference(identifier) => identifier.name.to_string(),
2416 JSXMemberExpressionObject::MemberExpression(member) => jsx_member_expression_name(member),
2417 JSXMemberExpressionObject::ThisExpression(_) => "this".to_string(),
2418 };
2419 format!("{object}.{}", member.property.name)
2420}
2421
2422fn parsed_jsx_attribute(attribute: &JSXAttributeItem<'_>, source: &str) -> ParsedJsxAttribute {
2423 match attribute {
2424 JSXAttributeItem::Attribute(attribute) => {
2425 let name = jsx_attribute_name(&attribute.name);
2426 let expression = attribute.value.as_ref().and_then(|value| match value {
2427 JSXAttributeValue::ExpressionContainer(container) => {
2428 container.expression.as_expression()
2429 }
2430 _ => None,
2431 });
2432 let value = match &attribute.value {
2433 Some(JSXAttributeValue::StringLiteral(literal)) => {
2434 ParsedJsxAttributeValue::Static(literal.value.to_string())
2435 }
2436 Some(JSXAttributeValue::ExpressionContainer(container)) => {
2437 ParsedJsxAttributeValue::Expression(jsx_expression_summary(
2438 &container.expression,
2439 ))
2440 }
2441 Some(JSXAttributeValue::Element(_)) | Some(JSXAttributeValue::Fragment(_)) => {
2442 ParsedJsxAttributeValue::Unsupported
2443 }
2444 None => ParsedJsxAttributeValue::Boolean,
2445 };
2446
2447 ParsedJsxAttribute {
2448 name,
2449 value,
2450 name_span: source_span(source, attribute.name.span()),
2451 value_span: attribute
2452 .value
2453 .as_ref()
2454 .map(|value| source_span(source, value.span())),
2455 expression_span: expression
2456 .map(|expression| source_span(source, expression.span())),
2457 this_member: expression
2458 .and_then(|expression| parsed_this_member_expression(expression, source)),
2459 constant_value: match &attribute.value {
2460 Some(JSXAttributeValue::StringLiteral(literal)) => {
2461 Some(ParsedSerializableValue::String(literal.value.to_string()))
2462 }
2463 Some(JSXAttributeValue::ExpressionContainer(container)) => container
2464 .expression
2465 .as_expression()
2466 .and_then(serializable_value_from_expression),
2467 _ => None,
2468 },
2469 span: source_span(source, attribute.span),
2470 }
2471 }
2472 JSXAttributeItem::SpreadAttribute(spread) => ParsedJsxAttribute {
2473 name: "{...}".to_string(),
2474 value: ParsedJsxAttributeValue::Spread(expression_summary(&spread.argument)),
2475 name_span: source_span(source, spread.span),
2476 value_span: Some(source_span(source, spread.argument.span())),
2477 expression_span: Some(source_span(source, spread.argument.span())),
2478 this_member: parsed_this_member_expression(&spread.argument, source),
2479 constant_value: serializable_value_from_expression(&spread.argument),
2480 span: source_span(source, spread.span),
2481 },
2482 }
2483}
2484
2485fn jsx_attribute_name(name: &JSXAttributeName<'_>) -> String {
2486 match name {
2487 JSXAttributeName::Identifier(identifier) => identifier.name.to_string(),
2488 JSXAttributeName::NamespacedName(namespaced) => {
2489 format!("{}:{}", namespaced.namespace.name, namespaced.name.name)
2490 }
2491 }
2492}
2493
2494fn jsx_event_handler(attribute: &JSXAttributeItem<'_>, source: &str) -> Option<ParsedEventHandler> {
2495 let JSXAttributeItem::Attribute(attribute) = attribute else {
2496 return None;
2497 };
2498
2499 let attribute_name = match &attribute.name {
2500 JSXAttributeName::Identifier(identifier) => identifier.name.as_str(),
2501 JSXAttributeName::NamespacedName(_) => return None,
2502 };
2503
2504 let event = jsx_event_type(attribute_name)?;
2505
2506 let Some(JSXAttributeValue::ExpressionContainer(container)) = &attribute.value else {
2507 return None;
2508 };
2509
2510 let (handler, arguments) = jsx_expression_event_handler_ref(&container.expression)?;
2511
2512 Some(ParsedEventHandler {
2513 event,
2514 handler,
2515 arguments,
2516 span: source_span(source, attribute.span),
2517 })
2518}
2519
2520fn jsx_event_type(attribute_name: &str) -> Option<String> {
2521 let name = attribute_name.strip_prefix("on")?;
2522
2523 if !name.chars().next().is_some_and(char::is_uppercase) {
2524 return None;
2525 }
2526
2527 let mut chars = name.chars();
2528 let first = chars.next()?.to_ascii_lowercase();
2529 let rest = chars.collect::<String>();
2530
2531 Some(format!("{first}{rest}"))
2532}
2533
2534fn jsx_expression_event_handler_ref(
2535 expression: &JSXExpression<'_>,
2536) -> Option<(String, Vec<ParsedSerializableValue>)> {
2537 let expression = expression.as_expression()?;
2538 expression_event_handler_ref(expression)
2539}
2540
2541fn expression_event_handler_ref(
2542 expression: &Expression<'_>,
2543) -> Option<(String, Vec<ParsedSerializableValue>)> {
2544 match expression {
2545 Expression::ArrowFunctionExpression(arrow) => {
2546 for statement in &arrow.body.statements {
2547 if let Statement::ExpressionStatement(statement) = statement {
2548 if let Some(reference) = expression_event_handler_ref(&statement.expression) {
2549 return Some(reference);
2550 }
2551 }
2552 }
2553
2554 None
2555 }
2556 Expression::CallExpression(call) => Some((
2557 expression_summary(&call.callee)?,
2558 call.arguments
2559 .iter()
2560 .map(|argument| serializable_value_from_expression(argument.as_expression()?))
2561 .collect::<Option<Vec<_>>>()?,
2562 )),
2563 Expression::StaticMemberExpression(_) => {
2564 Some((expression_summary(expression)?, Vec::new()))
2565 }
2566 _ => None,
2567 }
2568}
2569
2570fn parse_diagnostic(source: &str, diagnostic: &oxc_diagnostics::OxcDiagnostic) -> ParseDiagnostic {
2571 let severity = match diagnostic.severity {
2572 OxcSeverity::Advice => ParseSeverity::Info,
2573 OxcSeverity::Warning => ParseSeverity::Warning,
2574 OxcSeverity::Error => ParseSeverity::Error,
2575 };
2576
2577 let labels = diagnostic
2578 .labels
2579 .as_ref()
2580 .map(|labels| {
2581 labels
2582 .iter()
2583 .map(|label| {
2584 let start = label.offset();
2585 let end = start + label.len();
2586
2587 ParseLabel {
2588 span: source_span_from_offsets(source, start, end),
2589 }
2590 })
2591 .collect::<Vec<_>>()
2592 })
2593 .unwrap_or_default();
2594
2595 ParseDiagnostic {
2596 message: diagnostic.message.to_string(),
2597 severity,
2598 labels,
2599 }
2600}
2601
2602fn source_span(source: &str, span: Span) -> SourceSpan {
2603 source_span_from_offsets(source, span.start as usize, span.end as usize)
2604}
2605
2606fn source_span_from_offsets(source: &str, start: usize, end: usize) -> SourceSpan {
2607 let (line, column) = line_column_at(source, start);
2608
2609 SourceSpan {
2610 start,
2611 end,
2612 line,
2613 column,
2614 }
2615}
2616
2617fn line_column_at(source: &str, offset: usize) -> (usize, usize) {
2618 let clamped = offset.min(source.len());
2619 let prefix = &source[..clamped];
2620
2621 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
2622
2623 let column = prefix
2624 .rsplit_once('\n')
2625 .map_or(prefix.len() + 1, |(_, tail)| tail.len() + 1);
2626
2627 (line, column)
2628}