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(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
351/// A JSON Field path is a compiler-owned shape, not a bag of author strings.
352/// Therefore a leaf may neither duplicate another leaf nor become an object
353/// prefix of another leaf in the same Form. Invalid candidates are excluded:
354/// their primary error is more useful and they have no executable path.
355fn 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}