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