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#[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}