Skip to main content

presolve_compiler/
form_field.rs

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/// First-class immutable compiler-owned Form Field declaration.
12#[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    /// Compiler-issued serialized leaf path. Current one-argument Fields use
20    /// their name as a single root segment; N7-B will admit retained nested
21    /// segments only with the complete artifact/runtime contract.
22    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/// Resolve normalized I3 candidates exclusively through canonical I2 Forms and
42/// the existing Phase C type/value authorities.
43///
44/// # Panics
45///
46/// Panics if a candidate with no retained violations lacks a canonical Form,
47/// component, name, type, value, or provenance input. Such a candidate violates
48/// the I3 staged-lowering invariant.
49#[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
343/// A JSON Field path is a compiler-owned shape, not a bag of author strings.
344/// Therefore a leaf may neither duplicate another leaf nor become an object
345/// prefix of another leaf in the same Form. Invalid candidates are excluded:
346/// their primary error is more useful and they have no executable path.
347fn 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}