1use 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 pub(crate) loaded_mods_labels: Vec<String>,
32
33 pub(crate) loaded_dlcs_labels: Vec<String>,
35
36 pub(crate) cache: Cache,
37
38 pub(crate) filter: ReportFilter,
40 pub(crate) styles: OutputStyle,
42
43 pub(crate) suppress: TigerHashMap<SuppressionKey, Vec<Suppression>>,
44
45 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 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 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 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 pub fn push_header(&mut self, _key: ErrorKey, msg: &str) {
117 _ = writeln!(self.output.get_mut(), "{msg}");
118 }
119
120 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 let mut cmp = b.severity.cmp(&a.severity);
127 if cmp != Ordering::Equal {
128 return cmp;
129 }
130 cmp = b.confidence.cmp(&a.confidence);
132 if cmp != Ordering::Equal {
133 return cmp;
134 }
135 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 cmp = b.pointers.len().cmp(&a.pointers.len());
144 if cmp != Ordering::Equal {
145 return cmp;
146 }
147 if cmp == Ordering::Equal {
149 cmp = a.msg.cmp(&b.msg);
150 }
151 cmp
152 });
153 reports
154 }
155
156 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 pub fn get_mut() -> MutexGuard<'static, Errors> {
191 ERRORS.lock().unwrap()
192 }
193
194 pub fn get() -> MutexGuard<'static, Errors> {
202 ERRORS.lock().unwrap()
203 }
204}
205
206#[derive(Debug, Default)]
207pub(crate) struct Cache {
208 filecache: TigerHashMap<PathBuf, &'static str>,
211
212 linecache: TigerHashMap<PathBuf, Vec<&'static str>>,
214}
215
216impl Cache {
217 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 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
249pub fn add_loaded_mod_root(label: String) {
252 let mut errors = Errors::get_mut();
253 errors.loaded_mods_labels.push(label);
254}
255
256pub fn add_loaded_dlc_root(label: String) {
259 let mut errors = Errors::get_mut();
260 errors.loaded_dlcs_labels.push(label);
261}
262
263pub 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
270pub 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
282fn 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
299pub fn will_maybe_log<E: ErrorLoc>(eloc: E, key: ErrorKey) -> bool {
301 Errors::get().filter.should_maybe_print(key, eloc.into_loc())
302}
303
304pub fn emit_reports(json: bool) {
311 Errors::get_mut().emit_reports(json);
312}
313
314pub 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
324pub(crate) fn warn_header(key: ErrorKey, msg: &str) {
331 Errors::get_mut().push_header(key, msg);
332}
333
334pub(crate) fn warn_abbreviated<E: ErrorLoc>(eloc: E, key: ErrorKey) {
339 Errors::get_mut().push_abbreviated(eloc, key);
340}
341
342pub fn set_output_style(style: OutputStyle) {
348 Errors::get_mut().styles = style;
349}
350
351pub fn disable_ansi_colors() {
353 Errors::get_mut().styles = OutputStyle::no_color();
354}
355
356pub fn set_show_vanilla(v: bool) {
363 Errors::get_mut().filter.show_vanilla = v;
364}
365
366pub fn set_show_loaded_mods(v: bool) {
369 Errors::get_mut().filter.show_loaded_mods = v;
370}
371
372pub(crate) fn set_predicate(predicate: FilterRule) {
374 Errors::get_mut().filter.predicate = predicate;
375}