1pub mod model;
2mod oxc_adapter;
3
4pub use model::{
5 ParseDiagnostic, ParseLabel, ParseSeverity, ParsedArithmeticExpression,
6 ParsedArithmeticExpressionKind, ParsedArithmeticOperator, ParsedCallArgument,
7 ParsedCallExpression, ParsedClass, ParsedClassHeritage, ParsedComparisonOperator,
8 ParsedComputedExpression, ParsedComputedExpressionKind, ParsedConstantExpression,
9 ParsedConstantExpressionKind, ParsedDecorator, ParsedEffectBody, ParsedEffectExpression,
10 ParsedEffectExpressionKind, ParsedEffectStatement, ParsedEffectStatementKind,
11 ParsedEventHandler, ParsedExport, ParsedExportKind, ParsedExportSpecifier, ParsedFile,
12 ParsedImport, ParsedImportSpecifier, ParsedInlineHandler, ParsedJsxAttribute,
13 ParsedJsxAttributeValue, ParsedJsxChild, ParsedJsxConditional, ParsedJsxElement,
14 ParsedJsxFragment, ParsedJsxList, ParsedJsxNode, ParsedLocalVariable, ParsedLogicalOperator,
15 ParsedMethod, ParsedMethodCall, ParsedMethodParameter, ParsedProperty, ParsedSerializableValue,
16 ParsedSourceAst, ParsedStateOperation, ParsedStateUpdate, ParsedStaticMemberDesignator,
17 ParsedThisMemberDesignator, ParsedTypeAlias, ParsedTypeAnnotation, ParsedUnaryOperator,
18 ParsedUnsupportedEffectStatementKind, ParsedValidationRuleArgument,
19 ParsedValidationRuleArgumentKind, ParsedValidationRuleExpression,
20 ParsedValidationRuleExpressionKind, SourceSpan,
21};
22pub use oxc_adapter::parse_file;
23
24#[must_use]
27pub fn is_valid_ecmascript_pattern(pattern: &str) -> bool {
28 let allocator = oxc_allocator::Allocator::default();
29 oxc_regular_expression::LiteralParser::new(
30 &allocator,
31 pattern,
32 None,
33 oxc_regular_expression::Options {
34 pattern_span_offset: 0,
35 flags_span_offset: 0,
36 },
37 )
38 .parse()
39 .is_ok()
40}
41
42#[cfg(test)]
43mod tests {
44 use super::{
45 parse_file, ParsedEffectStatementKind, ParsedJsxAttributeValue, ParsedSerializableValue,
46 ParsedUnsupportedEffectStatementKind, ParsedValidationRuleArgumentKind,
47 ParsedValidationRuleExpressionKind,
48 };
49
50 #[test]
51 fn retains_validation_rule_calls_constants_and_direct_field_designators() {
52 let source = r#"
53@component("profile")
54class Profile {
55 @form() profile!: Form;
56
57 @validate(min(1 + 2))
58 @validate(pattern("^[a-z]+$"))
59 @field(this.profile)
60 name = "";
61
62 @validate(equals(this.name))
63 @field(this.profile)
64 confirmation = "";
65}
66"#;
67 let parsed = parse_file("src/Profile.tsx", source);
68 let name = &parsed.classes[0].properties[1];
69 let validations = name
70 .decorators
71 .iter()
72 .filter(|decorator| decorator.name == "validate")
73 .collect::<Vec<_>>();
74 assert_eq!(validations.len(), 2);
75 let expression = validations[0]
76 .validation_rule_expression
77 .as_ref()
78 .expect("rule expression");
79 let ParsedValidationRuleExpressionKind::Call { callee, arguments } = &expression.kind
80 else {
81 panic!("expected validation call");
82 };
83 assert_eq!(callee.as_deref(), Some("min"));
84 assert!(matches!(
85 arguments[0].kind,
86 ParsedValidationRuleArgumentKind::Constant(_)
87 ));
88 let equals = parsed.classes[0].properties[2]
89 .decorators
90 .iter()
91 .find(|decorator| decorator.name == "validate")
92 .unwrap()
93 .validation_rule_expression
94 .as_ref()
95 .unwrap();
96 let ParsedValidationRuleExpressionKind::Call { arguments, .. } = &equals.kind else {
97 panic!("expected equals call");
98 };
99 assert!(matches!(
100 &arguments[0].kind,
101 ParsedValidationRuleArgumentKind::ThisMember(designator)
102 if designator.member == "name"
103 ));
104 }
105
106 #[test]
107 fn retains_a_source_faithful_general_estree_product() {
108 let source = r#"
109import type { CardProps } from "./types";
110export const Card = <section aria-label="card">{1 + 2}</section>;
111"#;
112 let parsed = parse_file("src/Card.tsx", source);
113 assert_eq!(parsed.syntax.source, source);
114 assert_eq!(parsed.syntax.span.start, 0);
115 assert_eq!(parsed.syntax.span.end, source.len());
116 assert!(parsed.syntax.estree_json.contains("\"Program\""));
117 assert!(parsed.syntax.estree_json.contains("\"ImportDeclaration\""));
118 assert!(parsed.syntax.estree_json.contains("\"JSXElement\""));
119 assert!(
120 parsed.syntax.estree_json.contains("\"TSImportType\"")
121 || parsed
122 .syntax
123 .estree_json
124 .contains("\"importKind\":\"type\"")
125 );
126 }
127
128 #[test]
129 fn retains_general_call_sites_without_framework_classification() {
130 let source = r#"
131const name = environment.public("PRESOLVE_PUBLIC_APP_NAME");
132const dynamic = environment.public(getName());
133"#;
134 let parsed = parse_file("src/environment.ts", source);
135 assert_eq!(parsed.call_expressions.len(), 3);
136 let public_calls = parsed
137 .call_expressions
138 .iter()
139 .filter(|call| {
140 &source[call.callee_span.start..call.callee_span.end] == "environment.public"
141 })
142 .collect::<Vec<_>>();
143 assert_eq!(public_calls.len(), 2);
144 assert!(public_calls.iter().all(|call| {
145 call.member_object_span
146 .is_some_and(|span| &source[span.start..span.end] == "environment")
147 && call
148 .member_property_span
149 .is_some_and(|span| &source[span.start..span.end] == "public")
150 }));
151 assert!(matches!(
152 &public_calls[0].arguments[0],
153 super::ParsedCallArgument::StringLiteral { value, .. }
154 if value == "PRESOLVE_PUBLIC_APP_NAME"
155 ));
156 assert!(matches!(
157 public_calls[1].arguments[0],
158 super::ParsedCallArgument::Other { .. }
159 ));
160 }
161
162 #[test]
163 fn retains_invalid_outer_validation_invocation_and_expression_shapes() {
164 let source = r#"
165@component("profile")
166class Profile {
167 @validate
168 first = "";
169 @validate()
170 second = "";
171 @validate(required(), email())
172 third = "";
173 @validate(schema.required())
174 fourth = "";
175}
176"#;
177 let parsed = parse_file("src/Profile.tsx", source);
178 let decorators = parsed.classes[0]
179 .properties
180 .iter()
181 .map(|property| &property.decorators[0])
182 .collect::<Vec<_>>();
183 assert!(!decorators[0].is_invoked);
184 assert_eq!(decorators[1].argument_count, 0);
185 assert_eq!(decorators[2].argument_count, 2);
186 assert!(matches!(
187 decorators[3]
188 .validation_rule_expression
189 .as_ref()
190 .unwrap()
191 .kind,
192 ParsedValidationRuleExpressionKind::Call { callee: None, .. }
193 ));
194 }
195
196 #[test]
197 fn retains_source_faithful_class_heritage() {
198 let source = "@component(\"x-child\") class Child extends Base.Component {}";
199 let parsed = parse_file("src/Child.tsx", source);
200 let heritage = parsed.classes[0].heritage.as_ref().expect("heritage");
201 assert_eq!(heritage.base, "Base.Component");
202 assert_eq!(
203 &source[heritage.span.start..heritage.span.end],
204 "Base.Component"
205 );
206 }
207
208 #[test]
209 fn retains_source_faithful_import_binding_spans() {
210 let source = "import { Component as FrameworkBase } from \"presolve\";";
211 let parsed = parse_file("src/Card.tsx", source);
212 let specifier = &parsed.imports[0].specifiers[0];
213 assert_eq!(specifier.imported, "Component");
214 assert_eq!(specifier.local, "FrameworkBase");
215 assert_eq!(
216 &source[specifier.local_span.start..specifier.local_span.end],
217 "FrameworkBase"
218 );
219 }
220
221 #[test]
222 fn retains_direct_initializer_call_spans_without_intrinsic_classification() {
223 let source = "class Counter { count = reactiveCell(0); plain = 1; }";
224 let parsed = parse_file("src/Counter.tsx", source);
225 let property = &parsed.classes[0].properties[0];
226 let call = property
227 .initializer_call
228 .as_ref()
229 .expect("direct calls are retained as syntax facts");
230 assert_eq!(
231 &source[call.callee_span.start..call.callee_span.end],
232 "reactiveCell"
233 );
234 assert_eq!(&source[call.span.start..call.span.end], "reactiveCell(0)");
235 assert_eq!(call.argument_count, 1);
236 assert!(parsed.classes[0].properties[1].initializer_call.is_none());
237 }
238
239 #[test]
240 fn retains_inline_initializer_handler_updates_without_classifying_the_call() {
241 let source = r#"
242class Counter {
243 increment = activate(() => { this.count += 1; unrelated(); });
244 reset = activate(async function () { this.count = 0; });
245}
246"#;
247 let parsed = parse_file("src/Counter.tsx", source);
248 let increment = parsed.classes[0].properties[0]
249 .initializer_call
250 .as_ref()
251 .and_then(|call| call.inline_handler.as_ref())
252 .expect("inline arrow handler should remain a syntax fact");
253 assert!(!increment.is_async);
254 assert!(!increment.is_expression_body);
255 assert_eq!(increment.state_updates.len(), 1);
256 assert_eq!(increment.state_updates[0].field, "count");
257 assert_eq!(increment.unsupported_statement_spans.len(), 1);
258 assert_eq!(
259 increment
260 .effect_body
261 .as_ref()
262 .expect("inline block body")
263 .statements
264 .len(),
265 2
266 );
267 assert_eq!(
268 &source[increment.unsupported_statement_spans[0].start
269 ..increment.unsupported_statement_spans[0].end],
270 "unrelated();"
271 );
272
273 let reset = parsed.classes[0].properties[1]
274 .initializer_call
275 .as_ref()
276 .and_then(|call| call.inline_handler.as_ref())
277 .expect("inline function handler should remain a syntax fact");
278 assert!(reset.is_async);
279 assert_eq!(reset.state_updates.len(), 1);
280 assert!(reset.unsupported_statement_spans.is_empty());
281 assert!(reset.effect_body.is_some());
282 }
283
284 #[test]
285 fn retains_inline_effect_cleanup_without_recognizing_the_callee() {
286 let source = r#"
287class Counter {
288 sync = observe(() => {
289 document.title = this.title;
290 return () => { document.title = ""; };
291 });
292}
293"#;
294 let parsed = parse_file("src/Counter.tsx", source);
295 let body = parsed.classes[0].properties[0]
296 .initializer_call
297 .as_ref()
298 .and_then(|call| call.inline_handler.as_ref())
299 .and_then(|handler| handler.effect_body.as_ref())
300 .expect("general inline block body");
301 assert_eq!(body.statements.len(), 1);
302 let cleanup = body.cleanup.as_ref().expect("cleanup callback");
303 assert!(!cleanup.is_async);
304 assert_eq!(cleanup.body.statements.len(), 1);
305 }
306
307 #[test]
308 fn retains_decorated_context_field_declaration_facts() {
309 let source = r#"
310@component("x-app-shell")
311class AppShell extends Component {
312 @context()
313 locale: string = "en";
314}
315"#;
316 let parsed = parse_file("src/AppShell.tsx", source);
317 let property = &parsed.classes[0].properties[0];
318
319 assert_eq!(property.name, "locale");
320 assert_eq!(property.decorators[0].name, "context");
321 assert_eq!(property.decorators[0].argument_count, 0);
322 assert_eq!(property.type_annotation.as_ref().unwrap().text, "string");
323 assert!(property.initializer_literal.is_some());
324 assert!(!property.is_static);
325 assert_eq!(property.span.start, source.find("@context()").unwrap());
326 assert!(property.span.end > property.initializer_span.unwrap().end);
327 }
328
329 #[test]
330 fn retains_slot_declaration_and_invalid_declaration_form_facts() {
331 let source = r#"
332@component("x-card")
333class Card extends Component {
334 @slot()
335 children!: SlotContent;
336
337 @slot("header")
338 static invalid: string = "bad";
339
340 @slot()
341 outlet() {}
342
343 attach(@slot() content: SlotContent) {}
344}
345"#;
346 let parsed = parse_file("src/Card.tsx", source);
347 assert!(parsed.diagnostics.is_empty(), "{:#?}", parsed.diagnostics);
348 let class = &parsed.classes[0];
349 let children = &class.properties[0];
350 assert_eq!(children.decorators[0].name, "slot");
351 assert_eq!(children.decorators[0].argument_count, 0);
352 assert_eq!(
353 children.type_annotation.as_ref().unwrap().text,
354 "SlotContent"
355 );
356 assert!(children.is_definite_assignment);
357 assert!(children.initializer.is_none());
358
359 let invalid = &class.properties[1];
360 assert_eq!(invalid.decorators[0].argument_count, 1);
361 assert!(invalid.is_static);
362 assert_eq!(invalid.initializer.as_deref(), Some("\"bad\""));
363
364 assert_eq!(class.methods[0].decorators[0].name, "slot");
365 assert_eq!(class.methods[1].parameters[0].decorators[0].name, "slot");
366 }
367
368 #[test]
369 fn retains_normalized_form_declaration_targets_and_invocation_facts() {
370 let source = r#"
371@form()
372class NotAFormField {}
373
374@component("x-profile")
375class Profile extends Component {
376 @form()
377 profile!: Form;
378
379 @form()
380 declare settings: Form;
381
382 @form
383 bare!: Form;
384
385 @form("named")
386 named!: Form;
387
388 @form()
389 @state
390 conflicting!: Form;
391
392 @form()
393 get current(): Form { return this.profile; }
394
395 @form()
396 set current(value: Form) {}
397
398 parameter(@form() value: Form) {}
399}
400"#;
401 let parsed = parse_file("src/Profile.tsx", source);
402
403 assert!(parsed.diagnostics.is_empty(), "{:#?}", parsed.diagnostics);
404 assert!(parsed.classes[0].decorators[0].is_invoked);
405 let profile = &parsed.classes[1];
406 assert!(profile.properties[0].is_identifier_name);
407 assert!(profile.properties[0].is_definite_assignment);
408 assert!(!profile.properties[0].is_declare);
409 assert!(profile.properties[1].is_declare);
410 assert!(!profile.properties[2].decorators[0].is_invoked);
411 assert_eq!(profile.properties[2].decorators[0].argument_count, 0);
412 assert!(profile.properties[3].decorators[0].is_invoked);
413 assert_eq!(profile.properties[3].decorators[0].argument_count, 1);
414 assert_eq!(profile.properties[3].decorators[0].argument_spans.len(), 1);
415 let named_argument = profile.properties[3].decorators[0].argument_spans[0];
416 assert_eq!(
417 &source[named_argument.start..named_argument.end],
418 "\"named\""
419 );
420 assert_eq!(profile.properties[4].decorators.len(), 2);
421 assert_eq!(profile.properties[4].decorators[1].name, "state");
422 assert!(!profile.properties[4].decorators[1].is_invoked);
423 assert!(profile.methods[0].is_getter);
424 assert!(!profile.methods[0].is_setter);
425 assert!(profile.methods[1].is_setter);
426 assert_eq!(profile.methods[2].parameters[0].decorators[0].name, "form");
427 assert_eq!(
428 profile.properties[0].span.start,
429 source.find("@form()\n profile").unwrap()
430 );
431 assert!(parsed.local_type_bindings.contains(&"Profile".to_string()));
432 }
433
434 #[test]
435 fn retains_module_local_type_namespace_bindings() {
436 let parsed = parse_file(
437 "src/types.ts",
438 r#"
439class ClassType {}
440type AliasType = string;
441interface InterfaceType {}
442enum EnumType { Value }
443import ImportEqualsType = require("./type");
444"#,
445 );
446
447 assert_eq!(
448 parsed.local_type_bindings,
449 [
450 "AliasType",
451 "ClassType",
452 "EnumType",
453 "ImportEqualsType",
454 "InterfaceType",
455 ]
456 );
457 }
458
459 #[test]
460 fn retains_normalized_form_field_designators_targets_values_and_provenance() {
461 let source = r#"
462@field(this.profileForm)
463class InvalidTarget {}
464
465@component("profile-editor")
466class ProfileEditor {
467 @form() profileForm!: Form;
468 @field(this.profileForm) displayName = "Austin";
469 @field(this.profileForm) address = { city: "", postalCode: "" };
470 @field
471 bare = "";
472 @field("profileForm") stringDesignator = "";
473 @field(this.forms.profile) chained = "";
474 @field(this.profileForm) ["computed"] = "";
475 @field(this.profileForm) #privateName = "";
476 @field(this.profileForm) method() {}
477 parameter(@field(this.profileForm) value: string) {}
478}
479"#;
480 let parsed = parse_file("src/ProfileEditor.tsx", source);
481 let editor = &parsed.classes[1];
482 let display = &editor.properties[1];
483
484 assert_eq!(display.decorators[0].argument_count, 1);
485 assert_eq!(
486 display.decorators[0]
487 .this_member_argument
488 .as_ref()
489 .map(|designator| designator.member.as_str()),
490 Some("profileForm")
491 );
492 assert_eq!(
493 display.initializer_literal,
494 Some(ParsedSerializableValue::String("Austin".to_string()))
495 );
496 assert!(matches!(
497 editor.properties[2].initializer_literal,
498 Some(ParsedSerializableValue::Object(_))
499 ));
500 assert!(!editor.properties[3].decorators[0].is_invoked);
501 assert!(editor.properties[4].decorators[0]
502 .this_member_argument
503 .is_none());
504 assert_eq!(
505 editor.properties[4].decorators[0].argument.as_deref(),
506 Some("profileForm")
507 );
508 assert!(editor.properties[5].decorators[0]
509 .this_member_argument
510 .is_none());
511 assert!(!editor.properties[6].is_identifier_name);
512 assert!(!editor.properties[7].is_identifier_name);
513 assert_eq!(editor.methods[0].decorators[0].name, "field");
514 assert_eq!(editor.methods[1].parameters[0].decorators[0].name, "field");
515 assert_eq!(
516 display.span.start,
517 source.find("@field(this.profileForm) displayName").unwrap()
518 );
519 }
520
521 #[test]
522 fn retains_every_static_decorator_argument_without_admitting_its_semantics() {
523 let parsed = parse_file(
524 "src/NestedField.tsx",
525 r#"@component("x-nested") class NestedField {
526 @field("profile", "address.street") street = "";
527 render() { return <div />; }
528}"#,
529 );
530 let decorator = &parsed.classes[0].properties[0].decorators[0];
531 assert_eq!(decorator.name, "field");
532 assert_eq!(decorator.argument_count, 2);
533 assert_eq!(
534 decorator.arguments,
535 vec![
536 Some("profile".to_string()),
537 Some("address.street".to_string())
538 ]
539 );
540 assert_eq!(decorator.argument, Some("profile".to_string()));
541 }
542
543 #[test]
544 fn retains_normalized_form_control_attribute_facts() {
545 let source = r#"
546@component("profile-editor")
547class ProfileEditor {
548 render() {
549 return <main>
550 <input type="radio" value="email" field={this.contact} />
551 <input type={this.kind} field={this.contact} {...props} />
552 <input field={this["contact"]} />
553 <select multiple={this.multiple} field={this.tags} />
554 </main>;
555 }
556}
557"#;
558 let parsed = parse_file("src/ProfileEditor.tsx", source);
559 let super::ParsedJsxNode::Element(root) = &parsed.classes[0].methods[0].jsx_roots[0] else {
560 panic!("element root");
561 };
562 let elements = root
563 .children
564 .iter()
565 .filter_map(|child| match child {
566 super::ParsedJsxChild::Element(element) => Some(element),
567 _ => None,
568 })
569 .collect::<Vec<_>>();
570
571 let radio = &elements[0];
572 let value = radio
573 .attributes
574 .iter()
575 .find(|attribute| attribute.name == "value")
576 .expect("radio value");
577 let field = radio
578 .attributes
579 .iter()
580 .find(|attribute| attribute.name == "field")
581 .expect("field binding");
582 assert_eq!(
583 value.constant_value,
584 Some(ParsedSerializableValue::String("email".to_string()))
585 );
586 assert_eq!(
587 field
588 .this_member
589 .as_ref()
590 .map(|member| member.member.as_str()),
591 Some("contact")
592 );
593 assert_eq!(
594 &source[field.expression_span.unwrap().start..field.expression_span.unwrap().end],
595 "this.contact"
596 );
597 assert!(elements[1]
598 .attributes
599 .iter()
600 .any(|attribute| matches!(attribute.value, ParsedJsxAttributeValue::Spread(_))));
601 assert!(elements[2].attributes[0].this_member.is_none());
602 assert!(elements[3]
603 .attributes
604 .iter()
605 .find(|attribute| attribute.name == "multiple")
606 .expect("multiple")
607 .this_member
608 .is_some());
609 }
610
611 #[test]
612 fn retains_canonical_component_tag_names_and_exact_name_spans() {
613 let source = r#"
614@component("x-page")
615class Page extends Component {
616 render() { return <main><Card /><Registry.Card /></main>; }
617}
618"#;
619 let parsed = parse_file("src/Page.tsx", source);
620 let super::ParsedJsxNode::Element(root) = &parsed.classes[0].methods[0].jsx_roots[0] else {
621 panic!("element root");
622 };
623 let elements = root
624 .children
625 .iter()
626 .filter_map(|child| match child {
627 super::ParsedJsxChild::Element(element) => Some(element),
628 _ => None,
629 })
630 .collect::<Vec<_>>();
631
632 assert_eq!(elements[0].name, "Card");
633 assert_eq!(elements[1].name, "Registry.Card");
634 assert_eq!(
635 &source[elements[0].name_span.start..elements[0].name_span.end],
636 "Card"
637 );
638 assert_eq!(
639 &source[elements[1].name_span.start..elements[1].name_span.end],
640 "Registry.Card"
641 );
642 }
643
644 #[test]
645 fn retains_static_member_provider_designators_and_value_expressions() {
646 let parsed = parse_file(
647 "src/AppShell.tsx",
648 r#"
649@component("x-app-shell")
650class AppShell extends Component {
651 @provide(AppShell.theme)
652 providedTheme: string = this.theme ?? "light";
653}
654"#,
655 );
656 let property = &parsed.classes[0].properties[0];
657 let decorator = &property.decorators[0];
658 let designator = decorator.static_member_argument.as_ref().unwrap();
659
660 assert_eq!(decorator.name, "provide");
661 assert_eq!(decorator.argument_count, 1);
662 assert_eq!(designator.object, "AppShell");
663 assert_eq!(designator.member, "theme");
664 assert!(property.initializer_expression.is_some());
665 }
666
667 #[test]
668 fn retains_ordered_effect_statement_syntax_and_unsupported_forms() {
669 let parsed = parse_file(
670 "src/Effects.tsx",
671 r#"
672@component("x-effects")
673class Effects extends Component {
674 @effect()
675 sync() {
676 document.title = this.title;
677 analytics.track("view", this.total + this.tax);
678 return;
679 }
680
681 @effect()
682 invalid() {
683 const title = this.title;
684 if (this.enabled) { analytics.track("enabled"); }
685 }
686}
687"#,
688 );
689 let methods = &parsed.classes[0].methods;
690 let body = methods[0].effect_body.as_ref().expect("effect body");
691 assert_eq!(body.statements.len(), 3);
692 assert!(matches!(
693 body.statements[0].kind,
694 ParsedEffectStatementKind::StaticMemberAssignment { .. }
695 ));
696 assert!(matches!(
697 body.statements[1].kind,
698 ParsedEffectStatementKind::CapabilityCall { .. }
699 ));
700 assert!(matches!(
701 body.statements[2].kind,
702 ParsedEffectStatementKind::EffectReturn { value: None }
703 ));
704
705 let invalid = methods[1].effect_body.as_ref().expect("effect body");
706 assert!(matches!(
707 invalid.statements[0].kind,
708 ParsedEffectStatementKind::Unsupported(
709 ParsedUnsupportedEffectStatementKind::LocalDeclaration
710 )
711 ));
712 assert!(matches!(
713 invalid.statements[1].kind,
714 ParsedEffectStatementKind::Unsupported(ParsedUnsupportedEffectStatementKind::Branch)
715 ));
716 }
717
718 #[test]
719 fn retains_submit_decorator_designator_and_method_signature_facts() {
720 let parsed = parse_file(
721 "src/Profile.tsx",
722 r#"
723@component("profile")
724class Profile {
725 @action() @submit(this.profileForm) save(): void {}
726 @submit invalid(value: string): string { return ""; }
727 @action() @submit(this.profileForm) static saveStatic(): void {}
728}
729"#,
730 );
731 let methods = &parsed.classes[0].methods;
732 let submit = methods[0]
733 .decorators
734 .iter()
735 .find(|decorator| decorator.name == "submit")
736 .expect("submit decorator");
737 assert!(submit.is_invoked);
738 assert_eq!(submit.argument_count, 1);
739 assert_eq!(
740 submit
741 .this_member_argument
742 .as_ref()
743 .map(|value| value.member.as_str()),
744 Some("profileForm")
745 );
746 assert_eq!(
747 methods[0]
748 .return_type_annotation
749 .as_ref()
750 .map(|annotation| annotation.text.as_str()),
751 Some("void")
752 );
753 assert_eq!(methods[1].parameters.len(), 1);
754 assert!(
755 !methods[1]
756 .decorators
757 .iter()
758 .find(|decorator| decorator.name == "submit")
759 .expect("bare submit")
760 .is_invoked
761 );
762 assert!(methods[2].is_static);
763 }
764}