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