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(authority_type) = candidate.authority_type.clone() {
260 if !matches!(initializer, SerializableValue::Array(ref values) if values.is_empty()) {
261 candidate.add_violation(FormFieldDeclarationViolation::InitializerTypeMismatch);
262 }
263 authority_type
264 } else if let Some(declared_type) = &candidate.declared_type {
265 let Some(resolved) = semantic_types.resolve_declared_type(declared_type, bindings) else {
266 candidate.add_violation(FormFieldDeclarationViolation::InvalidDeclaredType);
267 return;
268 };
269 if let Some(origin) = resolved.alias_origin {
270 alias_origins.insert(candidate.id.clone(), origin);
271 }
272 if !is_supported_form_field_type(&resolved.semantic_type) {
273 candidate.add_violation(FormFieldDeclarationViolation::InvalidDeclaredType);
274 }
275 let source = state_initializer_value_type(&initializer);
276 if !is_assignable(&source, &resolved.semantic_type) {
277 candidate.add_violation(FormFieldDeclarationViolation::InitializerTypeMismatch);
278 }
279 resolved.semantic_type
280 } else {
281 infer_serializable_value_type(&initializer)
282 };
283 if candidate.authority_type.is_none()
284 && serialization_compatibility(&semantic_type) != SerializationCompatibility::Serializable
285 {
286 candidate.add_violation(FormFieldDeclarationViolation::NonSerializableType);
287 }
288 candidate.semantic_type = Some(semantic_type);
289}
290
291fn is_supported_form_field_type(semantic_type: &SemanticType) -> bool {
292 match semantic_type {
293 SemanticType::Null
294 | SemanticType::Boolean
295 | SemanticType::Number
296 | SemanticType::String
297 | SemanticType::BooleanLiteral(_)
298 | SemanticType::NumberLiteral(_)
299 | SemanticType::StringLiteral(_) => true,
300 SemanticType::File => false,
301 SemanticType::Array(element) => is_supported_form_field_type(element),
302 SemanticType::Tuple(items) | SemanticType::Union(items) => {
303 !items.is_empty() && items.iter().all(is_supported_form_field_type)
304 }
305 SemanticType::Object(object) => {
306 object.properties.values().all(is_supported_form_field_type)
307 }
308 SemanticType::Unknown
309 | SemanticType::Never
310 | SemanticType::Form
311 | SemanticType::SlotContent
312 | SemanticType::Resource(_) => false,
313 }
314}
315
316fn mark_duplicate_names(candidates: &mut [FormFieldDeclarationCandidate]) {
317 let mut groups = BTreeMap::<(FormId, String), Vec<usize>>::new();
318 for (index, candidate) in candidates.iter().enumerate() {
319 if let (Some(form), Some(name)) = (&candidate.resolved_form, &candidate.authored_name) {
320 groups
321 .entry((form.clone(), name.clone()))
322 .or_default()
323 .push(index);
324 }
325 }
326 let duplicate_groups = groups
327 .into_values()
328 .filter(|indexes| {
329 indexes
330 .iter()
331 .map(|index| {
332 let provenance = &candidates[*index].provenance;
333 (
334 provenance.path.as_path(),
335 provenance.span.start,
336 provenance.span.end,
337 )
338 })
339 .collect::<BTreeSet<_>>()
340 .len()
341 > 1
342 })
343 .collect::<Vec<_>>();
344 for indexes in duplicate_groups {
345 for index in indexes {
346 candidates[index].add_violation(FormFieldDeclarationViolation::DuplicateName);
347 }
348 }
349}
350
351fn mark_conflicting_paths(candidates: &mut [FormFieldDeclarationCandidate]) {
356 let mut groups = BTreeMap::<(FormId, Vec<String>), Vec<usize>>::new();
357 for (index, candidate) in candidates.iter().enumerate() {
358 if !candidate.is_valid() {
359 continue;
360 }
361 if let (Some(form), Some(name)) = (&candidate.resolved_form, &candidate.authored_name) {
362 let path = candidate
363 .nested_path_segments
364 .clone()
365 .unwrap_or_else(|| vec![name.clone()]);
366 groups.entry((form.clone(), path)).or_default().push(index);
367 }
368 }
369 let entries = groups.into_iter().collect::<Vec<_>>();
370 let mut conflicting = BTreeSet::new();
371 for (_, candidates) in &entries {
372 if candidates.len() > 1 {
373 conflicting.extend(candidates.iter().copied());
374 }
375 }
376 for (left_index, ((left_form, left_path), left_candidates)) in entries.iter().enumerate() {
377 for (right_form, right_path, right_candidates) in entries
378 .iter()
379 .skip(left_index + 1)
380 .map(|((form, path), indexes)| (form, path, indexes))
381 {
382 if left_form != right_form || !paths_conflict(left_path, right_path) {
383 continue;
384 }
385 conflicting.extend(left_candidates.iter().copied());
386 conflicting.extend(right_candidates.iter().copied());
387 }
388 }
389 for index in conflicting {
390 candidates[index].add_violation(FormFieldDeclarationViolation::ConflictingPath);
391 }
392}
393
394fn paths_conflict(left: &[String], right: &[String]) -> bool {
395 (left.len() <= right.len() && left.iter().zip(right).all(|(left, right)| left == right))
396 || (right.len() <= left.len() && right.iter().zip(left).all(|(left, right)| left == right))
397}
398
399fn candidate_source_order(
400 left: &FormFieldDeclarationCandidate,
401 right: &FormFieldDeclarationCandidate,
402) -> std::cmp::Ordering {
403 (
404 left.provenance.path.as_path(),
405 left.provenance.span.start,
406 left.provenance.span.end,
407 left.id.as_str(),
408 )
409 .cmp(&(
410 right.provenance.path.as_path(),
411 right.provenance.span.start,
412 right.provenance.span.end,
413 right.id.as_str(),
414 ))
415}
416
417#[cfg(test)]
418mod tests {
419 use crate::{
420 build_application_semantic_model, build_application_semantic_model_for_unit,
421 build_semantic_graph, validate_application_semantic_model, CompilationUnit,
422 ExecutionBoundary, FieldId, FormFieldDeclarationViolation, FormId, SemanticEntityKind,
423 SemanticOwner, SemanticType, SemanticTypeStatus, SerializableValue,
424 SEMANTIC_GRAPH_SCHEMA_VERSION,
425 };
426
427 #[test]
428 fn lowers_valid_fields_with_exact_ownership_types_values_order_and_provenance() {
429 let source = r#"
430@component("profile-editor")
431class ProfileEditor {
432 @form() profileForm!: Form;
433 @field(this.profileForm) displayName = "Austin";
434 @field(this.profileForm) age: number = 30;
435 @field(this.profileForm) selection: string | null = null;
436 @field(this.profileForm) tags: string[] = [];
437 @field(this.profileForm) point: [string, number] = ["x", 1];
438 @field(this.profileForm) address: { city: string; postalCode: string } = { city: "", postalCode: "" };
439 @field(this.profileForm) score: number = 1 + 2;
440 render() { return <main />; }
441}
442"#;
443 let parsed = presolve_parser::parse_file("src/ProfileEditor.tsx", source);
444 let asm = build_application_semantic_model(&parsed);
445 let component = &asm.components[0];
446 let form_id = FormId::for_owner(&component.id, "profileForm");
447 let display_id = FieldId::for_form(&form_id, "displayName");
448 let display = asm.form_field(&display_id).expect("displayName Field");
449
450 assert_eq!(asm.form_fields().len(), 7);
451 assert_eq!(
452 display.id.as_str(),
453 "module:src/ProfileEditor.tsx/component:profile-editor/form:profileForm/field:displayName"
454 );
455 assert_eq!(display.owner_form, form_id);
456 assert_eq!(display.owner_component, component.id);
457 assert_eq!(display.semantic_type, SemanticType::String);
458 assert_eq!(
459 display.initial_value,
460 SerializableValue::String("Austin".to_string())
461 );
462 assert_eq!(display.type_assignment.status, SemanticTypeStatus::Inferred);
463 assert_eq!(display.boundary, ExecutionBoundary::Client);
464 assert_eq!(display.declaration_order, 0);
465 assert_eq!(
466 asm.owner(display.id.as_semantic_id()),
467 Some(&SemanticOwner::entity(
468 display.owner_form.as_semantic_id().clone()
469 ))
470 );
471 assert_eq!(
472 asm.semantic_type_of(display.id.as_semantic_id()),
473 Some(&SemanticType::String)
474 );
475 assert!(asm
476 .entity(display.id.as_semantic_id())
477 .is_some_and(|entity| entity.kind() == SemanticEntityKind::FormField));
478 assert_eq!(
479 asm.form_fields()
480 .iter()
481 .map(|field| field.declaration_order)
482 .collect::<Vec<_>>(),
483 (0..7).collect::<Vec<_>>()
484 );
485 assert_eq!(
486 asm.form_field(&FieldId::for_form(&display.owner_form, "score"))
487 .expect("score")
488 .initial_value,
489 SerializableValue::Number("3".to_string())
490 );
491 assert_eq!(
492 display.provenance.span.start,
493 source.find("@field(this.profileForm) displayName").unwrap()
494 );
495 assert!(
496 display.form_designator_provenance.span.start
497 < display.initializer_provenance.span.start
498 );
499 let validation = validate_application_semantic_model(&asm);
500 assert!(validation.is_empty(), "{validation:#?}");
501 let graph = build_semantic_graph(&asm);
502 assert_eq!(graph.schema_version, SEMANTIC_GRAPH_SCHEMA_VERSION);
503 assert!(graph
504 .nodes
505 .iter()
506 .any(|node| node.id == *display.id.as_semantic_id()));
507 }
508
509 #[test]
510 fn scopes_names_to_forms_and_is_stable_under_reversed_files() {
511 let first_source = r#"
512@component("profile")
513class Profile {
514 @form() billing!: Form;
515 @form() shipping!: Form;
516 @field(this.billing) address = "billing";
517 @field(this.shipping) address = "shipping";
518 render() { return <main />; }
519}
520"#;
521 let second_source = r#"
522@component("account")
523class Account {
524 @form() billing!: Form;
525 @field(this.billing) address = "account";
526 render() { return <main />; }
527}
528"#;
529 let first = CompilationUnit::parse_sources([
530 ("src/Profile.tsx", first_source),
531 ("src/Account.tsx", second_source),
532 ]);
533 let reversed = CompilationUnit::parse_sources([
534 ("src/Account.tsx", second_source),
535 ("src/Profile.tsx", first_source),
536 ]);
537 let first = build_application_semantic_model_for_unit(&first);
538 let reversed = build_application_semantic_model_for_unit(&reversed);
539
540 assert_eq!(first.form_fields, reversed.form_fields);
541 assert_eq!(
542 first.form_field_declaration_candidates,
543 reversed.form_field_declaration_candidates
544 );
545 assert_eq!(first.form_fields.len(), 3);
546 assert!(first
547 .form_fields
548 .values()
549 .all(|field| field.name == "address" && field.path == ["address"]));
550 }
551
552 #[test]
553 fn lowers_nested_paths_and_rejects_exact_or_prefix_conflicts() {
554 let valid = presolve_parser::parse_file(
555 "src/Profile.tsx",
556 r#"
557@component("profile") class Profile {
558 @form() profile!: Form;
559 @field(this.profile, "address.street") street = "South Congress";
560 @field(this.profile, "address.city") city = "Austin";
561 render() { return <main />; }
562}
563"#,
564 );
565 let valid = build_application_semantic_model(&valid);
566 assert_eq!(valid.form_fields().len(), 2);
567 assert!(valid
568 .form_fields()
569 .iter()
570 .any(|field| field.name == "street" && field.path == ["address", "street"]));
571 assert!(valid
572 .form_fields()
573 .iter()
574 .any(|field| field.name == "city" && field.path == ["address", "city"]));
575 let form = FormId::for_owner(&valid.components[0].id, "profile");
576 assert_eq!(
577 valid.serialization.plans[&crate::SerializationPlanId::for_form(&form)]
578 .fields
579 .iter()
580 .map(|field| field.key.as_str())
581 .collect::<Vec<_>>(),
582 vec!["address.street", "address.city"]
583 );
584
585 let conflicting = presolve_parser::parse_file(
586 "src/Conflicting.tsx",
587 r#"
588@component("conflicting") class Conflicting {
589 @form() profile!: Form;
590 @field(this.profile, "address") address = "flat";
591 @field(this.profile, "address.street") street = "nested";
592 render() { return <main />; }
593}
594"#,
595 );
596 let conflicting = build_application_semantic_model(&conflicting);
597 assert!(conflicting.form_fields().is_empty());
598 assert!(conflicting
599 .form_field_declaration_candidates()
600 .iter()
601 .all(|candidate| candidate
602 .violations
603 .contains(&FormFieldDeclarationViolation::ConflictingPath)));
604
605 let duplicate = presolve_parser::parse_file(
606 "src/Duplicate.tsx",
607 r#"
608@component("duplicate") class Duplicate {
609 @form() profile!: Form;
610 @field(this.profile, "address.street") primary = "one";
611 @field(this.profile, "address.street") secondary = "two";
612 render() { return <main />; }
613}
614"#,
615 );
616 let duplicate = build_application_semantic_model(&duplicate);
617 assert!(duplicate.form_fields().is_empty());
618 assert!(duplicate
619 .form_field_declaration_candidates()
620 .iter()
621 .all(|candidate| candidate
622 .violations
623 .contains(&FormFieldDeclarationViolation::ConflictingPath)));
624 }
625
626 #[test]
627 #[allow(clippy::too_many_lines)]
628 fn retains_invalid_decorators_targets_designators_and_owning_forms_without_field_ids() {
629 let source = r#"
630@field(this.profileForm)
631class ClassTarget {}
632
633class BaseEditor {
634 @form() baseForm!: Form;
635}
636
637@component("other")
638class Other {
639 @form() otherForm!: Form;
640 render() { return <main />; }
641}
642
643@component("profile")
644class Profile extends BaseEditor {
645 @form() validForm!: Form;
646 @form("bad") invalidForm!: Form;
647 normalProperty = {};
648 @field validBare = "";
649 @field() zero = "";
650 @field(this.validForm, "invalid-path") many = "";
651 @field("validForm") stringArg = "";
652 @field(validForm) identifierArg = "";
653 @field(this.forms.validForm) chain = "";
654 @field(getForm()) call = "";
655 @field(this.missing) missing = "";
656 @field(this.normalProperty) ordinary = "";
657 @field(this.invalidForm) invalid = "";
658 @field(this.baseForm) inherited = "";
659 @field(Other.otherForm) cross = "";
660 @field(this.validForm) static staticField = "";
661 @field(this.validForm) ["computed"] = "";
662 @field(this.validForm) #privateField = "";
663 @field(this.validForm) method() {}
664 @field(this.validForm) get getter() { return ""; }
665 @field(this.validForm) set setter(value: string) {}
666 parameter(@field(this.validForm) value: string) {}
667 render() { return <main />; }
668}
669"#;
670 let parsed = presolve_parser::parse_file("src/InvalidFields.tsx", source);
671 let asm = build_application_semantic_model(&parsed);
672 let candidates = asm.form_field_declaration_candidates();
673
674 assert_eq!(asm.form_fields().len(), 1);
675 assert_eq!(asm.form_fields()[0].name, "stringArg");
676 assert_eq!(candidates.len(), 20);
677 assert_eq!(
678 candidates
679 .iter()
680 .map(|candidate| candidate.id.clone())
681 .collect::<std::collections::BTreeSet<_>>()
682 .len(),
683 candidates.len()
684 );
685 assert!(candidates.iter().any(|candidate| {
686 candidate.authored_name.as_deref() == Some("stringArg")
687 && candidate.field_id.is_some()
688 && candidate.type_assignment.is_some()
689 && candidate.violations.is_empty()
690 }));
691 assert!(candidates
692 .iter()
693 .filter(|candidate| { candidate.authored_name.as_deref() != Some("stringArg") })
694 .all(|candidate| {
695 candidate.field_id.is_none()
696 && candidate.type_assignment.is_none()
697 && !candidate.violations.is_empty()
698 }));
699 assert!(candidates.iter().any(|candidate| candidate
700 .violations
701 .contains(&FormFieldDeclarationViolation::InvalidDecoratorInvocation)));
702 assert!(candidates.iter().any(|candidate| candidate
703 .violations
704 .contains(&FormFieldDeclarationViolation::InvalidPath)));
705 assert!(candidates
706 .iter()
707 .any(|candidate| candidate.violations.contains(
708 &FormFieldDeclarationViolation::InvalidDecoratorArity {
709 actual: 0,
710 expected: 1,
711 }
712 )));
713 assert!(candidates.iter().any(|candidate| candidate
714 .violations
715 .contains(&FormFieldDeclarationViolation::UnresolvedForm)));
716 assert!(candidates.iter().any(|candidate| candidate
717 .violations
718 .contains(&FormFieldDeclarationViolation::InvalidForm)));
719 assert!(candidates.iter().any(|candidate| candidate
720 .violations
721 .contains(&FormFieldDeclarationViolation::InheritedDeclaration)));
722 assert!(candidates.iter().any(|candidate| candidate
723 .violations
724 .contains(&FormFieldDeclarationViolation::CrossComponentForm)));
725 assert!(candidates.iter().any(|candidate| candidate
726 .violations
727 .contains(&FormFieldDeclarationViolation::StaticField)));
728 assert!(candidates.iter().any(|candidate| candidate
729 .violations
730 .contains(&FormFieldDeclarationViolation::UnsupportedFieldName)));
731 }
732
733 #[test]
734 fn rejects_invalid_values_types_duplicates_and_conflicts_without_poisoning_valid_fields() {
735 let source = r#"
736@component("profile")
737class Profile {
738 @form() profileForm!: Form;
739 @form() otherForm!: Form;
740 @field(this.profileForm) good = "ok";
741 @field(this.profileForm) missing!: string;
742 @field(this.profileForm) declare declared: string;
743 @field(this.profileForm) noInitializer: string;
744 @field(this.profileForm) call = loadName();
745 @field(this.profileForm) wrapped = state("");
746 @field(this.profileForm) mismatch: number = "bad";
747 @field(this.profileForm) unresolved: MissingType = "";
748 @field(this.profileForm) nonSerializable: SlotContent = "";
749 @field(this.profileForm) duplicate = "first";
750 @field(this.profileForm) duplicate = "second";
751 @field(this.otherForm) duplicate = "other";
752 @field(this.profileForm) @state() stateConflict = "";
753 @form() @field(this.profileForm) formConflict = "";
754 @slot() @field(this.profileForm) slotConflict = "";
755 @custom() @field(this.profileForm) customConflict = "";
756 @field(this.profileForm) @field(this.profileForm) repeatedDecorator = "";
757 render() { return <main />; }
758}
759"#;
760 let parsed = presolve_parser::parse_file("src/InvalidValues.tsx", source);
761 let asm = build_application_semantic_model(&parsed);
762 let candidates = asm.form_field_declaration_candidates();
763
764 assert_eq!(asm.form_fields().len(), 2);
765 assert!(asm.form_fields().iter().any(|field| field.name == "good"));
766 assert!(asm.form_fields().iter().any(|field| {
767 field.name == "duplicate" && field.owner_form.as_str().contains("form:otherForm")
768 }));
769 assert!(candidates
770 .iter()
771 .filter(|candidate| {
772 candidate.authored_name.as_deref() == Some("duplicate")
773 && candidate
774 .resolved_form
775 .as_ref()
776 .is_some_and(|form| form.as_str().contains("form:profileForm"))
777 })
778 .all(|candidate| candidate
779 .violations
780 .contains(&FormFieldDeclarationViolation::DuplicateName)
781 && candidate.field_id.is_none()));
782 for violation in [
783 FormFieldDeclarationViolation::MissingInitializer,
784 FormFieldDeclarationViolation::UnsupportedInitializer,
785 FormFieldDeclarationViolation::InitializerTypeMismatch,
786 FormFieldDeclarationViolation::InvalidDeclaredType,
787 FormFieldDeclarationViolation::NonSerializableType,
788 FormFieldDeclarationViolation::ConflictingSemanticDecorator,
789 ] {
790 assert!(
791 candidates
792 .iter()
793 .any(|candidate| candidate.violations.contains(&violation)),
794 "{violation:?}"
795 );
796 }
797 assert!(candidates
798 .iter()
799 .filter(|candidate| candidate.authored_name.as_deref() == Some("repeatedDecorator"))
800 .all(|candidate| {
801 candidate
802 .violations
803 .contains(&FormFieldDeclarationViolation::DuplicateFieldDecorator)
804 && !candidate
805 .violations
806 .contains(&FormFieldDeclarationViolation::DuplicateName)
807 && candidate.field_id.is_none()
808 }));
809 }
810
811 #[test]
812 fn resolves_local_and_imported_aliases_through_existing_type_authorities() {
813 let types = r"
814export type NullableName = string | null;
815";
816 let editor = r#"
817import { NullableName as Name } from "./types";
818type LocalAge = number;
819@component("profile")
820class Profile {
821 @form() profileForm!: Form;
822 @field(this.profileForm) name: Name = null;
823 @field(this.profileForm) age: LocalAge = 18;
824 render() { return <main />; }
825}
826"#;
827 let unit =
828 CompilationUnit::parse_sources([("src/types.ts", types), ("src/Profile.tsx", editor)]);
829 let asm = build_application_semantic_model_for_unit(&unit);
830
831 assert_eq!(asm.form_fields().len(), 2);
832 assert!(asm.form_fields().iter().all(|field| {
833 field.type_assignment.status == SemanticTypeStatus::Declared
834 && field.type_assignment.origin != field.id.as_semantic_id().clone()
835 }));
836 }
837}