1use ifc_model::{Model, Value};
4use ifc_schema::Schema;
5
6use super::defined::{check, Mismatch};
7use super::select::admits_entity;
8use crate::report::{Finding, Path, Report};
9use crate::structure::expected_references;
10
11pub fn unknown_entity_types(model: &Model, schema: &Schema, report: &mut Report) {
17 let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
18 ids.sort_unstable();
19 for id in ids {
20 let Some(entity) = model.get(id) else {
21 continue;
22 };
23 if schema.entity(&entity.type_name).is_none() {
24 report.push(Finding::warning(
25 "type.entity.unknown",
26 Path::Entity(id),
27 format!(
28 "{} is not declared by schema {}",
29 entity.type_name,
30 schema.name()
31 ),
32 ));
33 }
34 }
35}
36
37pub fn abstract_instances(model: &Model, schema: &Schema, report: &mut Report) {
42 let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
43 ids.sort_unstable();
44 for id in ids {
45 let Some(entity) = model.get(id) else {
46 continue;
47 };
48 let Some(definition) = schema.entity(&entity.type_name) else {
49 continue;
50 };
51 if definition.abstract_ {
52 report.push(Finding::error(
53 "type.entity.abstract",
54 Path::Entity(id),
55 format!("{} is abstract and cannot be instantiated", definition.name),
56 ));
57 }
58 }
59}
60
61pub fn attribute_types(model: &Model, schema: &Schema, report: &mut Report) {
66 let mut ids: Vec<_> = model.iter().map(|(id, _)| id).collect();
67 ids.sort_unstable();
68 for id in ids {
69 let Some(entity) = model.get(id) else {
70 continue;
71 };
72 let declared = schema.attributes(&entity.type_name);
73 for (index, value) in entity.attributes.iter().enumerate() {
74 let Some(attribute) = declared.get(index) else {
75 continue;
76 };
77 let path = || Path::Attribute {
78 entity: id,
79 index,
80 name: Some(attribute.name.clone()),
81 };
82 select_references(model, schema, &attribute.type_name, value, &path, report);
83 let Some(mismatch) = check(schema, &attribute.type_name, value) else {
84 continue;
85 };
86 let path = path();
87 let finding = match mismatch {
88 Mismatch::Primitive { expected, actual } => Finding::error(
89 "type.scalar.mismatch",
90 path,
91 format!(
92 "{} is {expected}, the file wrote {actual}",
93 attribute.type_name
94 ),
95 ),
96 Mismatch::FixedWidth { expected, actual } => Finding::error(
97 "type.scalar.fixed_width",
98 path,
99 format!(
100 "{} is STRING({expected}) FIXED, the file wrote {actual} characters",
101 attribute.type_name
102 ),
103 ),
104 Mismatch::EnumMember { member, declared } => Finding::error(
105 "type.enumeration.member",
106 path,
107 format!(
108 "{member} is not a member of {} ({})",
109 attribute.type_name,
110 if declared.is_empty() {
111 "no members declared".to_string()
112 } else {
113 declared.join(", ")
114 }
115 ),
116 ),
117 Mismatch::SelectMember { written, select } => Finding::error(
118 "type.select.member",
119 path,
120 format!("{written} is not a member of {select}"),
121 ),
122 Mismatch::ExpectedReference { declared, actual } => Finding::error(
123 "type.entity.expected_reference",
124 path,
125 format!(
126 "{} takes a reference to {declared}, the file wrote {actual}",
127 attribute.name
128 ),
129 ),
130 };
131 report.push(finding);
132 }
133 }
134}
135
136fn select_references(
145 model: &Model,
146 schema: &Schema,
147 declared: &str,
148 value: &Value,
149 path: &dyn Fn() -> Path,
150 report: &mut Report,
151) {
152 let mut offenders = Vec::new();
153 for (expected, target) in expected_references(schema, declared, value) {
154 let Some(target_entity) = model.get(target) else {
155 continue;
156 };
157 if schema.entity(&target_entity.type_name).is_none() {
158 continue;
159 }
160 if admits_entity(schema, &expected, &target_entity.type_name) == Some(false) {
161 offenders.push((target, expected, &*target_entity.type_name));
162 }
163 }
164 offenders.sort_unstable();
165 offenders.dedup();
166 for (target, select, actual) in offenders {
167 report.push(Finding::error(
168 "type.select.member",
169 path(),
170 format!("{target} is {actual}, which is not a member of {select}"),
171 ));
172 }
173}