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