1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4 ApplicationSemanticModel, ComponentBuildRoot, ComponentNode, ComponentRootId, FieldBindingId,
5 FieldId, FormEntity, FormFieldBinding, FormFieldEntity, FormId, FormOwnershipGraphId,
6 SemanticId, SemanticOwner, SemanticReference, SemanticReferenceKind, SourceProvenance,
7 TemplateSemanticEntity, TemplateSemanticKind,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
13pub enum FormOwnershipNodeKey {
14 Component(SemanticId),
15 Form(FormId),
16 FormField(FieldId),
17 TemplateControl(SemanticId),
18 FieldBinding(FieldBindingId),
19}
20
21impl FormOwnershipNodeKey {
22 #[must_use]
23 pub fn semantic_id(&self) -> &SemanticId {
24 match self {
25 Self::Component(id) | Self::TemplateControl(id) => id,
26 Self::Form(id) => id.as_semantic_id(),
27 Self::FormField(id) => id.as_semantic_id(),
28 Self::FieldBinding(id) => id.as_semantic_id(),
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum FormOwnershipNode {
35 Component {
36 id: SemanticId,
37 provenance: SourceProvenance,
38 },
39 Form {
40 id: FormId,
41 provenance: SourceProvenance,
42 },
43 FormField {
44 id: FieldId,
45 provenance: SourceProvenance,
46 },
47 TemplateControl {
48 id: SemanticId,
49 provenance: SourceProvenance,
50 },
51 FieldBinding {
52 id: FieldBindingId,
53 provenance: SourceProvenance,
54 },
55}
56
57impl FormOwnershipNode {
58 #[must_use]
59 pub fn key(&self) -> FormOwnershipNodeKey {
60 match self {
61 Self::Component { id, .. } => FormOwnershipNodeKey::Component(id.clone()),
62 Self::Form { id, .. } => FormOwnershipNodeKey::Form(id.clone()),
63 Self::FormField { id, .. } => FormOwnershipNodeKey::FormField(id.clone()),
64 Self::TemplateControl { id, .. } => FormOwnershipNodeKey::TemplateControl(id.clone()),
65 Self::FieldBinding { id, .. } => FormOwnershipNodeKey::FieldBinding(id.clone()),
66 }
67 }
68
69 #[must_use]
70 pub const fn provenance(&self) -> &SourceProvenance {
71 match self {
72 Self::Component { provenance, .. }
73 | Self::Form { provenance, .. }
74 | Self::FormField { provenance, .. }
75 | Self::TemplateControl { provenance, .. }
76 | Self::FieldBinding { provenance, .. } => provenance,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
82pub enum FormOwnershipEdgeKind {
83 ComponentOwnsForm,
84 FormOwnsField,
85 TemplateControlOwnsBinding,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct FormOwnershipEdge {
90 pub owner: FormOwnershipNodeKey,
91 pub child: FormOwnershipNodeKey,
92 pub kind: FormOwnershipEdgeKind,
93 pub provenance: SourceProvenance,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
97pub enum FormReferenceKind {
98 FieldBindingField,
99 FieldBindingForm,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct FormReferenceEdge {
104 pub kind: FormReferenceKind,
105 pub source: FormOwnershipNodeKey,
106 pub target: FormOwnershipNodeKey,
107 pub provenance: SourceProvenance,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
111pub enum FormOwnershipIntegrityKind {
112 DuplicateNode,
113 MissingOwner,
114 UnknownOwnershipEndpoint,
115 MultipleOwners,
116 OwnershipCycle,
117 UnreachableNode,
118 UnknownReferenceEndpoint,
119 ComponentOwnershipMismatch,
120 FieldFormMismatch,
121 BindingFieldMismatch,
122 BindingFormMismatch,
123 BindingTemplateMismatch,
124 InvalidCandidatePromoted,
125 InstanceIdentityInDeclarationGraph,
126 MissingProvenance,
127 NonCanonicalOrdering,
128 GraphIdentityMismatch,
129}
130
131impl FormOwnershipIntegrityKind {
132 #[must_use]
133 pub const fn code(self) -> &'static str {
134 match self {
135 Self::DuplicateNode => "PSASM1203",
136 Self::MissingOwner => "PSASM1204",
137 Self::UnknownOwnershipEndpoint => "PSASM1205",
138 Self::MultipleOwners => "PSASM1206",
139 Self::OwnershipCycle => "PSASM1207",
140 Self::UnreachableNode => "PSASM1208",
141 Self::UnknownReferenceEndpoint => "PSASM1209",
142 Self::ComponentOwnershipMismatch => "PSASM1210",
143 Self::FieldFormMismatch => "PSASM1211",
144 Self::BindingFieldMismatch => "PSASM1212",
145 Self::BindingFormMismatch => "PSASM1213",
146 Self::BindingTemplateMismatch => "PSASM1214",
147 Self::InvalidCandidatePromoted => "PSASM1215",
148 Self::InstanceIdentityInDeclarationGraph => "PSASM1216",
149 Self::MissingProvenance => "PSASM1217",
150 Self::NonCanonicalOrdering => "PSASM1218",
151 Self::GraphIdentityMismatch => "PSASM1219",
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct FormOwnershipIntegrityDiagnostic {
158 pub code: String,
159 pub kind: FormOwnershipIntegrityKind,
160 pub message: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct FormOwnershipValidation {
165 pub diagnostics: Vec<FormOwnershipIntegrityDiagnostic>,
166 pub is_valid: bool,
167}
168
169impl Default for FormOwnershipValidation {
170 fn default() -> Self {
171 Self {
172 diagnostics: Vec::new(),
173 is_valid: true,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct FormOwnershipGraph {
182 pub id: FormOwnershipGraphId,
183 pub nodes: BTreeMap<FormOwnershipNodeKey, FormOwnershipNode>,
184 pub ownership_edges: Vec<FormOwnershipEdge>,
185 pub reference_edges: Vec<FormReferenceEdge>,
186 pub roots: Vec<SemanticId>,
187 pub validation: FormOwnershipValidation,
188}
189
190impl FormOwnershipGraph {
191 #[must_use]
192 pub fn node(&self, id: &FormOwnershipNodeKey) -> Option<&FormOwnershipNode> {
193 self.nodes.get(id)
194 }
195
196 #[must_use]
197 pub fn owner_of(&self, id: &FormOwnershipNodeKey) -> Option<&FormOwnershipNodeKey> {
198 self.ownership_edges
199 .iter()
200 .find_map(|edge| (&edge.child == id).then_some(&edge.owner))
201 }
202
203 #[must_use]
204 pub fn children_of(&self, id: &FormOwnershipNodeKey) -> Vec<&FormOwnershipNodeKey> {
205 self.ownership_edges
206 .iter()
207 .filter_map(|edge| (&edge.owner == id).then_some(&edge.child))
208 .collect()
209 }
210
211 #[must_use]
212 pub fn forms_of(&self, component: &SemanticId) -> Vec<&FormId> {
213 self.children_of(&FormOwnershipNodeKey::Component(component.clone()))
214 .into_iter()
215 .filter_map(|child| match child {
216 FormOwnershipNodeKey::Form(id) => Some(id),
217 _ => None,
218 })
219 .collect()
220 }
221
222 #[must_use]
223 pub fn fields_of(&self, form: &FormId) -> Vec<&FieldId> {
224 self.children_of(&FormOwnershipNodeKey::Form(form.clone()))
225 .into_iter()
226 .filter_map(|child| match child {
227 FormOwnershipNodeKey::FormField(id) => Some(id),
228 _ => None,
229 })
230 .collect()
231 }
232
233 #[must_use]
234 pub fn binding_owner(&self, binding: &FieldBindingId) -> Option<&SemanticId> {
235 match self.owner_of(&FormOwnershipNodeKey::FieldBinding(binding.clone()))? {
236 FormOwnershipNodeKey::TemplateControl(id) => Some(id),
237 _ => None,
238 }
239 }
240
241 #[must_use]
242 pub fn bindings_of_field(&self, field: &FieldId) -> Vec<&FieldBindingId> {
243 self.reference_edges
244 .iter()
245 .filter_map(|edge| {
246 if edge.kind == FormReferenceKind::FieldBindingField
247 && edge.target == FormOwnershipNodeKey::FormField(field.clone())
248 {
249 match &edge.source {
250 FormOwnershipNodeKey::FieldBinding(id) => Some(id),
251 _ => None,
252 }
253 } else {
254 None
255 }
256 })
257 .collect()
258 }
259
260 #[must_use]
261 pub fn bindings_of_form(&self, form: &FormId) -> Vec<&FieldBindingId> {
262 self.reference_edges
263 .iter()
264 .filter_map(|edge| {
265 if edge.kind == FormReferenceKind::FieldBindingForm
266 && edge.target == FormOwnershipNodeKey::Form(form.clone())
267 {
268 match &edge.source {
269 FormOwnershipNodeKey::FieldBinding(id) => Some(id),
270 _ => None,
271 }
272 } else {
273 None
274 }
275 })
276 .collect()
277 }
278
279 #[must_use]
280 pub fn field_of_binding(&self, binding: &FieldBindingId) -> Option<&FieldId> {
281 self.reference_edges.iter().find_map(|edge| {
282 if edge.kind == FormReferenceKind::FieldBindingField
283 && edge.source == FormOwnershipNodeKey::FieldBinding(binding.clone())
284 {
285 match &edge.target {
286 FormOwnershipNodeKey::FormField(id) => Some(id),
287 _ => None,
288 }
289 } else {
290 None
291 }
292 })
293 }
294
295 #[must_use]
296 pub fn form_of_binding(&self, binding: &FieldBindingId) -> Option<&FormId> {
297 self.reference_edges.iter().find_map(|edge| {
298 if edge.kind == FormReferenceKind::FieldBindingForm
299 && edge.source == FormOwnershipNodeKey::FieldBinding(binding.clone())
300 {
301 match &edge.target {
302 FormOwnershipNodeKey::Form(id) => Some(id),
303 _ => None,
304 }
305 } else {
306 None
307 }
308 })
309 }
310
311 #[must_use]
312 pub fn component_of_form(&self, form: &FormId) -> Option<&SemanticId> {
313 match self.owner_of(&FormOwnershipNodeKey::Form(form.clone()))? {
314 FormOwnershipNodeKey::Component(id) => Some(id),
315 _ => None,
316 }
317 }
318
319 #[must_use]
320 pub fn component_of_field(&self, field: &FieldId) -> Option<&SemanticId> {
321 self.component_of_form(
322 match self.owner_of(&FormOwnershipNodeKey::FormField(field.clone()))? {
323 FormOwnershipNodeKey::Form(id) => id,
324 _ => return None,
325 },
326 )
327 }
328
329 #[must_use]
330 pub fn component_of_binding(&self, binding: &FieldBindingId) -> Option<&SemanticId> {
331 self.component_of_form(self.form_of_binding(binding)?)
332 }
333
334 #[must_use]
335 pub fn roots(&self) -> &[SemanticId] {
336 &self.roots
337 }
338}
339
340#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
341#[must_use]
342pub fn collect_form_ownership_graph(
343 build_roots: &BTreeMap<ComponentRootId, ComponentBuildRoot>,
344 components: &[ComponentNode],
345 forms: &BTreeMap<FormId, FormEntity>,
346 fields: &BTreeMap<FieldId, FormFieldEntity>,
347 bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
348 template_entities: &[TemplateSemanticEntity],
349 ownership: &BTreeMap<SemanticId, SemanticOwner>,
350 references: &[SemanticReference],
351 provenance: &BTreeMap<SemanticId, SourceProvenance>,
352) -> FormOwnershipGraph {
353 let id = FormOwnershipGraphId::for_build_roots(build_roots.keys());
354 let component_ids = components
355 .iter()
356 .map(|component| component.id.clone())
357 .collect::<BTreeSet<_>>();
358 let participating_components = forms
359 .values()
360 .filter_map(|form| form.owner.entity_id().cloned())
361 .chain(bindings.values().map(|binding| binding.component.clone()))
362 .filter(|component| component_ids.contains(component))
363 .collect::<BTreeSet<_>>();
364 let template_entities = template_entities
365 .iter()
366 .map(|entity| (entity.id.clone(), entity))
367 .collect::<BTreeMap<_, _>>();
368
369 let mut nodes = BTreeMap::new();
370 for component in &participating_components {
371 if let Some(provenance) = provenance.get(component) {
372 insert_node(
373 &mut nodes,
374 FormOwnershipNode::Component {
375 id: component.clone(),
376 provenance: provenance.clone(),
377 },
378 );
379 }
380 }
381 for form in forms.values() {
382 insert_node(
383 &mut nodes,
384 FormOwnershipNode::Form {
385 id: form.id.clone(),
386 provenance: form.provenance.clone(),
387 },
388 );
389 }
390 for field in fields.values() {
391 insert_node(
392 &mut nodes,
393 FormOwnershipNode::FormField {
394 id: field.id.clone(),
395 provenance: field.provenance.clone(),
396 },
397 );
398 }
399 for binding in bindings.values() {
400 if let Some(control) = template_entities.get(&binding.control_entity) {
401 insert_node(
402 &mut nodes,
403 FormOwnershipNode::TemplateControl {
404 id: control.id.clone(),
405 provenance: control.provenance.clone(),
406 },
407 );
408 }
409 insert_node(
410 &mut nodes,
411 FormOwnershipNode::FieldBinding {
412 id: binding.id.clone(),
413 provenance: binding.provenance.clone(),
414 },
415 );
416 }
417
418 let mut ownership_edges = Vec::new();
419 for form in forms.values() {
420 push_ownership_edge(
421 &mut ownership_edges,
422 ownership,
423 &nodes,
424 form.id.as_semantic_id(),
425 FormOwnershipEdgeKind::ComponentOwnsForm,
426 form.provenance.clone(),
427 );
428 }
429 for field in fields.values() {
430 push_ownership_edge(
431 &mut ownership_edges,
432 ownership,
433 &nodes,
434 field.id.as_semantic_id(),
435 FormOwnershipEdgeKind::FormOwnsField,
436 field.provenance.clone(),
437 );
438 }
439 for binding in bindings.values() {
440 push_ownership_edge(
441 &mut ownership_edges,
442 ownership,
443 &nodes,
444 binding.id.as_semantic_id(),
445 FormOwnershipEdgeKind::TemplateControlOwnsBinding,
446 binding.attribute_provenance.clone(),
447 );
448 }
449 sort_ownership_edges(&mut ownership_edges);
450
451 let mut reference_edges = references
452 .iter()
453 .filter_map(|reference| form_reference_edge(reference, &nodes))
454 .collect::<Vec<_>>();
455 sort_reference_edges(&mut reference_edges);
456
457 let roots = participating_components.into_iter().collect::<Vec<_>>();
458 let mut graph = FormOwnershipGraph {
459 id,
460 nodes,
461 ownership_edges,
462 reference_edges,
463 roots,
464 validation: FormOwnershipValidation::default(),
465 };
466 graph.validation = validate_form_ownership_graph_sources(
467 &graph,
468 build_roots,
469 components,
470 forms,
471 fields,
472 bindings,
473 template_entities.values().copied(),
474 ownership,
475 references,
476 provenance,
477 &BTreeSet::new(),
478 );
479 graph
480}
481
482#[must_use]
483pub fn validate_form_ownership_graph(
484 graph: &FormOwnershipGraph,
485 model: &ApplicationSemanticModel,
486) -> FormOwnershipValidation {
487 let instance_ids = model
488 .component_instance_plan
489 .instances
490 .keys()
491 .map(|id| id.as_semantic_id().clone())
492 .chain(
493 model
494 .component_instance_plan
495 .blocked
496 .keys()
497 .map(|id| id.as_semantic_id().clone()),
498 )
499 .collect();
500 validate_form_ownership_graph_sources(
501 graph,
502 &model.component_instance_plan.roots,
503 &model.components,
504 &model.forms,
505 &model.form_fields,
506 &model.form_field_bindings,
507 model.template_entities.iter(),
508 &model.ownership,
509 &model.references,
510 &model.provenance,
511 &instance_ids,
512 )
513}
514
515fn insert_node(
516 nodes: &mut BTreeMap<FormOwnershipNodeKey, FormOwnershipNode>,
517 node: FormOwnershipNode,
518) {
519 nodes.insert(node.key(), node);
520}
521
522fn node_key_for_semantic_id(
523 nodes: &BTreeMap<FormOwnershipNodeKey, FormOwnershipNode>,
524 id: &SemanticId,
525) -> Option<FormOwnershipNodeKey> {
526 nodes.keys().find(|key| key.semantic_id() == id).cloned()
527}
528
529fn push_ownership_edge(
530 edges: &mut Vec<FormOwnershipEdge>,
531 ownership: &BTreeMap<SemanticId, SemanticOwner>,
532 nodes: &BTreeMap<FormOwnershipNodeKey, FormOwnershipNode>,
533 child: &SemanticId,
534 kind: FormOwnershipEdgeKind,
535 provenance: SourceProvenance,
536) {
537 let Some(child) = node_key_for_semantic_id(nodes, child) else {
538 return;
539 };
540 let Some(owner) = ownership
541 .get(child.semantic_id())
542 .and_then(SemanticOwner::entity_id)
543 .and_then(|owner| node_key_for_semantic_id(nodes, owner))
544 else {
545 return;
546 };
547 edges.push(FormOwnershipEdge {
548 owner,
549 child,
550 kind,
551 provenance,
552 });
553}
554
555fn form_reference_edge(
556 reference: &SemanticReference,
557 nodes: &BTreeMap<FormOwnershipNodeKey, FormOwnershipNode>,
558) -> Option<FormReferenceEdge> {
559 let kind = match reference.kind {
560 SemanticReferenceKind::FieldBindingField => FormReferenceKind::FieldBindingField,
561 SemanticReferenceKind::FieldBindingForm => FormReferenceKind::FieldBindingForm,
562 _ => return None,
563 };
564 Some(FormReferenceEdge {
565 kind,
566 source: node_key_for_semantic_id(nodes, &reference.source)?,
567 target: node_key_for_semantic_id(nodes, &reference.target)?,
568 provenance: reference.provenance.clone(),
569 })
570}
571
572fn sort_ownership_edges(edges: &mut [FormOwnershipEdge]) {
573 edges.sort_by(|left, right| {
574 (
575 left.owner.semantic_id().as_str(),
576 left.child.semantic_id().as_str(),
577 left.kind,
578 )
579 .cmp(&(
580 right.owner.semantic_id().as_str(),
581 right.child.semantic_id().as_str(),
582 right.kind,
583 ))
584 });
585}
586
587fn sort_reference_edges(edges: &mut [FormReferenceEdge]) {
588 edges.sort_by(|left, right| {
589 (
590 left.source.semantic_id().as_str(),
591 left.kind,
592 left.target.semantic_id().as_str(),
593 )
594 .cmp(&(
595 right.source.semantic_id().as_str(),
596 right.kind,
597 right.target.semantic_id().as_str(),
598 ))
599 });
600}
601
602#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
603fn validate_form_ownership_graph_sources<'a>(
604 graph: &FormOwnershipGraph,
605 build_roots: &BTreeMap<ComponentRootId, ComponentBuildRoot>,
606 components: &[ComponentNode],
607 forms: &BTreeMap<FormId, FormEntity>,
608 fields: &BTreeMap<FieldId, FormFieldEntity>,
609 bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
610 template_entities: impl IntoIterator<Item = &'a TemplateSemanticEntity>,
611 ownership: &BTreeMap<SemanticId, SemanticOwner>,
612 references: &[SemanticReference],
613 provenance: &BTreeMap<SemanticId, SourceProvenance>,
614 instance_ids: &BTreeSet<SemanticId>,
615) -> FormOwnershipValidation {
616 let mut diagnostics = Vec::new();
617 let template_entities = template_entities
618 .into_iter()
619 .map(|entity| (entity.id.clone(), entity))
620 .collect::<BTreeMap<_, _>>();
621 let components = components
622 .iter()
623 .map(|component| (component.id.clone(), component))
624 .collect::<BTreeMap<_, _>>();
625
626 if graph.id != FormOwnershipGraphId::for_build_roots(build_roots.keys()) {
627 push_diagnostic(
628 &mut diagnostics,
629 FormOwnershipIntegrityKind::GraphIdentityMismatch,
630 "Form ownership graph identity does not match the canonical build-root set",
631 );
632 }
633
634 let mut semantic_ids = BTreeSet::new();
635 for (key, node) in &graph.nodes {
636 if node.key() != *key || !semantic_ids.insert(key.semantic_id().clone()) {
637 push_diagnostic(
638 &mut diagnostics,
639 FormOwnershipIntegrityKind::DuplicateNode,
640 format!(
641 "duplicate or mismatched Form ownership node `{}`",
642 key.semantic_id()
643 ),
644 );
645 }
646 if missing_provenance(node.provenance()) {
647 push_diagnostic(
648 &mut diagnostics,
649 FormOwnershipIntegrityKind::MissingProvenance,
650 format!(
651 "Form ownership node `{}` has no canonical provenance",
652 key.semantic_id()
653 ),
654 );
655 }
656 if instance_ids.contains(key.semantic_id()) {
657 push_diagnostic(
658 &mut diagnostics,
659 FormOwnershipIntegrityKind::InstanceIdentityInDeclarationGraph,
660 format!(
661 "instance identity `{}` leaked into the declaration graph",
662 key.semantic_id()
663 ),
664 );
665 }
666 }
667
668 validate_node_domain(
669 graph,
670 &components,
671 forms,
672 fields,
673 bindings,
674 &template_entities,
675 provenance,
676 &mut diagnostics,
677 );
678 validate_edge_domains(graph, &mut diagnostics);
679
680 for form in forms.values() {
681 let child = FormOwnershipNodeKey::Form(form.id.clone());
682 let expected_component = form.owner.entity_id();
683 let expected_owner =
684 expected_component.map(|id| FormOwnershipNodeKey::Component(id.clone()));
685 validate_exact_owner(
686 graph,
687 &child,
688 expected_owner.as_ref(),
689 FormOwnershipEdgeKind::ComponentOwnsForm,
690 FormOwnershipIntegrityKind::ComponentOwnershipMismatch,
691 &mut diagnostics,
692 );
693 validate_ownership_provenance(graph, &child, &form.provenance, &mut diagnostics);
694 if ownership.get(form.id.as_semantic_id()) != Some(&form.owner) {
695 push_diagnostic(
696 &mut diagnostics,
697 FormOwnershipIntegrityKind::ComponentOwnershipMismatch,
698 format!("Form `{}` disagrees with canonical ASM ownership", form.id),
699 );
700 }
701 }
702
703 for field in fields.values() {
704 let child = FormOwnershipNodeKey::FormField(field.id.clone());
705 let expected_owner = FormOwnershipNodeKey::Form(field.owner_form.clone());
706 validate_exact_owner(
707 graph,
708 &child,
709 Some(&expected_owner),
710 FormOwnershipEdgeKind::FormOwnsField,
711 FormOwnershipIntegrityKind::FieldFormMismatch,
712 &mut diagnostics,
713 );
714 validate_ownership_provenance(graph, &child, &field.provenance, &mut diagnostics);
715 let form_component = forms
716 .get(&field.owner_form)
717 .and_then(|form| form.owner.entity_id());
718 if form_component != Some(&field.owner_component)
719 || ownership.get(field.id.as_semantic_id())
720 != Some(&SemanticOwner::entity(
721 field.owner_form.as_semantic_id().clone(),
722 ))
723 {
724 push_diagnostic(
725 &mut diagnostics,
726 FormOwnershipIntegrityKind::FieldFormMismatch,
727 format!(
728 "Field `{}` disagrees with its canonical Form owner",
729 field.id
730 ),
731 );
732 }
733 }
734
735 for binding in bindings.values() {
736 let child = FormOwnershipNodeKey::FieldBinding(binding.id.clone());
737 let expected_owner = FormOwnershipNodeKey::TemplateControl(binding.control_entity.clone());
738 validate_exact_owner(
739 graph,
740 &child,
741 Some(&expected_owner),
742 FormOwnershipEdgeKind::TemplateControlOwnsBinding,
743 FormOwnershipIntegrityKind::BindingTemplateMismatch,
744 &mut diagnostics,
745 );
746 validate_ownership_provenance(
747 graph,
748 &child,
749 &binding.attribute_provenance,
750 &mut diagnostics,
751 );
752 if ownership.get(binding.id.as_semantic_id())
753 != Some(&SemanticOwner::entity(binding.control_entity.clone()))
754 {
755 push_diagnostic(
756 &mut diagnostics,
757 FormOwnershipIntegrityKind::BindingTemplateMismatch,
758 format!(
759 "binding `{}` disagrees with canonical control ownership",
760 binding.id
761 ),
762 );
763 }
764
765 let expected_field = FormOwnershipNodeKey::FormField(binding.field.clone());
766 let expected_form = FormOwnershipNodeKey::Form(binding.form.clone());
767 validate_exact_reference(
768 graph,
769 &child,
770 &expected_field,
771 FormReferenceKind::FieldBindingField,
772 FormOwnershipIntegrityKind::BindingFieldMismatch,
773 &mut diagnostics,
774 );
775 validate_reference_provenance(
776 graph,
777 &child,
778 FormReferenceKind::FieldBindingField,
779 &binding.expression_provenance,
780 &mut diagnostics,
781 );
782 validate_exact_reference(
783 graph,
784 &child,
785 &expected_form,
786 FormReferenceKind::FieldBindingForm,
787 FormOwnershipIntegrityKind::BindingFormMismatch,
788 &mut diagnostics,
789 );
790 validate_reference_provenance(
791 graph,
792 &child,
793 FormReferenceKind::FieldBindingForm,
794 &binding.expression_provenance,
795 &mut diagnostics,
796 );
797 let field = fields.get(&binding.field);
798 let form = forms.get(&binding.form);
799 let template_component =
800 component_owner_of(&binding.control_entity, &components, ownership);
801 if field.is_none_or(|field| field.owner_form != binding.form) {
802 push_diagnostic(
803 &mut diagnostics,
804 FormOwnershipIntegrityKind::BindingFieldMismatch,
805 format!(
806 "binding `{}` references a Field with a different Form",
807 binding.id
808 ),
809 );
810 }
811 if form
812 .and_then(|form| form.owner.entity_id())
813 .is_none_or(|component| component != &binding.component)
814 {
815 push_diagnostic(
816 &mut diagnostics,
817 FormOwnershipIntegrityKind::BindingFormMismatch,
818 format!(
819 "binding `{}` references a Form from another component",
820 binding.id
821 ),
822 );
823 }
824 if field.is_none_or(|field| field.owner_component != binding.component)
825 || template_component.as_ref() != Some(&binding.component)
826 {
827 push_diagnostic(
828 &mut diagnostics,
829 FormOwnershipIntegrityKind::ComponentOwnershipMismatch,
830 format!(
831 "binding `{}` crosses canonical component ownership",
832 binding.id
833 ),
834 );
835 }
836 }
837
838 for edge in &graph.ownership_edges {
839 if !graph.nodes.contains_key(&edge.owner) || !graph.nodes.contains_key(&edge.child) {
840 push_diagnostic(
841 &mut diagnostics,
842 FormOwnershipIntegrityKind::UnknownOwnershipEndpoint,
843 format!(
844 "ownership edge `{}` -> `{}` has an unknown endpoint",
845 edge.owner.semantic_id(),
846 edge.child.semantic_id()
847 ),
848 );
849 }
850 if missing_provenance(&edge.provenance) {
851 push_diagnostic(
852 &mut diagnostics,
853 FormOwnershipIntegrityKind::MissingProvenance,
854 format!(
855 "ownership edge to `{}` lacks provenance",
856 edge.child.semantic_id()
857 ),
858 );
859 }
860 }
861 for edge in &graph.reference_edges {
862 if !graph.nodes.contains_key(&edge.source) || !graph.nodes.contains_key(&edge.target) {
863 push_diagnostic(
864 &mut diagnostics,
865 FormOwnershipIntegrityKind::UnknownReferenceEndpoint,
866 format!(
867 "reference `{}` -> `{}` has an unknown endpoint",
868 edge.source.semantic_id(),
869 edge.target.semantic_id()
870 ),
871 );
872 }
873 if missing_provenance(&edge.provenance) {
874 push_diagnostic(
875 &mut diagnostics,
876 FormOwnershipIntegrityKind::MissingProvenance,
877 format!(
878 "reference from `{}` lacks provenance",
879 edge.source.semantic_id()
880 ),
881 );
882 }
883 }
884
885 validate_parents_and_reachability(graph, &mut diagnostics);
886 if ownership_has_cycle(graph) {
887 push_diagnostic(
888 &mut diagnostics,
889 FormOwnershipIntegrityKind::OwnershipCycle,
890 "Form ownership edges contain a cycle",
891 );
892 }
893
894 let expected_roots = forms
895 .values()
896 .filter_map(|form| form.owner.entity_id().cloned())
897 .chain(bindings.values().map(|binding| binding.component.clone()))
898 .collect::<BTreeSet<_>>()
899 .into_iter()
900 .collect::<Vec<_>>();
901 if graph.roots != expected_roots || !is_strictly_sorted(&graph.roots) {
902 push_diagnostic(
903 &mut diagnostics,
904 FormOwnershipIntegrityKind::NonCanonicalOrdering,
905 "Form ownership roots are incomplete or non-canonical",
906 );
907 }
908 let mut ownership_sorted = graph.ownership_edges.clone();
909 sort_ownership_edges(&mut ownership_sorted);
910 let mut references_sorted = graph.reference_edges.clone();
911 sort_reference_edges(&mut references_sorted);
912 if ownership_sorted != graph.ownership_edges
913 || references_sorted != graph.reference_edges
914 || has_duplicate_ownership_edge(&graph.ownership_edges)
915 || has_duplicate_reference_edge(&graph.reference_edges)
916 {
917 push_diagnostic(
918 &mut diagnostics,
919 FormOwnershipIntegrityKind::NonCanonicalOrdering,
920 "Form ownership edges or references are not canonically ordered and unique",
921 );
922 }
923
924 for binding in bindings.values() {
927 for (kind, target) in [
928 (
929 SemanticReferenceKind::FieldBindingField,
930 binding.field.as_semantic_id(),
931 ),
932 (
933 SemanticReferenceKind::FieldBindingForm,
934 binding.form.as_semantic_id(),
935 ),
936 ] {
937 if references
938 .iter()
939 .filter(|reference| {
940 reference.kind == kind
941 && reference.source == *binding.id.as_semantic_id()
942 && reference.target == *target
943 })
944 .count()
945 != 1
946 {
947 push_diagnostic(
948 &mut diagnostics,
949 if kind == SemanticReferenceKind::FieldBindingField {
950 FormOwnershipIntegrityKind::BindingFieldMismatch
951 } else {
952 FormOwnershipIntegrityKind::BindingFormMismatch
953 },
954 format!("binding `{}` lacks one canonical ASM reference", binding.id),
955 );
956 }
957 }
958 }
959
960 diagnostics.sort_by(|left, right| {
961 (&left.code, left.kind, &left.message).cmp(&(&right.code, right.kind, &right.message))
962 });
963 diagnostics.dedup();
964 FormOwnershipValidation {
965 is_valid: diagnostics.is_empty(),
966 diagnostics,
967 }
968}
969
970#[allow(clippy::too_many_arguments)]
971fn validate_node_domain(
972 graph: &FormOwnershipGraph,
973 components: &BTreeMap<SemanticId, &ComponentNode>,
974 forms: &BTreeMap<FormId, FormEntity>,
975 fields: &BTreeMap<FieldId, FormFieldEntity>,
976 bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
977 templates: &BTreeMap<SemanticId, &TemplateSemanticEntity>,
978 provenance: &BTreeMap<SemanticId, SourceProvenance>,
979 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
980) {
981 for (key, node) in &graph.nodes {
982 let valid = match key {
983 FormOwnershipNodeKey::Component(id) => {
984 components.contains_key(id)
985 && (forms
986 .values()
987 .any(|form| form.owner.entity_id() == Some(id))
988 || bindings.values().any(|binding| binding.component == *id))
989 && provenance.get(id) == Some(node.provenance())
990 }
991 FormOwnershipNodeKey::Form(id) => forms
992 .get(id)
993 .is_some_and(|form| form.provenance == *node.provenance()),
994 FormOwnershipNodeKey::FormField(id) => fields
995 .get(id)
996 .is_some_and(|field| field.provenance == *node.provenance()),
997 FormOwnershipNodeKey::TemplateControl(id) => templates.get(id).is_some_and(|entity| {
998 entity.kind == TemplateSemanticKind::Element
999 && entity.provenance == *node.provenance()
1000 && bindings
1001 .values()
1002 .any(|binding| binding.control_entity == *id)
1003 }),
1004 FormOwnershipNodeKey::FieldBinding(id) => bindings
1005 .get(id)
1006 .is_some_and(|binding| binding.provenance == *node.provenance()),
1007 };
1008 if !valid {
1009 push_diagnostic(
1010 diagnostics,
1011 FormOwnershipIntegrityKind::InvalidCandidatePromoted,
1012 format!(
1013 "node `{}` is not one valid canonical I2-I4 entity",
1014 key.semantic_id()
1015 ),
1016 );
1017 }
1018 }
1019
1020 for expected in
1021 forms
1022 .keys()
1023 .cloned()
1024 .map(FormOwnershipNodeKey::Form)
1025 .chain(fields.keys().cloned().map(FormOwnershipNodeKey::FormField))
1026 .chain(
1027 bindings
1028 .keys()
1029 .cloned()
1030 .map(FormOwnershipNodeKey::FieldBinding),
1031 )
1032 .chain(bindings.values().map(|binding| {
1033 FormOwnershipNodeKey::TemplateControl(binding.control_entity.clone())
1034 }))
1035 {
1036 if !graph.nodes.contains_key(&expected) {
1037 push_diagnostic(
1038 diagnostics,
1039 FormOwnershipIntegrityKind::UnreachableNode,
1040 format!(
1041 "canonical entity `{}` is absent from the Form ownership graph",
1042 expected.semantic_id()
1043 ),
1044 );
1045 }
1046 }
1047}
1048
1049fn validate_edge_domains(
1050 graph: &FormOwnershipGraph,
1051 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
1052) {
1053 for edge in &graph.ownership_edges {
1054 let valid = matches!(
1055 (&edge.owner, &edge.child, edge.kind),
1056 (
1057 FormOwnershipNodeKey::Component(_),
1058 FormOwnershipNodeKey::Form(_),
1059 FormOwnershipEdgeKind::ComponentOwnsForm
1060 ) | (
1061 FormOwnershipNodeKey::Form(_),
1062 FormOwnershipNodeKey::FormField(_),
1063 FormOwnershipEdgeKind::FormOwnsField
1064 ) | (
1065 FormOwnershipNodeKey::TemplateControl(_),
1066 FormOwnershipNodeKey::FieldBinding(_),
1067 FormOwnershipEdgeKind::TemplateControlOwnsBinding
1068 )
1069 );
1070 if !valid {
1071 push_diagnostic(
1072 diagnostics,
1073 FormOwnershipIntegrityKind::UnknownOwnershipEndpoint,
1074 format!(
1075 "ownership edge to `{}` has an invalid typed domain",
1076 edge.child.semantic_id()
1077 ),
1078 );
1079 }
1080 }
1081 for edge in &graph.reference_edges {
1082 let valid = matches!(
1083 (&edge.source, &edge.target, edge.kind),
1084 (
1085 FormOwnershipNodeKey::FieldBinding(_),
1086 FormOwnershipNodeKey::FormField(_),
1087 FormReferenceKind::FieldBindingField
1088 ) | (
1089 FormOwnershipNodeKey::FieldBinding(_),
1090 FormOwnershipNodeKey::Form(_),
1091 FormReferenceKind::FieldBindingForm
1092 )
1093 );
1094 if !valid {
1095 push_diagnostic(
1096 diagnostics,
1097 FormOwnershipIntegrityKind::UnknownReferenceEndpoint,
1098 format!(
1099 "reference from `{}` has an invalid typed domain",
1100 edge.source.semantic_id()
1101 ),
1102 );
1103 }
1104 }
1105}
1106
1107fn validate_exact_owner(
1108 graph: &FormOwnershipGraph,
1109 child: &FormOwnershipNodeKey,
1110 expected: Option<&FormOwnershipNodeKey>,
1111 kind: FormOwnershipEdgeKind,
1112 mismatch_kind: FormOwnershipIntegrityKind,
1113 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
1114) {
1115 let parents = graph
1116 .ownership_edges
1117 .iter()
1118 .filter(|edge| &edge.child == child)
1119 .collect::<Vec<_>>();
1120 if parents.is_empty() {
1121 push_diagnostic(
1122 diagnostics,
1123 FormOwnershipIntegrityKind::MissingOwner,
1124 format!("`{}` has no ownership parent", child.semantic_id()),
1125 );
1126 return;
1127 }
1128 if parents.len() > 1 {
1129 push_diagnostic(
1130 diagnostics,
1131 FormOwnershipIntegrityKind::MultipleOwners,
1132 format!("`{}` has multiple ownership parents", child.semantic_id()),
1133 );
1134 }
1135 if expected.is_none_or(|expected| {
1136 parents.len() != 1 || parents[0].owner != *expected || parents[0].kind != kind
1137 }) {
1138 push_diagnostic(
1139 diagnostics,
1140 mismatch_kind,
1141 format!("`{}` has a non-canonical direct owner", child.semantic_id()),
1142 );
1143 }
1144}
1145
1146fn validate_exact_reference(
1147 graph: &FormOwnershipGraph,
1148 source: &FormOwnershipNodeKey,
1149 target: &FormOwnershipNodeKey,
1150 kind: FormReferenceKind,
1151 mismatch_kind: FormOwnershipIntegrityKind,
1152 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
1153) {
1154 if graph
1155 .reference_edges
1156 .iter()
1157 .filter(|edge| &edge.source == source && edge.kind == kind)
1158 .count()
1159 != 1
1160 || !graph
1161 .reference_edges
1162 .iter()
1163 .any(|edge| &edge.source == source && &edge.target == target && edge.kind == kind)
1164 {
1165 push_diagnostic(
1166 diagnostics,
1167 mismatch_kind,
1168 format!("`{}` has a non-canonical reference", source.semantic_id()),
1169 );
1170 }
1171}
1172
1173fn validate_ownership_provenance(
1174 graph: &FormOwnershipGraph,
1175 child: &FormOwnershipNodeKey,
1176 expected: &SourceProvenance,
1177 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
1178) {
1179 if graph
1180 .ownership_edges
1181 .iter()
1182 .find(|edge| &edge.child == child)
1183 .is_none_or(|edge| &edge.provenance != expected)
1184 {
1185 push_diagnostic(
1186 diagnostics,
1187 FormOwnershipIntegrityKind::MissingProvenance,
1188 format!(
1189 "ownership edge to `{}` lacks canonical child/use-site provenance",
1190 child.semantic_id()
1191 ),
1192 );
1193 }
1194}
1195
1196fn validate_reference_provenance(
1197 graph: &FormOwnershipGraph,
1198 source: &FormOwnershipNodeKey,
1199 kind: FormReferenceKind,
1200 expected: &SourceProvenance,
1201 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
1202) {
1203 if graph
1204 .reference_edges
1205 .iter()
1206 .find(|edge| &edge.source == source && edge.kind == kind)
1207 .is_none_or(|edge| &edge.provenance != expected)
1208 {
1209 push_diagnostic(
1210 diagnostics,
1211 FormOwnershipIntegrityKind::MissingProvenance,
1212 format!(
1213 "reference from `{}` lacks canonical binding-expression provenance",
1214 source.semantic_id()
1215 ),
1216 );
1217 }
1218}
1219
1220fn validate_parents_and_reachability(
1221 graph: &FormOwnershipGraph,
1222 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
1223) {
1224 let root_keys = graph
1225 .roots
1226 .iter()
1227 .cloned()
1228 .map(FormOwnershipNodeKey::Component)
1229 .collect::<Vec<_>>();
1230 for key in graph.nodes.keys() {
1231 match key {
1232 FormOwnershipNodeKey::Form(_) | FormOwnershipNodeKey::FormField(_) => {
1233 let reachable = root_keys
1234 .iter()
1235 .filter(|root| is_reachable(graph, root, key))
1236 .count();
1237 if reachable != 1 {
1238 push_diagnostic(
1239 diagnostics,
1240 FormOwnershipIntegrityKind::UnreachableNode,
1241 format!(
1242 "`{}` is reachable from {reachable} component roots",
1243 key.semantic_id()
1244 ),
1245 );
1246 }
1247 }
1248 FormOwnershipNodeKey::FieldBinding(_) => {
1249 let controls = graph
1250 .nodes
1251 .keys()
1252 .filter(|candidate| {
1253 matches!(candidate, FormOwnershipNodeKey::TemplateControl(_))
1254 })
1255 .filter(|control| is_reachable(graph, control, key))
1256 .count();
1257 if controls != 1 {
1258 push_diagnostic(
1259 diagnostics,
1260 FormOwnershipIntegrityKind::UnreachableNode,
1261 format!(
1262 "binding `{}` is reachable from {controls} controls",
1263 key.semantic_id()
1264 ),
1265 );
1266 }
1267 }
1268 FormOwnershipNodeKey::Component(_) | FormOwnershipNodeKey::TemplateControl(_) => {}
1269 }
1270 }
1271}
1272
1273fn is_reachable(
1274 graph: &FormOwnershipGraph,
1275 start: &FormOwnershipNodeKey,
1276 target: &FormOwnershipNodeKey,
1277) -> bool {
1278 let mut pending = vec![start.clone()];
1279 let mut seen = BTreeSet::new();
1280 while let Some(current) = pending.pop() {
1281 if !seen.insert(current.clone()) {
1282 continue;
1283 }
1284 if current == *target {
1285 return true;
1286 }
1287 pending.extend(graph.children_of(¤t).into_iter().cloned());
1288 }
1289 false
1290}
1291
1292fn ownership_has_cycle(graph: &FormOwnershipGraph) -> bool {
1293 graph.nodes.keys().any(|start| {
1294 let mut pending = graph
1295 .children_of(start)
1296 .into_iter()
1297 .cloned()
1298 .collect::<Vec<_>>();
1299 let mut seen = BTreeSet::new();
1300 while let Some(current) = pending.pop() {
1301 if current == *start {
1302 return true;
1303 }
1304 if seen.insert(current.clone()) {
1305 pending.extend(graph.children_of(¤t).into_iter().cloned());
1306 }
1307 }
1308 false
1309 })
1310}
1311
1312fn component_owner_of(
1313 id: &SemanticId,
1314 components: &BTreeMap<SemanticId, &ComponentNode>,
1315 ownership: &BTreeMap<SemanticId, SemanticOwner>,
1316) -> Option<SemanticId> {
1317 let mut current = id;
1318 let mut seen = BTreeSet::new();
1319 loop {
1320 if components.contains_key(current) {
1321 return Some(current.clone());
1322 }
1323 if !seen.insert(current.clone()) {
1324 return None;
1325 }
1326 current = ownership.get(current)?.entity_id()?;
1327 }
1328}
1329
1330fn missing_provenance(provenance: &SourceProvenance) -> bool {
1331 provenance.path.as_os_str().is_empty() || provenance.span.start >= provenance.span.end
1332}
1333
1334fn is_strictly_sorted<T: Ord>(items: &[T]) -> bool {
1335 items.windows(2).all(|window| window[0] < window[1])
1336}
1337
1338fn has_duplicate_ownership_edge(edges: &[FormOwnershipEdge]) -> bool {
1339 let mut seen = BTreeSet::new();
1340 edges
1341 .iter()
1342 .any(|edge| !seen.insert((edge.owner.clone(), edge.child.clone(), edge.kind)))
1343}
1344
1345fn has_duplicate_reference_edge(edges: &[FormReferenceEdge]) -> bool {
1346 let mut seen = BTreeSet::new();
1347 edges
1348 .iter()
1349 .any(|edge| !seen.insert((edge.source.clone(), edge.kind, edge.target.clone())))
1350}
1351
1352fn push_diagnostic(
1353 diagnostics: &mut Vec<FormOwnershipIntegrityDiagnostic>,
1354 kind: FormOwnershipIntegrityKind,
1355 message: impl Into<String>,
1356) {
1357 diagnostics.push(FormOwnershipIntegrityDiagnostic {
1358 code: kind.code().to_string(),
1359 kind,
1360 message: message.into(),
1361 });
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366 use std::collections::BTreeSet;
1367
1368 use presolve_parser::SourceSpan;
1369
1370 use super::{
1371 validate_form_ownership_graph, FormOwnershipEdge, FormOwnershipEdgeKind,
1372 FormOwnershipIntegrityKind, FormOwnershipNode, FormOwnershipNodeKey, FormReferenceEdge,
1373 FormReferenceKind,
1374 };
1375 use crate::{
1376 build_application_semantic_model, build_application_semantic_model_for_unit,
1377 build_semantic_graph, validate_application_semantic_model, CompilationUnit, FieldId,
1378 FormId, FormInstanceId, SemanticId, SourceProvenance, SEMANTIC_GRAPH_SCHEMA_VERSION,
1379 };
1380
1381 fn form_source() -> &'static str {
1382 r#"
1383@component("profile-editor")
1384class ProfileEditor {
1385 @form() profileForm!: Form;
1386 @form() passwordForm!: Form;
1387 @field(this.profileForm) displayName = "Austin";
1388 @field(this.profileForm) contactMethod: "email" | "phone" = "email";
1389 @field(this.passwordForm) password = "";
1390 render() {
1391 return <main>
1392 <input field={this.displayName} />
1393 <input type="radio" value="email" field={this.contactMethod} />
1394 <input type="radio" value="phone" field={this.contactMethod} />
1395 </main>;
1396 }
1397}
1398"#
1399 }
1400
1401 #[test]
1402 fn projects_exact_declaration_and_control_ownership_with_read_only_queries() {
1403 let parsed = presolve_parser::parse_file("src/ProfileEditor.tsx", form_source());
1404 let asm = build_application_semantic_model(&parsed);
1405 let graph = asm.form_ownership();
1406 let component = &asm.components[0].id;
1407 let profile = FormId::for_owner(component, "profileForm");
1408 let password = FormId::for_owner(component, "passwordForm");
1409 let display = FieldId::for_form(&profile, "displayName");
1410 let contact = FieldId::for_form(&profile, "contactMethod");
1411
1412 assert!(
1413 graph.validation.is_valid,
1414 "{:?}",
1415 graph.validation.diagnostics
1416 );
1417 assert!(validate_application_semantic_model(&asm).is_empty());
1418 assert_eq!(graph.roots(), std::slice::from_ref(component));
1419 assert_eq!(graph.forms_of(component), vec![&password, &profile]);
1420 assert_eq!(graph.fields_of(&profile), vec![&contact, &display]);
1421 assert_eq!(graph.bindings_of_field(&contact).len(), 2);
1422 assert_eq!(graph.bindings_of_form(&profile).len(), 3);
1423 assert_eq!(graph.ownership_edges.len(), 8);
1424 assert_eq!(graph.reference_edges.len(), 6);
1425
1426 for binding in asm.form_field_bindings.values() {
1427 assert_eq!(
1428 graph.binding_owner(&binding.id),
1429 Some(&binding.control_entity)
1430 );
1431 assert_eq!(graph.field_of_binding(&binding.id), Some(&binding.field));
1432 assert_eq!(graph.form_of_binding(&binding.id), Some(&binding.form));
1433 assert_eq!(graph.component_of_binding(&binding.id), Some(component));
1434 assert_eq!(
1435 asm.parent_of(binding.id.as_semantic_id()),
1436 Some(&binding.control_entity)
1437 );
1438 assert!(!graph
1439 .children_of(&FormOwnershipNodeKey::Form(binding.form.clone()))
1440 .contains(&&FormOwnershipNodeKey::FieldBinding(binding.id.clone())));
1441 assert!(!graph
1442 .children_of(&FormOwnershipNodeKey::FormField(binding.field.clone()))
1443 .contains(&&FormOwnershipNodeKey::FieldBinding(binding.id.clone())));
1444 }
1445
1446 assert!(graph.id.as_str().starts_with("form-ownership-graph:root:"));
1447 assert!(graph.nodes.values().all(|node| {
1448 !node.provenance().path.as_os_str().is_empty()
1449 && node.provenance().span.start < node.provenance().span.end
1450 }));
1451 assert!(graph.nodes.keys().all(|key| {
1452 !asm.component_instance_plan
1453 .instances
1454 .keys()
1455 .any(|instance| instance.as_semantic_id() == key.semantic_id())
1456 }));
1457 }
1458
1459 #[test]
1460 fn excludes_invalid_candidates_without_poisoning_valid_products() {
1461 let parsed = presolve_parser::parse_file(
1462 "src/Mixed.tsx",
1463 r#"
1464@component("mixed-editor")
1465class MixedEditor {
1466 @form("invalid") brokenForm!: Form;
1467 @form() validForm!: Form;
1468 @field(this.brokenForm) broken = "";
1469 @field(this.validForm) valid = "ok";
1470 render() {
1471 return <main>
1472 <input field={this.broken} />
1473 <input field={this.valid} />
1474 <div field={this.valid} />
1475 </main>;
1476 }
1477}
1478"#,
1479 );
1480 let asm = build_application_semantic_model(&parsed);
1481 let graph = asm.form_ownership();
1482 let component = &asm.components[0].id;
1483 let valid_form = FormId::for_owner(component, "validForm");
1484 let valid_field = FieldId::for_form(&valid_form, "valid");
1485
1486 assert!(
1487 graph.validation.is_valid,
1488 "{:?}",
1489 graph.validation.diagnostics
1490 );
1491 assert_eq!(asm.forms.len(), 1);
1492 assert_eq!(asm.form_fields.len(), 1);
1493 assert_eq!(asm.form_field_bindings.len(), 1);
1494 assert!(graph
1495 .node(&FormOwnershipNodeKey::Form(valid_form))
1496 .is_some());
1497 assert!(graph
1498 .node(&FormOwnershipNodeKey::FormField(valid_field))
1499 .is_some());
1500 assert!(asm
1501 .form_declaration_candidates()
1502 .iter()
1503 .any(|candidate| !candidate.violations().is_empty()));
1504 assert!(asm
1505 .form_field_declaration_candidates()
1506 .iter()
1507 .any(|candidate| !candidate.is_valid()));
1508 assert!(asm
1509 .form_field_binding_candidates()
1510 .iter()
1511 .any(|candidate| !candidate.is_valid()));
1512 }
1513
1514 #[test]
1515 fn validates_malformed_graphs_without_repairing_them() {
1516 let parsed = presolve_parser::parse_file("src/ProfileEditor.tsx", form_source());
1517 let asm = build_application_semantic_model(&parsed);
1518 let mut graph = asm.form_ownership.clone();
1519 let form = asm.forms.values().next().expect("Form");
1520 let field = asm.form_fields.values().next().expect("Field");
1521 let binding = asm.form_field_bindings.values().next().expect("binding");
1522
1523 graph
1524 .ownership_edges
1525 .retain(|edge| edge.child != FormOwnershipNodeKey::FieldBinding(binding.id.clone()));
1526 graph.ownership_edges.push(FormOwnershipEdge {
1527 owner: FormOwnershipNodeKey::FormField(field.id.clone()),
1528 child: FormOwnershipNodeKey::Form(form.id.clone()),
1529 kind: FormOwnershipEdgeKind::ComponentOwnsForm,
1530 provenance: form.provenance.clone(),
1531 });
1532 graph.ownership_edges.push(FormOwnershipEdge {
1533 owner: FormOwnershipNodeKey::Component(SemanticId::component_in_module(
1534 "src/Missing.tsx",
1535 Some("missing-owner"),
1536 "MissingOwner",
1537 )),
1538 child: FormOwnershipNodeKey::Form(form.id.clone()),
1539 kind: FormOwnershipEdgeKind::ComponentOwnsForm,
1540 provenance: form.provenance.clone(),
1541 });
1542 graph.ownership_edges.push(FormOwnershipEdge {
1543 owner: FormOwnershipNodeKey::FormField(field.id.clone()),
1544 child: FormOwnershipNodeKey::Form(form.id.clone()),
1545 kind: FormOwnershipEdgeKind::ComponentOwnsForm,
1546 provenance: form.provenance.clone(),
1547 });
1548 let fake_form = FormId::for_owner(&asm.components[0].id, "notCanonical");
1549 let fake_field = FieldId::for_form(&fake_form, "missing");
1550 graph.reference_edges.push(FormReferenceEdge {
1551 kind: FormReferenceKind::FieldBindingField,
1552 source: FormOwnershipNodeKey::FieldBinding(binding.id.clone()),
1553 target: FormOwnershipNodeKey::FormField(fake_field),
1554 provenance: binding.expression_provenance.clone(),
1555 });
1556 let node = graph
1557 .nodes
1558 .get_mut(&FormOwnershipNodeKey::FormField(field.id.clone()))
1559 .expect("field node");
1560 if let FormOwnershipNode::FormField { provenance, .. } = node {
1561 *provenance = SourceProvenance::new(
1562 "",
1563 SourceSpan {
1564 start: 0,
1565 end: 0,
1566 line: 0,
1567 column: 0,
1568 },
1569 );
1570 }
1571 let instance = asm
1572 .component_instance_plan
1573 .instances
1574 .values()
1575 .next()
1576 .expect("component instance");
1577 graph.nodes.insert(
1578 FormOwnershipNodeKey::Component(instance.id.as_semantic_id().clone()),
1579 FormOwnershipNode::Component {
1580 id: instance.id.as_semantic_id().clone(),
1581 provenance: instance.provenance.clone(),
1582 },
1583 );
1584
1585 let validation = validate_form_ownership_graph(&graph, &asm);
1586 let kinds = validation
1587 .diagnostics
1588 .iter()
1589 .map(|diagnostic| diagnostic.kind)
1590 .collect::<BTreeSet<_>>();
1591 assert!(!validation.is_valid);
1592 assert!(kinds.contains(&FormOwnershipIntegrityKind::MissingOwner));
1593 assert!(kinds.contains(&FormOwnershipIntegrityKind::MultipleOwners));
1594 assert!(kinds.contains(&FormOwnershipIntegrityKind::OwnershipCycle));
1595 assert!(kinds.contains(&FormOwnershipIntegrityKind::UnknownOwnershipEndpoint));
1596 assert!(kinds.contains(&FormOwnershipIntegrityKind::UnknownReferenceEndpoint));
1597 assert!(kinds.contains(&FormOwnershipIntegrityKind::MissingProvenance));
1598 assert!(kinds.contains(&FormOwnershipIntegrityKind::InstanceIdentityInDeclarationGraph));
1599 assert!(kinds.contains(&FormOwnershipIntegrityKind::NonCanonicalOrdering));
1600 assert_eq!(
1601 graph.ownership_edges.len(),
1602 asm.form_ownership.ownership_edges.len() + 2
1603 );
1604 }
1605
1606 #[test]
1607 fn validates_product_reciprocity_and_component_mismatches() {
1608 let parsed = presolve_parser::parse_file("src/ProfileEditor.tsx", form_source());
1609 let asm = build_application_semantic_model(&parsed);
1610 let mut malformed = asm.clone();
1611 let form_ids = malformed.forms.keys().cloned().collect::<Vec<_>>();
1612 let field_id = malformed.form_fields.keys().next().expect("Field").clone();
1613 let binding_id = malformed
1614 .form_field_bindings
1615 .keys()
1616 .next()
1617 .expect("binding")
1618 .clone();
1619 malformed
1620 .form_fields
1621 .get_mut(&field_id)
1622 .expect("Field")
1623 .owner_form = form_ids[1].clone();
1624 let binding = malformed
1625 .form_field_bindings
1626 .get_mut(&binding_id)
1627 .expect("binding");
1628 binding.form = form_ids[1].clone();
1629 binding.component =
1630 SemanticId::component_in_module("src/Other.tsx", Some("other-editor"), "OtherEditor");
1631
1632 let validation = validate_form_ownership_graph(&malformed.form_ownership, &malformed);
1633 let kinds = validation
1634 .diagnostics
1635 .iter()
1636 .map(|diagnostic| diagnostic.kind)
1637 .collect::<BTreeSet<_>>();
1638 assert!(kinds.contains(&FormOwnershipIntegrityKind::FieldFormMismatch));
1639 assert!(kinds.contains(&FormOwnershipIntegrityKind::BindingFormMismatch));
1640 assert!(kinds.contains(&FormOwnershipIntegrityKind::ComponentOwnershipMismatch));
1641 }
1642
1643 #[test]
1644 fn is_byte_stable_under_repeated_and_reversed_multi_file_construction() {
1645 let first = r#"
1646@component("first-editor")
1647class FirstEditor {
1648 @form() editor!: Form;
1649 @field(this.editor) value = "first";
1650 render() { return <input field={this.value} />; }
1651}
1652"#;
1653 let second = r#"
1654@component("second-editor")
1655class SecondEditor {
1656 @form() editor!: Form;
1657 @field(this.editor) value = "second";
1658 render() { return <textarea field={this.value} />; }
1659}
1660"#;
1661 let ordered =
1662 CompilationUnit::parse_sources([("src/First.tsx", first), ("src/Second.tsx", second)]);
1663 let reversed =
1664 CompilationUnit::parse_sources([("src/Second.tsx", second), ("src/First.tsx", first)]);
1665 let ordered = build_application_semantic_model_for_unit(&ordered);
1666 let repeated =
1667 build_application_semantic_model_for_unit(&CompilationUnit::parse_sources([
1668 ("src/First.tsx", first),
1669 ("src/Second.tsx", second),
1670 ]));
1671 let reversed = build_application_semantic_model_for_unit(&reversed);
1672
1673 assert_eq!(ordered.form_ownership, repeated.form_ownership);
1674 assert_eq!(ordered.form_ownership, reversed.form_ownership);
1675 assert_eq!(ordered.form_ownership.roots.len(), 2);
1676 assert_eq!(ordered.form_ownership.nodes.len(), 10);
1677 assert!(ordered.form_ownership.validation.is_valid);
1678 }
1679
1680 #[test]
1681 fn keeps_repeated_component_instances_out_of_the_declaration_graph() {
1682 let parsed = presolve_parser::parse_file(
1683 "src/App.tsx",
1684 r#"
1685@component("profile-editor")
1686class ProfileEditor {
1687 @form() profile!: Form;
1688 @field(this.profile) name = "";
1689 render() { return <input field={this.name} />; }
1690}
1691
1692@component("app-root")
1693class AppRoot {
1694 render() {
1695 return <main><ProfileEditor /><ProfileEditor /></main>;
1696 }
1697}
1698"#,
1699 );
1700 let asm = build_application_semantic_model(&parsed);
1701 let editor = asm
1702 .components
1703 .iter()
1704 .find(|component| component.element_name.as_deref() == Some("profile-editor"))
1705 .expect("ProfileEditor");
1706 let instances = asm
1707 .component_instances_for_definition(&editor.id)
1708 .into_iter()
1709 .collect::<Vec<_>>();
1710 let form = FormId::for_owner(&editor.id, "profile");
1711
1712 assert_eq!(instances.len(), 2);
1713 assert_eq!(
1714 asm.form_ownership
1715 .nodes
1716 .keys()
1717 .filter(|key| matches!(key, FormOwnershipNodeKey::Form(_)))
1718 .count(),
1719 1
1720 );
1721 let first = FormInstanceId::for_component_instance(&instances[0].id, &form);
1722 let second = FormInstanceId::for_component_instance(&instances[1].id, &form);
1723 assert_ne!(first, second);
1724 assert_eq!(
1725 first,
1726 FormInstanceId::for_component_instance(&instances[0].id, &form)
1727 );
1728 assert!(asm.form_ownership.nodes.keys().all(|key| {
1729 key.semantic_id() != first.as_semantic_id()
1730 && key.semantic_id() != second.as_semantic_id()
1731 }));
1732 assert!(asm.form_ownership.validation.is_valid);
1733 }
1734
1735 #[test]
1736 fn projects_form_ownership_products_into_semantic_graph_v6() {
1737 let parsed = presolve_parser::parse_file("src/ProfileEditor.tsx", form_source());
1738 let asm = build_application_semantic_model(&parsed);
1739 let public = build_semantic_graph(&asm);
1740
1741 assert_eq!(public.schema_version, SEMANTIC_GRAPH_SCHEMA_VERSION);
1742 assert_eq!(SEMANTIC_GRAPH_SCHEMA_VERSION, 6);
1743 assert!(public
1744 .nodes
1745 .iter()
1746 .any(|node| matches!(node.kind, crate::SemanticGraphNodeKind::Form)));
1747 assert!(public
1748 .edges
1749 .iter()
1750 .any(|edge| matches!(edge.kind, crate::SemanticGraphEdgeKind::FormOwnsField)));
1751 }
1752}