Skip to main content

presolve_compiler/
form_validation.rs

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