1use crate::catalog::{Catalog, Kind};
8use crate::core::source::Span;
9use crate::settings::SettingsNode;
10use crate::settings::table;
11use crate::wir::{Action, Program, Value};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum IncompletenessKind {
15 RawSetting,
16 UnknownAction,
17 UnknownValue,
18 OpaqueAction,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ResidualClassification {
23 ProjectDefinedConstruct,
24 SourceDeclaredVariable,
25 ProducerExtension,
26 LegacyOpaque,
27 UnresolvedIdentifier,
28}
29
30impl ResidualClassification {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::ProjectDefinedConstruct => "project-defined-construct",
34 Self::SourceDeclaredVariable => "source-declared-variable",
35 Self::ProducerExtension => "producer-extension",
36 Self::LegacyOpaque => "legacy-opaque-construct",
37 Self::UnresolvedIdentifier => "truly-unresolved-identifier",
38 }
39 }
40
41 pub fn evidence(self) -> &'static str {
42 match self {
43 Self::ProjectDefinedConstruct => {
44 "source settings or construct was preserved without a canonical catalog identity"
45 }
46 Self::SourceDeclaredVariable => {
47 "the identifier matches a variable declaration in the parsed source program"
48 }
49 Self::ProducerExtension => {
50 "the source uses an action-shaped identity outside the canonical catalog and no declaration resolves it"
51 }
52 Self::LegacyOpaque => {
53 "the parser preserved a legacy raw construct without a canonical contract"
54 }
55 Self::UnresolvedIdentifier => {
56 "the identifier matches neither a source declaration nor a canonical catalog identity"
57 }
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63#[non_exhaustive]
64pub struct SemanticIssue {
65 pub kind: IncompletenessKind,
66 pub name: String,
67 pub span: Option<Span>,
68 pub classification: ResidualClassification,
69}
70
71pub fn inspect(program: &crate::Program, catalog: &Catalog) -> Vec<SemanticIssue> {
74 let Ok(storage) = program.to_wir() else {
75 return Vec::new();
76 };
77 inspect_wir(&storage, catalog)
78}
79
80#[doc(hidden)]
81pub(crate) fn inspect_wir(program: &Program, catalog: &Catalog) -> Vec<SemanticIssue> {
82 let mut issues = Vec::new();
83 if let Some(settings) = &program.settings {
84 for node in &settings.children {
85 inspect_setting(node, &mut issues);
86 }
87 }
88 for action in program.actions.iter() {
89 inspect_action(action, program, catalog, &mut issues);
90 }
91 for value in program.values.iter() {
92 inspect_value(value, program, catalog, &mut issues);
93 }
94 issues
95}
96
97fn inspect_setting(node: &SettingsNode, issues: &mut Vec<SemanticIssue>) {
98 match node {
99 SettingsNode::Workshop { .. } => {}
100 SettingsNode::Group { children, .. } => {
101 for child in children {
102 inspect_setting(child, issues);
103 }
104 }
105 SettingsNode::Raw { name, span, .. } => issues.push(SemanticIssue {
106 kind: IncompletenessKind::RawSetting,
107 name: name.clone(),
108 span: *span,
109 classification: ResidualClassification::ProjectDefinedConstruct,
110 }),
111 SettingsNode::List { name, elements, .. } => {
112 let element_known: fn(&crate::settings::SettingsListElement) -> bool =
113 match name.as_str() {
114 "enabledMaps" | "disabledMaps" => {
115 |element: &crate::settings::SettingsListElement| {
116 table::map_name(&element.value).is_some()
117 }
118 }
119 "enabledHeroes" | "disabledHeroes" => {
120 |element: &crate::settings::SettingsListElement| {
121 table::hero_name(&element.value).is_some()
122 }
123 }
124 _ => |_| true,
125 };
126 for element in elements {
127 if !element_known(element) {
128 issues.push(SemanticIssue {
129 kind: IncompletenessKind::RawSetting,
130 name: element.value.clone(),
131 span: element.span,
132 classification: ResidualClassification::ProjectDefinedConstruct,
133 });
134 }
135 }
136 }
137 SettingsNode::Number { .. }
138 | SettingsNode::Bool { .. }
139 | SettingsNode::Flag { .. }
140 | SettingsNode::String { .. } => {}
141 }
142}
143
144fn inspect_action(
145 action: &Action,
146 program: &Program,
147 catalog: &Catalog,
148 issues: &mut Vec<SemanticIssue>,
149) {
150 match action {
151 Action::Call { name, span, .. } => {
152 let kind = if name == "rawWorkshopAction" {
153 Some(IncompletenessKind::OpaqueAction)
154 } else if catalog.entry(Kind::Action, name).is_none() {
155 Some(IncompletenessKind::UnknownAction)
156 } else {
157 None
158 };
159 if let Some(kind) = kind {
160 let classification = if kind == IncompletenessKind::OpaqueAction {
161 ResidualClassification::LegacyOpaque
162 } else {
163 ResidualClassification::ProducerExtension
164 };
165 issues.push(SemanticIssue {
166 kind,
167 name: name.clone(),
168 span: *span,
169 classification,
170 });
171 }
172 }
173 Action::If {
174 branches,
175 else_body,
176 ..
177 } => {
178 for branch in branches {
179 inspect_action_id(branch.body.as_slice(), program, catalog, issues);
180 }
181 if let Some(body) = else_body {
182 inspect_action_id(body.as_slice(), program, catalog, issues);
183 }
184 }
185 Action::While { body, .. }
186 | Action::ForGlobalVariable { body, .. }
187 | Action::ForPlayerVariable { body, .. } => {
188 inspect_action_id(body.as_slice(), program, catalog, issues);
189 }
190 Action::Disabled { action, .. } => {
191 inspect_action_id(std::slice::from_ref(action), program, catalog, issues);
192 }
193 Action::SetGlobalVariable { .. }
194 | Action::ModifyGlobalVariable { .. }
195 | Action::SetPlayerVariable { .. }
196 | Action::ModifyPlayerVariable { .. }
197 | Action::AssignMember { .. }
198 | Action::CallSubroutine { .. } => {}
199 }
200}
201
202fn inspect_action_id(
203 ids: &[crate::wir::ActionId],
204 program: &Program,
205 catalog: &Catalog,
206 issues: &mut Vec<SemanticIssue>,
207) {
208 for id in ids {
209 if let Some(action) = program.actions.get(*id) {
210 inspect_action(action, program, catalog, issues);
211 }
212 }
213}
214
215fn inspect_value(
216 node: &crate::wir::ValueNode,
217 program: &Program,
218 catalog: &Catalog,
219 issues: &mut Vec<SemanticIssue>,
220) {
221 if let Value::Call { name, args } = &node.value {
222 let canonical_helper = name == crate::wir::AMBIGUOUS_ENUM_CALL
226 || crate::wir::is_canonical_helper_call(name, args.len());
227 if canonical_helper {
228 return;
229 }
230 if catalog.entry(Kind::Value, name).is_none()
231 && catalog.entry(Kind::Operator, name).is_none()
232 {
233 issues.push(SemanticIssue {
234 kind: IncompletenessKind::UnknownValue,
235 name: name.clone(),
236 span: node.span,
237 classification: if program
238 .global_variables
239 .iter()
240 .any(|variable| variable.name == *name)
241 || program
242 .player_variables
243 .iter()
244 .any(|variable| variable.name == *name)
245 {
246 ResidualClassification::SourceDeclaredVariable
247 } else {
248 ResidualClassification::UnresolvedIdentifier
249 },
250 });
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::settings::{Settings, SettingsNode};
259
260 #[test]
261 fn reports_preserved_and_unknown_nodes() {
262 let catalog = Catalog::builtin().expect("builtin catalog");
263 let mut program = Program {
264 settings: Some(Settings {
265 span: None,
266 children: vec![SettingsNode::Raw {
267 name: "Future Setting".to_string(),
268 value: "opaque".to_string(),
269 span: None,
270 }],
271 }),
272 ..Program::default()
273 };
274 program.actions.push(Action::Call {
275 name: "rawWorkshopAction".to_string(),
276 args: Vec::new(),
277 span: None,
278 });
279 program.actions.push(Action::Call {
280 name: "futureAction".to_string(),
281 args: Vec::new(),
282 span: None,
283 });
284 program.values.push(crate::wir::ValueNode::new(
285 Value::Call {
286 name: "futureValue".to_string(),
287 args: Vec::new(),
288 },
289 None,
290 ));
291
292 let issues = inspect_wir(&program, &catalog);
293 assert!(
294 issues
295 .iter()
296 .any(|issue| issue.kind == IncompletenessKind::RawSetting)
297 );
298 assert!(
299 issues
300 .iter()
301 .any(|issue| issue.kind == IncompletenessKind::OpaqueAction)
302 );
303 assert!(
304 issues
305 .iter()
306 .any(|issue| issue.kind == IncompletenessKind::UnknownAction)
307 );
308 assert!(
309 issues
310 .iter()
311 .any(|issue| issue.kind == IncompletenessKind::UnknownValue)
312 );
313 assert!(issues.iter().any(|issue| {
314 issue.kind == IncompletenessKind::RawSetting
315 && issue.classification == ResidualClassification::ProjectDefinedConstruct
316 }));
317 assert!(issues.iter().any(|issue| {
318 issue.kind == IncompletenessKind::OpaqueAction
319 && issue.classification == ResidualClassification::LegacyOpaque
320 }));
321 assert!(issues.iter().any(|issue| {
322 issue.kind == IncompletenessKind::UnknownValue
323 && issue.classification == ResidualClassification::UnresolvedIdentifier
324 }));
325 }
326}