1use crate::core::error::WorkshopError;
14use crate::core::suggest;
15
16use super::member::{self, Member, rejected};
17use super::table;
18use super::{PathPart, Settings, SettingsNode};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub struct SettingsDiagnostic {
27 pub error: WorkshopError,
29 pub suggestion: Option<String>,
35}
36
37pub fn check_emission_diagnostics(settings: &Settings) -> Vec<SettingsDiagnostic> {
41 let mut diagnostics = Vec::new();
42 walk(settings, &mut |visit| match visit {
43 Visit::Rejected(diagnostic) => diagnostics.push(diagnostic),
44 Visit::Member(node, path) => check_member(node, path, &mut diagnostics),
45 Visit::OpaqueLeaf(node) => check_member(node, &[], &mut diagnostics),
46 });
47 diagnostics
48}
49
50pub fn check_emission(settings: &Settings) -> Vec<WorkshopError> {
53 check_emission_diagnostics(settings)
54 .into_iter()
55 .map(|diagnostic| diagnostic.error)
56 .collect()
57}
58
59pub(crate) struct UncataloguedMember<'a> {
61 pub(crate) name: &'a str,
63 pub(crate) span: Option<crate::source::Span>,
64 pub(crate) suggestion: Option<String>,
66}
67
68pub(crate) fn uncatalogued_members(settings: &Settings) -> Vec<UncataloguedMember<'_>> {
73 let mut members = Vec::new();
74 walk(settings, &mut |visit| match visit {
75 Visit::Rejected(_) => {}
76 Visit::Member(node, path) => {
77 let mut full = path.to_vec();
78 full.push(PathPart::Part(node.name()));
79 let catalogued = table::lookup(&full);
80 let member = match (node, catalogued) {
81 (SettingsNode::Raw { .. }, _) | (SettingsNode::Group { .. }, None) => {
82 UncataloguedMember {
83 name: node.name(),
84 span: node.span(),
85 suggestion: suggest::suggest(
86 node.name(),
87 table::key_spellings(path).into_iter(),
88 ),
89 }
90 }
91 (SettingsNode::RawValue { value, .. }, Some(entry)) => UncataloguedMember {
92 name: value,
93 span: node.span(),
94 suggestion: match entry.kind {
95 table::KeyKind::Enum(domain) => {
96 suggest::suggest(value, table::enum_spellings(domain))
97 }
98 _ => None,
99 },
100 },
101 (SettingsNode::RawValue { .. }, None) => UncataloguedMember {
102 name: node.name(),
103 span: node.span(),
104 suggestion: suggest::suggest(
105 node.name(),
106 table::key_spellings(path).into_iter(),
107 ),
108 },
109 _ => return,
110 };
111 members.push(member);
112 }
113 Visit::OpaqueLeaf(node @ (SettingsNode::Raw { .. } | SettingsNode::RawValue { .. })) => {
114 members.push(UncataloguedMember {
115 name: node.name(),
116 span: node.span(),
117 suggestion: None,
118 });
119 }
120 Visit::OpaqueLeaf(_) => {}
121 });
122 members
123}
124
125enum Visit<'a, 'p> {
126 Rejected(SettingsDiagnostic),
128 Member(&'a SettingsNode, &'p [PathPart<'a>]),
130 OpaqueLeaf(&'a SettingsNode),
132}
133
134fn walk<'a>(settings: &'a Settings, visit: &mut dyn FnMut(Visit<'a, '_>)) {
136 for child in &settings.children {
137 match child {
138 SettingsNode::Workshop { children, .. } => walk_workshop(children, visit),
139 SettingsNode::Group { name, children, .. } => match name.as_str() {
140 "main" | "lobby" | "extensions" => {
141 for member in children {
142 visit(Visit::Member(member, &[PathPart::Part(name)]));
143 }
144 }
145 "gamemodes" => walk_modes(children, visit),
146 "heroes" => walk_heroes(children, visit),
147 _ => walk_opaque(children, visit),
148 },
149 other => {
150 visit(Visit::Rejected(rejected(
151 other.span(),
152 "settings block children must be groups".to_string(),
153 None,
154 )));
155 if matches!(
157 other,
158 SettingsNode::Raw { .. } | SettingsNode::RawValue { .. }
159 ) {
160 visit(Visit::OpaqueLeaf(other));
161 }
162 }
163 }
164 }
165}
166
167fn walk_workshop<'a>(children: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
168 for child in children {
169 match child {
170 SettingsNode::Group { children, .. } | SettingsNode::Workshop { children, .. } => {
171 walk_workshop(children, visit);
172 }
173 SettingsNode::Raw { .. } | SettingsNode::RawValue { .. } => {}
174 other => visit(Visit::Rejected(rejected(
175 other.span(),
176 "settings.workshop contains a typed builtin setting".to_string(),
177 None,
178 ))),
179 }
180 }
181}
182
183fn walk_modes<'a>(modes: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
184 for mode in modes {
185 let SettingsNode::Group { name, children, .. } = mode else {
186 visit(Visit::Rejected(rejected(
187 mode.span(),
188 "mode entries must be groups".to_string(),
189 None,
190 )));
191 continue;
192 };
193 let path = [PathPart::Part("gamemodes"), PathPart::Part(name)];
194 for member in children {
195 if matches!(member, SettingsNode::Bool { name, .. } if name == "enabled") {
198 continue;
199 }
200 visit(Visit::Member(member, &path));
201 }
202 }
203}
204
205fn walk_heroes<'a>(teams: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
206 const TEAM: [PathPart<'static>; 2] = [PathPart::Part("heroes"), PathPart::Team];
207 const HERO: [PathPart<'static>; 3] = [PathPart::Part("heroes"), PathPart::Team, PathPart::Hero];
208 for team in teams {
209 let SettingsNode::Group { name, children, .. } = team else {
210 visit(Visit::Rejected(rejected(
211 team.span(),
212 "team entries must be groups".to_string(),
213 None,
214 )));
215 continue;
216 };
217 if table::team_name(name).is_none() {
218 visit(Visit::Rejected(rejected(
219 team.span(),
220 format!("unknown team '{name}'"),
221 suggest::suggest(name, table::team_spellings()),
222 )));
223 continue;
224 }
225 for member in children {
226 match member {
227 SettingsNode::Group { name, children, .. } => {
228 if table::hero_name(name).is_none() {
229 visit(Visit::Rejected(rejected(
230 member.span(),
231 format!("unknown hero '{name}'"),
232 suggest::suggest(name, table::hero_spellings()),
233 )));
234 continue;
235 }
236 for inner in children {
237 visit(Visit::Member(inner, &HERO));
238 }
239 }
240 other => visit(Visit::Member(other, &TEAM)),
241 }
242 }
243 }
244}
245
246fn walk_opaque<'a>(children: &'a [SettingsNode], visit: &mut dyn FnMut(Visit<'a, '_>)) {
247 for child in children {
248 match child {
249 SettingsNode::Group { children, .. } => walk_opaque(children, visit),
252 other => visit(Visit::OpaqueLeaf(other)),
253 }
254 }
255}
256
257fn check_member(node: &SettingsNode, path: &[PathPart<'_>], errors: &mut Vec<SettingsDiagnostic>) {
258 let name = node.name();
259 let mut full = path.to_vec();
260 full.push(PathPart::Part(name));
261 match (node, table::lookup(&full)) {
262 (SettingsNode::Raw { .. } | SettingsNode::RawValue { .. }, _) => return,
263 (SettingsNode::Group { children, .. }, None) => {
266 for child in children {
267 check_member(child, &[], errors);
268 }
269 return;
270 }
271 _ => {}
272 }
273 match member::lookup(node, &full).and_then(|entry| member::accept(node, entry)) {
274 Ok(Member::List { elements, kind }) => {
275 for element in elements {
276 if let Err(diagnostic) = kind.resolve(element, name) {
277 errors.push(*diagnostic);
278 }
279 }
280 }
281 Ok(_) => {}
282 Err(diagnostic) => errors.push(*diagnostic),
283 }
284}