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