tiger_lib/report/
filter.rs

1use glob::Pattern;
2use std::path::Path;
3
4use crate::block::Comparator;
5
6use crate::Token;
7use crate::fileset::FileKind;
8use crate::report::{
9    Confidence, ErrorKey, ErrorLoc, LogReportMetadata, LogReportPointers, Severity, err,
10};
11use crate::token::Loc;
12
13/// Determines whether a given Report should be printed.
14/// If a report is matched by both the blacklist and the whitelist, it will not be printed.
15#[derive(Default, Debug)]
16pub struct ReportFilter {
17    /// Whether to log errors in vanilla CK3 files
18    pub show_vanilla: bool,
19    /// Whether to log errors in other loaded mods
20    pub show_loaded_mods: bool,
21    /// A complex trigger that evaluates a report to assess whether it should be printed.
22    pub predicate: FilterRule,
23}
24
25impl ReportFilter {
26    /// Returns true iff the report should be printed.
27    /// A print will be rejected if the report matches at least one of the following conditions:
28    /// - Its Severity or Confidence level is too low.
29    /// - It's from vanilla or a loaded mod and the program is configured to ignore those locations.
30    /// - The filter has a trigger, and the report doesn't match it.
31    pub fn should_print_report(
32        &self,
33        report: &LogReportMetadata,
34        pointers: &LogReportPointers,
35    ) -> bool {
36        if report.key == ErrorKey::Config {
37            // Any errors concerning the Config should be easy to fix and will fundamentally
38            // undermine the operation of the application. They must always be printed.
39            return true;
40        }
41        // If every single Loc in the chain is out of scope, the report is out of scope.
42        let out_of_scope = pointers.iter().map(|p| &p.loc).all(|loc| {
43            (loc.kind.counts_as_vanilla() && !self.show_vanilla)
44                || (matches!(loc.kind, FileKind::LoadedMod(_)) && !self.show_loaded_mods)
45        });
46        if out_of_scope {
47            return false;
48        }
49        self.predicate.apply(report, pointers)
50    }
51
52    /// TODO: Check the filter rules to be more sure.
53    pub fn should_maybe_print(&self, key: ErrorKey, loc: Loc) -> bool {
54        if key == ErrorKey::Config {
55            // Any errors concerning the Config should be easy to fix and will fundamentally
56            // undermine the operation of the application. They must always be printed.
57            return true;
58        }
59        if (loc.kind.counts_as_vanilla() && !self.show_vanilla)
60            || (matches!(loc.kind, FileKind::LoadedMod(_)) && !self.show_loaded_mods)
61        {
62            return false;
63        }
64        true
65    }
66}
67
68#[derive(Default, Debug)]
69pub enum FilterRule {
70    /// Always true.
71    #[default]
72    Tautology,
73    /// Always false.
74    Contradiction,
75    /// Configured by the AND-key. The top-level rule is always a conjunction.
76    /// Reports must match all enclosed rules to match the conjunction.
77    Conjunction(Vec<FilterRule>),
78    /// Configured by the OR-key.
79    /// Reports must match at least one of the enclosed rules to match the disjunction.
80    Disjunction(Vec<FilterRule>),
81    /// Configured by the NOT-key.
82    /// Reports must not match the enclosed rule to match the negation.
83    Negation(Box<FilterRule>),
84    /// Reports must be within the given Severity range to match the rule.
85    /// The condition is built like `severity >= error` in the filter trigger.
86    Severity(Comparator, Severity),
87    /// Reports must be within the given Confidence range to match the rule.
88    /// The condition is built like `confidence > weak` in the filter trigger.
89    Confidence(Comparator, Confidence),
90    /// The report's `ErrorKey` must be the listed key for the report to match the rule.
91    Key(ErrorKey),
92    /// The report's pointers must contain the given file for the report to match the rule.
93    File(Pattern),
94    /// The report's msg must contain the given text for the report to match the rule.
95    Text(String),
96}
97
98impl FilterRule {
99    fn apply(&self, report: &LogReportMetadata, pointers: &LogReportPointers) -> bool {
100        match self {
101            FilterRule::Tautology => true,
102            FilterRule::Contradiction => false,
103            FilterRule::Conjunction(children) => {
104                children.iter().all(|child| child.apply(report, pointers))
105            }
106            FilterRule::Disjunction(children) => {
107                children.iter().any(|child| child.apply(report, pointers))
108            }
109            FilterRule::Negation(child) => !child.apply(report, pointers),
110            FilterRule::Severity(comparator, level) => match comparator {
111                Comparator::Equals(_) => report.severity == *level,
112                Comparator::NotEquals => report.severity != *level,
113                Comparator::GreaterThan => report.severity > *level,
114                Comparator::AtLeast => report.severity >= *level,
115                Comparator::LessThan => report.severity < *level,
116                Comparator::AtMost => report.severity <= *level,
117            },
118            FilterRule::Confidence(comparator, level) => match comparator {
119                Comparator::Equals(_) => report.confidence == *level,
120                Comparator::NotEquals => report.confidence != *level,
121                Comparator::GreaterThan => report.confidence > *level,
122                Comparator::AtLeast => report.confidence >= *level,
123                Comparator::LessThan => report.confidence < *level,
124                Comparator::AtMost => report.confidence <= *level,
125            },
126            FilterRule::Key(key) => report.key == *key,
127            FilterRule::File(pattern) => pointers.iter().any(|pointer| {
128                pattern.matches_path(pointer.loc.pathname())
129                    || pointer.loc.pathname().starts_with(Path::new(pattern.as_str()))
130            }),
131            FilterRule::Text(s) => {
132                report.msg.to_ascii_lowercase().contains(&s.to_ascii_lowercase())
133            }
134        }
135    }
136    pub fn file_from_token(token: &Token) -> Option<FilterRule> {
137        let pattern = Pattern::new(token.as_str());
138        match pattern {
139            Ok(p) => Some(FilterRule::File(p)),
140            Err(e) => {
141                err(ErrorKey::Config)
142                    .msg("Expected valid file path or glob pattern")
143                    .loc_msg(
144                        {
145                            let mut loc = token.into_loc();
146                            loc.column += u32::try_from(e.pos).unwrap_or(0);
147                            loc
148                        },
149                        e.msg,
150                    )
151                    .push();
152                None
153            }
154        }
155    }
156}