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 pub suggestion: Option<String>,
75}
76
77pub fn inspect(program: &crate::Program, catalog: &Catalog) -> Vec<SemanticIssue> {
90 let mut issues = Vec::new();
91 if let Some(settings) = &program.settings {
92 for member in crate::settings::check::uncatalogued_members(settings) {
93 issues.push(SemanticIssue {
94 kind: IncompletenessKind::RawSetting,
95 name: member.name.to_string(),
96 span: member.span,
97 classification: ResidualClassification::ProjectDefinedConstruct,
98 suggestion: member.suggestion,
99 });
100 }
101 for node in &settings.children {
102 inspect_setting(node, &mut issues);
103 }
104 }
105 for (rule, rule_data) in program.rules.iter().enumerate() {
106 for (position, action) in rule_data.actions.iter().enumerate() {
107 inspect_action(action, program, rule, position, catalog, &mut issues);
108 }
109 }
110 for (rule, rule_data) in program.rules.iter().enumerate() {
111 for (condition, condition_data) in rule_data.conditions.iter().enumerate() {
112 inspect_value_tree(
113 &condition_data.value,
114 &mut Vec::new(),
115 &|path| program.condition_value_node_span(rule, condition, path),
116 program,
117 catalog,
118 &mut issues,
119 );
120 }
121 let mut position = 0;
122 inspect_action_values(
123 &rule_data.actions,
124 &mut position,
125 false,
126 rule,
127 program,
128 catalog,
129 &mut issues,
130 );
131 }
132 issues
133}
134
135#[cfg(test)]
136pub(crate) fn inspect_wir(program: &crate::wir::Program, catalog: &Catalog) -> Vec<SemanticIssue> {
137 let program = crate::Program::from_wir(program.clone())
138 .expect("test WIR materializes as a public program");
139 inspect(&program, catalog)
140}
141
142fn inspect_setting(node: &SettingsNode, issues: &mut Vec<SemanticIssue>) {
143 match node {
144 SettingsNode::Workshop { .. } => {}
145 SettingsNode::Group { children, .. } => {
146 for child in children {
147 inspect_setting(child, issues);
148 }
149 }
150
151 SettingsNode::List { name, elements, .. } => {
152 let element_known: fn(&crate::settings::SettingsListElement) -> bool =
153 match name.as_str() {
154 "enabledMaps" | "disabledMaps" => {
155 |element: &crate::settings::SettingsListElement| {
156 table::map_name(&element.value).is_some()
157 }
158 }
159 "enabledHeroes" | "disabledHeroes" => {
160 |element: &crate::settings::SettingsListElement| {
161 table::hero_name(&element.value).is_some()
162 }
163 }
164 _ => |_| true,
165 };
166 for element in elements {
167 if !element_known(element) {
168 issues.push(SemanticIssue {
169 kind: IncompletenessKind::RawSetting,
170 name: element.value.clone(),
171 span: element.span,
172 classification: ResidualClassification::ProjectDefinedConstruct,
173 suggestion: None,
174 });
175 }
176 }
177 }
178 SettingsNode::Number { .. }
179 | SettingsNode::Bool { .. }
180 | SettingsNode::Flag { .. }
181 | SettingsNode::String { .. }
182 | SettingsNode::Raw { .. }
183 | SettingsNode::RawValue { .. } => {}
184 }
185}
186
187fn inspect_action(
188 action: &crate::Action,
189 program: &crate::Program,
190 rule: usize,
191 position: usize,
192 catalog: &Catalog,
193 issues: &mut Vec<SemanticIssue>,
194) {
195 match action {
196 crate::Action::Call { name, .. } => {
197 let kind = if name == "rawWorkshopAction" {
198 Some(IncompletenessKind::OpaqueAction)
199 } else if catalog.entry(Kind::Action, name).is_none() {
200 Some(IncompletenessKind::UnknownAction)
201 } else {
202 None
203 };
204 if let Some(kind) = kind {
205 let classification = if kind == IncompletenessKind::OpaqueAction {
206 ResidualClassification::LegacyOpaque
207 } else {
208 ResidualClassification::ProducerExtension
209 };
210 issues.push(SemanticIssue {
211 kind,
212 name: name.clone(),
213 span: program.action_span(rule, position),
214 classification,
215 suggestion: None,
216 });
217 }
218 }
219 crate::Action::Disabled { action } => {
220 inspect_action(action, program, rule, position, catalog, issues);
221 }
222 _ => {}
223 }
224}
225
226fn inspect_action_values(
233 actions: &[crate::Action],
234 position: &mut usize,
235 stop_at_terminator: bool,
236 rule: usize,
237 program: &crate::Program,
238 catalog: &Catalog,
239 issues: &mut Vec<SemanticIssue>,
240) {
241 while *position < actions.len() {
242 let index = *position;
243 let current = match &actions[index] {
244 crate::Action::Disabled { action } => action.as_ref(),
245 action => action,
246 };
247 match current {
248 crate::Action::ElseIf { .. } | crate::Action::Else | crate::Action::End
249 if stop_at_terminator =>
250 {
251 return;
252 }
253 crate::Action::If { .. } => {
254 inspect_action_arguments(rule, index, &actions[index], program, catalog, issues);
255 *position += 1;
256 inspect_action_values(actions, position, true, rule, program, catalog, issues);
257 while matches!(actions.get(*position), Some(crate::Action::ElseIf { .. })) {
258 let elseif = *position;
259 *position += 1;
260 inspect_action_values(actions, position, true, rule, program, catalog, issues);
261 inspect_action_arguments(
262 rule,
263 elseif,
264 &actions[elseif],
265 program,
266 catalog,
267 issues,
268 );
269 }
270 if matches!(actions.get(*position), Some(crate::Action::Else)) {
271 *position += 1;
272 inspect_action_values(actions, position, true, rule, program, catalog, issues);
273 }
274 if matches!(actions.get(*position), Some(crate::Action::End)) {
275 *position += 1;
276 }
277 }
278 crate::Action::While { .. }
279 | crate::Action::ForGlobalVariable { .. }
280 | crate::Action::ForPlayerVariable { .. } => {
281 *position += 1;
282 inspect_action_values(actions, position, true, rule, program, catalog, issues);
283 if matches!(actions.get(*position), Some(crate::Action::End)) {
284 *position += 1;
285 }
286 inspect_action_arguments(rule, index, &actions[index], program, catalog, issues);
287 }
288 _ => {
289 inspect_action_arguments(rule, index, &actions[index], program, catalog, issues);
290 *position += 1;
291 }
292 }
293 }
294}
295
296fn inspect_action_arguments(
299 rule: usize,
300 action: usize,
301 action_data: &crate::Action,
302 program: &crate::Program,
303 catalog: &Catalog,
304 issues: &mut Vec<SemanticIssue>,
305) {
306 for (argument, value) in action_argument_values(action_data).iter().enumerate() {
307 inspect_value_tree(
308 value,
309 &mut Vec::new(),
310 &|path| program.action_argument_value_node_span(rule, action, argument, path),
311 program,
312 catalog,
313 issues,
314 );
315 }
316}
317
318fn inspect_value_tree(
322 value: &crate::Value,
323 path: &mut Vec<usize>,
324 span_at: &impl Fn(&[usize]) -> Option<Span>,
325 program: &crate::Program,
326 catalog: &Catalog,
327 issues: &mut Vec<SemanticIssue>,
328) {
329 for (index, child) in value_children(value).into_iter().enumerate() {
330 path.push(index);
331 inspect_value_tree(child, path, span_at, program, catalog, issues);
332 path.pop();
333 }
334 if let crate::Value::Call { name, args } = value {
335 let canonical_helper = name == crate::wir::AMBIGUOUS_ENUM_CALL
339 || crate::wir::is_canonical_helper_call(name, args.len());
340 if canonical_helper {
341 return;
342 }
343 if catalog.entry(Kind::Value, name).is_none()
344 && catalog.entry(Kind::Operator, name).is_none()
345 {
346 issues.push(SemanticIssue {
347 kind: IncompletenessKind::UnknownValue,
348 name: name.clone(),
349 span: span_at(path),
350 classification: if program
351 .global_variables
352 .iter()
353 .any(|variable| variable.name == *name)
354 || program
355 .player_variables
356 .iter()
357 .any(|variable| variable.name == *name)
358 {
359 ResidualClassification::SourceDeclaredVariable
360 } else {
361 ResidualClassification::UnresolvedIdentifier
362 },
363 suggestion: None,
364 });
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::settings::{Settings, SettingsNode};
373 use crate::{Action, Condition, Event, Program, Rule, Value};
374
375 #[test]
376 fn reports_preserved_and_unknown_nodes() {
377 let catalog = Catalog::builtin().expect("builtin catalog");
378 let mut program = Program::new();
379 program.settings = Some(Settings {
380 span: None,
381 children: vec![SettingsNode::Raw {
382 name: "Future Setting".to_string(),
383 value: "opaque".to_string(),
384 span: None,
385 }],
386 });
387 program.rule(
388 Rule::new("residuals", Event::Global)
389 .condition(Condition::new(Value::call("futureValue", [])))
390 .action(Action::call("rawWorkshopAction", []))
391 .action(Action::call("futureAction", [])),
392 );
393
394 let issues = inspect(&program, &catalog);
395 assert!(
396 issues
397 .iter()
398 .any(|issue| issue.kind == IncompletenessKind::RawSetting)
399 );
400 assert!(
401 issues
402 .iter()
403 .any(|issue| issue.kind == IncompletenessKind::OpaqueAction)
404 );
405 assert!(
406 issues
407 .iter()
408 .any(|issue| issue.kind == IncompletenessKind::UnknownAction)
409 );
410 assert!(
411 issues
412 .iter()
413 .any(|issue| issue.kind == IncompletenessKind::UnknownValue)
414 );
415 assert!(issues.iter().any(|issue| {
416 issue.kind == IncompletenessKind::RawSetting
417 && issue.classification == ResidualClassification::ProjectDefinedConstruct
418 }));
419 assert!(issues.iter().any(|issue| {
420 issue.kind == IncompletenessKind::OpaqueAction
421 && issue.classification == ResidualClassification::LegacyOpaque
422 }));
423 assert!(issues.iter().any(|issue| {
424 issue.kind == IncompletenessKind::UnknownValue
425 && issue.classification == ResidualClassification::UnresolvedIdentifier
426 }));
427 }
428}