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