1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4 is_assignable, serialization_compatibility, AuthoredDeclarationKind,
5 AuthoredValidationRuleArgumentKind, AuthoredValidationRuleDeclarationFact,
6 AuthoredValidationRuleExpressionKind, ComponentBuildRoot, ComponentNode, ComponentRootId,
7 ExecutionBoundary, FieldId, FormEntity, FormFieldEntity, FormId, FormOwnershipGraph,
8 FormOwnershipNodeKey, SemanticId, SemanticOwner, SemanticReference, SemanticReferenceKind,
9 SemanticType, SerializableValue, SerializationCompatibility, SourceProvenance,
10 ValidationDependencyCycleId, ValidationGraphId, ValidationRuleCandidateId, ValidationRuleId,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum ValidationRuleKind {
15 Required,
16 Min,
17 Max,
18 MinLength,
19 MaxLength,
20 Pattern,
21 Email,
22 Equals,
23 NotEquals,
24}
25
26impl ValidationRuleKind {
27 fn from_name(name: &str) -> Option<Self> {
28 match name {
29 "required" => Some(Self::Required),
30 "min" => Some(Self::Min),
31 "max" => Some(Self::Max),
32 "minLength" => Some(Self::MinLength),
33 "maxLength" => Some(Self::MaxLength),
34 "pattern" => Some(Self::Pattern),
35 "email" => Some(Self::Email),
36 "equals" => Some(Self::Equals),
37 "notEquals" => Some(Self::NotEquals),
38 _ => None,
39 }
40 }
41
42 const fn expected_arity(self) -> usize {
43 match self {
44 Self::Required | Self::Email => 0,
45 Self::Min
46 | Self::Max
47 | Self::MinLength
48 | Self::MaxLength
49 | Self::Pattern
50 | Self::Equals
51 | Self::NotEquals => 1,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
57pub enum ValidationRuleArgument {
58 None,
59 Number(String),
60 Length(u64),
61 Pattern(String),
62 Field(FieldId),
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum ValidationCompatibility {
67 Compatible,
68 Incompatible {
69 kind: ValidationRuleKind,
70 field_type: SemanticType,
71 },
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
75pub enum ValidationRuleViolation {
76 InvalidOwner,
77 InvalidTarget { actual: AuthoredDeclarationKind },
78 StaticField,
79 InvalidFieldDeclaration,
80 InvalidDecoratorInvocation,
81 InvalidDecoratorArity { actual: usize, expected: usize },
82 InvalidRuleExpression,
83 UnknownRule,
84 ShadowedCompilerRule,
85 InvalidRuleArity { actual: usize, expected: usize },
86 UnsupportedArgument,
87 InvalidConstantArgument,
88 InvalidPattern,
89 UnresolvedDependency,
90 CrossComponentDependency,
91 CrossFormDependency,
92 SelfDependency,
93 IncompatibleType,
94 DuplicateRule,
95 ContradictoryRule,
96 DependencyCycle,
97 ConflictingSemanticDecorator,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ValidationDependencyDesignator {
102 pub authored_name: String,
103 pub provenance: SourceProvenance,
104 pub name_provenance: SourceProvenance,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ValidationRuleCandidate {
109 pub id: ValidationRuleCandidateId,
110 pub rule_id: Option<ValidationRuleId>,
111 pub owner_component: Option<SemanticId>,
112 pub target_declaration_field: Option<SemanticId>,
113 pub target_field: Option<FieldId>,
114 pub target_form: Option<FormId>,
115 pub authored_target_name: Option<String>,
116 pub declaration_kind: AuthoredDeclarationKind,
117 pub is_static: bool,
118 pub authored_ordinal: usize,
119 pub kind: Option<ValidationRuleKind>,
120 pub argument: Option<ValidationRuleArgument>,
121 pub dependency_designator: Option<ValidationDependencyDesignator>,
122 pub resolved_dependency: Option<FieldId>,
123 pub compatibility: Option<ValidationCompatibility>,
124 pub conflicting_decorators: Vec<String>,
125 pub decorator_provenance: SourceProvenance,
126 pub rule_expression_provenance: Option<SourceProvenance>,
127 pub argument_provenance: Option<SourceProvenance>,
128 pub target_provenance: SourceProvenance,
129 pub target_name_provenance: Option<SourceProvenance>,
130 pub violations: Vec<ValidationRuleViolation>,
131}
132
133impl ValidationRuleCandidate {
134 #[must_use]
135 pub fn is_valid(&self) -> bool {
136 self.violations.is_empty()
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct ValidationRule {
142 pub id: ValidationRuleId,
143 pub candidate_id: ValidationRuleCandidateId,
144 pub owner_form: FormId,
145 pub target_field: FieldId,
146 pub owner_component: SemanticId,
147 pub kind: ValidationRuleKind,
148 pub argument: ValidationRuleArgument,
149 pub dependency: Option<FieldId>,
150 pub compatibility: ValidationCompatibility,
151 pub field_authored_order: usize,
152 pub rule_authored_order: usize,
153 pub provenance: SourceProvenance,
154 pub decorator_provenance: SourceProvenance,
155 pub argument_provenance: Option<SourceProvenance>,
156 pub boundary: ExecutionBoundary,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ValidationDependencyCycle {
161 pub id: ValidationDependencyCycleId,
162 pub form: FormId,
163 pub fields: Vec<FieldId>,
164 pub candidates: Vec<ValidationRuleCandidateId>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct ValidationProducts {
169 pub candidates: Vec<ValidationRuleCandidate>,
170 pub rules: BTreeMap<ValidationRuleId, ValidationRule>,
171 pub cycles: Vec<ValidationDependencyCycle>,
172}
173
174#[allow(clippy::too_many_lines)]
183#[must_use]
184pub fn collect_validation_products(
185 components: &[ComponentNode],
186 forms: &BTreeMap<FormId, FormEntity>,
187 fields: &BTreeMap<FieldId, FormFieldEntity>,
188) -> ValidationProducts {
189 let mut facts = components
190 .iter()
191 .flat_map(|component| component.validation_rule_declaration_facts.iter())
192 .cloned()
193 .collect::<Vec<_>>();
194 facts.sort_by(fact_source_order);
195
196 let fields_by_declaration = fields
197 .values()
198 .map(|field| (field.authored_field.clone(), field))
199 .collect::<BTreeMap<_, _>>();
200 let fields_by_component_name = fields
201 .values()
202 .map(|field| ((field.owner_component.clone(), field.name.clone()), field))
203 .collect::<BTreeMap<_, _>>();
204 let fields_by_name = fields.values().fold(
205 BTreeMap::<String, Vec<&FormFieldEntity>>::new(),
206 |mut grouped, field| {
207 grouped.entry(field.name.clone()).or_default().push(field);
208 grouped
209 },
210 );
211 let shadowed_intrinsics = components
212 .iter()
213 .map(|component| {
214 let mut shadows = component.shadowed_validation_intrinsics.clone();
215 shadows.extend(component.methods.iter().map(|method| method.name.clone()));
216 (component.id.clone(), shadows)
217 })
218 .collect::<BTreeMap<_, _>>();
219
220 let mut candidates = facts
221 .iter()
222 .map(|fact| {
223 lower_candidate(
224 fact,
225 forms,
226 &fields_by_declaration,
227 &fields_by_component_name,
228 &fields_by_name,
229 &shadowed_intrinsics,
230 )
231 })
232 .collect::<Vec<_>>();
233
234 mark_duplicate_rules(&mut candidates);
235 mark_contradictions(&mut candidates);
236 let cycles = mark_dependency_cycles(&mut candidates);
237
238 let field_orders = fields
239 .values()
240 .map(|field| (field.id.clone(), field.declaration_order))
241 .collect::<BTreeMap<_, _>>();
242 let mut rules = BTreeMap::new();
243 for candidate in &mut candidates {
244 if !candidate.is_valid() {
245 candidate.rule_id = None;
246 continue;
247 }
248 let target_field = candidate
249 .target_field
250 .clone()
251 .expect("valid validation candidate has target field");
252 let id = ValidationRuleId::for_field(&target_field, candidate.authored_ordinal);
253 let rule = ValidationRule {
254 id: id.clone(),
255 candidate_id: candidate.id.clone(),
256 owner_form: candidate
257 .target_form
258 .clone()
259 .expect("valid validation candidate has target form"),
260 target_field: target_field.clone(),
261 owner_component: candidate
262 .owner_component
263 .clone()
264 .expect("valid validation candidate has component"),
265 kind: candidate
266 .kind
267 .expect("valid validation candidate has rule kind"),
268 argument: candidate
269 .argument
270 .clone()
271 .expect("valid validation candidate has normalized argument"),
272 dependency: candidate.resolved_dependency.clone(),
273 compatibility: candidate
274 .compatibility
275 .clone()
276 .expect("valid validation candidate has compatibility"),
277 field_authored_order: *field_orders
278 .get(&target_field)
279 .expect("valid validation target has authored order"),
280 rule_authored_order: candidate.authored_ordinal,
281 provenance: candidate.target_provenance.clone(),
282 decorator_provenance: candidate.decorator_provenance.clone(),
283 argument_provenance: candidate.argument_provenance.clone(),
284 boundary: ExecutionBoundary::Client,
285 };
286 candidate.rule_id = Some(id.clone());
287 rules.insert(id, rule);
288 }
289
290 candidates.sort_by(candidate_source_order);
291 ValidationProducts {
292 candidates,
293 rules,
294 cycles,
295 }
296}
297
298#[allow(clippy::too_many_lines)]
299fn lower_candidate(
300 fact: &AuthoredValidationRuleDeclarationFact,
301 forms: &BTreeMap<FormId, FormEntity>,
302 fields_by_declaration: &BTreeMap<SemanticId, &FormFieldEntity>,
303 fields_by_component_name: &BTreeMap<(SemanticId, String), &FormFieldEntity>,
304 fields_by_name: &BTreeMap<String, Vec<&FormFieldEntity>>,
305 shadowed_intrinsics: &BTreeMap<SemanticId, BTreeSet<String>>,
306) -> ValidationRuleCandidate {
307 let target = fact
308 .declaration_field
309 .as_ref()
310 .and_then(|id| fields_by_declaration.get(id).copied())
311 .filter(|field| forms.contains_key(&field.owner_form));
312 let mut violations = Vec::new();
313 if fact.owner_component.is_none() {
314 violations.push(ValidationRuleViolation::InvalidOwner);
315 }
316 if fact.declaration_kind != AuthoredDeclarationKind::InstanceField {
317 violations.push(ValidationRuleViolation::InvalidTarget {
318 actual: fact.declaration_kind,
319 });
320 }
321 if fact.is_static {
322 violations.push(ValidationRuleViolation::StaticField);
323 }
324 if target.is_none() {
325 violations.push(ValidationRuleViolation::InvalidFieldDeclaration);
326 }
327 if !fact.decorator_invoked {
328 violations.push(ValidationRuleViolation::InvalidDecoratorInvocation);
329 }
330 if fact.decorator_argument_count != 1 {
331 violations.push(ValidationRuleViolation::InvalidDecoratorArity {
332 actual: fact.decorator_argument_count,
333 expected: 1,
334 });
335 }
336 if !fact.conflicting_decorators.is_empty() {
337 violations.push(ValidationRuleViolation::ConflictingSemanticDecorator);
338 }
339
340 let mut kind = None;
341 let mut argument = None;
342 let mut dependency_designator = None;
343 let mut argument_provenance = None;
344 let expression_provenance = fact
345 .expression
346 .as_ref()
347 .map(|expression| expression.provenance.clone());
348
349 match fact.expression.as_ref().map(|expression| &expression.kind) {
350 Some(AuthoredValidationRuleExpressionKind::Call { callee, arguments }) => {
351 let Some(callee) = callee.as_deref() else {
352 violations.push(ValidationRuleViolation::InvalidRuleExpression);
353 canonicalize_violations(&mut violations);
354 return candidate_from_parts(
355 fact,
356 target,
357 kind,
358 argument,
359 dependency_designator,
360 None,
361 None,
362 expression_provenance,
363 argument_provenance,
364 violations,
365 );
366 };
367 let Some(classified) = ValidationRuleKind::from_name(callee) else {
368 violations.push(ValidationRuleViolation::UnknownRule);
369 canonicalize_violations(&mut violations);
370 return candidate_from_parts(
371 fact,
372 target,
373 kind,
374 argument,
375 dependency_designator,
376 None,
377 None,
378 expression_provenance,
379 argument_provenance,
380 violations,
381 );
382 };
383 kind = Some(classified);
384 if fact.owner_component.as_ref().is_some_and(|component| {
385 shadowed_intrinsics
386 .get(component)
387 .is_some_and(|methods| methods.contains(callee))
388 }) {
389 violations.push(ValidationRuleViolation::ShadowedCompilerRule);
390 }
391 if arguments.len() == classified.expected_arity() {
392 match normalize_argument(classified, arguments) {
393 Ok((normalized, designator, provenance)) => {
394 argument = Some(normalized);
395 dependency_designator = designator;
396 argument_provenance = provenance;
397 }
398 Err(violation) => violations.push(violation),
399 }
400 } else {
401 violations.push(ValidationRuleViolation::InvalidRuleArity {
402 actual: arguments.len(),
403 expected: classified.expected_arity(),
404 });
405 }
406 }
407 Some(
408 AuthoredValidationRuleExpressionKind::Identifier(_)
409 | AuthoredValidationRuleExpressionKind::Unsupported,
410 )
411 | None => violations.push(ValidationRuleViolation::InvalidRuleExpression),
412 }
413
414 let mut resolved_dependency = None;
415 if let (Some(owner), Some(target), Some(designator)) = (
416 fact.owner_component.as_ref(),
417 target,
418 dependency_designator.as_ref(),
419 ) {
420 if let Some(dependency) = fields_by_component_name
421 .get(&(owner.clone(), designator.authored_name.clone()))
422 .copied()
423 {
424 if dependency.id == target.id {
425 violations.push(ValidationRuleViolation::SelfDependency);
426 } else if dependency.owner_form != target.owner_form {
427 violations.push(ValidationRuleViolation::CrossFormDependency);
428 } else {
429 resolved_dependency = Some(dependency.id.clone());
430 argument = Some(ValidationRuleArgument::Field(dependency.id.clone()));
431 }
432 } else if fields_by_name
433 .get(&designator.authored_name)
434 .is_some_and(|matches| matches.iter().any(|field| &field.owner_component != owner))
435 {
436 violations.push(ValidationRuleViolation::CrossComponentDependency);
437 } else {
438 violations.push(ValidationRuleViolation::UnresolvedDependency);
439 }
440 }
441
442 let compatibility = kind.zip(target).map(|(kind, target)| {
443 let dependency = resolved_dependency.as_ref().and_then(|id| {
444 fields_by_declaration
445 .values()
446 .copied()
447 .find(|field| &field.id == id)
448 });
449 if rule_is_compatible(
450 kind,
451 &target.semantic_type,
452 dependency.map(|field| &field.semantic_type),
453 ) {
454 ValidationCompatibility::Compatible
455 } else {
456 violations.push(ValidationRuleViolation::IncompatibleType);
457 ValidationCompatibility::Incompatible {
458 kind,
459 field_type: target.semantic_type.clone(),
460 }
461 }
462 });
463
464 canonicalize_violations(&mut violations);
465 candidate_from_parts(
466 fact,
467 target,
468 kind,
469 argument,
470 dependency_designator,
471 resolved_dependency,
472 compatibility,
473 expression_provenance,
474 argument_provenance,
475 violations,
476 )
477}
478
479#[allow(clippy::too_many_arguments)]
480fn candidate_from_parts(
481 fact: &AuthoredValidationRuleDeclarationFact,
482 target: Option<&FormFieldEntity>,
483 kind: Option<ValidationRuleKind>,
484 argument: Option<ValidationRuleArgument>,
485 dependency_designator: Option<ValidationDependencyDesignator>,
486 resolved_dependency: Option<FieldId>,
487 compatibility: Option<ValidationCompatibility>,
488 rule_expression_provenance: Option<SourceProvenance>,
489 argument_provenance: Option<SourceProvenance>,
490 violations: Vec<ValidationRuleViolation>,
491) -> ValidationRuleCandidate {
492 ValidationRuleCandidate {
493 id: fact.id.clone(),
494 rule_id: None,
495 owner_component: fact.owner_component.clone(),
496 target_declaration_field: fact.declaration_field.clone(),
497 target_field: target.map(|field| field.id.clone()),
498 target_form: target.map(|field| field.owner_form.clone()),
499 authored_target_name: fact.authored_name.clone(),
500 declaration_kind: fact.declaration_kind,
501 is_static: fact.is_static,
502 authored_ordinal: fact.authored_ordinal,
503 kind,
504 argument,
505 dependency_designator,
506 resolved_dependency,
507 compatibility,
508 conflicting_decorators: fact.conflicting_decorators.clone(),
509 decorator_provenance: fact.decorator_provenance.clone(),
510 rule_expression_provenance,
511 argument_provenance,
512 target_provenance: fact.provenance.clone(),
513 target_name_provenance: fact.name_provenance.clone(),
514 violations,
515 }
516}
517
518fn normalize_argument(
519 kind: ValidationRuleKind,
520 arguments: &[crate::AuthoredValidationRuleArgument],
521) -> Result<
522 (
523 ValidationRuleArgument,
524 Option<ValidationDependencyDesignator>,
525 Option<SourceProvenance>,
526 ),
527 ValidationRuleViolation,
528> {
529 if arguments.is_empty() {
530 return Ok((ValidationRuleArgument::None, None, None));
531 }
532 let argument = &arguments[0];
533 let provenance = Some(argument.provenance.clone());
534 match (kind, &argument.kind) {
535 (
536 ValidationRuleKind::Min | ValidationRuleKind::Max,
537 AuthoredValidationRuleArgumentKind::Constant(expression),
538 ) => {
539 let number = constant_number(expression)?;
540 Ok((ValidationRuleArgument::Number(number), None, provenance))
541 }
542 (
543 ValidationRuleKind::MinLength | ValidationRuleKind::MaxLength,
544 AuthoredValidationRuleArgumentKind::Constant(expression),
545 ) => {
546 let number = constant_number(expression)?;
547 let number = number
548 .parse::<u64>()
549 .map_err(|_| ValidationRuleViolation::InvalidConstantArgument)?;
550 Ok((ValidationRuleArgument::Length(number), None, provenance))
551 }
552 (
553 ValidationRuleKind::Pattern,
554 AuthoredValidationRuleArgumentKind::StringLiteral(pattern),
555 ) => {
556 if !presolve_parser::is_valid_ecmascript_pattern(pattern) {
557 return Err(ValidationRuleViolation::InvalidPattern);
558 }
559 Ok((
560 ValidationRuleArgument::Pattern(pattern.clone()),
561 None,
562 provenance,
563 ))
564 }
565 (
566 ValidationRuleKind::Equals | ValidationRuleKind::NotEquals,
567 AuthoredValidationRuleArgumentKind::ThisMember {
568 name,
569 name_provenance,
570 },
571 ) => Ok((
572 ValidationRuleArgument::None,
573 Some(ValidationDependencyDesignator {
574 authored_name: name.clone(),
575 provenance: argument.provenance.clone(),
576 name_provenance: name_provenance.clone(),
577 }),
578 provenance,
579 )),
580 _ => Err(ValidationRuleViolation::UnsupportedArgument),
581 }
582}
583
584fn constant_number(
585 expression: &crate::ConstantExpression,
586) -> Result<String, ValidationRuleViolation> {
587 let SerializableValue::Number(number) = expression
588 .evaluate()
589 .map_err(|_| ValidationRuleViolation::InvalidConstantArgument)?
590 else {
591 return Err(ValidationRuleViolation::InvalidConstantArgument);
592 };
593 let value = number
594 .parse::<f64>()
595 .map_err(|_| ValidationRuleViolation::InvalidConstantArgument)?;
596 value
597 .is_finite()
598 .then(|| value.to_string())
599 .ok_or(ValidationRuleViolation::InvalidConstantArgument)
600}
601
602fn rule_is_compatible(
603 kind: ValidationRuleKind,
604 target: &SemanticType,
605 dependency: Option<&SemanticType>,
606) -> bool {
607 match kind {
608 ValidationRuleKind::Required => {
609 serialization_compatibility(target) == SerializationCompatibility::Serializable
610 && !matches!(
611 target,
612 SemanticType::Null | SemanticType::Unknown | SemanticType::Never
613 )
614 }
615 ValidationRuleKind::Min | ValidationRuleKind::Max => {
616 type_has_domain(target, TypeDomain::Number)
617 }
618 ValidationRuleKind::MinLength | ValidationRuleKind::MaxLength => {
619 length_domain(target).is_some()
620 }
621 ValidationRuleKind::Pattern | ValidationRuleKind::Email => {
622 type_has_domain(target, TypeDomain::String)
623 }
624 ValidationRuleKind::Equals | ValidationRuleKind::NotEquals => {
625 dependency.is_some_and(|dependency| {
626 !contains_unknown_or_never(target)
627 && !contains_unknown_or_never(dependency)
628 && (is_assignable(target, dependency) || is_assignable(dependency, target))
629 })
630 }
631 }
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq)]
635enum TypeDomain {
636 Number,
637 String,
638}
639
640fn type_has_domain(semantic_type: &SemanticType, domain: TypeDomain) -> bool {
641 let members = non_null_members(semantic_type);
642 !members.is_empty()
643 && members.iter().all(|member| {
644 matches!(
645 (domain, *member),
646 (
647 TypeDomain::Number,
648 SemanticType::Number | SemanticType::NumberLiteral(_)
649 ) | (
650 TypeDomain::String,
651 SemanticType::String | SemanticType::StringLiteral(_)
652 )
653 )
654 })
655}
656
657#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658enum LengthDomain {
659 String,
660 Sequence,
661}
662
663fn length_domain(semantic_type: &SemanticType) -> Option<LengthDomain> {
664 let members = non_null_members(semantic_type);
665 let mut domain = None;
666 for member in members {
667 let current = match member {
668 SemanticType::String | SemanticType::StringLiteral(_) => LengthDomain::String,
669 SemanticType::Array(_) | SemanticType::Tuple(_) => LengthDomain::Sequence,
670 _ => return None,
671 };
672 if domain.is_some_and(|domain| domain != current) {
673 return None;
674 }
675 domain = Some(current);
676 }
677 domain
678}
679
680fn non_null_members(semantic_type: &SemanticType) -> Vec<&SemanticType> {
681 match semantic_type {
682 SemanticType::Union(members) => members
683 .iter()
684 .filter(|member| !matches!(member, SemanticType::Null))
685 .collect(),
686 SemanticType::Null => Vec::new(),
687 semantic_type => vec![semantic_type],
688 }
689}
690
691fn contains_unknown_or_never(semantic_type: &SemanticType) -> bool {
692 match semantic_type {
693 SemanticType::Unknown | SemanticType::Never => true,
694 SemanticType::Array(element) => contains_unknown_or_never(element),
695 SemanticType::Tuple(items) | SemanticType::Union(items) => {
696 items.iter().any(contains_unknown_or_never)
697 }
698 SemanticType::Object(object) => object.properties.values().any(contains_unknown_or_never),
699 SemanticType::Resource(resource) => {
700 contains_unknown_or_never(&resource.data) || contains_unknown_or_never(&resource.error)
701 }
702 _ => false,
703 }
704}
705
706fn mark_duplicate_rules(candidates: &mut [ValidationRuleCandidate]) {
707 let mut groups = BTreeMap::<
708 (
709 FieldId,
710 ValidationRuleKind,
711 ValidationRuleArgument,
712 Option<FieldId>,
713 ),
714 Vec<usize>,
715 >::new();
716 for (index, candidate) in candidates.iter().enumerate() {
717 if let (Some(field), Some(kind), Some(argument)) = (
718 candidate.target_field.clone(),
719 candidate.kind,
720 candidate.argument.clone(),
721 ) {
722 groups
723 .entry((field, kind, argument, candidate.resolved_dependency.clone()))
724 .or_default()
725 .push(index);
726 }
727 }
728 for group in groups.values().filter(|group| group.len() > 1) {
729 for &index in group {
730 add_violation(
731 &mut candidates[index],
732 ValidationRuleViolation::DuplicateRule,
733 );
734 }
735 }
736}
737
738fn mark_contradictions(candidates: &mut [ValidationRuleCandidate]) {
739 let mut contradictory = BTreeSet::new();
740 for (left_index, left) in candidates.iter().enumerate() {
741 for (right_index, right) in candidates.iter().enumerate().skip(left_index + 1) {
742 if left.target_field != right.target_field {
743 continue;
744 }
745 let contradiction = match (left.kind, &left.argument, right.kind, &right.argument) {
746 (
747 Some(ValidationRuleKind::Min),
748 Some(ValidationRuleArgument::Number(minimum)),
749 Some(ValidationRuleKind::Max),
750 Some(ValidationRuleArgument::Number(maximum)),
751 )
752 | (
753 Some(ValidationRuleKind::Max),
754 Some(ValidationRuleArgument::Number(maximum)),
755 Some(ValidationRuleKind::Min),
756 Some(ValidationRuleArgument::Number(minimum)),
757 ) => numeric_value(minimum) > numeric_value(maximum),
758 (
759 Some(ValidationRuleKind::MinLength),
760 Some(ValidationRuleArgument::Length(minimum)),
761 Some(ValidationRuleKind::MaxLength),
762 Some(ValidationRuleArgument::Length(maximum)),
763 )
764 | (
765 Some(ValidationRuleKind::MaxLength),
766 Some(ValidationRuleArgument::Length(maximum)),
767 Some(ValidationRuleKind::MinLength),
768 Some(ValidationRuleArgument::Length(minimum)),
769 ) => minimum > maximum,
770 (Some(ValidationRuleKind::Equals), _, Some(ValidationRuleKind::NotEquals), _)
771 | (Some(ValidationRuleKind::NotEquals), _, Some(ValidationRuleKind::Equals), _) => {
772 left.resolved_dependency.is_some()
773 && left.resolved_dependency == right.resolved_dependency
774 }
775 _ => false,
776 };
777 if contradiction {
778 contradictory.insert(left_index);
779 contradictory.insert(right_index);
780 }
781 }
782 }
783 for index in contradictory {
784 add_violation(
785 &mut candidates[index],
786 ValidationRuleViolation::ContradictoryRule,
787 );
788 }
789}
790
791fn numeric_value(value: &str) -> f64 {
792 value
793 .parse::<f64>()
794 .expect("normalized validation number is finite")
795}
796
797fn mark_dependency_cycles(
798 candidates: &mut [ValidationRuleCandidate],
799) -> Vec<ValidationDependencyCycle> {
800 let mut adjacency = BTreeMap::<FieldId, BTreeSet<FieldId>>::new();
801 for candidate in candidates
802 .iter()
803 .filter(|candidate| candidate.violations.is_empty())
804 {
805 if let (Some(target), Some(dependency)) =
806 (&candidate.target_field, &candidate.resolved_dependency)
807 {
808 adjacency
809 .entry(target.clone())
810 .or_default()
811 .insert(dependency.clone());
812 adjacency.entry(dependency.clone()).or_default();
813 }
814 }
815
816 let all_fields = adjacency.keys().cloned().collect::<Vec<_>>();
817 let mut assigned = BTreeSet::new();
818 let mut cycles = Vec::new();
819 for field in all_fields {
820 if assigned.contains(&field) {
821 continue;
822 }
823 let forward = reachable_fields(&field, &adjacency);
824 let mut strongly_connected = forward
825 .into_iter()
826 .filter(|other| reachable_fields(other, &adjacency).contains(&field))
827 .collect::<Vec<_>>();
828 strongly_connected.sort();
829 if strongly_connected.len() < 2 {
830 assigned.insert(field);
831 continue;
832 }
833 assigned.extend(strongly_connected.iter().cloned());
834 let field_set = strongly_connected.iter().cloned().collect::<BTreeSet<_>>();
835 let candidate_indexes = candidates
836 .iter()
837 .enumerate()
838 .filter_map(|(index, candidate)| {
839 let target = candidate.target_field.as_ref()?;
840 let dependency = candidate.resolved_dependency.as_ref()?;
841 (field_set.contains(target) && field_set.contains(dependency)).then_some(index)
842 })
843 .collect::<Vec<_>>();
844 let mut candidate_ids = candidate_indexes
845 .iter()
846 .map(|index| candidates[*index].id.clone())
847 .collect::<Vec<_>>();
848 candidate_ids.sort();
849 let form = candidates[*candidate_indexes.first().expect("cycle has rule")]
850 .target_form
851 .clone()
852 .expect("cycle candidate has target form");
853 for index in candidate_indexes {
854 add_violation(
855 &mut candidates[index],
856 ValidationRuleViolation::DependencyCycle,
857 );
858 }
859 cycles.push(ValidationDependencyCycle {
860 id: ValidationDependencyCycleId::for_fields(&form, &strongly_connected),
861 form,
862 fields: strongly_connected,
863 candidates: candidate_ids,
864 });
865 }
866 cycles.sort_by(|left, right| left.id.cmp(&right.id));
867 cycles
868}
869
870fn reachable_fields(
871 start: &FieldId,
872 adjacency: &BTreeMap<FieldId, BTreeSet<FieldId>>,
873) -> BTreeSet<FieldId> {
874 let mut visited = BTreeSet::new();
875 let mut pending = vec![start.clone()];
876 while let Some(field) = pending.pop() {
877 if !visited.insert(field.clone()) {
878 continue;
879 }
880 if let Some(next) = adjacency.get(&field) {
881 pending.extend(next.iter().rev().cloned());
882 }
883 }
884 visited
885}
886
887fn add_violation(candidate: &mut ValidationRuleCandidate, violation: ValidationRuleViolation) {
888 candidate.violations.push(violation);
889 canonicalize_violations(&mut candidate.violations);
890}
891
892fn canonicalize_violations(violations: &mut Vec<ValidationRuleViolation>) {
893 violations.sort();
894 violations.dedup();
895}
896
897fn fact_source_order(
898 left: &AuthoredValidationRuleDeclarationFact,
899 right: &AuthoredValidationRuleDeclarationFact,
900) -> std::cmp::Ordering {
901 (
902 left.provenance.path.as_path(),
903 left.decorator_provenance.span.start,
904 left.id.as_str(),
905 )
906 .cmp(&(
907 right.provenance.path.as_path(),
908 right.decorator_provenance.span.start,
909 right.id.as_str(),
910 ))
911}
912
913fn candidate_source_order(
914 left: &ValidationRuleCandidate,
915 right: &ValidationRuleCandidate,
916) -> std::cmp::Ordering {
917 (
918 left.target_provenance.path.as_path(),
919 left.decorator_provenance.span.start,
920 left.id.as_str(),
921 )
922 .cmp(&(
923 right.target_provenance.path.as_path(),
924 right.decorator_provenance.span.start,
925 right.id.as_str(),
926 ))
927}
928
929#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
930pub enum ValidationGraphNodeKey {
931 Form(FormId),
932 FormField(FieldId),
933 ValidationRule(ValidationRuleId),
934}
935
936impl ValidationGraphNodeKey {
937 #[must_use]
938 pub fn semantic_id(&self) -> &SemanticId {
939 match self {
940 Self::Form(id) => id.as_semantic_id(),
941 Self::FormField(id) => id.as_semantic_id(),
942 Self::ValidationRule(id) => id.as_semantic_id(),
943 }
944 }
945}
946
947#[derive(Debug, Clone, PartialEq, Eq)]
948pub enum ValidationGraphNode {
949 Form {
950 id: FormId,
951 provenance: SourceProvenance,
952 },
953 FormField {
954 id: FieldId,
955 provenance: SourceProvenance,
956 },
957 ValidationRule {
958 id: ValidationRuleId,
959 provenance: SourceProvenance,
960 },
961}
962
963impl ValidationGraphNode {
964 #[must_use]
965 pub fn key(&self) -> ValidationGraphNodeKey {
966 match self {
967 Self::Form { id, .. } => ValidationGraphNodeKey::Form(id.clone()),
968 Self::FormField { id, .. } => ValidationGraphNodeKey::FormField(id.clone()),
969 Self::ValidationRule { id, .. } => ValidationGraphNodeKey::ValidationRule(id.clone()),
970 }
971 }
972
973 #[must_use]
974 pub const fn provenance(&self) -> &SourceProvenance {
975 match self {
976 Self::Form { provenance, .. }
977 | Self::FormField { provenance, .. }
978 | Self::ValidationRule { provenance, .. } => provenance,
979 }
980 }
981}
982
983#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
984pub enum ValidationGraphEdgeKind {
985 FormOwnsField,
986 FieldOwnsRule,
987 RuleDependsOnField,
988}
989
990#[derive(Debug, Clone, PartialEq, Eq)]
991pub struct ValidationGraphEdge {
992 pub kind: ValidationGraphEdgeKind,
993 pub source: ValidationGraphNodeKey,
994 pub target: ValidationGraphNodeKey,
995 pub provenance: SourceProvenance,
996}
997
998#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
999pub enum ValidationGraphIntegrityKind {
1000 DuplicateNode,
1001 MissingFormNode,
1002 MissingFieldNode,
1003 MissingRuleNode,
1004 UnknownEdgeEndpoint,
1005 MultipleRuleOwners,
1006 FieldFormMismatch,
1007 RuleTargetMismatch,
1008 DependencyMismatch,
1009 CrossFormDependency,
1010 CrossComponentDependency,
1011 SelfDependency,
1012 OwnershipCycle,
1013 DependencyCycleLeakage,
1014 InvalidCandidatePromoted,
1015 InstanceIdentityInValidationGraph,
1016 MissingProvenance,
1017 NonCanonicalOrdering,
1018 GraphIdentityMismatch,
1019}
1020
1021impl ValidationGraphIntegrityKind {
1022 #[must_use]
1023 pub const fn code(self) -> &'static str {
1024 match self {
1025 Self::DuplicateNode => "PSASM1221",
1026 Self::MissingFormNode => "PSASM1222",
1027 Self::MissingFieldNode => "PSASM1223",
1028 Self::MissingRuleNode => "PSASM1224",
1029 Self::UnknownEdgeEndpoint => "PSASM1225",
1030 Self::MultipleRuleOwners => "PSASM1226",
1031 Self::FieldFormMismatch => "PSASM1227",
1032 Self::RuleTargetMismatch => "PSASM1228",
1033 Self::DependencyMismatch => "PSASM1229",
1034 Self::CrossFormDependency => "PSASM1230",
1035 Self::CrossComponentDependency => "PSASM1231",
1036 Self::SelfDependency => "PSASM1232",
1037 Self::OwnershipCycle => "PSASM1233",
1038 Self::DependencyCycleLeakage => "PSASM1234",
1039 Self::InvalidCandidatePromoted => "PSASM1235",
1040 Self::InstanceIdentityInValidationGraph => "PSASM1236",
1041 Self::MissingProvenance => "PSASM1237",
1042 Self::NonCanonicalOrdering => "PSASM1238",
1043 Self::GraphIdentityMismatch => "PSASM1239",
1044 }
1045 }
1046}
1047
1048#[derive(Debug, Clone, PartialEq, Eq)]
1049pub struct ValidationGraphIntegrityDiagnostic {
1050 pub code: String,
1051 pub kind: ValidationGraphIntegrityKind,
1052 pub message: String,
1053}
1054
1055#[derive(Debug, Clone, PartialEq, Eq)]
1056pub struct ValidationGraphValidation {
1057 pub diagnostics: Vec<ValidationGraphIntegrityDiagnostic>,
1058 pub is_valid: bool,
1059}
1060
1061impl Default for ValidationGraphValidation {
1062 fn default() -> Self {
1063 Self {
1064 diagnostics: Vec::new(),
1065 is_valid: true,
1066 }
1067 }
1068}
1069
1070#[derive(Debug, Clone, PartialEq, Eq)]
1071pub struct ValidationGraph {
1072 pub id: ValidationGraphId,
1073 pub nodes: BTreeMap<ValidationGraphNodeKey, ValidationGraphNode>,
1074 pub edges: Vec<ValidationGraphEdge>,
1075 pub cycles: Vec<ValidationDependencyCycle>,
1076 pub validation: ValidationGraphValidation,
1077}
1078
1079impl ValidationGraph {
1080 #[must_use]
1081 pub fn node(&self, key: &ValidationGraphNodeKey) -> Option<&ValidationGraphNode> {
1082 self.nodes.get(key)
1083 }
1084
1085 #[must_use]
1086 pub fn rules_of_field(&self, field: &FieldId) -> Vec<&ValidationRuleId> {
1087 let mut rules = self
1088 .edges
1089 .iter()
1090 .filter_map(|edge| {
1091 (edge.kind == ValidationGraphEdgeKind::FieldOwnsRule
1092 && edge.source == ValidationGraphNodeKey::FormField(field.clone()))
1093 .then_some(match &edge.target {
1094 ValidationGraphNodeKey::ValidationRule(id) => Some(id),
1095 _ => None,
1096 })
1097 .flatten()
1098 })
1099 .collect::<Vec<_>>();
1100 rules.sort_by_key(|rule| validation_rule_ordinal(rule));
1101 rules
1102 }
1103
1104 #[must_use]
1105 pub fn rules_of_form(&self, form: &FormId) -> Vec<&ValidationRuleId> {
1106 let fields = self
1107 .edges
1108 .iter()
1109 .filter_map(|edge| {
1110 (edge.kind == ValidationGraphEdgeKind::FormOwnsField
1111 && edge.source == ValidationGraphNodeKey::Form(form.clone()))
1112 .then_some(match &edge.target {
1113 ValidationGraphNodeKey::FormField(field) => Some(field),
1114 _ => None,
1115 })
1116 .flatten()
1117 })
1118 .collect::<BTreeSet<_>>();
1119 let mut rules = self
1120 .edges
1121 .iter()
1122 .filter_map(|edge| {
1123 (edge.kind == ValidationGraphEdgeKind::FieldOwnsRule
1124 && matches!(&edge.source, ValidationGraphNodeKey::FormField(field) if fields.contains(field)))
1125 .then_some(match &edge.target {
1126 ValidationGraphNodeKey::ValidationRule(rule) => Some(rule),
1127 _ => None,
1128 })
1129 .flatten()
1130 })
1131 .collect::<Vec<_>>();
1132 rules.sort();
1133 rules
1134 }
1135
1136 #[must_use]
1137 pub fn target_of_rule(&self, rule: &ValidationRuleId) -> Option<&FieldId> {
1138 self.edges.iter().find_map(|edge| {
1139 (edge.kind == ValidationGraphEdgeKind::FieldOwnsRule
1140 && edge.target == ValidationGraphNodeKey::ValidationRule(rule.clone()))
1141 .then_some(match &edge.source {
1142 ValidationGraphNodeKey::FormField(field) => Some(field),
1143 _ => None,
1144 })
1145 .flatten()
1146 })
1147 }
1148
1149 #[must_use]
1150 pub fn dependencies_of_rule(&self, rule: &ValidationRuleId) -> Vec<&FieldId> {
1151 self.edges
1152 .iter()
1153 .filter_map(|edge| {
1154 (edge.kind == ValidationGraphEdgeKind::RuleDependsOnField
1155 && edge.source == ValidationGraphNodeKey::ValidationRule(rule.clone()))
1156 .then_some(match &edge.target {
1157 ValidationGraphNodeKey::FormField(id) => Some(id),
1158 _ => None,
1159 })
1160 .flatten()
1161 })
1162 .collect()
1163 }
1164
1165 #[must_use]
1166 pub fn dependents_of_field(&self, field: &FieldId) -> Vec<&ValidationRuleId> {
1167 self.edges
1168 .iter()
1169 .filter_map(|edge| {
1170 (edge.kind == ValidationGraphEdgeKind::RuleDependsOnField
1171 && edge.target == ValidationGraphNodeKey::FormField(field.clone()))
1172 .then_some(match &edge.source {
1173 ValidationGraphNodeKey::ValidationRule(id) => Some(id),
1174 _ => None,
1175 })
1176 .flatten()
1177 })
1178 .collect()
1179 }
1180
1181 #[must_use]
1182 pub fn cycles_of_form(&self, form: &FormId) -> Vec<&ValidationDependencyCycle> {
1183 self.cycles
1184 .iter()
1185 .filter(|cycle| &cycle.form == form)
1186 .collect()
1187 }
1188}
1189
1190fn validation_rule_ordinal(rule: &ValidationRuleId) -> usize {
1191 rule.as_str()
1192 .rsplit(':')
1193 .next()
1194 .and_then(|ordinal| ordinal.parse().ok())
1195 .unwrap_or(usize::MAX)
1196}
1197
1198#[allow(clippy::too_many_arguments)]
1199#[must_use]
1200pub fn collect_validation_graph(
1201 build_roots: &BTreeMap<ComponentRootId, ComponentBuildRoot>,
1202 form_ownership: &FormOwnershipGraph,
1203 forms: &BTreeMap<FormId, FormEntity>,
1204 fields: &BTreeMap<FieldId, FormFieldEntity>,
1205 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
1206 candidates: &[ValidationRuleCandidate],
1207 cycles: &[ValidationDependencyCycle],
1208 ownership: &BTreeMap<SemanticId, SemanticOwner>,
1209 references: &[SemanticReference],
1210) -> ValidationGraph {
1211 let mut nodes = BTreeMap::new();
1212 for form in forms.values() {
1213 nodes.insert(
1214 ValidationGraphNodeKey::Form(form.id.clone()),
1215 ValidationGraphNode::Form {
1216 id: form.id.clone(),
1217 provenance: form.provenance.clone(),
1218 },
1219 );
1220 }
1221 for field in fields.values() {
1222 nodes.insert(
1223 ValidationGraphNodeKey::FormField(field.id.clone()),
1224 ValidationGraphNode::FormField {
1225 id: field.id.clone(),
1226 provenance: field.provenance.clone(),
1227 },
1228 );
1229 }
1230 for rule in rules.values() {
1231 nodes.insert(
1232 ValidationGraphNodeKey::ValidationRule(rule.id.clone()),
1233 ValidationGraphNode::ValidationRule {
1234 id: rule.id.clone(),
1235 provenance: rule.provenance.clone(),
1236 },
1237 );
1238 }
1239
1240 let mut edges = Vec::new();
1241 for edge in &form_ownership.ownership_edges {
1242 if let (FormOwnershipNodeKey::Form(form), FormOwnershipNodeKey::FormField(field)) =
1243 (&edge.owner, &edge.child)
1244 {
1245 edges.push(ValidationGraphEdge {
1246 kind: ValidationGraphEdgeKind::FormOwnsField,
1247 source: ValidationGraphNodeKey::Form(form.clone()),
1248 target: ValidationGraphNodeKey::FormField(field.clone()),
1249 provenance: edge.provenance.clone(),
1250 });
1251 }
1252 }
1253 for rule in rules.values() {
1254 if let Some(SemanticOwner::Entity(owner)) = ownership.get(rule.id.as_semantic_id()) {
1255 if let Some(field) = fields.keys().find(|field| field.as_semantic_id() == owner) {
1256 edges.push(ValidationGraphEdge {
1257 kind: ValidationGraphEdgeKind::FieldOwnsRule,
1258 source: ValidationGraphNodeKey::FormField(field.clone()),
1259 target: ValidationGraphNodeKey::ValidationRule(rule.id.clone()),
1260 provenance: rule.decorator_provenance.clone(),
1261 });
1262 }
1263 }
1264 }
1265 for reference in references
1266 .iter()
1267 .filter(|reference| reference.kind == SemanticReferenceKind::ValidationRuleField)
1268 {
1269 let Some(rule) = rules
1270 .keys()
1271 .find(|rule| rule.as_semantic_id() == &reference.source)
1272 else {
1273 continue;
1274 };
1275 let Some(field) = fields
1276 .keys()
1277 .find(|field| field.as_semantic_id() == &reference.target)
1278 else {
1279 continue;
1280 };
1281 edges.push(ValidationGraphEdge {
1282 kind: ValidationGraphEdgeKind::RuleDependsOnField,
1283 source: ValidationGraphNodeKey::ValidationRule(rule.clone()),
1284 target: ValidationGraphNodeKey::FormField(field.clone()),
1285 provenance: reference.provenance.clone(),
1286 });
1287 }
1288 edges.sort_by(|left, right| {
1289 (&left.source, left.kind, &left.target).cmp(&(&right.source, right.kind, &right.target))
1290 });
1291 let mut graph = ValidationGraph {
1292 id: ValidationGraphId::for_build_roots(build_roots.keys()),
1293 nodes,
1294 edges,
1295 cycles: cycles.to_vec(),
1296 validation: ValidationGraphValidation::default(),
1297 };
1298 graph.validation = validate_validation_graph(
1299 &graph,
1300 build_roots,
1301 form_ownership,
1302 forms,
1303 fields,
1304 rules,
1305 candidates,
1306 );
1307 graph
1308}
1309
1310#[allow(clippy::too_many_lines)]
1311#[must_use]
1312pub fn validate_validation_graph(
1313 graph: &ValidationGraph,
1314 build_roots: &BTreeMap<ComponentRootId, ComponentBuildRoot>,
1315 form_ownership: &FormOwnershipGraph,
1316 forms: &BTreeMap<FormId, FormEntity>,
1317 fields: &BTreeMap<FieldId, FormFieldEntity>,
1318 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
1319 candidates: &[ValidationRuleCandidate],
1320) -> ValidationGraphValidation {
1321 let mut diagnostics = Vec::new();
1322 if graph.id != ValidationGraphId::for_build_roots(build_roots.keys()) {
1323 push_integrity(
1324 &mut diagnostics,
1325 ValidationGraphIntegrityKind::GraphIdentityMismatch,
1326 "validation graph identity does not match canonical build roots",
1327 );
1328 }
1329 if graph.nodes.iter().any(|(key, node)| key != &node.key()) {
1330 push_integrity(
1331 &mut diagnostics,
1332 ValidationGraphIntegrityKind::DuplicateNode,
1333 "validation graph node key does not match its canonical node identity",
1334 );
1335 }
1336 for node in graph.nodes.values() {
1337 if provenance_is_missing(node.provenance()) {
1338 push_integrity(
1339 &mut diagnostics,
1340 ValidationGraphIntegrityKind::MissingProvenance,
1341 "validation graph node has no canonical provenance",
1342 );
1343 }
1344 if node.key().semantic_id().as_str().contains("form-instance:")
1345 || node
1346 .key()
1347 .semantic_id()
1348 .as_str()
1349 .contains("component-instance:")
1350 {
1351 push_integrity(
1352 &mut diagnostics,
1353 ValidationGraphIntegrityKind::InstanceIdentityInValidationGraph,
1354 "instance identity leaked into declaration validation graph",
1355 );
1356 }
1357 }
1358 if !graph.edges.windows(2).all(|pair| {
1359 (&pair[0].source, pair[0].kind, &pair[0].target)
1360 <= (&pair[1].source, pair[1].kind, &pair[1].target)
1361 }) {
1362 push_integrity(
1363 &mut diagnostics,
1364 ValidationGraphIntegrityKind::NonCanonicalOrdering,
1365 "validation graph edges are not canonically ordered",
1366 );
1367 }
1368 for edge in &graph.edges {
1369 if !graph.nodes.contains_key(&edge.source) || !graph.nodes.contains_key(&edge.target) {
1370 push_integrity(
1371 &mut diagnostics,
1372 ValidationGraphIntegrityKind::UnknownEdgeEndpoint,
1373 "validation graph edge has an unknown endpoint",
1374 );
1375 }
1376 if provenance_is_missing(&edge.provenance) {
1377 push_integrity(
1378 &mut diagnostics,
1379 ValidationGraphIntegrityKind::MissingProvenance,
1380 "validation graph edge has no canonical provenance",
1381 );
1382 }
1383 let shape_is_valid = matches!(
1384 (&edge.kind, &edge.source, &edge.target),
1385 (
1386 ValidationGraphEdgeKind::FormOwnsField,
1387 ValidationGraphNodeKey::Form(_),
1388 ValidationGraphNodeKey::FormField(_)
1389 ) | (
1390 ValidationGraphEdgeKind::FieldOwnsRule,
1391 ValidationGraphNodeKey::FormField(_),
1392 ValidationGraphNodeKey::ValidationRule(_)
1393 ) | (
1394 ValidationGraphEdgeKind::RuleDependsOnField,
1395 ValidationGraphNodeKey::ValidationRule(_),
1396 ValidationGraphNodeKey::FormField(_)
1397 )
1398 );
1399 if !shape_is_valid {
1400 push_integrity(
1401 &mut diagnostics,
1402 match edge.kind {
1403 ValidationGraphEdgeKind::FormOwnsField => {
1404 ValidationGraphIntegrityKind::FieldFormMismatch
1405 }
1406 ValidationGraphEdgeKind::FieldOwnsRule => {
1407 ValidationGraphIntegrityKind::RuleTargetMismatch
1408 }
1409 ValidationGraphEdgeKind::RuleDependsOnField => {
1410 ValidationGraphIntegrityKind::DependencyMismatch
1411 }
1412 },
1413 "validation graph edge kind has invalid endpoint domains",
1414 );
1415 }
1416 }
1417 if validation_ownership_has_cycle(graph) {
1418 push_integrity(
1419 &mut diagnostics,
1420 ValidationGraphIntegrityKind::OwnershipCycle,
1421 "validation graph ownership edges contain a cycle",
1422 );
1423 }
1424 for form in forms.keys() {
1425 if !graph
1426 .nodes
1427 .contains_key(&ValidationGraphNodeKey::Form(form.clone()))
1428 {
1429 push_integrity(
1430 &mut diagnostics,
1431 ValidationGraphIntegrityKind::MissingFormNode,
1432 "canonical form is missing from validation graph",
1433 );
1434 }
1435 }
1436 for field in fields.values() {
1437 if !graph
1438 .nodes
1439 .contains_key(&ValidationGraphNodeKey::FormField(field.id.clone()))
1440 {
1441 push_integrity(
1442 &mut diagnostics,
1443 ValidationGraphIntegrityKind::MissingFieldNode,
1444 "canonical form field is missing from validation graph",
1445 );
1446 }
1447 if form_ownership.owner_of(&FormOwnershipNodeKey::FormField(field.id.clone()))
1448 != Some(&FormOwnershipNodeKey::Form(field.owner_form.clone()))
1449 {
1450 push_integrity(
1451 &mut diagnostics,
1452 ValidationGraphIntegrityKind::FieldFormMismatch,
1453 "validation graph field ownership disagrees with I5",
1454 );
1455 }
1456 }
1457 for rule in rules.values() {
1458 if !graph
1459 .nodes
1460 .contains_key(&ValidationGraphNodeKey::ValidationRule(rule.id.clone()))
1461 {
1462 push_integrity(
1463 &mut diagnostics,
1464 ValidationGraphIntegrityKind::MissingRuleNode,
1465 "canonical validation rule is missing from validation graph",
1466 );
1467 }
1468 let owners = graph
1469 .edges
1470 .iter()
1471 .filter(|edge| {
1472 edge.kind == ValidationGraphEdgeKind::FieldOwnsRule
1473 && edge.target == ValidationGraphNodeKey::ValidationRule(rule.id.clone())
1474 })
1475 .collect::<Vec<_>>();
1476 if owners.len() != 1 {
1477 push_integrity(
1478 &mut diagnostics,
1479 ValidationGraphIntegrityKind::MultipleRuleOwners,
1480 "validation rule does not have exactly one field owner",
1481 );
1482 } else if owners[0].source != ValidationGraphNodeKey::FormField(rule.target_field.clone()) {
1483 push_integrity(
1484 &mut diagnostics,
1485 ValidationGraphIntegrityKind::RuleTargetMismatch,
1486 "validation rule owner does not match its target field",
1487 );
1488 }
1489 if let Some(dependency) = &rule.dependency {
1490 if dependency == &rule.target_field {
1491 push_integrity(
1492 &mut diagnostics,
1493 ValidationGraphIntegrityKind::SelfDependency,
1494 "validation rule depends on its own target",
1495 );
1496 }
1497 let Some(target) = fields.get(&rule.target_field) else {
1498 continue;
1499 };
1500 let Some(dependency_field) = fields.get(dependency) else {
1501 push_integrity(
1502 &mut diagnostics,
1503 ValidationGraphIntegrityKind::DependencyMismatch,
1504 "validation rule dependency is not a canonical field",
1505 );
1506 continue;
1507 };
1508 if target.owner_component != dependency_field.owner_component {
1509 push_integrity(
1510 &mut diagnostics,
1511 ValidationGraphIntegrityKind::CrossComponentDependency,
1512 "validation dependency crosses component ownership",
1513 );
1514 }
1515 if target.owner_form != dependency_field.owner_form {
1516 push_integrity(
1517 &mut diagnostics,
1518 ValidationGraphIntegrityKind::CrossFormDependency,
1519 "validation dependency crosses form ownership",
1520 );
1521 }
1522 }
1523 let dependency_edges = graph
1524 .edges
1525 .iter()
1526 .filter(|edge| {
1527 edge.kind == ValidationGraphEdgeKind::RuleDependsOnField
1528 && edge.source == ValidationGraphNodeKey::ValidationRule(rule.id.clone())
1529 })
1530 .collect::<Vec<_>>();
1531 match &rule.dependency {
1532 None if !dependency_edges.is_empty() => push_integrity(
1533 &mut diagnostics,
1534 ValidationGraphIntegrityKind::DependencyMismatch,
1535 "unary validation rule has dependency edges",
1536 ),
1537 Some(dependency)
1538 if dependency_edges.len() != 1
1539 || dependency_edges[0].target
1540 != ValidationGraphNodeKey::FormField(dependency.clone()) =>
1541 {
1542 push_integrity(
1543 &mut diagnostics,
1544 ValidationGraphIntegrityKind::DependencyMismatch,
1545 "validation rule dependency edge disagrees with canonical rule",
1546 );
1547 }
1548 _ => {}
1549 }
1550 }
1551 let invalid_candidate_ids = candidates
1552 .iter()
1553 .filter(|candidate| !candidate.is_valid())
1554 .map(|candidate| candidate.id.as_str())
1555 .collect::<BTreeSet<_>>();
1556 if graph
1557 .nodes
1558 .keys()
1559 .any(|key| invalid_candidate_ids.contains(key.semantic_id().as_str()))
1560 {
1561 push_integrity(
1562 &mut diagnostics,
1563 ValidationGraphIntegrityKind::InvalidCandidatePromoted,
1564 "invalid validation candidate was promoted into the valid graph",
1565 );
1566 }
1567 let cycle_candidates = graph
1568 .cycles
1569 .iter()
1570 .flat_map(|cycle| cycle.candidates.iter())
1571 .collect::<BTreeSet<_>>();
1572 if rules
1573 .values()
1574 .any(|rule| cycle_candidates.contains(&rule.candidate_id))
1575 {
1576 push_integrity(
1577 &mut diagnostics,
1578 ValidationGraphIntegrityKind::DependencyCycleLeakage,
1579 "cycle-participating rule leaked into executable graph membership",
1580 );
1581 }
1582 let executable_adjacency = rules.values().fold(
1583 BTreeMap::<FieldId, BTreeSet<FieldId>>::new(),
1584 |mut adjacency, rule| {
1585 if let Some(dependency) = &rule.dependency {
1586 adjacency
1587 .entry(rule.target_field.clone())
1588 .or_default()
1589 .insert(dependency.clone());
1590 adjacency.entry(dependency.clone()).or_default();
1591 }
1592 adjacency
1593 },
1594 );
1595 if executable_adjacency.keys().any(|field| {
1596 reachable_fields(field, &executable_adjacency)
1597 .into_iter()
1598 .any(|other| {
1599 other != *field && reachable_fields(&other, &executable_adjacency).contains(field)
1600 })
1601 }) {
1602 push_integrity(
1603 &mut diagnostics,
1604 ValidationGraphIntegrityKind::DependencyCycleLeakage,
1605 "executable validation rules contain a dependency cycle",
1606 );
1607 }
1608 if !graph.cycles.windows(2).all(|pair| pair[0].id <= pair[1].id)
1609 || graph.cycles.iter().any(|cycle| {
1610 !cycle.fields.windows(2).all(|pair| pair[0] < pair[1])
1611 || !cycle.candidates.windows(2).all(|pair| pair[0] < pair[1])
1612 })
1613 {
1614 push_integrity(
1615 &mut diagnostics,
1616 ValidationGraphIntegrityKind::NonCanonicalOrdering,
1617 "validation dependency cycles are not canonically ordered",
1618 );
1619 }
1620 diagnostics.sort_by(|left, right| {
1621 (left.code.as_str(), left.message.as_str())
1622 .cmp(&(right.code.as_str(), right.message.as_str()))
1623 });
1624 diagnostics.dedup();
1625 ValidationGraphValidation {
1626 is_valid: diagnostics.is_empty(),
1627 diagnostics,
1628 }
1629}
1630
1631fn provenance_is_missing(provenance: &SourceProvenance) -> bool {
1632 provenance.path.as_os_str().is_empty() || provenance.span.end <= provenance.span.start
1633}
1634
1635fn validation_ownership_has_cycle(graph: &ValidationGraph) -> bool {
1636 let adjacency = graph
1637 .edges
1638 .iter()
1639 .filter(|edge| edge.kind != ValidationGraphEdgeKind::RuleDependsOnField)
1640 .fold(
1641 BTreeMap::<ValidationGraphNodeKey, BTreeSet<ValidationGraphNodeKey>>::new(),
1642 |mut adjacency, edge| {
1643 adjacency
1644 .entry(edge.source.clone())
1645 .or_default()
1646 .insert(edge.target.clone());
1647 adjacency.entry(edge.target.clone()).or_default();
1648 adjacency
1649 },
1650 );
1651 adjacency.keys().any(|start| {
1652 adjacency
1653 .get(start)
1654 .into_iter()
1655 .flatten()
1656 .any(|next| validation_graph_reaches(next, start, &adjacency))
1657 })
1658}
1659
1660fn validation_graph_reaches(
1661 start: &ValidationGraphNodeKey,
1662 target: &ValidationGraphNodeKey,
1663 adjacency: &BTreeMap<ValidationGraphNodeKey, BTreeSet<ValidationGraphNodeKey>>,
1664) -> bool {
1665 let mut visited = BTreeSet::new();
1666 let mut pending = vec![start.clone()];
1667 while let Some(node) = pending.pop() {
1668 if &node == target {
1669 return true;
1670 }
1671 if !visited.insert(node.clone()) {
1672 continue;
1673 }
1674 if let Some(next) = adjacency.get(&node) {
1675 pending.extend(next.iter().rev().cloned());
1676 }
1677 }
1678 false
1679}
1680
1681fn push_integrity(
1682 diagnostics: &mut Vec<ValidationGraphIntegrityDiagnostic>,
1683 kind: ValidationGraphIntegrityKind,
1684 message: &str,
1685) {
1686 diagnostics.push(ValidationGraphIntegrityDiagnostic {
1687 code: kind.code().to_string(),
1688 kind,
1689 message: message.to_string(),
1690 });
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695 use super::{
1696 validate_validation_graph, ValidationGraphEdgeKind, ValidationGraphIntegrityKind,
1697 ValidationGraphNodeKey, ValidationRuleArgument, ValidationRuleKind,
1698 ValidationRuleViolation,
1699 };
1700 use crate::{
1701 build_application_semantic_model, build_application_semantic_model_for_unit,
1702 build_semantic_graph, semantic_graph_json, validate_application_semantic_model,
1703 CompilationUnit, ExecutionBoundary, SemanticEntityKind, SemanticOwner,
1704 SemanticReferenceKind, SEMANTIC_GRAPH_SCHEMA_VERSION,
1705 };
1706
1707 fn build(source: &str) -> crate::ApplicationSemanticModel {
1708 build_application_semantic_model(&presolve_parser::parse_file("src/Profile.tsx", source))
1709 }
1710
1711 #[test]
1712 fn lowers_unary_and_cross_field_rules_with_canonical_identity_and_ownership() {
1713 let source = r#"
1714@component("profile")
1715class Profile {
1716 @form()
1717 profile!: Form;
1718
1719 @validate(required())
1720 @validate(minLength(2))
1721 @validate(pattern("^[a-z]+$"))
1722 @field(this.profile)
1723 email: string = "";
1724
1725 @validate(equals(this.email))
1726 @field(this.profile)
1727 confirmation: string = "";
1728
1729 render() { return <input field={this.email} />; }
1730}
1731"#;
1732 let asm = build(source);
1733 assert_eq!(asm.validation_rule_candidates.len(), 4);
1734 assert!(
1735 asm.validation_rule_candidates
1736 .iter()
1737 .all(|candidate| candidate.is_valid() && candidate.rule_id.is_some()),
1738 "{:#?}",
1739 asm.validation_rule_candidates
1740 );
1741 let rules = asm.validation_rules();
1742 assert_eq!(rules.len(), 4);
1743 assert_eq!(rules[0].kind, ValidationRuleKind::Required);
1744 assert_eq!(rules[0].rule_authored_order, 0);
1745 assert_eq!(rules[1].kind, ValidationRuleKind::MinLength);
1746 assert_eq!(rules[1].argument, ValidationRuleArgument::Length(2));
1747 assert_eq!(rules[2].kind, ValidationRuleKind::Pattern);
1748 assert_eq!(rules[3].kind, ValidationRuleKind::Equals);
1749 assert_eq!(rules[3].dependency.as_ref(), Some(&rules[0].target_field));
1750 assert!(rules
1751 .iter()
1752 .all(|rule| rule.boundary == ExecutionBoundary::Client));
1753 assert!(rules.iter().all(|rule| {
1754 asm.owner(rule.id.as_semantic_id())
1755 == Some(&SemanticOwner::entity(
1756 rule.target_field.as_semantic_id().clone(),
1757 ))
1758 && asm
1759 .entity(rule.id.as_semantic_id())
1760 .is_some_and(|entity| entity.kind() == SemanticEntityKind::ValidationRule)
1761 }));
1762 assert_eq!(
1763 asm.references_of_kind(SemanticReferenceKind::ValidationRuleField)
1764 .len(),
1765 1
1766 );
1767 assert!(asm.validation_graph.validation.is_valid);
1768 assert_eq!(
1769 asm.validation_graph
1770 .edges
1771 .iter()
1772 .filter(|edge| edge.kind == ValidationGraphEdgeKind::FieldOwnsRule)
1773 .count(),
1774 4
1775 );
1776 assert!(validate_application_semantic_model(&asm).is_empty());
1777 }
1778
1779 #[test]
1780 fn invalidates_duplicate_contradictory_and_cycle_groups_without_winners() {
1781 let source = r#"
1782@component("profile")
1783class Profile {
1784 @form() profile!: Form;
1785
1786 @validate(required())
1787 @validate(required())
1788 @field(this.profile)
1789 duplicate = "";
1790
1791 @validate(min(10))
1792 @validate(max(5))
1793 @field(this.profile)
1794 age = 20;
1795
1796 @validate(equals(this.right))
1797 @field(this.profile)
1798 left = "";
1799
1800 @validate(equals(this.left))
1801 @field(this.profile)
1802 right = "";
1803
1804 render() { return <div />; }
1805}
1806"#;
1807 let asm = build(source);
1808 assert_eq!(asm.validation_rule_candidates.len(), 6);
1809 let duplicate = asm
1810 .validation_rule_candidates
1811 .iter()
1812 .filter(|candidate| candidate.authored_target_name.as_deref() == Some("duplicate"))
1813 .collect::<Vec<_>>();
1814 assert_eq!(duplicate.len(), 2);
1815 assert!(duplicate.iter().all(|candidate| {
1816 candidate.rule_id.is_none()
1817 && candidate
1818 .violations
1819 .contains(&ValidationRuleViolation::DuplicateRule)
1820 }));
1821 assert!(asm
1822 .validation_rule_candidates
1823 .iter()
1824 .filter(|candidate| candidate.authored_target_name.as_deref() == Some("age"))
1825 .all(|candidate| candidate
1826 .violations
1827 .contains(&ValidationRuleViolation::ContradictoryRule)));
1828 assert_eq!(asm.validation_graph.cycles.len(), 1);
1829 assert_eq!(asm.validation_graph.cycles[0].fields.len(), 2);
1830 assert!(asm
1831 .validation_rule_candidates
1832 .iter()
1833 .filter(|candidate| matches!(
1834 candidate.authored_target_name.as_deref(),
1835 Some("left" | "right")
1836 ))
1837 .all(|candidate| candidate
1838 .violations
1839 .contains(&ValidationRuleViolation::DependencyCycle)));
1840 assert!(asm.validation_rules.is_empty());
1841 assert!(asm.validation_graph.validation.is_valid);
1842 }
1843
1844 #[test]
1845 fn retains_invalid_targets_rules_arguments_dependencies_and_type_evidence() {
1846 let source = r#"
1847@validate(required())
1848@component("profile")
1849class Profile {
1850 @form() profile!: Form;
1851
1852 @validate
1853 @field(this.profile)
1854 uninvoked = "";
1855
1856 @validate(schema.required())
1857 @field(this.profile)
1858 memberCall = "";
1859
1860 @validate(min("one"))
1861 @field(this.profile)
1862 wrongArgument = 1;
1863
1864 @validate(email())
1865 @field(this.profile)
1866 wrongType = 1;
1867
1868 @validate(equals(this.missing))
1869 @field(this.profile)
1870 unresolved = "";
1871
1872 @validate(required())
1873 method() {}
1874
1875 render() { return <div />; }
1876}
1877"#;
1878 let asm = build(source);
1879 assert_eq!(asm.validation_rule_candidates.len(), 7);
1880 assert!(asm
1881 .validation_rule_candidates
1882 .iter()
1883 .all(|candidate| { candidate.rule_id.is_none() && !candidate.violations.is_empty() }));
1884 assert!(asm
1885 .validation_rule_candidates
1886 .iter()
1887 .any(|candidate| candidate
1888 .violations
1889 .contains(&ValidationRuleViolation::InvalidDecoratorInvocation)));
1890 assert!(asm
1891 .validation_rule_candidates
1892 .iter()
1893 .any(|candidate| candidate
1894 .violations
1895 .contains(&ValidationRuleViolation::InvalidRuleExpression)));
1896 assert!(asm
1897 .validation_rule_candidates
1898 .iter()
1899 .any(|candidate| candidate
1900 .violations
1901 .contains(&ValidationRuleViolation::UnsupportedArgument)));
1902 assert!(asm
1903 .validation_rule_candidates
1904 .iter()
1905 .any(|candidate| candidate
1906 .violations
1907 .contains(&ValidationRuleViolation::IncompatibleType)));
1908 assert!(asm
1909 .validation_rule_candidates
1910 .iter()
1911 .any(|candidate| candidate
1912 .violations
1913 .contains(&ValidationRuleViolation::UnresolvedDependency)));
1914 assert!(asm
1915 .validation_rule_candidates
1916 .iter()
1917 .any(|candidate| candidate.violations.contains(
1918 &ValidationRuleViolation::InvalidTarget {
1919 actual: crate::AuthoredDeclarationKind::Method,
1920 }
1921 )));
1922 }
1923
1924 #[test]
1925 fn graph_validation_is_deterministic_and_detects_stale_integrity() {
1926 let source = r#"
1927@component("profile")
1928class Profile {
1929 @form() profile!: Form;
1930 @validate(required())
1931 @field(this.profile)
1932 name = "";
1933 render() { return <div />; }
1934}
1935"#;
1936 let asm = build(source);
1937 let mut malformed = asm.validation_graph.clone();
1938 let rule = asm.validation_rules.values().next().unwrap();
1939 malformed
1940 .nodes
1941 .remove(&ValidationGraphNodeKey::ValidationRule(rule.id.clone()));
1942 let validation = validate_validation_graph(
1943 &malformed,
1944 &asm.component_instance_plan.roots,
1945 &asm.form_ownership,
1946 &asm.forms,
1947 &asm.form_fields,
1948 &asm.validation_rules,
1949 &asm.validation_rule_candidates,
1950 );
1951 assert!(!validation.is_valid);
1952 assert!(validation.diagnostics.iter().any(|diagnostic| {
1953 diagnostic.kind == ValidationGraphIntegrityKind::MissingRuleNode
1954 }));
1955 assert_eq!(
1956 validation,
1957 validate_validation_graph(
1958 &malformed,
1959 &asm.component_instance_plan.roots,
1960 &asm.form_ownership,
1961 &asm.forms,
1962 &asm.form_fields,
1963 &asm.validation_rules,
1964 &asm.validation_rule_candidates,
1965 )
1966 );
1967
1968 let mut cyclic = asm.validation_graph.clone();
1969 let ownership = cyclic
1970 .edges
1971 .iter()
1972 .find(|edge| edge.kind == ValidationGraphEdgeKind::FieldOwnsRule)
1973 .unwrap()
1974 .clone();
1975 cyclic.edges.push(super::ValidationGraphEdge {
1976 kind: ValidationGraphEdgeKind::FieldOwnsRule,
1977 source: ownership.target,
1978 target: ownership.source,
1979 provenance: ownership.provenance,
1980 });
1981 cyclic.edges.sort_by(|left, right| {
1982 (&left.source, left.kind, &left.target).cmp(&(&right.source, right.kind, &right.target))
1983 });
1984 let cyclic_validation = validate_validation_graph(
1985 &cyclic,
1986 &asm.component_instance_plan.roots,
1987 &asm.form_ownership,
1988 &asm.forms,
1989 &asm.form_fields,
1990 &asm.validation_rules,
1991 &asm.validation_rule_candidates,
1992 );
1993 assert!(cyclic_validation
1994 .diagnostics
1995 .iter()
1996 .any(|diagnostic| diagnostic.kind == ValidationGraphIntegrityKind::OwnershipCycle));
1997 }
1998
1999 #[test]
2000 fn rejects_authored_functions_and_imports_shadowing_compiler_rule_names() {
2001 let local = build(
2002 r#"
2003function required() { return true; }
2004@component("profile")
2005class Profile {
2006 @form() profile!: Form;
2007 @validate(required())
2008 @field(this.profile)
2009 name = "";
2010 render() { return <div />; }
2011}
2012"#,
2013 );
2014 assert!(local.validation_rule_candidates[0]
2015 .violations
2016 .contains(&ValidationRuleViolation::ShadowedCompilerRule));
2017 assert!(local.validation_rules.is_empty());
2018
2019 let imported = build(
2020 r#"
2021import { authored as email } from "./rules";
2022@component("profile")
2023class Profile {
2024 @form() profile!: Form;
2025 @validate(email())
2026 @field(this.profile)
2027 address = "";
2028 render() { return <div />; }
2029}
2030"#,
2031 );
2032 assert!(imported.validation_rule_candidates[0]
2033 .violations
2034 .contains(&ValidationRuleViolation::ShadowedCompilerRule));
2035 assert!(imported.validation_rules.is_empty());
2036 }
2037
2038 #[test]
2039 fn applies_canonical_type_domains_and_exact_same_form_dependency_scope() {
2040 let source = r#"
2041@component("profile")
2042class Profile {
2043 @form() primary!: Form;
2044 @form() secondary!: Form;
2045
2046 @validate(min(0))
2047 @field(this.primary)
2048 amount: number | null = null;
2049
2050 @validate(minLength(1))
2051 @field(this.primary)
2052 tags: string[] = [];
2053
2054 @validate(maxLength(2))
2055 @field(this.primary)
2056 pair: [string, string] = ["", ""];
2057
2058 @validate(email())
2059 @field(this.primary)
2060 address: string | null = null;
2061
2062 @validate(equals(this.foreign))
2063 @field(this.primary)
2064 local = "";
2065
2066 @field(this.secondary)
2067 foreign = "";
2068
2069 @validate(equals(this.selfReference))
2070 @field(this.primary)
2071 selfReference = "";
2072
2073 @validate(min(1))
2074 @field(this.primary)
2075 wrongDomain = "";
2076
2077 render() { return <div />; }
2078}
2079"#;
2080 let asm = build(source);
2081 assert_eq!(asm.validation_rules.len(), 4);
2082 assert!(asm
2083 .validation_rule_candidates
2084 .iter()
2085 .any(|candidate| candidate
2086 .violations
2087 .contains(&ValidationRuleViolation::CrossFormDependency)));
2088 assert!(asm
2089 .validation_rule_candidates
2090 .iter()
2091 .any(|candidate| candidate
2092 .violations
2093 .contains(&ValidationRuleViolation::SelfDependency)));
2094 assert!(asm
2095 .validation_rule_candidates
2096 .iter()
2097 .any(|candidate| candidate
2098 .violations
2099 .contains(&ValidationRuleViolation::IncompatibleType)));
2100 }
2101
2102 #[test]
2103 fn reversed_files_preserve_validation_products_and_public_schema() {
2104 let first = presolve_parser::parse_file(
2105 "src/A.tsx",
2106 r#"@component("a-x") class A { @form() form!: Form; @validate(required()) @field(this.form) value = ""; render() { return <div />; } }"#,
2107 );
2108 let second = presolve_parser::parse_file(
2109 "src/B.tsx",
2110 r#"@component("b-x") class B { @form() form!: Form; @validate(min(1 + 1)) @field(this.form) value = 2; render() { return <div />; } }"#,
2111 );
2112 let forward =
2113 build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
2114 first.clone(),
2115 second.clone(),
2116 ]));
2117 let reversed =
2118 build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
2119 second, first,
2120 ]));
2121 assert_eq!(
2122 forward.validation_rule_candidates,
2123 reversed.validation_rule_candidates
2124 );
2125 assert_eq!(forward.validation_rules, reversed.validation_rules);
2126 assert_eq!(forward.validation_graph, reversed.validation_graph);
2127 assert_eq!(SEMANTIC_GRAPH_SCHEMA_VERSION, 6);
2128 let json = semantic_graph_json(&build_semantic_graph(&forward));
2129 assert!(json.contains("validation-rule"));
2130 }
2131}