1use std::path::PathBuf;
7
8use strum::IntoEnumIterator;
9
10use crate::block::{BV, Block, BlockItem, Comparator, Eq::*, Field};
11use crate::helpers::stringify_list;
12use crate::report::{
13 Confidence, ErrorKey, ErrorLoc, FilterRule, PointedMessage, Severity, err, set_predicate,
14 set_show_loaded_mods, set_show_vanilla,
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 let Some(keys) = keys {
236 if let Some(files) = files {
237 Some(FilterRule::Negation(Box::new(FilterRule::Conjunction(vec![keys, files]))))
238 } else {
239 err(ErrorKey::Config)
240 .strong()
241 .msg("There are no valid files. This `ignore_keys_in_files` trigger will be ignored.")
242 .info("Add at least one file. Example: ignore_keys_in_files = { files = { common/ }")
243 .loc(block)
244 .push();
245 None
246 }
247 } else {
248 err(ErrorKey::Config)
249 .strong()
250 .msg("There are no valid keys. This `ignore_keys_in_files` trigger will be ignored.")
251 .info(
252 "Add at least one key. Example: ignore_keys_in_files = { keys = { unknown-field }",
253 )
254 .loc(block)
255 .push();
256 None
257 }
258}
259
260fn load_keys_array(array_block: &Block) -> Option<FilterRule> {
261 let keys: Vec<_> = array_block.iter_values_warn()
262 .filter_map(|token| {
263 if let Ok(error_key) = token.as_str().parse() {
264 Some(FilterRule::Key(error_key))
265 } else {
266 err(ErrorKey::Config).strong()
267 .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`.")
268 .loc(token)
269 .push();
270 None
271 }
272 }).collect();
273 if keys.is_empty() { None } else { Some(FilterRule::Disjunction(keys)) }
274}
275fn load_files_array(array_block: &Block) -> Option<FilterRule> {
276 let files: Vec<_> =
277 array_block.iter_values_warn().filter_map(FilterRule::file_from_token).collect();
278 if files.is_empty() { None } else { Some(FilterRule::Disjunction(files)) }
279}
280
281fn load_rule_severity(comparator: Comparator, value: &BV) -> Option<FilterRule> {
282 match value {
283 BV::Block(_) => {
284 err(ErrorKey::Config)
285 .msg("`severity` can't open a block. Example usage: `severity >= Warning`")
286 .loc(value)
287 .push();
288 None
289 }
290 BV::Value(token) => {
291 if let Ok(severity) = token.as_str().to_ascii_lowercase().parse() {
292 Some(FilterRule::Severity(comparator, severity))
293 } else {
294 err(ErrorKey::Config)
295 .msg(format!(
296 "Invalid Severity value. Valid values: {}",
297 stringify_list(&Severity::iter().map(Severity::into).collect::<Vec<_>>()),
298 ))
299 .loc(token)
300 .push();
301 None
302 }
303 }
304 }
305}
306
307fn load_rule_confidence(comparator: Comparator, value: &BV) -> Option<FilterRule> {
308 match value {
309 BV::Block(_) => {
310 err(ErrorKey::Config)
311 .msg("`confidence` can't open a block. Example usage: `confidence >= Reasonable`")
312 .loc(value)
313 .push();
314 None
315 }
316 BV::Value(token) => {
317 if let Ok(confidence) = token.as_str().to_ascii_lowercase().parse() {
318 Some(FilterRule::Confidence(comparator, confidence))
319 } else {
320 err(ErrorKey::Config)
321 .msg(format!(
322 "Invalid Confidence value. Valid values are {}",
323 stringify_list(
324 &Confidence::iter().map(Confidence::into).collect::<Vec<_>>()
325 )
326 ))
327 .loc(token)
328 .push();
329 None
330 }
331 }
332 }
333}
334
335fn load_rule_key(value: &BV) -> Option<FilterRule> {
336 match value {
337 BV::Block(_) => {
338 err(ErrorKey::Config)
339 .msg("`key` can't open a block. Example usage: `key = missing-item`")
340 .loc(value)
341 .push();
342 None
343 }
344 BV::Value(token) => {
345 if let Ok(error_key) = token.as_str().parse() {
346 Some(FilterRule::Key(error_key))
347 } else {
348 err(ErrorKey::Config).msg(
349 "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`.",
350 ).loc(token).push();
351 None
352 }
353 }
354 }
355}
356
357fn load_rule_file(value: &BV) -> Option<FilterRule> {
358 match value {
359 BV::Block(_) => {
360 err(
361 ErrorKey::Config).msg(
362 "`file` can't open a block. Example usage: `file = common/traits/00_traits.txt`",
363 ).loc(value).push();
364 None
365 }
366 BV::Value(token) => FilterRule::file_from_token(token),
367 }
368}
369
370fn load_rule_text(bv: &BV) -> Option<FilterRule> {
371 match bv {
372 BV::Block(_) => {
373 err(
374 ErrorKey::Config).msg(
375 "`text` can't open a block. Example usage: `text = \"coat of arms is redefined\"`",
376 ).loc(bv).push();
377 None
378 }
379 BV::Value(token) => Some(FilterRule::Text(token.to_string())),
380 }
381}
382
383pub fn assert_one_key(assert_key: &str, block: &Block) {
386 let keys: Vec<_> = block
387 .iter_items()
388 .filter_map(|item| {
389 if let BlockItem::Field(Field(key, _, _)) = item {
390 (key.as_str() == assert_key).then_some(key)
391 } else {
392 None
393 }
394 })
395 .collect();
396 if keys.len() > 1 {
397 let pointers = keys
398 .iter()
399 .enumerate()
400 .map(|(index, key)| PointedMessage {
401 loc: key.into_loc(),
402 length: 1,
403 msg: Some((if index == 0 { "It occurs here" } else { "and here" }).into()),
404 })
405 .collect();
406 err(ErrorKey::Config)
407 .strong()
408 .msg(format!("Detected more than one `{assert_key}`: there can be only one here!"))
409 .pointers(pointers)
410 .push();
411 }
412}