Skip to main content

presolve_compiler/
form_validation.rs

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/// Lowers normalized parser facts through canonical I3 field products. No
175/// parser syntax or template/DOM state is consulted.
176///
177/// # Panics
178///
179/// Panics only when a candidate classified as valid is missing one of the
180/// canonical target, form, type, ordering, or provenance facts required by
181/// the I6 staged-lowering invariant.
182#[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}