Skip to main content

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}