Skip to main content

presolve_parser/
oxc_adapter.rs

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
1005/// Retain general syntax facts for a single inline function argument.  This
1006/// deliberately makes no inference from the callee spelling or field name.
1007fn 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(&parameter.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(&parameter.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    // Getter expression retention is syntax only. A later canonical V2
1203    // analysis decides whether a getter is computed; decorators are not a
1204    // parser recognition condition.
1205    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(&parameter.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        // A whitespace-only child on one line is intentional JSX content. This
2237        // commonly appears between inline elements (for example syntax-token
2238        // spans in a code sample) and must not concatenate the surrounding
2239        // text in generated HTML. Multiline whitespace is source formatting.
2240        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}