Skip to main content

presolve_compiler/
form_binding.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    is_assignable, state_initializer_value_type, ComponentNode, ElementNode, FieldBindingId,
5    FieldId, FormEntity, FormFieldBindingCandidateId, FormFieldDeclarationCandidate,
6    FormFieldEntity, FormId, RenderAttribute, RenderAttributeValue, SemanticId, SemanticType,
7    SerializableValue, SourceProvenance, TemplateChild, TemplateNode,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum FormInputKind {
12    Text,
13    Email,
14    Password,
15    Search,
16    Tel,
17    Url,
18    Number,
19    Checkbox,
20    Radio,
21    Date,
22    Time,
23    DateTimeLocal,
24    Month,
25    Week,
26    Range,
27    Hidden,
28}
29
30impl FormInputKind {
31    fn from_static(value: &str) -> Option<Self> {
32        Some(match value {
33            "text" => Self::Text,
34            "email" => Self::Email,
35            "password" => Self::Password,
36            "search" => Self::Search,
37            "tel" => Self::Tel,
38            "url" => Self::Url,
39            "number" => Self::Number,
40            "checkbox" => Self::Checkbox,
41            "radio" => Self::Radio,
42            "date" => Self::Date,
43            "time" => Self::Time,
44            "datetime-local" => Self::DateTimeLocal,
45            "month" => Self::Month,
46            "week" => Self::Week,
47            "range" => Self::Range,
48            "hidden" => Self::Hidden,
49            _ => return None,
50        })
51    }
52
53    const fn channel(self) -> FormControlChannel {
54        match self {
55            Self::Number | Self::Range => FormControlChannel::NumericValue,
56            Self::Checkbox => FormControlChannel::Checked,
57            Self::Radio => FormControlChannel::RadioValue,
58            Self::Text
59            | Self::Email
60            | Self::Password
61            | Self::Search
62            | Self::Tel
63            | Self::Url
64            | Self::Date
65            | Self::Time
66            | Self::DateTimeLocal
67            | Self::Month
68            | Self::Week
69            | Self::Hidden => FormControlChannel::Value,
70        }
71    }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
75pub enum FormControlChannel {
76    Value,
77    NumericValue,
78    Checked,
79    RadioValue,
80    SelectedValue,
81    SelectedValues,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum FormControlNormalization {
86    Text,
87    NullableText,
88    Number,
89    NullableNumber,
90    Boolean,
91    Scalar,
92    ScalarArray,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum FormControlCompatibility {
97    Compatible(FormControlNormalization),
98    Incompatible,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum FormFieldBindingExpressionFact {
103    DirectThisMember { name: String },
104    Bare,
105    Static(String),
106    Unsupported { expression: Option<String> },
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
110pub enum FormFieldBindingEvidenceKind {
111    InputType,
112    RadioValue,
113    Multiple,
114    Value,
115    Checked,
116    DefaultValue,
117    Selected,
118    OnInput,
119    OnChange,
120    Spread,
121    TextareaChildren,
122    FormAttribute,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct FormFieldBindingEvidence {
127    pub kind: FormFieldBindingEvidenceKind,
128    pub provenance: SourceProvenance,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
132pub enum FormFieldBindingViolation {
133    InvalidControlElement,
134    InvalidInputType,
135    DynamicInputType,
136    InvalidBindingExpression,
137    UnresolvedField,
138    AmbiguousField,
139    InvalidFieldDeclaration,
140    CrossComponentField,
141    DuplicateBindingAttribute,
142    SpreadConflict,
143    DuplicateFieldControl,
144    InvalidRadioGroup,
145    MissingRadioValue,
146    DuplicateRadioValue,
147    IncompatibleControlType,
148    CompetingValueBinding,
149    CompetingCheckedBinding,
150    CompetingDefaultValue,
151    CompetingSelectedState,
152    CompetingChangeHandler,
153    UnsupportedTextareaChildren,
154    UnsupportedDynamicMultiple,
155    UnsupportedFormAttribute,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct FormFieldBindingCandidate {
160    pub id: FormFieldBindingCandidateId,
161    pub binding_id: Option<FieldBindingId>,
162    pub owner_component: Option<SemanticId>,
163    pub owner_template: Option<SemanticId>,
164    pub control_entity: Option<SemanticId>,
165    pub element_name: Option<String>,
166    pub authored_input_type: Option<String>,
167    pub input_kind: Option<FormInputKind>,
168    pub field_expression: FormFieldBindingExpressionFact,
169    pub authored_field_name: Option<String>,
170    pub resolved_field: Option<FieldId>,
171    pub resolved_form: Option<FormId>,
172    pub channel: Option<FormControlChannel>,
173    pub compatibility: Option<FormControlCompatibility>,
174    pub field_type: Option<SemanticType>,
175    pub radio_value: Option<SerializableValue>,
176    pub authored_order: usize,
177    pub provenance: SourceProvenance,
178    pub attribute_provenance: SourceProvenance,
179    pub expression_provenance: Option<SourceProvenance>,
180    pub field_name_provenance: Option<SourceProvenance>,
181    pub control_kind_provenance: Option<SourceProvenance>,
182    pub radio_value_provenance: Option<SourceProvenance>,
183    pub evidence: Vec<FormFieldBindingEvidence>,
184    pub violations: Vec<FormFieldBindingViolation>,
185}
186
187impl FormFieldBindingCandidate {
188    #[must_use]
189    pub fn is_valid(&self) -> bool {
190        self.violations.is_empty()
191    }
192
193    fn add_violation(&mut self, violation: FormFieldBindingViolation) {
194        if !self.violations.contains(&violation) {
195            self.violations.push(violation);
196            self.violations.sort();
197        }
198    }
199}
200
201/// Immutable compiler-owned use-site binding between one template control and
202/// one canonical Form Field declaration.
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct FormFieldBinding {
205    pub id: FieldBindingId,
206    pub owner_template: SemanticId,
207    pub control_entity: SemanticId,
208    pub field: FieldId,
209    pub form: FormId,
210    pub component: SemanticId,
211    pub element_name: String,
212    pub input_kind: Option<FormInputKind>,
213    pub channel: FormControlChannel,
214    pub compatibility: FormControlCompatibility,
215    pub field_type: SemanticType,
216    pub radio_value: Option<SerializableValue>,
217    pub authored_order: usize,
218    pub provenance: SourceProvenance,
219    pub attribute_provenance: SourceProvenance,
220    pub expression_provenance: SourceProvenance,
221    pub control_kind_provenance: SourceProvenance,
222    pub radio_value_provenance: Option<SourceProvenance>,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Default)]
226pub struct FormFieldBindingProducts {
227    pub candidates: Vec<FormFieldBindingCandidate>,
228    pub bindings: BTreeMap<FieldBindingId, FormFieldBinding>,
229}
230
231#[must_use]
232pub fn collect_form_field_binding_products(
233    components: &[ComponentNode],
234    templates: &[TemplateNode],
235    forms: &BTreeMap<FormId, FormEntity>,
236    fields: &BTreeMap<FieldId, FormFieldEntity>,
237    field_candidates: &[FormFieldDeclarationCandidate],
238) -> FormFieldBindingProducts {
239    let components = components
240        .iter()
241        .map(|component| (component.id.clone(), component))
242        .collect::<BTreeMap<_, _>>();
243    let mut templates = templates.iter().collect::<Vec<_>>();
244    templates.sort_by(|left, right| left.id.cmp(&right.id));
245
246    let mut candidates = Vec::new();
247    for template in templates {
248        let component = template
249            .owner
250            .entity_id()
251            .and_then(|component| components.get(component).copied());
252        if let Some(root) = &template.root {
253            collect_element_candidates(
254                root,
255                template,
256                "root",
257                component,
258                forms,
259                fields,
260                field_candidates,
261                &mut candidates,
262            );
263        }
264        if let Some(root) = &template.root_fragment {
265            collect_child_candidates(
266                &root.children,
267                template,
268                "root",
269                component,
270                forms,
271                fields,
272                field_candidates,
273                &mut candidates,
274            );
275        }
276    }
277
278    candidates.sort_by(candidate_source_order);
279    let mut orders = BTreeMap::<SemanticId, usize>::new();
280    for candidate in &mut candidates {
281        if let Some(template) = &candidate.owner_template {
282            let order = orders.entry(template.clone()).or_default();
283            candidate.authored_order = *order;
284            *order += 1;
285        }
286    }
287    mark_binding_multiplicity(&mut candidates);
288
289    let mut bindings = BTreeMap::new();
290    for candidate in &mut candidates {
291        if !candidate.is_valid() {
292            candidate.binding_id = None;
293            continue;
294        }
295        let binding = lower_valid_binding(candidate);
296        candidate.binding_id = Some(binding.id.clone());
297        bindings.insert(binding.id.clone(), binding);
298    }
299
300    FormFieldBindingProducts {
301        candidates,
302        bindings,
303    }
304}
305
306#[allow(clippy::too_many_arguments)]
307fn collect_element_candidates(
308    element: &ElementNode,
309    template: &TemplateNode,
310    path: &str,
311    component: Option<&ComponentNode>,
312    forms: &BTreeMap<FormId, FormEntity>,
313    fields: &BTreeMap<FieldId, FormFieldEntity>,
314    field_candidates: &[FormFieldDeclarationCandidate],
315    candidates: &mut Vec<FormFieldBindingCandidate>,
316) {
317    let control = template.id.template_entity("element", path);
318    let field_attributes = element
319        .authored_attributes
320        .iter()
321        .filter(|attribute| attribute.name == "field")
322        .collect::<Vec<_>>();
323    for attribute in &field_attributes {
324        let mut candidate = binding_candidate(element, template, &control, component, attribute);
325        if field_attributes.len() > 1 {
326            candidate.add_violation(FormFieldBindingViolation::DuplicateBindingAttribute);
327        }
328        classify_control(element, template, &mut candidate);
329        resolve_field(component, fields, field_candidates, forms, &mut candidate);
330        classify_compatibility(&mut candidate);
331        candidates.push(candidate);
332    }
333
334    collect_child_candidates(
335        &element.children,
336        template,
337        path,
338        component,
339        forms,
340        fields,
341        field_candidates,
342        candidates,
343    );
344}
345
346#[allow(clippy::too_many_arguments)]
347fn collect_child_candidates(
348    children: &[TemplateChild],
349    template: &TemplateNode,
350    parent_path: &str,
351    component: Option<&ComponentNode>,
352    forms: &BTreeMap<FormId, FormEntity>,
353    fields: &BTreeMap<FieldId, FormFieldEntity>,
354    field_candidates: &[FormFieldDeclarationCandidate],
355    candidates: &mut Vec<FormFieldBindingCandidate>,
356) {
357    for (index, child) in children.iter().enumerate() {
358        let path = format!("{parent_path}.{index}");
359        match child {
360            TemplateChild::Element(element) => collect_element_candidates(
361                element,
362                template,
363                &path,
364                component,
365                forms,
366                fields,
367                field_candidates,
368                candidates,
369            ),
370            TemplateChild::Fragment(fragment) => collect_child_candidates(
371                &fragment.children,
372                template,
373                &path,
374                component,
375                forms,
376                fields,
377                field_candidates,
378                candidates,
379            ),
380            TemplateChild::Conditional(conditional) => {
381                collect_child_candidates(
382                    &conditional.when_true,
383                    template,
384                    &format!("{path}.true"),
385                    component,
386                    forms,
387                    fields,
388                    field_candidates,
389                    candidates,
390                );
391                collect_child_candidates(
392                    &conditional.when_false,
393                    template,
394                    &format!("{path}.false"),
395                    component,
396                    forms,
397                    fields,
398                    field_candidates,
399                    candidates,
400                );
401            }
402            TemplateChild::List(list) => collect_child_candidates(
403                &list.item_template,
404                template,
405                &format!("{path}.item"),
406                component,
407                forms,
408                fields,
409                field_candidates,
410                candidates,
411            ),
412            TemplateChild::Text { .. } | TemplateChild::Binding { .. } => {}
413        }
414    }
415}
416
417fn binding_candidate(
418    element: &ElementNode,
419    template: &TemplateNode,
420    control: &SemanticId,
421    component: Option<&ComponentNode>,
422    attribute: &RenderAttribute,
423) -> FormFieldBindingCandidate {
424    let path = &template.provenance.path;
425    let field_expression = match &attribute.value {
426        RenderAttributeValue::Boolean => FormFieldBindingExpressionFact::Bare,
427        RenderAttributeValue::Static(value) => {
428            FormFieldBindingExpressionFact::Static(value.clone())
429        }
430        RenderAttributeValue::Expression(expression) => attribute.this_member.as_ref().map_or_else(
431            || FormFieldBindingExpressionFact::Unsupported {
432                expression: expression.clone(),
433            },
434            |member| FormFieldBindingExpressionFact::DirectThisMember {
435                name: member.member.clone(),
436            },
437        ),
438        RenderAttributeValue::Spread(expression) => FormFieldBindingExpressionFact::Unsupported {
439            expression: expression.clone(),
440        },
441        RenderAttributeValue::Unsupported => {
442            FormFieldBindingExpressionFact::Unsupported { expression: None }
443        }
444    };
445    let authored_field_name = match &field_expression {
446        FormFieldBindingExpressionFact::DirectThisMember { name } => Some(name.clone()),
447        _ => None,
448    };
449    let mut candidate = FormFieldBindingCandidate {
450        id: FormFieldBindingCandidateId::for_source_position(path, attribute.span.start),
451        binding_id: None,
452        owner_component: component.map(|component| component.id.clone()),
453        owner_template: Some(template.id.clone()),
454        control_entity: Some(control.clone()),
455        element_name: Some(element.tag_name.clone()),
456        authored_input_type: None,
457        input_kind: None,
458        field_expression,
459        authored_field_name,
460        resolved_field: None,
461        resolved_form: None,
462        channel: None,
463        compatibility: None,
464        field_type: None,
465        radio_value: None,
466        authored_order: 0,
467        provenance: SourceProvenance::new(path, element.span),
468        attribute_provenance: SourceProvenance::new(path, attribute.span),
469        expression_provenance: attribute
470            .expression_span
471            .or(attribute.value_span)
472            .map(|span| SourceProvenance::new(path, span)),
473        field_name_provenance: attribute
474            .this_member
475            .as_ref()
476            .map(|member| SourceProvenance::new(path, member.member_span)),
477        control_kind_provenance: Some(SourceProvenance::new(path, element.tag_name_span)),
478        radio_value_provenance: None,
479        evidence: Vec::new(),
480        violations: Vec::new(),
481    };
482    if !matches!(
483        candidate.field_expression,
484        FormFieldBindingExpressionFact::DirectThisMember { .. }
485    ) {
486        candidate.add_violation(FormFieldBindingViolation::InvalidBindingExpression);
487    }
488    candidate
489}
490
491fn classify_control(
492    element: &ElementNode,
493    template: &TemplateNode,
494    candidate: &mut FormFieldBindingCandidate,
495) {
496    let path = &template.provenance.path;
497    for attribute in &element.authored_attributes {
498        let evidence_kind = match attribute.name.as_str() {
499            "type" => Some(FormFieldBindingEvidenceKind::InputType),
500            "value" => Some(FormFieldBindingEvidenceKind::Value),
501            "checked" => Some(FormFieldBindingEvidenceKind::Checked),
502            "defaultValue" => Some(FormFieldBindingEvidenceKind::DefaultValue),
503            "multiple" => Some(FormFieldBindingEvidenceKind::Multiple),
504            "onInput" => Some(FormFieldBindingEvidenceKind::OnInput),
505            "onChange" => Some(FormFieldBindingEvidenceKind::OnChange),
506            "form" => Some(FormFieldBindingEvidenceKind::FormAttribute),
507            "{...}" => Some(FormFieldBindingEvidenceKind::Spread),
508            _ => None,
509        };
510        if let Some(kind) = evidence_kind {
511            candidate.evidence.push(FormFieldBindingEvidence {
512                kind,
513                provenance: SourceProvenance::new(path, attribute.span),
514            });
515        }
516    }
517
518    if element
519        .authored_attributes
520        .iter()
521        .any(|attribute| matches!(attribute.value, RenderAttributeValue::Spread(_)))
522    {
523        candidate.add_violation(FormFieldBindingViolation::SpreadConflict);
524    }
525    if element
526        .authored_attributes
527        .iter()
528        .any(|attribute| attribute.name == "form")
529    {
530        candidate.add_violation(FormFieldBindingViolation::UnsupportedFormAttribute);
531    }
532    if element
533        .authored_attributes
534        .iter()
535        .any(|attribute| matches!(attribute.name.as_str(), "onInput" | "onChange"))
536    {
537        candidate.add_violation(FormFieldBindingViolation::CompetingChangeHandler);
538    }
539
540    match element.tag_name.as_str() {
541        "input" => classify_input(element, path, candidate),
542        "textarea" => {
543            candidate.channel = Some(FormControlChannel::Value);
544            if !element.children.is_empty() {
545                candidate.evidence.push(FormFieldBindingEvidence {
546                    kind: FormFieldBindingEvidenceKind::TextareaChildren,
547                    provenance: SourceProvenance::new(path, element.span),
548                });
549                candidate.add_violation(FormFieldBindingViolation::UnsupportedTextareaChildren);
550            }
551            if has_attribute(element, "value") {
552                candidate.add_violation(FormFieldBindingViolation::CompetingValueBinding);
553            }
554        }
555        "select" => classify_select(element, path, candidate),
556        _ => candidate.add_violation(FormFieldBindingViolation::InvalidControlElement),
557    }
558    if has_attribute(element, "defaultValue") {
559        candidate.add_violation(FormFieldBindingViolation::CompetingDefaultValue);
560    }
561}
562
563fn classify_input(
564    element: &ElementNode,
565    path: &std::path::Path,
566    candidate: &mut FormFieldBindingCandidate,
567) {
568    let types = attributes_named(element, "type");
569    let input_kind = match types.as_slice() {
570        [] => Some(FormInputKind::Text),
571        [attribute] => {
572            if let RenderAttributeValue::Static(value) = &attribute.value {
573                candidate.authored_input_type = Some(value.clone());
574                FormInputKind::from_static(value).or_else(|| {
575                    candidate.add_violation(FormFieldBindingViolation::InvalidInputType);
576                    None
577                })
578            } else {
579                candidate.add_violation(FormFieldBindingViolation::DynamicInputType);
580                None
581            }
582        }
583        _ => {
584            candidate.add_violation(FormFieldBindingViolation::InvalidInputType);
585            None
586        }
587    };
588    candidate.input_kind = input_kind;
589    candidate.channel = input_kind.map(FormInputKind::channel);
590    if let Some(type_attribute) = types.first() {
591        candidate.control_kind_provenance = Some(SourceProvenance::new(path, type_attribute.span));
592    }
593
594    match input_kind {
595        Some(FormInputKind::Radio) => classify_radio_value(element, path, candidate),
596        Some(FormInputKind::Checkbox) => {
597            if has_attribute(element, "checked") {
598                candidate.add_violation(FormFieldBindingViolation::CompetingCheckedBinding);
599            }
600        }
601        Some(_) if has_attribute(element, "value") => {
602            candidate.add_violation(FormFieldBindingViolation::CompetingValueBinding);
603        }
604        Some(_) | None => {}
605    }
606}
607
608fn classify_radio_value(
609    element: &ElementNode,
610    path: &std::path::Path,
611    candidate: &mut FormFieldBindingCandidate,
612) {
613    let values = attributes_named(element, "value");
614    let Some(attribute) = values.first() else {
615        candidate.add_violation(FormFieldBindingViolation::MissingRadioValue);
616        candidate.add_violation(FormFieldBindingViolation::InvalidRadioGroup);
617        return;
618    };
619    candidate.evidence.push(FormFieldBindingEvidence {
620        kind: FormFieldBindingEvidenceKind::RadioValue,
621        provenance: SourceProvenance::new(path, attribute.span),
622    });
623    if values.len() != 1 {
624        candidate.add_violation(FormFieldBindingViolation::InvalidRadioGroup);
625        return;
626    }
627    let value = attribute.constant_value.as_ref().filter(|value| {
628        matches!(
629            value,
630            SerializableValue::String(_)
631                | SerializableValue::Number(_)
632                | SerializableValue::Boolean(_)
633        )
634    });
635    if let Some(value) = value {
636        candidate.radio_value = Some(value.clone());
637        candidate.radio_value_provenance = Some(SourceProvenance::new(path, attribute.span));
638    } else {
639        candidate.add_violation(FormFieldBindingViolation::MissingRadioValue);
640        candidate.add_violation(FormFieldBindingViolation::InvalidRadioGroup);
641    }
642}
643
644fn classify_select(
645    element: &ElementNode,
646    path: &std::path::Path,
647    candidate: &mut FormFieldBindingCandidate,
648) {
649    let multiple = attributes_named(element, "multiple");
650    candidate.channel = match multiple.as_slice() {
651        [] => Some(FormControlChannel::SelectedValue),
652        [attribute] if matches!(attribute.value, RenderAttributeValue::Boolean) => {
653            Some(FormControlChannel::SelectedValues)
654        }
655        _ => {
656            candidate.add_violation(FormFieldBindingViolation::UnsupportedDynamicMultiple);
657            None
658        }
659    };
660    if has_attribute(element, "value") {
661        candidate.add_violation(FormFieldBindingViolation::CompetingValueBinding);
662    }
663    if selected_option_provenance(&element.children, path).is_some() {
664        if let Some(provenance) = selected_option_provenance(&element.children, path) {
665            candidate.evidence.push(FormFieldBindingEvidence {
666                kind: FormFieldBindingEvidenceKind::Selected,
667                provenance,
668            });
669        }
670        candidate.add_violation(FormFieldBindingViolation::CompetingSelectedState);
671    }
672}
673
674fn resolve_field(
675    component: Option<&ComponentNode>,
676    fields: &BTreeMap<FieldId, FormFieldEntity>,
677    field_candidates: &[FormFieldDeclarationCandidate],
678    forms: &BTreeMap<FormId, FormEntity>,
679    candidate: &mut FormFieldBindingCandidate,
680) {
681    let (Some(component), Some(name)) = (component, candidate.authored_field_name.as_deref())
682    else {
683        if component.is_none() {
684            candidate.add_violation(FormFieldBindingViolation::CrossComponentField);
685        }
686        return;
687    };
688    let local = fields
689        .values()
690        .filter(|field| field.owner_component == component.id && field.name == name)
691        .collect::<Vec<_>>();
692    let local_invalid = field_candidates
693        .iter()
694        .filter(|field| {
695            field.owner_component.as_ref() == Some(&component.id)
696                && field.authored_name.as_deref() == Some(name)
697                && !field.is_valid()
698        })
699        .count();
700    let ordinary_collision = component
701        .state_fields
702        .iter()
703        .any(|field| field.name == name)
704        || component.methods.iter().any(|method| method.name == name);
705
706    match local.as_slice() {
707        [field] => {
708            candidate.resolved_field = Some(field.id.clone());
709            candidate.resolved_form = Some(field.owner_form.clone());
710            candidate.field_type = Some(field.semantic_type.clone());
711            let valid_form_owner = forms
712                .get(&field.owner_form)
713                .and_then(|form| form.owner.entity_id())
714                == Some(&component.id);
715            if field.owner_component != component.id || !valid_form_owner {
716                candidate.add_violation(FormFieldBindingViolation::CrossComponentField);
717            }
718            if ordinary_collision || local_invalid > 0 {
719                candidate.add_violation(FormFieldBindingViolation::AmbiguousField);
720            }
721        }
722        [] => {
723            if local_invalid > 0 {
724                candidate.add_violation(FormFieldBindingViolation::InvalidFieldDeclaration);
725                if local_invalid > 1 {
726                    candidate.add_violation(FormFieldBindingViolation::AmbiguousField);
727                }
728            } else if fields
729                .values()
730                .any(|field| field.name == name && field.owner_component != component.id)
731            {
732                candidate.add_violation(FormFieldBindingViolation::CrossComponentField);
733            } else {
734                candidate.add_violation(FormFieldBindingViolation::UnresolvedField);
735            }
736        }
737        _ => candidate.add_violation(FormFieldBindingViolation::AmbiguousField),
738    }
739}
740
741fn classify_compatibility(candidate: &mut FormFieldBindingCandidate) {
742    let (Some(channel), Some(field_type)) = (candidate.channel, candidate.field_type.as_ref())
743    else {
744        return;
745    };
746    let compatibility = match channel {
747        FormControlChannel::Value => string_compatibility(field_type),
748        FormControlChannel::NumericValue => numeric_compatibility(field_type),
749        FormControlChannel::Checked => boolean_compatibility(field_type),
750        FormControlChannel::RadioValue => scalar_compatibility(field_type, false),
751        FormControlChannel::SelectedValue => scalar_compatibility(field_type, true),
752        FormControlChannel::SelectedValues => array_scalar_compatibility(field_type),
753    };
754    candidate.compatibility = Some(compatibility);
755    if compatibility == FormControlCompatibility::Incompatible {
756        candidate.add_violation(FormFieldBindingViolation::IncompatibleControlType);
757        return;
758    }
759    if channel == FormControlChannel::RadioValue {
760        if let Some(value) = &candidate.radio_value {
761            if !is_assignable(&state_initializer_value_type(value), field_type) {
762                candidate.add_violation(FormFieldBindingViolation::IncompatibleControlType);
763                candidate.add_violation(FormFieldBindingViolation::InvalidRadioGroup);
764            }
765        }
766    }
767}
768
769fn string_compatibility(semantic_type: &SemanticType) -> FormControlCompatibility {
770    nullable_family_compatibility(
771        semantic_type,
772        |member| {
773            matches!(
774                member,
775                SemanticType::String | SemanticType::StringLiteral(_)
776            )
777        },
778        FormControlNormalization::Text,
779        FormControlNormalization::NullableText,
780    )
781}
782
783fn numeric_compatibility(semantic_type: &SemanticType) -> FormControlCompatibility {
784    nullable_family_compatibility(
785        semantic_type,
786        |member| {
787            matches!(
788                member,
789                SemanticType::Number | SemanticType::NumberLiteral(_)
790            )
791        },
792        FormControlNormalization::Number,
793        FormControlNormalization::NullableNumber,
794    )
795}
796
797fn nullable_family_compatibility(
798    semantic_type: &SemanticType,
799    matches_member: impl Fn(&SemanticType) -> bool,
800    normal: FormControlNormalization,
801    nullable: FormControlNormalization,
802) -> FormControlCompatibility {
803    let members = union_members(semantic_type);
804    let has_null = members
805        .iter()
806        .any(|member| matches!(member, SemanticType::Null));
807    let non_null = members
808        .iter()
809        .filter(|member| !matches!(member, SemanticType::Null))
810        .collect::<Vec<_>>();
811    if !non_null.is_empty() && non_null.iter().all(|member| matches_member(member)) {
812        FormControlCompatibility::Compatible(if has_null { nullable } else { normal })
813    } else {
814        FormControlCompatibility::Incompatible
815    }
816}
817
818fn boolean_compatibility(semantic_type: &SemanticType) -> FormControlCompatibility {
819    let members = union_members(semantic_type);
820    if !members.is_empty()
821        && members.iter().all(|member| {
822            matches!(
823                member,
824                SemanticType::Boolean | SemanticType::BooleanLiteral(_)
825            )
826        })
827    {
828        FormControlCompatibility::Compatible(FormControlNormalization::Boolean)
829    } else {
830        FormControlCompatibility::Incompatible
831    }
832}
833
834fn scalar_compatibility(
835    semantic_type: &SemanticType,
836    allow_null: bool,
837) -> FormControlCompatibility {
838    let members = union_members(semantic_type);
839    if !members.is_empty()
840        && members.iter().all(|member| {
841            matches!(
842                member,
843                SemanticType::String
844                    | SemanticType::StringLiteral(_)
845                    | SemanticType::Number
846                    | SemanticType::NumberLiteral(_)
847                    | SemanticType::Boolean
848                    | SemanticType::BooleanLiteral(_)
849            ) || (allow_null && matches!(member, SemanticType::Null))
850        })
851    {
852        FormControlCompatibility::Compatible(FormControlNormalization::Scalar)
853    } else {
854        FormControlCompatibility::Incompatible
855    }
856}
857
858fn array_scalar_compatibility(semantic_type: &SemanticType) -> FormControlCompatibility {
859    let SemanticType::Array(element) = semantic_type else {
860        return FormControlCompatibility::Incompatible;
861    };
862    if scalar_compatibility(element, true)
863        == FormControlCompatibility::Compatible(FormControlNormalization::Scalar)
864    {
865        FormControlCompatibility::Compatible(FormControlNormalization::ScalarArray)
866    } else {
867        FormControlCompatibility::Incompatible
868    }
869}
870
871fn union_members(semantic_type: &SemanticType) -> Vec<&SemanticType> {
872    match semantic_type {
873        SemanticType::Union(members) => members.iter().collect(),
874        _ => vec![semantic_type],
875    }
876}
877
878fn mark_binding_multiplicity(candidates: &mut [FormFieldBindingCandidate]) {
879    let mut groups = BTreeMap::<(SemanticId, FieldId), Vec<usize>>::new();
880    for (index, candidate) in candidates.iter().enumerate() {
881        if !candidate.is_valid() {
882            continue;
883        }
884        if let (Some(component), Some(field)) =
885            (&candidate.owner_component, &candidate.resolved_field)
886        {
887            groups
888                .entry((component.clone(), field.clone()))
889                .or_default()
890                .push(index);
891        }
892    }
893    for indexes in groups.into_values().filter(|indexes| indexes.len() > 1) {
894        let all_radio = indexes.iter().all(|index| {
895            candidates[*index].channel == Some(FormControlChannel::RadioValue)
896                && candidates[*index].input_kind == Some(FormInputKind::Radio)
897        });
898        if !all_radio {
899            for index in indexes {
900                candidates[index].add_violation(FormFieldBindingViolation::DuplicateFieldControl);
901            }
902            continue;
903        }
904        let mut values = BTreeMap::<String, Vec<usize>>::new();
905        for index in &indexes {
906            if let Some(value) = &candidates[*index].radio_value {
907                values.entry(format!("{value:?}")).or_default().push(*index);
908            }
909        }
910        let duplicate_indexes = values
911            .into_values()
912            .filter(|members| members.len() > 1)
913            .flatten()
914            .collect::<BTreeSet<_>>();
915        if !duplicate_indexes.is_empty() {
916            for index in indexes {
917                candidates[index].add_violation(FormFieldBindingViolation::InvalidRadioGroup);
918                if duplicate_indexes.contains(&index) {
919                    candidates[index].add_violation(FormFieldBindingViolation::DuplicateRadioValue);
920                }
921            }
922        }
923    }
924}
925
926fn lower_valid_binding(candidate: &FormFieldBindingCandidate) -> FormFieldBinding {
927    let control = candidate
928        .control_entity
929        .clone()
930        .expect("valid binding has control identity");
931    let field = candidate
932        .resolved_field
933        .clone()
934        .expect("valid binding has Field identity");
935    FormFieldBinding {
936        id: FieldBindingId::for_control(&control, &field),
937        owner_template: candidate
938            .owner_template
939            .clone()
940            .expect("valid binding has template owner"),
941        control_entity: control,
942        field,
943        form: candidate
944            .resolved_form
945            .clone()
946            .expect("valid binding has Form identity"),
947        component: candidate
948            .owner_component
949            .clone()
950            .expect("valid binding has component owner"),
951        element_name: candidate
952            .element_name
953            .clone()
954            .expect("valid binding has element name"),
955        input_kind: candidate.input_kind,
956        channel: candidate
957            .channel
958            .expect("valid binding has control channel"),
959        compatibility: candidate
960            .compatibility
961            .expect("valid binding has compatibility"),
962        field_type: candidate
963            .field_type
964            .clone()
965            .expect("valid binding has Field type"),
966        radio_value: candidate.radio_value.clone(),
967        authored_order: candidate.authored_order,
968        provenance: candidate.provenance.clone(),
969        attribute_provenance: candidate.attribute_provenance.clone(),
970        expression_provenance: candidate
971            .expression_provenance
972            .clone()
973            .expect("valid binding has expression provenance"),
974        control_kind_provenance: candidate
975            .control_kind_provenance
976            .clone()
977            .expect("valid binding has control-kind provenance"),
978        radio_value_provenance: candidate.radio_value_provenance.clone(),
979    }
980}
981
982fn attributes_named<'a>(element: &'a ElementNode, name: &str) -> Vec<&'a RenderAttribute> {
983    element
984        .authored_attributes
985        .iter()
986        .filter(|attribute| attribute.name == name)
987        .collect()
988}
989
990fn has_attribute(element: &ElementNode, name: &str) -> bool {
991    element
992        .authored_attributes
993        .iter()
994        .any(|attribute| attribute.name == name)
995}
996
997fn selected_option_provenance(
998    children: &[TemplateChild],
999    path: &std::path::Path,
1000) -> Option<SourceProvenance> {
1001    for child in children {
1002        match child {
1003            TemplateChild::Element(element) => {
1004                if element.tag_name == "option" {
1005                    if let Some(selected) = element
1006                        .authored_attributes
1007                        .iter()
1008                        .find(|attribute| attribute.name == "selected")
1009                    {
1010                        return Some(SourceProvenance::new(path, selected.span));
1011                    }
1012                }
1013                if let Some(provenance) = selected_option_provenance(&element.children, path) {
1014                    return Some(provenance);
1015                }
1016            }
1017            TemplateChild::Fragment(fragment) => {
1018                if let Some(provenance) = selected_option_provenance(&fragment.children, path) {
1019                    return Some(provenance);
1020                }
1021            }
1022            TemplateChild::Conditional(conditional) => {
1023                if let Some(provenance) = selected_option_provenance(&conditional.when_true, path)
1024                    .or_else(|| selected_option_provenance(&conditional.when_false, path))
1025                {
1026                    return Some(provenance);
1027                }
1028            }
1029            TemplateChild::List(list) => {
1030                if let Some(provenance) = selected_option_provenance(&list.item_template, path) {
1031                    return Some(provenance);
1032                }
1033            }
1034            TemplateChild::Text { .. } | TemplateChild::Binding { .. } => {}
1035        }
1036    }
1037    None
1038}
1039
1040fn candidate_source_order(
1041    left: &FormFieldBindingCandidate,
1042    right: &FormFieldBindingCandidate,
1043) -> std::cmp::Ordering {
1044    (
1045        left.provenance.path.as_path(),
1046        left.attribute_provenance.span.start,
1047        left.attribute_provenance.span.end,
1048        left.id.as_str(),
1049    )
1050        .cmp(&(
1051            right.provenance.path.as_path(),
1052            right.attribute_provenance.span.start,
1053            right.attribute_provenance.span.end,
1054            right.id.as_str(),
1055        ))
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060    use std::collections::BTreeSet;
1061
1062    use crate::{
1063        build_application_semantic_model, build_application_semantic_model_for_unit,
1064        build_semantic_graph, validate_application_semantic_model, CompilationUnit, FieldBindingId,
1065        FieldId, FormControlChannel, FormControlCompatibility, FormControlNormalization,
1066        FormFieldBindingViolation, FormId, SemanticEntityKind, SemanticOwner,
1067        SemanticReferenceKind,
1068    };
1069
1070    #[test]
1071    #[allow(clippy::too_many_lines)]
1072    fn lowers_supported_controls_with_exact_fields_channels_ownership_and_references() {
1073        let source = r#"
1074@component("profile-editor")
1075class ProfileEditor {
1076  @form() profile!: Form;
1077  @form() preferences!: Form;
1078  @field(this.profile) displayName = "";
1079  @field(this.profile) email = "";
1080  @field(this.profile) password: string | null = null;
1081  @field(this.profile) age = 18;
1082  @field(this.profile) distance = 5;
1083  @field(this.profile) enabled = false;
1084  @field(this.profile) contact: "email" | "phone" = "email";
1085  @field(this.profile) biography = "";
1086  @field(this.preferences) country = "us";
1087  @field(this.preferences) tags: string[] = [];
1088  render() {
1089    return <main>
1090      <section><input field={this.displayName} /></section>
1091      <input type="email" field={this.email} />
1092      <input type="password" field={this.password} />
1093      <input type="number" field={this.age} />
1094      <input type="range" field={this.distance} />
1095      <input type="checkbox" field={this.enabled} />
1096      <input type="radio" value="email" field={this.contact} />
1097      <input type="radio" value="phone" field={this.contact} />
1098      <textarea field={this.biography} />
1099      <select field={this.country}><option value="us">US</option></select>
1100      <select multiple field={this.tags}><option value="compiler">Compiler</option></select>
1101    </main>;
1102  }
1103}
1104"#;
1105        let parsed = presolve_parser::parse_file("src/ProfileEditor.tsx", source);
1106        let asm = build_application_semantic_model(&parsed);
1107
1108        assert_eq!(asm.form_field_binding_candidates().len(), 11);
1109        assert_eq!(asm.form_field_bindings().len(), 11);
1110        assert!(asm
1111            .form_field_binding_candidates()
1112            .iter()
1113            .all(|candidate| candidate.is_valid() && candidate.binding_id.is_some()));
1114        assert_eq!(
1115            asm.form_field_bindings()
1116                .iter()
1117                .map(|binding| binding.authored_order)
1118                .collect::<Vec<_>>(),
1119            (0..11).collect::<Vec<_>>()
1120        );
1121
1122        let component = &asm.components[0].id;
1123        let profile = FormId::for_owner(component, "profile");
1124        let preferences = FormId::for_owner(component, "preferences");
1125        let display = FieldId::for_form(&profile, "displayName");
1126        let display_binding = asm
1127            .form_field_bindings()
1128            .into_iter()
1129            .find(|binding| binding.field == display)
1130            .expect("display binding");
1131        assert_eq!(display_binding.channel, FormControlChannel::Value);
1132        assert_eq!(
1133            display_binding.compatibility,
1134            FormControlCompatibility::Compatible(FormControlNormalization::Text)
1135        );
1136        assert_eq!(display_binding.form, profile);
1137        assert_eq!(display_binding.component, *component);
1138        assert_eq!(
1139            asm.owner(display_binding.id.as_semantic_id()),
1140            Some(&SemanticOwner::entity(
1141                display_binding.control_entity.clone()
1142            ))
1143        );
1144        assert!(asm
1145            .entity(display_binding.id.as_semantic_id())
1146            .is_some_and(|entity| entity.kind() == SemanticEntityKind::FormFieldBinding));
1147        assert_eq!(
1148            display_binding.id,
1149            FieldBindingId::for_control(&display_binding.control_entity, &display)
1150        );
1151
1152        let password = asm
1153            .form_field_bindings()
1154            .into_iter()
1155            .find(|binding| binding.field == FieldId::for_form(&profile, "password"))
1156            .expect("password binding");
1157        assert_eq!(
1158            password.compatibility,
1159            FormControlCompatibility::Compatible(FormControlNormalization::NullableText)
1160        );
1161        assert!(asm.form_field_bindings().iter().any(|binding| {
1162            binding.form == preferences && binding.channel == FormControlChannel::SelectedValues
1163        }));
1164        assert_eq!(
1165            asm.references_of_kind(SemanticReferenceKind::FieldBindingField)
1166                .len(),
1167            11
1168        );
1169        assert_eq!(
1170            asm.references_of_kind(SemanticReferenceKind::FieldBindingForm)
1171                .len(),
1172            11
1173        );
1174        assert!(validate_application_semantic_model(&asm).is_empty());
1175        let graph = build_semantic_graph(&asm);
1176        assert!(graph
1177            .nodes
1178            .iter()
1179            .any(|node| node.id.as_str().contains("/field-binding:")));
1180        assert!(graph
1181            .edges
1182            .iter()
1183            .any(|edge| edge.kind == crate::SemanticGraphEdgeKind::FieldBindingBindsField));
1184
1185        let executable_attributes = &asm.templates[0]
1186            .root
1187            .as_ref()
1188            .expect("root")
1189            .children
1190            .iter()
1191            .find_map(|child| match child {
1192                crate::TemplateChild::Element(section) if section.tag_name == "section" => {
1193                    section.children.iter().find_map(|child| match child {
1194                        crate::TemplateChild::Element(input) => Some(&input.attributes),
1195                        _ => None,
1196                    })
1197                }
1198                _ => None,
1199            })
1200            .expect("nested input attributes");
1201        assert!(executable_attributes
1202            .iter()
1203            .all(|attribute| attribute.name != "field"));
1204    }
1205
1206    #[test]
1207    fn retains_invalid_syntax_controls_and_partially_resolved_evidence_without_binding_ids() {
1208        let source = r#"
1209@component("invalid-bindings")
1210class InvalidBindings {
1211  @form() form!: Form;
1212  @field(this.form) text = "";
1213  @field(this.form) tags: string[] = [];
1214  render() {
1215    return <main>
1216      <input field />
1217      <input field="text" />
1218      <input field={text} />
1219      <input field={this.form.text} />
1220      <input field={this["text"]} />
1221      <input field={getField()} />
1222      <input field={condition ? this.text : this.tags} />
1223      <input field={this.text} field={this.tags} />
1224      <input field={this.text} {...props} />
1225      <div field={this.text} />
1226      <button field={this.text} />
1227      <MyInput field={this.text} />
1228      <input type="file" field={this.text} />
1229      <input type={this.kind} field={this.text} />
1230      <textarea field={this.text}>Initial</textarea>
1231      <select multiple={this.multiple} field={this.tags} />
1232      <input field={this.text} />
1233    </main>;
1234  }
1235}
1236"#;
1237        let asm = build_application_semantic_model(&presolve_parser::parse_file(
1238            "src/InvalidBindings.tsx",
1239            source,
1240        ));
1241        let candidates = asm.form_field_binding_candidates();
1242        assert_eq!(candidates.len(), 18);
1243        assert_eq!(
1244            candidates
1245                .iter()
1246                .map(|candidate| candidate.id.clone())
1247                .collect::<BTreeSet<_>>()
1248                .len(),
1249            candidates.len()
1250        );
1251        assert!(candidates
1252            .iter()
1253            .filter(|candidate| !candidate.is_valid())
1254            .all(|candidate| candidate.binding_id.is_none()));
1255        assert!(candidates.iter().any(|candidate| candidate
1256            .violations
1257            .contains(&FormFieldBindingViolation::InvalidBindingExpression)));
1258        assert!(candidates.iter().any(|candidate| candidate
1259            .violations
1260            .contains(&FormFieldBindingViolation::DuplicateBindingAttribute)));
1261        assert!(candidates.iter().any(|candidate| candidate
1262            .violations
1263            .contains(&FormFieldBindingViolation::SpreadConflict)));
1264        assert!(candidates.iter().any(|candidate| candidate
1265            .violations
1266            .contains(&FormFieldBindingViolation::InvalidControlElement)));
1267        assert!(candidates.iter().any(|candidate| candidate
1268            .violations
1269            .contains(&FormFieldBindingViolation::InvalidInputType)));
1270        assert!(candidates.iter().any(|candidate| candidate
1271            .violations
1272            .contains(&FormFieldBindingViolation::DynamicInputType)));
1273        assert!(candidates.iter().any(|candidate| candidate
1274            .violations
1275            .contains(&FormFieldBindingViolation::UnsupportedTextareaChildren)));
1276        assert!(candidates.iter().any(|candidate| candidate
1277            .violations
1278            .contains(&FormFieldBindingViolation::UnsupportedDynamicMultiple)));
1279        let resolved_invalid = candidates
1280            .iter()
1281            .find(|candidate| {
1282                candidate
1283                    .violations
1284                    .contains(&FormFieldBindingViolation::InvalidControlElement)
1285                    && candidate.resolved_field.is_some()
1286            })
1287            .expect("partially resolved invalid control");
1288        assert!(resolved_invalid.resolved_form.is_some());
1289        assert!(!resolved_invalid.evidence.is_empty() || resolved_invalid.element_name.is_some());
1290        assert_eq!(asm.form_field_bindings().len(), 1);
1291    }
1292
1293    #[test]
1294    #[allow(clippy::too_many_lines)]
1295    fn enforces_resolution_compatibility_multiplicity_radio_and_channel_conflicts() {
1296        let source = r#"
1297@component("binding-rules")
1298class BindingRules {
1299  @form() first!: Form;
1300  @form() second!: Form;
1301  @field(this.first) duplicate = "";
1302  @field(this.first) contact: "email" | "phone" = "email";
1303  @field(this.first) badRadio: 1 | 2 = 1;
1304  @field(this.first) missingRadio = "";
1305  @field(this.first) enabled: boolean | null = null;
1306  @field(this.first) scalar = "";
1307  @field(this.first) object = { value: "" };
1308  @field(this.first) nullableNumber: number | null = null;
1309  @field(this.first) conflictText = "";
1310  @field(this.first) conflictCheck = false;
1311  @field(this.first) duplicateCheck = false;
1312  @field(this.first) conflictSelect = "us";
1313  @field(this.first) eventText = "";
1314  @field(this.first) changeText = "";
1315  @field(this.first) dynamicValue = "";
1316  @field(this.first) clickText = "";
1317  @field(this.first) selectedText = "us";
1318  @field(this.first) ambiguous = "first";
1319  @field(this.second) ambiguous = "second";
1320  ordinary = state("");
1321  render() {
1322    return <main>
1323      <input field={this.duplicate} />
1324      <textarea field={this.duplicate} />
1325      <input type="radio" value="email" field={this.contact} />
1326      <input type="radio" value="phone" field={this.contact} />
1327      <input type="radio" value="email" field={this.contact} />
1328      <input type="radio" value="one" field={this.badRadio} />
1329      <input type="radio" field={this.missingRadio} />
1330      <input type="checkbox" field={this.enabled} />
1331      <select multiple field={this.scalar} />
1332      <select field={this.object} />
1333      <input type="number" field={this.nullableNumber} />
1334      <input value="authored" field={this.conflictText} />
1335      <input type="checkbox" checked field={this.conflictCheck} />
1336      <input type="checkbox" field={this.duplicateCheck} />
1337      <input type="checkbox" field={this.duplicateCheck} />
1338      <select defaultValue="us" field={this.conflictSelect} />
1339      <input onInput={this.track} field={this.eventText} />
1340      <input onChange={this.track} field={this.changeText} />
1341      <input value={this.ordinary} field={this.dynamicValue} />
1342      <input onClick={this.track} field={this.clickText} />
1343      <select field={this.selectedText}><option selected value="us">US</option></select>
1344      <input field={this.ordinary} />
1345      <input field={this.missing} />
1346      <input field={this.ambiguous} />
1347      <input field={this.conflictText} />
1348    </main>;
1349  }
1350  track() {}
1351}
1352"#;
1353        let asm = build_application_semantic_model(&presolve_parser::parse_file(
1354            "src/BindingRules.tsx",
1355            source,
1356        ));
1357        let candidates = asm.form_field_binding_candidates();
1358        assert!(
1359            candidates
1360                .iter()
1361                .filter(|candidate| {
1362                    candidate
1363                        .violations
1364                        .contains(&FormFieldBindingViolation::DuplicateFieldControl)
1365                })
1366                .count()
1367                >= 4
1368        );
1369        assert!(candidates.iter().any(|candidate| candidate
1370            .violations
1371            .contains(&FormFieldBindingViolation::DuplicateRadioValue)));
1372        assert!(candidates.iter().any(|candidate| candidate
1373            .violations
1374            .contains(&FormFieldBindingViolation::MissingRadioValue)));
1375        assert!(candidates.iter().any(|candidate| candidate
1376            .violations
1377            .contains(&FormFieldBindingViolation::IncompatibleControlType)));
1378        assert!(candidates.iter().any(|candidate| candidate
1379            .violations
1380            .contains(&FormFieldBindingViolation::CompetingValueBinding)));
1381        assert!(candidates.iter().any(|candidate| candidate
1382            .violations
1383            .contains(&FormFieldBindingViolation::CompetingCheckedBinding)));
1384        assert!(candidates.iter().any(|candidate| candidate
1385            .violations
1386            .contains(&FormFieldBindingViolation::CompetingDefaultValue)));
1387        assert!(candidates.iter().any(|candidate| candidate
1388            .violations
1389            .contains(&FormFieldBindingViolation::CompetingSelectedState)));
1390        assert!(candidates.iter().any(|candidate| candidate
1391            .violations
1392            .contains(&FormFieldBindingViolation::CompetingChangeHandler)));
1393        assert!(candidates.iter().any(|candidate| candidate
1394            .violations
1395            .contains(&FormFieldBindingViolation::UnresolvedField)));
1396        assert!(candidates.iter().any(|candidate| candidate
1397            .violations
1398            .contains(&FormFieldBindingViolation::AmbiguousField)));
1399        let nullable_number = asm
1400            .form_field_bindings()
1401            .into_iter()
1402            .find(|binding| binding.channel == FormControlChannel::NumericValue)
1403            .expect("valid nullable number binding");
1404        assert_eq!(
1405            nullable_number.compatibility,
1406            FormControlCompatibility::Compatible(FormControlNormalization::NullableNumber)
1407        );
1408        assert!(asm.form_field_bindings().iter().all(|binding| {
1409            binding.field
1410                != FieldId::for_form(
1411                    &FormId::for_owner(&asm.components[0].id, "first"),
1412                    "contact",
1413                )
1414        }));
1415        assert!(asm.form_field_bindings().iter().any(|binding| {
1416            binding.field
1417                == FieldId::for_form(
1418                    &FormId::for_owner(&asm.components[0].id, "first"),
1419                    "clickText",
1420                )
1421        }));
1422    }
1423
1424    #[test]
1425    fn recognizes_the_complete_static_input_kind_contract() {
1426        let supported = [
1427            "text",
1428            "email",
1429            "password",
1430            "search",
1431            "tel",
1432            "url",
1433            "number",
1434            "checkbox",
1435            "radio",
1436            "date",
1437            "time",
1438            "datetime-local",
1439            "month",
1440            "week",
1441            "range",
1442            "hidden",
1443        ];
1444        assert!(supported
1445            .iter()
1446            .all(|kind| super::FormInputKind::from_static(kind).is_some()));
1447        assert!(super::FormInputKind::from_static("file").is_none());
1448        assert!(super::FormInputKind::from_static("color").is_none());
1449    }
1450
1451    #[test]
1452    fn preserves_valid_radio_groups_and_allows_the_same_name_in_separate_components() {
1453        let first = r#"
1454@component("first") class First {
1455  @form() form!: Form;
1456  @field(this.form) choice: "a" | "b" = "a";
1457  render() { return <main><input type="radio" value="a" field={this.choice} /><input type="radio" value="b" field={this.choice} /></main>; }
1458}
1459"#;
1460        let second = r#"
1461@component("second") class Second {
1462  @form() form!: Form;
1463  @field(this.form) choice = "";
1464  render() { return <input field={this.choice} />; }
1465}
1466"#;
1467        let unit =
1468            CompilationUnit::parse_sources([("src/First.tsx", first), ("src/Second.tsx", second)]);
1469        let reversed =
1470            CompilationUnit::parse_sources([("src/Second.tsx", second), ("src/First.tsx", first)]);
1471        let asm = build_application_semantic_model_for_unit(&unit);
1472        let reversed = build_application_semantic_model_for_unit(&reversed);
1473        assert_eq!(
1474            asm.form_field_binding_candidates,
1475            reversed.form_field_binding_candidates
1476        );
1477        assert_eq!(asm.form_field_bindings, reversed.form_field_bindings);
1478        assert_eq!(asm.form_field_bindings().len(), 3);
1479        assert_eq!(
1480            asm.form_field_bindings()
1481                .iter()
1482                .filter(|binding| binding.channel == FormControlChannel::RadioValue)
1483                .count(),
1484            2
1485        );
1486    }
1487
1488    #[test]
1489    fn retains_invalid_duplicate_cross_component_and_inherited_field_resolution() {
1490        let source = r#"
1491class BaseEditor {
1492  @form() baseForm!: Form;
1493  @field(this.baseForm) inherited = "";
1494}
1495
1496@component("owner")
1497class Owner {
1498  @form() form!: Form;
1499  @field(this.form) remote = "";
1500  render() { return <main />; }
1501}
1502
1503@component("consumer")
1504class Consumer extends BaseEditor {
1505  @form("invalid") invalidForm!: Form;
1506  @field(this.invalidForm) invalidField = "";
1507  @form() form!: Form;
1508  @field(this.form) duplicate = "first";
1509  @field(this.form) duplicate = "second";
1510  ordinary = state("");
1511  render() {
1512    return <main>
1513      <input field={this.invalidField} />
1514      <input field={this.duplicate} />
1515      <input field={this.remote} />
1516      <input field={this.inherited} />
1517      <input field={this.ordinary} />
1518    </main>;
1519  }
1520}
1521"#;
1522        let asm = build_application_semantic_model(&presolve_parser::parse_file(
1523            "src/Resolution.tsx",
1524            source,
1525        ));
1526        let candidates = asm.form_field_binding_candidates();
1527        assert!(asm.form_field_bindings().is_empty());
1528        assert!(candidates.iter().any(|candidate| {
1529            candidate.authored_field_name.as_deref() == Some("invalidField")
1530                && candidate
1531                    .violations
1532                    .contains(&FormFieldBindingViolation::InvalidFieldDeclaration)
1533        }));
1534        assert!(candidates.iter().any(|candidate| {
1535            candidate.authored_field_name.as_deref() == Some("duplicate")
1536                && candidate
1537                    .violations
1538                    .contains(&FormFieldBindingViolation::AmbiguousField)
1539        }));
1540        assert!(candidates.iter().any(|candidate| {
1541            candidate.authored_field_name.as_deref() == Some("remote")
1542                && candidate
1543                    .violations
1544                    .contains(&FormFieldBindingViolation::CrossComponentField)
1545        }));
1546        assert!(candidates.iter().any(|candidate| {
1547            candidate.authored_field_name.as_deref() == Some("inherited")
1548                && candidate
1549                    .violations
1550                    .contains(&FormFieldBindingViolation::UnresolvedField)
1551        }));
1552        assert!(candidates.iter().any(|candidate| {
1553            candidate.authored_field_name.as_deref() == Some("ordinary")
1554                && candidate
1555                    .violations
1556                    .contains(&FormFieldBindingViolation::UnresolvedField)
1557        }));
1558    }
1559}