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<_> =
282 array_block.iter_values_warn().filter_map(FilterRule::file_from_token).collect();
283 if files.is_empty() {
284 None
285 } else {
286 Some(FilterRule::Disjunction(files))
287 }
288}
289
290fn load_rule_severity(comparator: Comparator, value: &BV) -> Option<FilterRule> {
291 match value {
292 BV::Block(_) => {
293 err(ErrorKey::Config)
294 .msg("`severity` can't open a block. Example usage: `severity >= Warning`")
295 .loc(value)
296 .push();
297 None
298 }
299 BV::Value(token) => {
300 if let Ok(severity) = token.as_str().to_ascii_lowercase().parse() {
301 Some(FilterRule::Severity(comparator, severity))
302 } else {
303 err(ErrorKey::Config)
304 .msg(format!(
305 "Invalid Severity value. Valid values: {}",
306 stringify_list(&Severity::iter().map(Severity::into).collect::<Vec<_>>()),
307 ))
308 .loc(token)
309 .push();
310 None
311 }
312 }
313 }
314}
315
316fn load_rule_confidence(comparator: Comparator, value: &BV) -> Option<FilterRule> {
317 match value {
318 BV::Block(_) => {
319 err(ErrorKey::Config)
320 .msg("`confidence` can't open a block. Example usage: `confidence >= Reasonable`")
321 .loc(value)
322 .push();
323 None
324 }
325 BV::Value(token) => {
326 if let Ok(confidence) = token.as_str().to_ascii_lowercase().parse() {
327 Some(FilterRule::Confidence(comparator, confidence))
328 } else {
329 err(ErrorKey::Config)
330 .msg(format!(
331 "Invalid Confidence value. Valid values are {}",
332 stringify_list(
333 &Confidence::iter().map(Confidence::into).collect::<Vec<_>>()
334 )
335 ))
336 .loc(token)
337 .push();
338 None
339 }
340 }
341 }
342}
343
344fn load_rule_key(value: &BV) -> Option<FilterRule> {
345 match value {
346 BV::Block(_) => {
347 err(ErrorKey::Config)
348 .msg("`key` can't open a block. Example usage: `key = missing-item`")
349 .loc(value)
350 .push();
351 None
352 }
353 BV::Value(token) => {
354 if let Ok(error_key) = token.as_str().parse() {
355 Some(FilterRule::Key(error_key))
356 } else {
357 err(ErrorKey::Config).msg(
358 "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`.",
359 ).loc(token).push();
360 None
361 }
362 }
363 }
364}
365
366fn load_rule_file(value: &BV) -> Option<FilterRule> {
367 match value {
368 BV::Block(_) => {
369 err(
370 ErrorKey::Config).msg(
371 "`file` can't open a block. Example usage: `file = common/traits/00_traits.txt`",
372 ).loc(value).push();
373 None
374 }
375 BV::Value(token) => FilterRule::file_from_token(token),
376 }
377}
378
379fn load_rule_text(bv: &BV) -> Option<FilterRule> {
380 match bv {
381 BV::Block(_) => {
382 err(
383 ErrorKey::Config).msg(
384 "`text` can't open a block. Example usage: `text = \"coat of arms is redefined\"`",
385 ).loc(bv).push();
386 None
387 }
388 BV::Value(token) => Some(FilterRule::Text(token.to_string())),
389 }
390}
391
392pub fn assert_one_key(assert_key: &str, block: &Block) {
395 let keys: Vec<_> = block
396 .iter_items()
397 .filter_map(|item| {
398 if let BlockItem::Field(Field(key, _, _)) = item {
399 (key.as_str() == assert_key).then_some(key)
400 } else {
401 None
402 }
403 })
404 .collect();
405 if keys.len() > 1 {
406 let pointers = keys
407 .iter()
408 .enumerate()
409 .map(|(index, key)| PointedMessage {
410 loc: key.into_loc(),
411 length: 1,
412 msg: Some((if index == 0 { "It occurs here" } else { "and here" }).to_owned()),
413 })
414 .collect();
415 err(ErrorKey::Config)
416 .strong()
417 .msg(format!("Detected more than one `{assert_key}`: there can be only one here!"))
418 .pointers(pointers)
419 .push();
420 }
421}