1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4 infer_serializable_value_type, is_assignable, serialization_compatibility,
5 state_initializer_value_type, ComponentNode, ExecutionBoundary, FieldId, FormEntity,
6 FormFieldDeclarationCandidate, FormFieldDeclarationCandidateId, FormFieldDeclarationViolation,
7 FormId, SemanticId, SemanticType, SemanticTypeAssignment, SemanticTypeId, SemanticTypeModel,
8 SemanticTypeStatus, SerializableValue, SerializationCompatibility, SourceProvenance,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct FormFieldEntity {
14 pub id: FieldId,
15 pub owner_form: FormId,
16 pub owner_component: SemanticId,
17 pub authored_field: SemanticId,
18 pub name: String,
19 pub path: Vec<String>,
23 pub semantic_type: SemanticType,
24 pub type_assignment: SemanticTypeAssignment,
25 pub initial_value: SerializableValue,
26 pub declaration_order: usize,
27 pub provenance: SourceProvenance,
28 pub form_designator_provenance: SourceProvenance,
29 pub field_name_provenance: SourceProvenance,
30 pub type_provenance: SourceProvenance,
31 pub initializer_provenance: SourceProvenance,
32 pub boundary: ExecutionBoundary,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct FormFieldProducts {
37 pub candidates: Vec<FormFieldDeclarationCandidate>,
38 pub fields: BTreeMap<FieldId, FormFieldEntity>,
39}
40
41#[must_use]
50pub fn collect_form_field_products(
51 components: &[ComponentNode],
52 forms: &BTreeMap<FormId, FormEntity>,
53 semantic_types: &SemanticTypeModel,
54 bindings: Option<&crate::BindingTable>,
55) -> FormFieldProducts {
56 let mut candidates = components
57 .iter()
58 .flat_map(|component| component.form_field_declaration_candidates.clone())
59 .collect::<Vec<_>>();
60 candidates.sort_by(candidate_source_order);
61
62 let valid_forms = forms
63 .values()
64 .filter_map(|form| {
65 Some((
66 (form.owner.entity_id()?.clone(), form.name.clone()),
67 form.id.clone(),
68 ))
69 })
70 .collect::<BTreeMap<_, _>>();
71 let components_by_id = components
72 .iter()
73 .map(|component| (component.id.clone(), component))
74 .collect::<BTreeMap<_, _>>();
75 let mut alias_origins = BTreeMap::<FormFieldDeclarationCandidateId, SemanticId>::new();
76
77 for candidate in &mut candidates {
78 resolve_candidate_form(
79 candidate,
80 components,
81 &components_by_id,
82 &valid_forms,
83 forms,
84 );
85 resolve_candidate_type(candidate, semantic_types, bindings, &mut alias_origins);
86 }
87
88 mark_duplicate_names(&mut candidates);
89 mark_conflicting_paths(&mut candidates);
90
91 let mut fields = BTreeMap::new();
92 let mut authored_orders = BTreeMap::<FormId, usize>::new();
93 for candidate in &mut candidates {
94 if !candidate.is_valid() {
95 candidate.field_id = None;
96 candidate.type_assignment = None;
97 continue;
98 }
99 let (id, entity) = lower_valid_candidate(candidate, &alias_origins, &mut authored_orders);
100 fields.insert(id, entity);
101 }
102
103 FormFieldProducts { candidates, fields }
104}
105
106fn lower_valid_candidate(
107 candidate: &mut FormFieldDeclarationCandidate,
108 alias_origins: &BTreeMap<FormFieldDeclarationCandidateId, SemanticId>,
109 authored_orders: &mut BTreeMap<FormId, usize>,
110) -> (FieldId, FormFieldEntity) {
111 let owner_form = candidate
112 .resolved_form
113 .clone()
114 .expect("valid Form Field candidate has resolved Form");
115 let name = candidate
116 .authored_name
117 .clone()
118 .expect("valid Form Field candidate has authored name");
119 let id = FieldId::for_form(&owner_form, &name);
120 let semantic_type = candidate
121 .semantic_type
122 .clone()
123 .expect("valid Form Field candidate has semantic type");
124 let type_provenance = candidate
125 .declared_type
126 .as_ref()
127 .map(|declared| declared.provenance.clone())
128 .or_else(|| candidate.initializer_provenance.clone())
129 .expect("valid Form Field candidate has type provenance");
130 let type_assignment = SemanticTypeAssignment {
131 id: SemanticTypeId::for_subject(id.as_semantic_id()),
132 subject: id.as_semantic_id().clone(),
133 semantic_type: semantic_type.clone(),
134 origin: alias_origins
135 .get(&candidate.id)
136 .cloned()
137 .unwrap_or_else(|| id.as_semantic_id().clone()),
138 status: if candidate.declared_type.is_some() {
139 SemanticTypeStatus::Declared
140 } else {
141 SemanticTypeStatus::Inferred
142 },
143 provenance: candidate.provenance.clone(),
144 };
145 let declaration_order = authored_orders.entry(owner_form.clone()).or_default();
146 let entity = FormFieldEntity {
147 id: id.clone(),
148 owner_form,
149 owner_component: candidate
150 .owner_component
151 .clone()
152 .expect("valid Form Field candidate has component owner"),
153 authored_field: candidate
154 .declaration_field
155 .clone()
156 .expect("valid Form Field candidate has authored field identity"),
157 path: candidate
158 .nested_path_segments
159 .clone()
160 .unwrap_or_else(|| vec![name.clone()]),
161 name,
162 semantic_type,
163 type_assignment: type_assignment.clone(),
164 initial_value: candidate
165 .initializer
166 .clone()
167 .expect("valid Form Field candidate has initial value"),
168 declaration_order: *declaration_order,
169 provenance: candidate.provenance.clone(),
170 form_designator_provenance: candidate
171 .form_designator
172 .as_ref()
173 .expect("valid Form Field candidate has designator")
174 .provenance
175 .clone(),
176 field_name_provenance: candidate
177 .name_provenance
178 .clone()
179 .expect("valid Form Field candidate has name provenance"),
180 type_provenance,
181 initializer_provenance: candidate
182 .initializer_provenance
183 .clone()
184 .expect("valid Form Field candidate has initializer provenance"),
185 boundary: ExecutionBoundary::Client,
186 };
187 *declaration_order += 1;
188 candidate.field_id = Some(id.clone());
189 candidate.type_assignment = Some(type_assignment);
190 (id, entity)
191}
192
193fn resolve_candidate_form(
194 candidate: &mut FormFieldDeclarationCandidate,
195 components: &[ComponentNode],
196 components_by_id: &BTreeMap<SemanticId, &ComponentNode>,
197 valid_forms: &BTreeMap<(SemanticId, String), FormId>,
198 forms: &BTreeMap<FormId, FormEntity>,
199) {
200 if let (Some(owner), Some(designator)) = (
201 candidate.owner_component.as_ref(),
202 candidate.form_designator.as_ref(),
203 ) {
204 if let Some(form) = valid_forms.get(&(owner.clone(), designator.authored_name.clone())) {
205 candidate.resolved_form = Some(form.clone());
206 return;
207 }
208 let has_invalid_local_form = components_by_id.get(owner).is_some_and(|component| {
209 component
210 .form_declaration_candidates
211 .iter()
212 .any(|form| form.authored_name.as_deref() == Some(&designator.authored_name))
213 });
214 if has_invalid_local_form {
215 candidate.add_violation(FormFieldDeclarationViolation::InvalidForm);
216 return;
217 }
218 let inherited = components_by_id.get(owner).is_some_and(|component| {
219 component.heritage.as_ref().is_some_and(|heritage| {
220 components.iter().any(|base| {
221 base.class_name == heritage.base
222 && base.form_declaration_candidates.iter().any(|form| {
223 form.authored_name.as_deref() == Some(&designator.authored_name)
224 })
225 })
226 })
227 });
228 if inherited {
229 candidate.add_violation(FormFieldDeclarationViolation::InheritedDeclaration);
230 candidate.add_violation(FormFieldDeclarationViolation::InvalidForm);
231 } else {
232 candidate.add_violation(FormFieldDeclarationViolation::UnresolvedForm);
233 }
234 return;
235 }
236
237 if let Some(unsupported) = &candidate.unsupported_form_designator {
238 let cross_component = components.iter().any(|component| {
239 component.class_name == unsupported.object
240 && forms.values().any(|form| {
241 form.owner.entity_id() == Some(&component.id) && form.name == unsupported.member
242 })
243 });
244 if cross_component {
245 candidate.add_violation(FormFieldDeclarationViolation::CrossComponentForm);
246 }
247 }
248}
249
250fn resolve_candidate_type(
251 candidate: &mut FormFieldDeclarationCandidate,
252 semantic_types: &SemanticTypeModel,
253 bindings: Option<&crate::BindingTable>,
254 alias_origins: &mut BTreeMap<FormFieldDeclarationCandidateId, SemanticId>,
255) {
256 let Some(initializer) = candidate.initializer.clone() else {
257 return;
258 };
259 let semantic_type = if let Some(declared_type) = &candidate.declared_type {
260 let Some(resolved) = semantic_types.resolve_declared_type(declared_type, bindings) else {
261 candidate.add_violation(FormFieldDeclarationViolation::InvalidDeclaredType);
262 return;
263 };
264 if let Some(origin) = resolved.alias_origin {
265 alias_origins.insert(candidate.id.clone(), origin);
266 }
267 if !is_supported_form_field_type(&resolved.semantic_type) {
268 candidate.add_violation(FormFieldDeclarationViolation::InvalidDeclaredType);
269 }
270 let source = state_initializer_value_type(&initializer);
271 if !is_assignable(&source, &resolved.semantic_type) {
272 candidate.add_violation(FormFieldDeclarationViolation::InitializerTypeMismatch);
273 }
274 resolved.semantic_type
275 } else {
276 infer_serializable_value_type(&initializer)
277 };
278 if serialization_compatibility(&semantic_type) != SerializationCompatibility::Serializable {
279 candidate.add_violation(FormFieldDeclarationViolation::NonSerializableType);
280 }
281 candidate.semantic_type = Some(semantic_type);
282}
283
284fn is_supported_form_field_type(semantic_type: &SemanticType) -> bool {
285 match semantic_type {
286 SemanticType::Null
287 | SemanticType::Boolean
288 | SemanticType::Number
289 | SemanticType::String
290 | SemanticType::BooleanLiteral(_)
291 | SemanticType::NumberLiteral(_)
292 | SemanticType::StringLiteral(_) => true,
293 SemanticType::Array(element) => is_supported_form_field_type(element),
294 SemanticType::Tuple(items) | SemanticType::Union(items) => {
295 !items.is_empty() && items.iter().all(is_supported_form_field_type)
296 }
297 SemanticType::Object(object) => {
298 object.properties.values().all(is_supported_form_field_type)
299 }
300 SemanticType::Unknown
301 | SemanticType::Never
302 | SemanticType::Form
303 | SemanticType::SlotContent
304 | SemanticType::Resource(_) => false,
305 }
306}
307
308fn mark_duplicate_names(candidates: &mut [FormFieldDeclarationCandidate]) {
309 let mut groups = BTreeMap::<(FormId, String), Vec<usize>>::new();
310 for (index, candidate) in candidates.iter().enumerate() {
311 if let (Some(form), Some(name)) = (&candidate.resolved_form, &candidate.authored_name) {
312 groups
313 .entry((form.clone(), name.clone()))
314 .or_default()
315 .push(index);
316 }
317 }
318 let duplicate_groups = groups
319 .into_values()
320 .filter(|indexes| {
321 indexes
322 .iter()
323 .map(|index| {
324 let provenance = &candidates[*index].provenance;
325 (
326 provenance.path.as_path(),
327 provenance.span.start,
328 provenance.span.end,
329 )
330 })
331 .collect::<BTreeSet<_>>()
332 .len()
333 > 1
334 })
335 .collect::<Vec<_>>();
336 for indexes in duplicate_groups {
337 for index in indexes {
338 candidates[index].add_violation(FormFieldDeclarationViolation::DuplicateName);
339 }
340 }
341}
342
343fn mark_conflicting_paths(candidates: &mut [FormFieldDeclarationCandidate]) {
348 let mut groups = BTreeMap::<(FormId, Vec<String>), Vec<usize>>::new();
349 for (index, candidate) in candidates.iter().enumerate() {
350 if !candidate.is_valid() {
351 continue;
352 }
353 if let (Some(form), Some(name)) = (&candidate.resolved_form, &candidate.authored_name) {
354 let path = candidate
355 .nested_path_segments
356 .clone()
357 .unwrap_or_else(|| vec![name.clone()]);
358 groups.entry((form.clone(), path)).or_default().push(index);
359 }
360 }
361 let entries = groups.into_iter().collect::<Vec<_>>();
362 let mut conflicting = BTreeSet::new();
363 for (_, candidates) in &entries {
364 if candidates.len() > 1 {
365 conflicting.extend(candidates.iter().copied());
366 }
367 }
368 for (left_index, ((left_form, left_path), left_candidates)) in entries.iter().enumerate() {
369 for (right_form, right_path, right_candidates) in entries
370 .iter()
371 .skip(left_index + 1)
372 .map(|((form, path), indexes)| (form, path, indexes))
373 {
374 if left_form != right_form || !paths_conflict(left_path, right_path) {
375 continue;
376 }
377 conflicting.extend(left_candidates.iter().copied());
378 conflicting.extend(right_candidates.iter().copied());
379 }
380 }
381 for index in conflicting {
382 candidates[index].add_violation(FormFieldDeclarationViolation::ConflictingPath);
383 }
384}
385
386fn paths_conflict(left: &[String], right: &[String]) -> bool {
387 (left.len() <= right.len() && left.iter().zip(right).all(|(left, right)| left == right))
388 || (right.len() <= left.len() && right.iter().zip(left).all(|(left, right)| left == right))
389}
390
391fn candidate_source_order(
392 left: &FormFieldDeclarationCandidate,
393 right: &FormFieldDeclarationCandidate,
394) -> std::cmp::Ordering {
395 (
396 left.provenance.path.as_path(),
397 left.provenance.span.start,
398 left.provenance.span.end,
399 left.id.as_str(),
400 )
401 .cmp(&(
402 right.provenance.path.as_path(),
403 right.provenance.span.start,
404 right.provenance.span.end,
405 right.id.as_str(),
406 ))
407}
408
409#[cfg(test)]
410mod tests {
411 use crate::{
412 build_application_semantic_model, build_application_semantic_model_for_unit,
413 build_semantic_graph, validate_application_semantic_model, CompilationUnit,
414 ExecutionBoundary, FieldId, FormFieldDeclarationViolation, FormId, SemanticEntityKind,
415 SemanticOwner, SemanticType, SemanticTypeStatus, SerializableValue,
416 SEMANTIC_GRAPH_SCHEMA_VERSION,
417 };
418
419 #[test]
420 fn lowers_valid_fields_with_exact_ownership_types_values_order_and_provenance() {
421 let source = r#"
422@component("profile-editor")
423class ProfileEditor {
424 @form() profileForm!: Form;
425 @field(this.profileForm) displayName = "Austin";
426 @field(this.profileForm) age: number = 30;
427 @field(this.profileForm) selection: string | null = null;
428 @field(this.profileForm) tags: string[] = [];
429 @field(this.profileForm) point: [string, number] = ["x", 1];
430 @field(this.profileForm) address: { city: string; postalCode: string } = { city: "", postalCode: "" };
431 @field(this.profileForm) score: number = 1 + 2;
432 render() { return <main />; }
433}
434"#;
435 let parsed = presolve_parser::parse_file("src/ProfileEditor.tsx", source);
436 let asm = build_application_semantic_model(&parsed);
437 let component = &asm.components[0];
438 let form_id = FormId::for_owner(&component.id, "profileForm");
439 let display_id = FieldId::for_form(&form_id, "displayName");
440 let display = asm.form_field(&display_id).expect("displayName Field");
441
442 assert_eq!(asm.form_fields().len(), 7);
443 assert_eq!(
444 display.id.as_str(),
445 "module:src/ProfileEditor.tsx/component:profile-editor/form:profileForm/field:displayName"
446 );
447 assert_eq!(display.owner_form, form_id);
448 assert_eq!(display.owner_component, component.id);
449 assert_eq!(display.semantic_type, SemanticType::String);
450 assert_eq!(
451 display.initial_value,
452 SerializableValue::String("Austin".to_string())
453 );
454 assert_eq!(display.type_assignment.status, SemanticTypeStatus::Inferred);
455 assert_eq!(display.boundary, ExecutionBoundary::Client);
456 assert_eq!(display.declaration_order, 0);
457 assert_eq!(
458 asm.owner(display.id.as_semantic_id()),
459 Some(&SemanticOwner::entity(
460 display.owner_form.as_semantic_id().clone()
461 ))
462 );
463 assert_eq!(
464 asm.semantic_type_of(display.id.as_semantic_id()),
465 Some(&SemanticType::String)
466 );
467 assert!(asm
468 .entity(display.id.as_semantic_id())
469 .is_some_and(|entity| entity.kind() == SemanticEntityKind::FormField));
470 assert_eq!(
471 asm.form_fields()
472 .iter()
473 .map(|field| field.declaration_order)
474 .collect::<Vec<_>>(),
475 (0..7).collect::<Vec<_>>()
476 );
477 assert_eq!(
478 asm.form_field(&FieldId::for_form(&display.owner_form, "score"))
479 .expect("score")
480 .initial_value,
481 SerializableValue::Number("3".to_string())
482 );
483 assert_eq!(
484 display.provenance.span.start,
485 source.find("@field(this.profileForm) displayName").unwrap()
486 );
487 assert!(
488 display.form_designator_provenance.span.start
489 < display.initializer_provenance.span.start
490 );
491 let validation = validate_application_semantic_model(&asm);
492 assert!(validation.is_empty(), "{validation:#?}");
493 let graph = build_semantic_graph(&asm);
494 assert_eq!(graph.schema_version, SEMANTIC_GRAPH_SCHEMA_VERSION);
495 assert!(graph
496 .nodes
497 .iter()
498 .any(|node| node.id == *display.id.as_semantic_id()));
499 }
500
501 #[test]
502 fn scopes_names_to_forms_and_is_stable_under_reversed_files() {
503 let first_source = r#"
504@component("profile")
505class Profile {
506 @form() billing!: Form;
507 @form() shipping!: Form;
508 @field(this.billing) address = "billing";
509 @field(this.shipping) address = "shipping";
510 render() { return <main />; }
511}
512"#;
513 let second_source = r#"
514@component("account")
515class Account {
516 @form() billing!: Form;
517 @field(this.billing) address = "account";
518 render() { return <main />; }
519}
520"#;
521 let first = CompilationUnit::parse_sources([
522 ("src/Profile.tsx", first_source),
523 ("src/Account.tsx", second_source),
524 ]);
525 let reversed = CompilationUnit::parse_sources([
526 ("src/Account.tsx", second_source),
527 ("src/Profile.tsx", first_source),
528 ]);
529 let first = build_application_semantic_model_for_unit(&first);
530 let reversed = build_application_semantic_model_for_unit(&reversed);
531
532 assert_eq!(first.form_fields, reversed.form_fields);
533 assert_eq!(
534 first.form_field_declaration_candidates,
535 reversed.form_field_declaration_candidates
536 );
537 assert_eq!(first.form_fields.len(), 3);
538 assert!(first
539 .form_fields
540 .values()
541 .all(|field| field.name == "address" && field.path == ["address"]));
542 }
543
544 #[test]
545 fn lowers_nested_paths_and_rejects_exact_or_prefix_conflicts() {
546 let valid = presolve_parser::parse_file(
547 "src/Profile.tsx",
548 r#"
549@component("profile") class Profile {
550 @form() profile!: Form;
551 @field(this.profile, "address.street") street = "South Congress";
552 @field(this.profile, "address.city") city = "Austin";
553 render() { return <main />; }
554}
555"#,
556 );
557 let valid = build_application_semantic_model(&valid);
558 assert_eq!(valid.form_fields().len(), 2);
559 assert!(valid
560 .form_fields()
561 .iter()
562 .any(|field| field.name == "street" && field.path == ["address", "street"]));
563 assert!(valid
564 .form_fields()
565 .iter()
566 .any(|field| field.name == "city" && field.path == ["address", "city"]));
567 let form = FormId::for_owner(&valid.components[0].id, "profile");
568 assert_eq!(
569 valid.serialization.plans[&crate::SerializationPlanId::for_form(&form)]
570 .fields
571 .iter()
572 .map(|field| field.key.as_str())
573 .collect::<Vec<_>>(),
574 vec!["address.street", "address.city"]
575 );
576
577 let conflicting = presolve_parser::parse_file(
578 "src/Conflicting.tsx",
579 r#"
580@component("conflicting") class Conflicting {
581 @form() profile!: Form;
582 @field(this.profile, "address") address = "flat";
583 @field(this.profile, "address.street") street = "nested";
584 render() { return <main />; }
585}
586"#,
587 );
588 let conflicting = build_application_semantic_model(&conflicting);
589 assert!(conflicting.form_fields().is_empty());
590 assert!(conflicting
591 .form_field_declaration_candidates()
592 .iter()
593 .all(|candidate| candidate
594 .violations
595 .contains(&FormFieldDeclarationViolation::ConflictingPath)));
596
597 let duplicate = presolve_parser::parse_file(
598 "src/Duplicate.tsx",
599 r#"
600@component("duplicate") class Duplicate {
601 @form() profile!: Form;
602 @field(this.profile, "address.street") primary = "one";
603 @field(this.profile, "address.street") secondary = "two";
604 render() { return <main />; }
605}
606"#,
607 );
608 let duplicate = build_application_semantic_model(&duplicate);
609 assert!(duplicate.form_fields().is_empty());
610 assert!(duplicate
611 .form_field_declaration_candidates()
612 .iter()
613 .all(|candidate| candidate
614 .violations
615 .contains(&FormFieldDeclarationViolation::ConflictingPath)));
616 }
617
618 #[test]
619 #[allow(clippy::too_many_lines)]
620 fn retains_invalid_decorators_targets_designators_and_owning_forms_without_field_ids() {
621 let source = r#"
622@field(this.profileForm)
623class ClassTarget {}
624
625class BaseEditor {
626 @form() baseForm!: Form;
627}
628
629@component("other")
630class Other {
631 @form() otherForm!: Form;
632 render() { return <main />; }
633}
634
635@component("profile")
636class Profile extends BaseEditor {
637 @form() validForm!: Form;
638 @form("bad") invalidForm!: Form;
639 normalProperty = {};
640 @field validBare = "";
641 @field() zero = "";
642 @field(this.validForm, "invalid-path") many = "";
643 @field("validForm") stringArg = "";
644 @field(validForm) identifierArg = "";
645 @field(this.forms.validForm) chain = "";
646 @field(getForm()) call = "";
647 @field(this.missing) missing = "";
648 @field(this.normalProperty) ordinary = "";
649 @field(this.invalidForm) invalid = "";
650 @field(this.baseForm) inherited = "";
651 @field(Other.otherForm) cross = "";
652 @field(this.validForm) static staticField = "";
653 @field(this.validForm) ["computed"] = "";
654 @field(this.validForm) #privateField = "";
655 @field(this.validForm) method() {}
656 @field(this.validForm) get getter() { return ""; }
657 @field(this.validForm) set setter(value: string) {}
658 parameter(@field(this.validForm) value: string) {}
659 render() { return <main />; }
660}
661"#;
662 let parsed = presolve_parser::parse_file("src/InvalidFields.tsx", source);
663 let asm = build_application_semantic_model(&parsed);
664 let candidates = asm.form_field_declaration_candidates();
665
666 assert_eq!(asm.form_fields().len(), 1);
667 assert_eq!(asm.form_fields()[0].name, "stringArg");
668 assert_eq!(candidates.len(), 20);
669 assert_eq!(
670 candidates
671 .iter()
672 .map(|candidate| candidate.id.clone())
673 .collect::<std::collections::BTreeSet<_>>()
674 .len(),
675 candidates.len()
676 );
677 assert!(candidates.iter().any(|candidate| {
678 candidate.authored_name.as_deref() == Some("stringArg")
679 && candidate.field_id.is_some()
680 && candidate.type_assignment.is_some()
681 && candidate.violations.is_empty()
682 }));
683 assert!(candidates
684 .iter()
685 .filter(|candidate| { candidate.authored_name.as_deref() != Some("stringArg") })
686 .all(|candidate| {
687 candidate.field_id.is_none()
688 && candidate.type_assignment.is_none()
689 && !candidate.violations.is_empty()
690 }));
691 assert!(candidates.iter().any(|candidate| candidate
692 .violations
693 .contains(&FormFieldDeclarationViolation::InvalidDecoratorInvocation)));
694 assert!(candidates.iter().any(|candidate| candidate
695 .violations
696 .contains(&FormFieldDeclarationViolation::InvalidPath)));
697 assert!(candidates
698 .iter()
699 .any(|candidate| candidate.violations.contains(
700 &FormFieldDeclarationViolation::InvalidDecoratorArity {
701 actual: 0,
702 expected: 1,
703 }
704 )));
705 assert!(candidates.iter().any(|candidate| candidate
706 .violations
707 .contains(&FormFieldDeclarationViolation::UnresolvedForm)));
708 assert!(candidates.iter().any(|candidate| candidate
709 .violations
710 .contains(&FormFieldDeclarationViolation::InvalidForm)));
711 assert!(candidates.iter().any(|candidate| candidate
712 .violations
713 .contains(&FormFieldDeclarationViolation::InheritedDeclaration)));
714 assert!(candidates.iter().any(|candidate| candidate
715 .violations
716 .contains(&FormFieldDeclarationViolation::CrossComponentForm)));
717 assert!(candidates.iter().any(|candidate| candidate
718 .violations
719 .contains(&FormFieldDeclarationViolation::StaticField)));
720 assert!(candidates.iter().any(|candidate| candidate
721 .violations
722 .contains(&FormFieldDeclarationViolation::UnsupportedFieldName)));
723 }
724
725 #[test]
726 fn rejects_invalid_values_types_duplicates_and_conflicts_without_poisoning_valid_fields() {
727 let source = r#"
728@component("profile")
729class Profile {
730 @form() profileForm!: Form;
731 @form() otherForm!: Form;
732 @field(this.profileForm) good = "ok";
733 @field(this.profileForm) missing!: string;
734 @field(this.profileForm) declare declared: string;
735 @field(this.profileForm) noInitializer: string;
736 @field(this.profileForm) call = loadName();
737 @field(this.profileForm) wrapped = state("");
738 @field(this.profileForm) mismatch: number = "bad";
739 @field(this.profileForm) unresolved: MissingType = "";
740 @field(this.profileForm) nonSerializable: SlotContent = "";
741 @field(this.profileForm) duplicate = "first";
742 @field(this.profileForm) duplicate = "second";
743 @field(this.otherForm) duplicate = "other";
744 @field(this.profileForm) @state() stateConflict = "";
745 @form() @field(this.profileForm) formConflict = "";
746 @slot() @field(this.profileForm) slotConflict = "";
747 @custom() @field(this.profileForm) customConflict = "";
748 @field(this.profileForm) @field(this.profileForm) repeatedDecorator = "";
749 render() { return <main />; }
750}
751"#;
752 let parsed = presolve_parser::parse_file("src/InvalidValues.tsx", source);
753 let asm = build_application_semantic_model(&parsed);
754 let candidates = asm.form_field_declaration_candidates();
755
756 assert_eq!(asm.form_fields().len(), 2);
757 assert!(asm.form_fields().iter().any(|field| field.name == "good"));
758 assert!(asm.form_fields().iter().any(|field| {
759 field.name == "duplicate" && field.owner_form.as_str().contains("form:otherForm")
760 }));
761 assert!(candidates
762 .iter()
763 .filter(|candidate| {
764 candidate.authored_name.as_deref() == Some("duplicate")
765 && candidate
766 .resolved_form
767 .as_ref()
768 .is_some_and(|form| form.as_str().contains("form:profileForm"))
769 })
770 .all(|candidate| candidate
771 .violations
772 .contains(&FormFieldDeclarationViolation::DuplicateName)
773 && candidate.field_id.is_none()));
774 for violation in [
775 FormFieldDeclarationViolation::MissingInitializer,
776 FormFieldDeclarationViolation::UnsupportedInitializer,
777 FormFieldDeclarationViolation::InitializerTypeMismatch,
778 FormFieldDeclarationViolation::InvalidDeclaredType,
779 FormFieldDeclarationViolation::NonSerializableType,
780 FormFieldDeclarationViolation::ConflictingSemanticDecorator,
781 ] {
782 assert!(
783 candidates
784 .iter()
785 .any(|candidate| candidate.violations.contains(&violation)),
786 "{violation:?}"
787 );
788 }
789 assert!(candidates
790 .iter()
791 .filter(|candidate| candidate.authored_name.as_deref() == Some("repeatedDecorator"))
792 .all(|candidate| {
793 candidate
794 .violations
795 .contains(&FormFieldDeclarationViolation::DuplicateFieldDecorator)
796 && !candidate
797 .violations
798 .contains(&FormFieldDeclarationViolation::DuplicateName)
799 && candidate.field_id.is_none()
800 }));
801 }
802
803 #[test]
804 fn resolves_local_and_imported_aliases_through_existing_type_authorities() {
805 let types = r"
806export type NullableName = string | null;
807";
808 let editor = r#"
809import { NullableName as Name } from "./types";
810type LocalAge = number;
811@component("profile")
812class Profile {
813 @form() profileForm!: Form;
814 @field(this.profileForm) name: Name = null;
815 @field(this.profileForm) age: LocalAge = 18;
816 render() { return <main />; }
817}
818"#;
819 let unit =
820 CompilationUnit::parse_sources([("src/types.ts", types), ("src/Profile.tsx", editor)]);
821 let asm = build_application_semantic_model_for_unit(&unit);
822
823 assert_eq!(asm.form_fields().len(), 2);
824 assert!(asm.form_fields().iter().all(|field| {
825 field.type_assignment.status == SemanticTypeStatus::Declared
826 && field.type_assignment.origin != field.id.as_semantic_id().clone()
827 }));
828 }
829}