tiger_lib/report/
errors.rs

1//! Collect error reports and then write them out.
2
3use std::cell::RefCell;
4use std::cmp::Ordering;
5use std::fs::{read, File};
6use std::io::{stdout, Write};
7use std::mem::take;
8use std::path::{Path, PathBuf};
9use std::sync::{LazyLock, Mutex, MutexGuard};
10
11use anyhow::Result;
12use encoding_rs::{UTF_8, WINDOWS_1252};
13
14use crate::helpers::{TigerHashMap, TigerHashSet};
15use crate::macros::MACRO_MAP;
16use crate::report::error_loc::ErrorLoc;
17use crate::report::filter::ReportFilter;
18use crate::report::suppress::{Suppression, SuppressionKey};
19use crate::report::writer::log_report;
20use crate::report::writer_json::log_report_json;
21use crate::report::{ErrorKey, FilterRule, LogReport, OutputStyle, PointedMessage};
22use crate::token::{leak, Loc};
23
24static ERRORS: LazyLock<Mutex<Errors>> = LazyLock::new(|| Mutex::new(Errors::default()));
25
26#[allow(missing_debug_implementations)]
27pub struct Errors {
28    pub(crate) output: RefCell<Box<dyn Write + Send>>,
29
30    /// Extra loaded mods' error tags.
31    pub(crate) loaded_mods_labels: Vec<String>,
32
33    /// Loaded DLCs' error tags.
34    pub(crate) loaded_dlcs_labels: Vec<String>,
35
36    pub(crate) cache: Cache,
37
38    /// Determines whether a report should be printed.
39    pub(crate) filter: ReportFilter,
40    /// Output color and style configuration.
41    pub(crate) styles: OutputStyle,
42
43    pub(crate) suppress: TigerHashMap<SuppressionKey, Vec<Suppression>>,
44
45    /// All reports that passed the checks, stored here to be sorted before being emitted all at once.
46    /// The "abbreviated" reports don't participate in this. They are still emitted immediately.
47    /// It's a `HashSet` because duplicate reports are fairly common due to macro expansion and other revalidations.
48    storage: TigerHashSet<LogReport>,
49}
50
51impl Default for Errors {
52    fn default() -> Self {
53        Errors {
54            output: RefCell::new(Box::new(stdout())),
55            loaded_mods_labels: Vec::default(),
56            loaded_dlcs_labels: Vec::default(),
57            cache: Cache::default(),
58            filter: ReportFilter::default(),
59            styles: OutputStyle::default(),
60            storage: TigerHashSet::default(),
61            suppress: TigerHashMap::default(),
62        }
63    }
64}
65
66impl Errors {
67    fn should_suppress(&mut self, report: &LogReport) -> bool {
68        // TODO: see if this can be done without cloning
69        let key = SuppressionKey { key: report.key, message: report.msg.clone() };
70        if let Some(v) = self.suppress.get(&key) {
71            for suppression in v {
72                if suppression.len() != report.pointers.len() {
73                    continue;
74                }
75                for (s, p) in suppression.iter().zip(report.pointers.iter()) {
76                    if s.path == p.loc.pathname().to_string_lossy()
77                        && s.tag == p.msg
78                        && s.line.as_deref() == self.cache.get_line(p.loc)
79                    {
80                        return true;
81                    }
82                }
83            }
84        }
85        false
86    }
87
88    /// Perform some checks to see whether the report should actually be logged.
89    /// If yes, it will add it to the storage.
90    fn push_report(&mut self, report: LogReport) {
91        if !self.filter.should_print_report(&report) || self.should_suppress(&report) {
92            return;
93        }
94        self.storage.insert(report);
95    }
96
97    /// Immediately log a single-line report about this error.
98    ///
99    /// This is intended for voluminous almost-identical errors, such as from the "unused
100    /// localization" check.
101    // TODO: integrate this function into the error reporting framework.
102    pub fn push_abbreviated<E: ErrorLoc>(&mut self, eloc: E, key: ErrorKey) {
103        let loc = eloc.into_loc();
104        if self.filter.should_maybe_print(key, loc) {
105            if loc.line == 0 {
106                _ = writeln!(self.output.get_mut(), "({key}) {}", loc.pathname().to_string_lossy());
107            } else if let Some(line) = self.cache.get_line(loc) {
108                _ = writeln!(self.output.get_mut(), "({key}) {line}");
109            }
110        }
111    }
112
113    /// Immediately print an error message. It is intended to introduce a following block of
114    /// messages printed with [`Errors::push_abbreviated`].
115    // TODO: integrate this function into the error reporting framework.
116    pub fn push_header(&mut self, _key: ErrorKey, msg: &str) {
117        _ = writeln!(self.output.get_mut(), "{msg}");
118    }
119
120    /// Extract the stored reports, sort them, and return them as a vector of [`LogReport`].
121    /// The stored reports will be left empty.
122    pub fn take_reports(&mut self) -> Vec<LogReport> {
123        let mut reports: Vec<LogReport> = take(&mut self.storage).into_iter().collect();
124        reports.sort_unstable_by(|a, b| {
125            // Severity in descending order
126            let mut cmp = b.severity.cmp(&a.severity);
127            if cmp != Ordering::Equal {
128                return cmp;
129            }
130            // Confidence in descending order too
131            cmp = b.confidence.cmp(&a.confidence);
132            if cmp != Ordering::Equal {
133                return cmp;
134            }
135            // If severity and confidence are the same, order by loc. Check all locs in order.
136            for (a, b) in a.pointers.iter().zip(b.pointers.iter()) {
137                cmp = a.loc.cmp(&b.loc);
138                if cmp != Ordering::Equal {
139                    return cmp;
140                }
141            }
142            // Shorter chain goes first, if it comes to that.
143            cmp = b.pointers.len().cmp(&a.pointers.len());
144            if cmp != Ordering::Equal {
145                return cmp;
146            }
147            // Fallback: order by message text.
148            if cmp == Ordering::Equal {
149                cmp = a.msg.cmp(&b.msg);
150            }
151            cmp
152        });
153        reports
154    }
155
156    /// Print all the stored reports to the error output.
157    /// Set `json` if they should be printed as a JSON array. Otherwise they are printed in the
158    /// default output format.
159    ///
160    /// Note that the default output format is not stable across versions. It is meant for human
161    /// readability and occasionally gets changed to improve that.
162    pub fn emit_reports(&mut self, json: bool) {
163        let reports = self.take_reports();
164        if json {
165            _ = writeln!(self.output.get_mut(), "[");
166            let mut first = true;
167            for report in &reports {
168                if !first {
169                    _ = writeln!(self.output.get_mut(), ",");
170                }
171                first = false;
172                log_report_json(self, report);
173            }
174            _ = writeln!(self.output.get_mut(), "\n]");
175        } else {
176            for report in &reports {
177                log_report(self, report);
178            }
179        }
180    }
181
182    pub fn store_source_file(&mut self, fullpath: PathBuf, source: &'static str) {
183        self.cache.filecache.insert(fullpath, source);
184    }
185
186    /// Get a mutable lock on the global ERRORS struct.
187    ///
188    /// # Panics
189    /// May panic when the mutex has been poisoned by another thread.
190    pub fn get_mut() -> MutexGuard<'static, Errors> {
191        ERRORS.lock().unwrap()
192    }
193
194    /// Like [`Errors::get_mut`] but intended for read-only access.
195    ///
196    /// Currently there is no difference, but if the locking mechanism changes there may be a
197    /// difference.
198    ///
199    /// # Panics
200    /// May panic when the mutex has been poisoned by another thread.
201    pub fn get() -> MutexGuard<'static, Errors> {
202        ERRORS.lock().unwrap()
203    }
204}
205
206#[derive(Debug, Default)]
207pub(crate) struct Cache {
208    /// Files that have been read in to get the lines where errors occurred.
209    /// Cached here to avoid duplicate I/O and UTF-8 parsing.
210    filecache: TigerHashMap<PathBuf, &'static str>,
211
212    /// Files that have been linesplit, cached to avoid doing that work again
213    linecache: TigerHashMap<PathBuf, Vec<&'static str>>,
214}
215
216impl Cache {
217    /// Fetch the contents of a single line from a script file.
218    pub(crate) fn get_line(&mut self, loc: Loc) -> Option<&'static str> {
219        if loc.line == 0 {
220            return None;
221        }
222        let fullpath = loc.fullpath();
223        if let Some(lines) = self.linecache.get(fullpath) {
224            return lines.get(loc.line as usize - 1).copied();
225        }
226        if let Some(contents) = self.filecache.get(fullpath) {
227            let lines: Vec<_> = contents.lines().collect();
228            let line = lines.get(loc.line as usize - 1).copied();
229            self.linecache.insert(fullpath.to_path_buf(), lines);
230            return line;
231        }
232        let bytes = read(fullpath).ok()?;
233        // Try decoding it as UTF-8. If that succeeds without errors, use it, otherwise fall back
234        // to WINDOWS_1252. The decode method will do BOM stripping.
235        let contents = match UTF_8.decode(&bytes) {
236            (contents, _, false) => contents,
237            (_, _, true) => WINDOWS_1252.decode(&bytes).0,
238        };
239        let contents = leak(contents.into_owned());
240        self.filecache.insert(fullpath.to_path_buf(), contents);
241
242        let lines: Vec<_> = contents.lines().collect();
243        let line = lines.get(loc.line as usize - 1).copied();
244        self.linecache.insert(fullpath.to_path_buf(), lines);
245        line
246    }
247}
248
249/// Record a secondary mod to be loaded before the one being validated.
250/// `label` is what it should be called in the error reports; ideally only a few characters long.
251pub fn add_loaded_mod_root(label: String) {
252    let mut errors = Errors::get_mut();
253    errors.loaded_mods_labels.push(label);
254}
255
256/// Record a DLC directory from the vanilla installation.
257/// `label` is what it should be called in the error reports.
258pub fn add_loaded_dlc_root(label: String) {
259    let mut errors = Errors::get_mut();
260    errors.loaded_dlcs_labels.push(label);
261}
262
263/// Configure the error reports to be written to this file instead of to stdout.
264pub fn set_output_file(file: &Path) -> Result<()> {
265    let file = File::create(file)?;
266    Errors::get_mut().output = RefCell::new(Box::new(file));
267    Ok(())
268}
269
270/// Store an error report to be emitted when [`emit_reports`] is called.
271pub fn log(mut report: LogReport) {
272    let mut vec = Vec::new();
273    report.pointers.drain(..).for_each(|pointer| {
274        let index = vec.len();
275        recursive_pointed_msg_expansion(&mut vec, &pointer);
276        vec.insert(index, pointer);
277    });
278    report.pointers.extend(vec);
279    Errors::get_mut().push_report(report);
280}
281
282/// Expand `PointedMessage` recursively.
283/// That is; for the given `PointedMessage`, follow its location's link until such link is no
284/// longer available, adding a newly created `PointedMessage` to the given `Vec` for each linked
285/// location.
286fn recursive_pointed_msg_expansion(vec: &mut Vec<PointedMessage>, pointer: &PointedMessage) {
287    if let Some(link) = pointer.loc.link_idx {
288        let from_here = PointedMessage {
289            loc: MACRO_MAP.get_loc(link).unwrap(),
290            length: 0,
291            msg: Some("from here".to_owned()),
292        };
293        let index = vec.len();
294        recursive_pointed_msg_expansion(vec, &from_here);
295        vec.insert(index, from_here);
296    }
297}
298
299/// Tests whether the report might be printed. If false, the report will definitely not be printed.
300pub fn will_maybe_log<E: ErrorLoc>(eloc: E, key: ErrorKey) -> bool {
301    Errors::get().filter.should_maybe_print(key, eloc.into_loc())
302}
303
304/// Print all the stored reports to the error output.
305/// Set `json` if they should be printed as a JSON array. Otherwise they are printed in the
306/// default output format.
307///
308/// Note that the default output format is not stable across versions. It is meant for human
309/// readability and occasionally gets changed to improve that.
310pub fn emit_reports(json: bool) {
311    Errors::get_mut().emit_reports(json);
312}
313
314/// Extract the stored reports, sort them, and return them as a vector of [`LogReport`].
315/// The stored reports will be left empty.
316pub fn take_reports() -> Vec<LogReport> {
317    Errors::get_mut().take_reports()
318}
319
320pub fn store_source_file(fullpath: PathBuf, source: &'static str) {
321    Errors::get_mut().store_source_file(fullpath, source);
322}
323
324// =================================================================================================
325// =============== Deprecated legacy calls to submit reports:
326// =================================================================================================
327
328/// Immediately print an error message. It is intended to introduce a following block of
329/// messages printed with [`warn_abbreviated`].
330pub(crate) fn warn_header(key: ErrorKey, msg: &str) {
331    Errors::get_mut().push_header(key, msg);
332}
333
334/// Immediately log a single-line report about this error.
335///
336/// This is intended for voluminous almost-identical errors, such as from the "unused
337/// localization" check.
338pub(crate) fn warn_abbreviated<E: ErrorLoc>(eloc: E, key: ErrorKey) {
339    Errors::get_mut().push_abbreviated(eloc, key);
340}
341
342// =================================================================================================
343// =============== Configuration (Output style):
344// =================================================================================================
345
346/// Override the default `OutputStyle`. (Controls ansi colors)
347pub fn set_output_style(style: OutputStyle) {
348    Errors::get_mut().styles = style;
349}
350
351/// Disable color in the output.
352pub fn disable_ansi_colors() {
353    Errors::get_mut().styles = OutputStyle::no_color();
354}
355
356// =================================================================================================
357// =============== Configuration (Filter):
358// =================================================================================================
359
360/// Configure the error reporter to show errors that are in the base game code.
361/// Normally those are filtered out, to only show errors that involve the mod's code.
362pub fn set_show_vanilla(v: bool) {
363    Errors::get_mut().filter.show_vanilla = v;
364}
365
366/// Configure the error reporter to show errors that are in extra loaded mods.
367/// Normally those are filtered out, to only show errors that involve the mod's code.
368pub fn set_show_loaded_mods(v: bool) {
369    Errors::get_mut().filter.show_loaded_mods = v;
370}
371
372/// Configure the error reporter to only show errors that match this [`FilterRule`].
373pub(crate) fn set_predicate(predicate: FilterRule) {
374    Errors::get_mut().filter.predicate = predicate;
375}