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::{EntityId, Model, Value};
4use ifc_schema::Schema;
5
6use crate::report::{Finding, Path, Report};
7use crate::type_check::element_type;
8
9/// Reports every reference whose target the model does not contain.
10///
11/// A dangling reference is unambiguously a defect: Part 21 `#42` names an
12/// instance in the same exchange structure, so a missing target means the file
13/// was truncated, badly merged, or written by a tool that dropped an entity it
14/// still pointed at.
15///
16/// Traversal is by ascending entity id and, within an entity, by ascending
17/// slot, so findings arrive in a stable order.
18pub fn dangling_references(model: &Model, report: &mut Report) {
19    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
20    ids.sort_unstable();
21    for id in ids {
22        let Some(entity) = model.get(id) else {
23            continue;
24        };
25        for (index, value) in entity.attributes.iter().enumerate() {
26            let mut missing = Vec::new();
27            value.for_each_ref(&mut |target| {
28                if model.get(target).is_none() {
29                    missing.push(target);
30                }
31            });
32            missing.sort_unstable();
33            missing.dedup();
34            for target in missing {
35                report.push(Finding::error(
36                    "structure.reference.dangling",
37                    Path::Attribute {
38                        entity: id,
39                        index,
40                        name: None,
41                    },
42                    format!("references {target}, which the file does not contain"),
43                ));
44            }
45        }
46    }
47}
48
49/// Reports references whose target is not of the declared entity type.
50///
51/// Checked wherever the schema expects an entity: an entity-typed slot, the
52/// members of an aggregate of entities (`SET OF IfcProduct`), and aggregates
53/// reached through a type that aliases one. The target must be that entity or
54/// a subtype of it. References in SELECT slots are left to
55/// [`crate::type_check`], which walks the SELECT's closure; a value that is
56/// not a reference at all is `type.entity.expected_reference`. A target whose
57/// type the tables do not declare is not judged: it is
58/// `type.entity.unknown`'s finding.
59///
60/// One finding per distinct offending target in a slot, in ascending order.
61pub fn wrong_kind_references(model: &Model, schema: &Schema, report: &mut Report) {
62    let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
63    ids.sort_unstable();
64    for id in ids {
65        let Some(entity) = model.get(id) else {
66            continue;
67        };
68        let declared = schema.attributes(&entity.type_name);
69        for (index, value) in entity.attributes.iter().enumerate() {
70            let Some(attribute) = declared.get(index) else {
71                continue;
72            };
73            let mut offenders: Vec<(EntityId, String, &str)> = Vec::new();
74            for (expected, target) in expected_references(schema, &attribute.type_name, value) {
75                // Only entity expectations are judged here; a SELECT names a
76                // type declaration, not an entity.
77                if schema.entity(&expected).is_none() {
78                    continue;
79                }
80                let Some(target_entity) = model.get(target) else {
81                    continue; // already reported as dangling
82                };
83                // A target of a type these tables do not declare -- usually a
84                // later release's entity -- gives no basis for a subtype
85                // verdict; `type.entity.unknown` already reports it.
86                if schema.entity(&target_entity.type_name).is_none() {
87                    continue;
88                }
89                if !schema.is_a(&target_entity.type_name, &expected) {
90                    offenders.push((target, expected, &target_entity.type_name));
91                }
92            }
93            offenders.sort_unstable();
94            offenders.dedup();
95            for (target, expected, actual) in offenders {
96                report.push(Finding::error(
97                    "structure.reference.wrong_type",
98                    Path::Attribute {
99                        entity: id,
100                        index,
101                        name: Some(attribute.name.clone()),
102                    },
103                    format!("declared {expected} but {target} is {actual}"),
104                ));
105            }
106        }
107    }
108}
109
110/// How deeply nested aggregates are searched for references.
111///
112/// Matches the value check's bound: IFC nests aggregates three deep at
113/// most, and the depth of a written value is under the file's control.
114const MAX_NESTING: usize = 8;
115
116/// Every reference in `value`, paired with the type its position expects.
117///
118/// A reference directly in the slot expects the slot's `declared` type; one
119/// inside an aggregate expects the aggregate's element type, and one inside
120/// a typed wrapper expects the wrapper's type. The expected type may be an
121/// entity, a SELECT, or a type that takes no references at all -- telling
122/// those apart is the caller's business.
123pub(crate) fn expected_references(
124    schema: &Schema,
125    declared: &str,
126    value: &Value,
127) -> Vec<(String, EntityId)> {
128    let mut found = Vec::new();
129    collect(schema, declared, value, 0, &mut found);
130    found
131}
132
133fn collect(
134    schema: &Schema,
135    declared: &str,
136    value: &Value,
137    depth: usize,
138    found: &mut Vec<(String, EntityId)>,
139) {
140    match value {
141        Value::Ref(target) => found.push((declared.to_string(), *target)),
142        Value::List(items) if depth < MAX_NESTING => {
143            let element = element_type(schema, declared);
144            for item in items {
145                collect(schema, &element, item, depth + 1, found);
146            }
147        }
148        Value::Typed { type_name, value } => collect(schema, type_name, value, depth, found),
149        _ => {}
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    /// A target of a type the tables do not declare is not accused of being
158    /// the wrong kind -- an IFC4X3 `IfcReferent` read against IFC4 tables --
159    /// while a declared wrong-kind target in the same aggregate still is.
160    #[test]
161    fn an_undeclared_target_type_is_not_judged() {
162        let schema = ifc_schema::ifc4();
163        let mut model = Model::new();
164        model.insert(
165            EntityId(1),
166            ifc_model::Entity::new("IFCREFERENT", Vec::new()),
167        );
168        model.insert(
169            EntityId(2),
170            ifc_model::Entity::new("IFCPROPERTYSET", Vec::new()),
171        );
172        let mut attributes =
173            vec![Value::Null; schema.attributes("IFCRELDEFINESBYPROPERTIES").len()];
174        attributes[4] = Value::List(vec![Value::Ref(EntityId(1)), Value::Ref(EntityId(2))]);
175        model.insert(
176            EntityId(3),
177            ifc_model::Entity::new("IFCRELDEFINESBYPROPERTIES", attributes),
178        );
179        let mut report = Report::new();
180        wrong_kind_references(&model, schema, &mut report);
181        let messages: Vec<&str> = report
182            .findings()
183            .iter()
184            .map(|finding| finding.message.as_str())
185            .collect();
186        assert_eq!(
187            messages,
188            ["declared IfcObjectDefinition but #2 is IFCPROPERTYSET"]
189        );
190    }
191
192    /// References in aggregates expect the element type, including one
193    /// reached through a type that aliases an aggregate.
194    #[test]
195    fn references_are_paired_with_the_type_their_position_expects() {
196        let schema = ifc_schema::ifc4();
197        let set = Value::List(vec![Value::Ref(EntityId(1)), Value::Ref(EntityId(2))]);
198        assert_eq!(
199            expected_references(schema, "IfcProduct", &set),
200            [
201                ("IfcProduct".to_string(), EntityId(1)),
202                ("IfcProduct".to_string(), EntityId(2))
203            ]
204        );
205        let typed = Value::Typed {
206            type_name: "IFCPROPERTYSETDEFINITIONSET".into(),
207            value: Box::new(Value::List(vec![Value::Ref(EntityId(3))])),
208        };
209        assert_eq!(
210            expected_references(schema, "IfcPropertySetDefinitionSelect", &typed),
211            [("IfcPropertySetDefinition".to_string(), EntityId(3))]
212        );
213    }
214}