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