1use std::path::PathBuf;
7
8use strum::IntoEnumIterator;
9
10use crate::block::{Block, BlockItem, Comparator, Eq::*, Field, BV};
11use crate::helpers::stringify_list;
12use crate::report::{
13 err, set_predicate, set_show_loaded_mods, set_show_vanilla, Confidence, ErrorKey, ErrorLoc,
14 FilterRule, PointedMessage, Severity,
15};
16
17pub fn check_for_legacy_ignore(config: &Block) {
19 let pointers: Vec<PointedMessage> = config
21 .get_keys("ignore")
22 .into_iter()
23 .map(|key| PointedMessage::new(key.into_loc()))
24 .collect();
25 if !pointers.is_empty() {
26 err(ErrorKey::Config)
27 .strong()
28 .msg("`ignore` is deprecated, consider using `filter` instead.")
29 .info("Check out the filter.md guide on GitHub for tips on how to migrate.")
30 .pointers(pointers)
31 .push();
32 }
33}
34
35pub fn validate_config_file(config: Option<PathBuf>) -> Option<PathBuf> {
38 match config {
39 Some(config) => {
40 if config.is_file() {
41 if config.extension().is_some_and(|s| s != "conf") {
42 eprintln!(
43 "{} is not a valid .conf file. Using the default conf file instead.",
44 config.display()
45 );
46 None
47 } else {
48 eprintln!("Using conf file: {}", config.display());
49 Some(config)
50 }
51 } else {
52 eprintln!(
53 "{} is not a valid file. Using the default conf file instead.",
54 config.display()
55 );
56 None
57 }
58 }
59 None => None,
60 }
61}
62
63pub fn load_filter(config: &Block) {
64 assert_one_key("filter", config);
65 if let Some(filter) = config.get_field_block("filter") {
66 assert_one_key("trigger", filter);
67 assert_one_key("show_vanilla", filter);
68 assert_one_key("show_loaded_mods", filter);
69 set_show_vanilla(filter.get_field_bool("show_vanilla").unwrap_or(false));
70 set_show_loaded_mods(filter.get_field_bool("show_loaded_mods").unwrap_or(false));
71 if let Some(trigger) = filter.get_field_block("trigger") {
72 set_predicate(FilterRule::Conjunction(load_rules(trigger)));
73 } else {
74 set_predicate(FilterRule::default());
75 }
76 }
77}
78
79fn load_rules(block: &Block) -> Vec<FilterRule> {
81 block.iter_items().filter_map(BlockItem::expect_field).filter_map(load_rule).collect()
82}
83
84fn load_rules_from_bv(bv: &BV) -> Option<Vec<FilterRule>> {
87 match bv {
88 BV::Block(block) => Some(load_rules(block)),
89 BV::Value(_) => {
90 let msg = "Expected a trigger block. Example usage: `AND = { }`";
91 err(ErrorKey::Config).msg(msg).loc(bv).push();
92 None
93 }
94 }
95}
96
97fn load_rule(field: &Field) -> Option<FilterRule> {
99 let Field(key, cmp, bv) = field;
100 let cmp = *cmp;
101 if !key.is("severity") && !key.is("confidence") && !matches!(cmp, Comparator::Equals(Single)) {
102 err(ErrorKey::Config)
103 .msg(format!("Unexpected operator `{cmp}`, only `=` is valid here."))
104 .loc(key)
105 .push();
106 return None;
107 }
108 match key.as_str() {
109 "severity" => load_rule_severity(cmp, bv),
110 "confidence" => load_rule_confidence(cmp, bv),
111 "key" => load_rule_key(bv),
112 "file" => load_rule_file(bv),
113 "text" => load_rule_text(bv),
114 "always" => load_rule_always(bv),
115 "ignore_keys_in_files" => load_ignore_keys_in_files(bv),
116 "NOT" => load_not(bv),
117 "AND" => Some(FilterRule::Conjunction(load_rules_from_bv(bv)?)),
118 "OR" => Some(FilterRule::Disjunction(load_rules_from_bv(bv)?)),
119 "NAND" => {
120 Some(FilterRule::Negation(Box::new(FilterRule::Conjunction(load_rules_from_bv(bv)?))))
121 }
122 "NOR" => {
123 Some(FilterRule::Negation(Box::new(FilterRule::Disjunction(load_rules_from_bv(bv)?))))
124 }
125 _ => {
126 err(ErrorKey::Config).msg("Unexpected key").loc(key).push();
127 None
128 }
129 }
130}
131
132fn load_not(bv: &BV) -> Option<FilterRule> {
136 let mut children = load_rules_from_bv(bv)?;
137 if children.is_empty() {
138 err(ErrorKey::Config)
139 .msg("This NOT block contains no valid triggers. It will be ignored.")
140 .loc(bv)
141 .push();
142 None
143 } else if children.len() == 1 {
144 Some(FilterRule::Negation(Box::new(children.remove(0))))
145 } else {
146 Some(FilterRule::Negation(Box::new(FilterRule::Disjunction(children))))
147 }
148}
149
150fn load_rule_always(bv: &BV) -> Option<FilterRule> {
151 match bv {
152 BV::Block(_) => {
153 err(ErrorKey::Config)
154 .msg("`always` can't open a block. Valid values are `yes` and `no`.")
155 .loc(bv)
156 .push();
157 None
158 }
159 BV::Value(token) => match token.as_str() {
160 "yes" => Some(FilterRule::Tautology),
161 "no" => Some(FilterRule::Contradiction),
162 _ => {
163 err(ErrorKey::Config)
164 .msg("`always` value not recognised. Valid values are `yes` and `no`.")
165 .loc(bv)
166 .push();
167 None
168 }
169 },
170 }
171}
172
173fn load_ignore_keys_in_files(bv: &BV) -> Option<FilterRule> {
176 let Some(block) = bv.get_block() else {
177 err(ErrorKey::Config)
178 .strong()
179 .msg("This trigger should open a block.")
180 .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
181 .loc(bv)
182 .push();
183 return None;
184 };
185
186 let mut keys = None;
187 let mut files = None;
188
189 for item in block.iter_items() {
190 let Some(Field(key, cmp, bv)) = item.get_field() else {
191 err(ErrorKey::Config)
192 .strong()
193 .msg("Didn't expect a loose value here.")
194 .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
195 .loc(item)
196 .push();
197 return None;
198 };
199 let key_str = key.as_str();
200 if key_str != "keys" && key_str != "files" {
201 err(ErrorKey::Config)
202 .strong()
203 .msg("This key isn't valid here.")
204 .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
205 .loc(bv)
206 .push();
207 return None;
208 }
209 if !matches!(cmp, Comparator::Equals(Single)) {
210 err(ErrorKey::Config)
211 .strong()
212 .msg("Expected `=` here.")
213 .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
214 .loc(key)
215 .push();
216 return None;
217 }
218 if let BV::Value(_) = bv {
219 err(ErrorKey::Config)
220 .strong()
221 .msg("This should open a block.")
222 .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
223 .loc(bv)
224 .push();
225 return None;
226 }
227 let array_block = bv.expect_block().expect("Should be ok");
228 if key_str == "keys" {
229 keys = load_keys_array(array_block);
230 }
231 if key_str == "files" {
232 files = load_files_array(array_block);
233 }
234 }
235 if keys.is_none() {
236 err(ErrorKey::Config)
237 .strong()
238 .msg("There are no valid keys. This `ignore_keys_in_files` trigger will be ignored.")
239 .info(
240 "Add at least one key. Example: ignore_keys_in_files = { keys = { unknown-field }",
241 )
242 .loc(block)
243 .push();
244 None
245 } else if files.is_none() {
246 err(ErrorKey::Config)
247 .strong()
248 .msg("There are no valid files. This `ignore_keys_in_files` trigger will be ignored.")
249 .info("Add at least one file. Example: ignore_keys_in_files = { files = { common/ }")
250 .loc(block)
251 .push();
252 None
253 } else {
254 Some(FilterRule::Negation(Box::new(FilterRule::Conjunction(vec![
255 keys.expect("Should exist."),
256 files.expect("Should exist."),
257 ]))))
258 }
259}
260
261fn load_keys_array(array_block: &Block) -> Option<FilterRule> {
262 let keys: Vec<_> = array_block.iter_values_warn()
263 .filter_map(|token| {
264 if let Ok(error_key) = token.as_str().parse() {
265 Some(FilterRule::Key(error_key))
266 } else {
267 err(ErrorKey::Config).strong()
268 .msg("Invalid key. In the output, keys are listed between parentheses on the first line of each report. For example, in `Warning(missing-item)`, the key is `missing-item`.")
269 .loc(token)
270 .push();
271 None
272 }
273 }).collect();
274 if keys.is_empty() {
275 None
276 } else {
277 Some(FilterRule::Disjunction(keys))
278 }
279}
280fn load_files_array(array_block: &Block) -> Option<FilterRule> {
281 let files: Vec<_> = array_block
282 .iter_values_warn()
283 .map(|token| FilterRule::File(PathBuf::from(token.as_str())))
284 .collect();
285 if files.is_empty() {
286 None
287 } else {
288 Some(FilterRule::Disjunction(files))
289 }
290}
291
292fn load_rule_severity(comparator: Comparator, value: &BV) -> Option<FilterRule> {
293 match value {
294 BV::Block(_) => {
295 err(ErrorKey::Config)
296 .msg("`severity` can't open a block. Example usage: `severity >= Warning`")
297 .loc(value)
298 .push();
299 None
300 }
301 BV::Value(token) => {
302 if let Ok(severity) = token.as_str().to_ascii_lowercase().parse() {
303 Some(FilterRule::Severity(comparator, severity))
304 } else {
305 err(ErrorKey::Config)
306 .msg(format!(
307 "Invalid Severity value. Valid values: {}",
308 stringify_list(&Severity::iter().map(Severity::into).collect::<Vec<_>>()),
309 ))
310 .loc(token)
311 .push();
312 None
313 }
314 }
315 }
316}
317
318fn load_rule_confidence(comparator: Comparator, value: &BV) -> Option<FilterRule> {
319 match value {
320 BV::Block(_) => {
321 err(ErrorKey::Config)
322 .msg("`confidence` can't open a block. Example usage: `confidence >= Reasonable`")
323 .loc(value)
324 .push();
325 None
326 }
327 BV::Value(token) => {
328 if let Ok(confidence) = token.as_str().to_ascii_lowercase().parse() {
329 Some(FilterRule::Confidence(comparator, confidence))
330 } else {
331 err(ErrorKey::Config)
332 .msg(format!(
333 "Invalid Confidence value. Valid values are {}",
334 stringify_list(
335 &Confidence::iter().map(Confidence::into).collect::<Vec<_>>()
336 )
337 ))
338 .loc(token)
339 .push();
340 None
341 }
342 }
343 }
344}
345
346fn load_rule_key(value: &BV) -> Option<FilterRule> {
347 match value {
348 BV::Block(_) => {
349 err(ErrorKey::Config)
350 .msg("`key` can't open a block. Example usage: `key = missing-item`")
351 .loc(value)
352 .push();
353 None
354 }
355 BV::Value(token) => {
356 if let Ok(error_key) = token.as_str().parse() {
357 Some(FilterRule::Key(error_key))
358 } else {
359 err(ErrorKey::Config).msg(
360 "Invalid key. In the output, keys are listed between parentheses on the first line of each report. For example, in `Warning(missing-item)`, the key is `missing-item`.",
361 ).loc(token).push();
362 None
363 }
364 }
365 }
366}
367
368fn load_rule_file(value: &BV) -> Option<FilterRule> {
369 match value {
370 BV::Block(_) => {
371 err(
372 ErrorKey::Config).msg(
373 "`file` can't open a block. Example usage: `file = common/traits/00_traits.txt`",
374 ).loc(value).push();
375 None
376 }
377 BV::Value(token) => Some(FilterRule::File(PathBuf::from(token.as_str()))),
378 }
379}
380
381fn load_rule_text(bv: &BV) -> Option<FilterRule> {
382 match bv {
383 BV::Block(_) => {
384 err(
385 ErrorKey::Config).msg(
386 "`text` can't open a block. Example usage: `text = \"coat of arms is redefined\"`",
387 ).loc(bv).push();
388 None
389 }
390 BV::Value(token) => Some(FilterRule::Text(token.to_string())),
391 }
392}
393
394pub fn assert_one_key(assert_key: &str, block: &Block) {
397 let keys: Vec<_> = block
398 .iter_items()
399 .filter_map(|item| {
400 if let BlockItem::Field(Field(key, _, _)) = item {
401 (key.as_str() == assert_key).then_some(key)
402 } else {
403 None
404 }
405 })
406 .collect();
407 if keys.len() > 1 {
408 let pointers = keys
409 .iter()
410 .enumerate()
411 .map(|(index, key)| PointedMessage {
412 loc: key.into_loc(),
413 length: 1,
414 msg: Some((if index == 0 { "It occurs here" } else { "and here" }).to_owned()),
415 })
416 .collect();
417 err(ErrorKey::Config)
418 .strong()
419 .msg(format!("Detected more than one `{assert_key}`: there can be only one here!"))
420 .pointers(pointers)
421 .push();
422 }
423}