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