ifc_validate/structure/reference.rs
1//! References that point at nothing, or at the wrong kind of thing.
2
3use ifc_model::{Model, Value};
4use ifc_schema::Schema;
5
6use crate::report::{Finding, Path, Report};
7
8/// Reports every reference whose target the model does not contain.
9///
10/// A dangling reference is unambiguously a defect: Part 21 `#42` names an
11/// instance in the same exchange structure, so a missing target means the file
12/// was truncated, badly merged, or written by a tool that dropped an entity it
13/// still pointed at.
14///
15/// Traversal is by ascending entity id and, within an entity, by ascending
16/// slot, so findings arrive in a stable order.
17pub fn dangling_references(model: &Model, report: &mut Report) {
18 let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
19 ids.sort_unstable();
20 for id in ids {
21 let Some(entity) = model.get(id) else {
22 continue;
23 };
24 for (index, value) in entity.attributes.iter().enumerate() {
25 let mut missing = Vec::new();
26 value.for_each_ref(&mut |target| {
27 if model.get(target).is_none() {
28 missing.push(target);
29 }
30 });
31 missing.sort_unstable();
32 missing.dedup();
33 for target in missing {
34 report.push(Finding::error(
35 "structure.reference.dangling",
36 Path::Attribute {
37 entity: id,
38 index,
39 name: None,
40 },
41 format!("references {target}, which the file does not contain"),
42 ));
43 }
44 }
45 }
46}
47
48/// Reports references whose target is not of the declared entity type.
49///
50/// Only checked where the schema declares an entity-typed slot: the parser
51/// records the declared type token, and if that token names an entity, the
52/// target must be that entity or a subtype of it. Slots declared as SELECTs or
53/// defined types are left to [`crate::type_check`], which understands them.
54pub fn wrong_kind_references(model: &Model, schema: &Schema, report: &mut Report) {
55 let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
56 ids.sort_unstable();
57 for id in ids {
58 let Some(entity) = model.get(id) else {
59 continue;
60 };
61 let declared = schema.attributes(&entity.type_name);
62 for (index, value) in entity.attributes.iter().enumerate() {
63 let Some(attribute) = declared.get(index) else {
64 continue;
65 };
66 // Only entity-typed slots are checkable here. A SELECT names a
67 // type declaration, not an entity, and is handled elsewhere.
68 if schema.entity(&attribute.type_name).is_none() {
69 continue;
70 }
71 let Value::Ref(target) = value.unwrap_typed() else {
72 continue;
73 };
74 let Some(target_entity) = model.get(*target) else {
75 continue; // already reported as dangling
76 };
77 if !schema.is_a(&target_entity.type_name, &attribute.type_name) {
78 report.push(Finding::error(
79 "structure.reference.wrong_type",
80 Path::Attribute {
81 entity: id,
82 index,
83 name: Some(attribute.name.clone()),
84 },
85 format!(
86 "declared {} but {target} is {}",
87 attribute.type_name, target_entity.type_name
88 ),
89 ));
90 }
91 }
92 }
93}