1use crate::catalog::{Catalog, Kind};
12use crate::core::source::Span;
13use crate::program::{action_argument_values, value_children};
14use crate::settings::SettingsNode;
15use crate::settings::table;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum IncompletenessKind {
19 RawSetting,
20 UnknownAction,
21 UnknownValue,
22 OpaqueAction,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ResidualClassification {
27 ProjectDefinedConstruct,
28 SourceDeclaredVariable,
29 ProducerExtension,
30 LegacyOpaque,
31 UnresolvedIdentifier,
32}
33
34impl ResidualClassification {
35 pub fn as_str(self) -> &'static str {
36 match self {
37 Self::ProjectDefinedConstruct => "project-defined-construct",
38 Self::SourceDeclaredVariable => "source-declared-variable",
39 Self::ProducerExtension => "producer-extension",
40 Self::LegacyOpaque => "legacy-opaque-construct",
41 Self::UnresolvedIdentifier => "truly-unresolved-identifier",
42 }
43 }
44
45 pub fn evidence(self) -> &'static str {
46 match self {
47 Self::ProjectDefinedConstruct => {
48 "source settings or construct was preserved without a canonical catalog identity"
49 }
50 Self::SourceDeclaredVariable => {
51 "the identifier matches a variable declaration in the parsed source program"
52 }
53 Self::ProducerExtension => {
54 "the source uses an action-shaped identity outside the canonical catalog and no declaration resolves it"
55 }
56 Self::LegacyOpaque => {
57 "the parser preserved a legacy raw construct without a canonical contract"
58 }
59 Self::UnresolvedIdentifier => {
60 "the identifier matches neither a source declaration nor a canonical catalog identity"
61 }
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct SemanticIssue {
69 pub kind: IncompletenessKind,
70 pub name: String,
71 pub span: Option<Span>,
72 pub classification: ResidualClassification,
73}
74
75pub fn inspect(program: &crate::Program, catalog: &Catalog) -> Vec<SemanticIssue> {
88 let mut issues = Vec::new();
89 if let Some(settings) = &program.settings {
90 for node in &settings.children {
91 inspect_setting(node, &mut issues);
92 }
93 }
94 for (rule, rule_data) in program.rules.iter().enumerate() {
95 for (position, action) in rule_data.actions.iter().enumerate() {
96 inspect_action(action, program, rule, position, catalog, &mut issues);
97 }
98 }
99 for (rule, rule_data) in program.rules.iter().enumerate() {
100 for (condition, condition_data) in rule_data.conditions.iter().enumerate() {
101 inspect_value_tree(
102 &condition_data.value,
103 &mut Vec::new(),
104 &|path| program.condition_value_node_span(rule, condition, path),
105 program,
106 catalog,
107 &mut issues,
108 );
109 }
110 let mut position = 0;
111 inspect_action_values(
112 &rule_data.actions,
113 &mut position,
114 false,
115 rule,
116 program,
117 catalog,
118 &mut issues,
119 );
120 }
121 issues
122}
123
124#[cfg(test)]
125pub(crate) fn inspect_wir(program: &crate::wir::Program, catalog: &Catalog) -> Vec<SemanticIssue> {
126 let program = crate::Program::from_wir(program.clone())
127 .expect("test WIR materializes as a public program");
128 inspect(&program, catalog)
129}
130
131fn inspect_setting(node: &SettingsNode, issues: &mut Vec<SemanticIssue>) {
132 match node {
133 SettingsNode::Workshop { .. } => {}
134 SettingsNode::Group { children, .. } => {
135 for child in children {
136 inspect_setting(child, issues);
137 }
138 }
139 SettingsNode::Raw { name, span, .. } => issues.push(SemanticIssue {
140 kind: IncompletenessKind::RawSetting,
141 name: name.clone(),
142 span: *span,
143 classification: ResidualClassification::ProjectDefinedConstruct,
144 }),
145 SettingsNode::List { name, elements, .. } => {
146 let element_known: fn(&crate::settings::SettingsListElement) -> bool =
147 match name.as_str() {
148 "enabledMaps" | "disabledMaps" => {
149 |element: &crate::settings::SettingsListElement| {
150 table::map_name(&element.value).is_some()
151 }
152 }
153 "enabledHeroes" | "disabledHeroes" => {
154 |element: &crate::settings::SettingsListElement| {
155 table::hero_name(&element.value).is_some()
156 }
157 }
158 _ => |_| true,
159 };
160 for element in elements {
161 if !element_known(element) {
162 issues.push(SemanticIssue {
163 kind: IncompletenessKind::RawSetting,
164 name: element.value.clone(),
165 span: element.span,
166 classification: ResidualClassification::ProjectDefinedConstruct,
167 });
168 }
169 }
170 }
171 SettingsNode::Number { .. }
172 | SettingsNode::Bool { .. }
173 | SettingsNode::Flag { .. }
174 | SettingsNode::String { .. } => {}
175 }
176}
177
178fn inspect_action(
179 action: &crate::Action,
180 program: &crate::Program,
181 rule: usize,
182 position: usize,
183 catalog: &Catalog,
184 issues: &mut Vec<SemanticIssue>,
185) {
186 match action {
187 crate::Action::Call { name, .. } => {
188 let kind = if name == "rawWorkshopAction" {
189 Some(IncompletenessKind::OpaqueAction)
190 } else if catalog.entry(Kind::Action, name).is_none() {
191 Some(IncompletenessKind::UnknownAction)
192 } else {
193 None
194 };
195 if let Some(kind) = kind {
196 let classification = if kind == IncompletenessKind::OpaqueAction {
197 ResidualClassification::LegacyOpaque
198 } else {
199 ResidualClassification::ProducerExtension
200 };
201 issues.push(SemanticIssue {
202 kind,
203 name: name.clone(),
204 span: program.action_span(rule, position),
205 classification,
206 });
207 }
208 }
209 crate::Action::Disabled { action } => {
210 inspect_action(action, program, rule, position, catalog, issues);
211 }
212 _ => {}
213 }
214}
215
216fn inspect_action_values(
223 actions: &[crate::Action],
224 position: &mut usize,
225 stop_at_terminator: bool,
226 rule: usize,
227 program: &crate::Program,
228 catalog: &Catalog,
229 issues: &mut Vec<SemanticIssue>,
230) {
231 while *position < actions.len() {
232 let index = *position;
233 let current = match &actions[index] {
234 crate::Action::Disabled { action } => action.as_ref(),
235 action => action,
236 };
237 match current {
238 crate::Action::ElseIf { .. } | crate::Action::Else | crate::Action::End
239 if stop_at_terminator =>
240 {
241 return;
242 }
243 crate::Action::If { .. } => {
244 inspect_action_arguments(rule, index, &actions[index], program, catalog, issues);
245 *position += 1;
246 inspect_action_values(actions, position, true, rule, program, catalog, issues);
247 while matches!(actions.get(*position), Some(crate::Action::ElseIf { .. })) {
248 let elseif = *position;
249 *position += 1;
250 inspect_action_values(actions, position, true, rule, program, catalog, issues);
251 inspect_action_arguments(
252 rule,
253 elseif,
254 &actions[elseif],
255 program,
256 catalog,
257 issues,
258 );
259 }
260 if matches!(actions.get(*position), Some(crate::Action::Else)) {
261 *position += 1;
262 inspect_action_values(actions, position, true, rule, program, catalog, issues);
263 }
264 if matches!(actions.get(*position), Some(crate::Action::End)) {
265 *position += 1;
266 }
267 }
268 crate::Action::While { .. }
269 | crate::Action::ForGlobalVariable { .. }
270 | crate::Action::ForPlayerVariable { .. } => {
271 *position += 1;
272 inspect_action_values(actions, position, true, rule, program, catalog, issues);
273 if matches!(actions.get(*position), Some(crate::Action::End)) {
274 *position += 1;
275 }
276 inspect_action_arguments(rule, index, &actions[index], program, catalog, issues);
277 }
278 _ => {
279 inspect_action_arguments(rule, index, &actions[index], program, catalog, issues);
280 *position += 1;
281 }
282 }
283 }
284}
285
286fn inspect_action_arguments(
289 rule: usize,
290 action: usize,
291 action_data: &crate::Action,
292 program: &crate::Program,
293 catalog: &Catalog,
294 issues: &mut Vec<SemanticIssue>,
295) {
296 for (argument, value) in action_argument_values(action_data).iter().enumerate() {
297 inspect_value_tree(
298 value,
299 &mut Vec::new(),
300 &|path| program.action_argument_value_node_span(rule, action, argument, path),
301 program,
302 catalog,
303 issues,
304 );
305 }
306}
307
308fn inspect_value_tree(
312 value: &crate::Value,
313 path: &mut Vec<usize>,
314 span_at: &impl Fn(&[usize]) -> Option<Span>,
315 program: &crate::Program,
316 catalog: &Catalog,
317 issues: &mut Vec<SemanticIssue>,
318) {
319 for (index, child) in value_children(value).into_iter().enumerate() {
320 path.push(index);
321 inspect_value_tree(child, path, span_at, program, catalog, issues);
322 path.pop();
323 }
324 if let crate::Value::Call { name, args } = value {
325 let canonical_helper = name == crate::wir::AMBIGUOUS_ENUM_CALL
329 || crate::wir::is_canonical_helper_call(name, args.len());
330 if canonical_helper {
331 return;
332 }
333 if catalog.entry(Kind::Value, name).is_none()
334 && catalog.entry(Kind::Operator, name).is_none()
335 {
336 issues.push(SemanticIssue {
337 kind: IncompletenessKind::UnknownValue,
338 name: name.clone(),
339 span: span_at(path),
340 classification: if program
341 .global_variables
342 .iter()
343 .any(|variable| variable.name == *name)
344 || program
345 .player_variables
346 .iter()
347 .any(|variable| variable.name == *name)
348 {
349 ResidualClassification::SourceDeclaredVariable
350 } else {
351 ResidualClassification::UnresolvedIdentifier
352 },
353 });
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::settings::{Settings, SettingsNode};
362 use crate::{Action, Condition, Event, Program, Rule, Value};
363
364 #[test]
365 fn reports_preserved_and_unknown_nodes() {
366 let catalog = Catalog::builtin().expect("builtin catalog");
367 let mut program = Program::new();
368 program.settings = Some(Settings {
369 span: None,
370 children: vec![SettingsNode::Raw {
371 name: "Future Setting".to_string(),
372 value: "opaque".to_string(),
373 span: None,
374 }],
375 });
376 program.rule(
377 Rule::new("residuals", Event::Global)
378 .condition(Condition::new(Value::call("futureValue", [])))
379 .action(Action::call("rawWorkshopAction", []))
380 .action(Action::call("futureAction", [])),
381 );
382
383 let issues = inspect(&program, &catalog);
384 assert!(
385 issues
386 .iter()
387 .any(|issue| issue.kind == IncompletenessKind::RawSetting)
388 );
389 assert!(
390 issues
391 .iter()
392 .any(|issue| issue.kind == IncompletenessKind::OpaqueAction)
393 );
394 assert!(
395 issues
396 .iter()
397 .any(|issue| issue.kind == IncompletenessKind::UnknownAction)
398 );
399 assert!(
400 issues
401 .iter()
402 .any(|issue| issue.kind == IncompletenessKind::UnknownValue)
403 );
404 assert!(issues.iter().any(|issue| {
405 issue.kind == IncompletenessKind::RawSetting
406 && issue.classification == ResidualClassification::ProjectDefinedConstruct
407 }));
408 assert!(issues.iter().any(|issue| {
409 issue.kind == IncompletenessKind::OpaqueAction
410 && issue.classification == ResidualClassification::LegacyOpaque
411 }));
412 assert!(issues.iter().any(|issue| {
413 issue.kind == IncompletenessKind::UnknownValue
414 && issue.classification == ResidualClassification::UnresolvedIdentifier
415 }));
416 }
417}