1use std::borrow::Cow;
4use std::cell::RefCell;
5use std::cmp::{Ordering, min_by};
6use std::fs::read;
7use std::io::Write;
8use std::iter::{empty, once};
9use std::mem::take;
10use std::ops::{Bound, RangeBounds};
11use std::path::{Path, PathBuf};
12use std::sync::{LazyLock, Mutex, MutexGuard};
13
14use encoding_rs::{UTF_8, WINDOWS_1252};
15
16use crate::helpers::{TigerHashMap, TigerHashSet};
17use crate::macros::MACRO_MAP;
18use crate::parse::ignore::IgnoreFilter;
19use crate::report::error_loc::ErrorLoc;
20use crate::report::filter::ReportFilter;
21use crate::report::suppress::{Suppression, SuppressionKey};
22use crate::report::writer::{log_report, log_summary};
23use crate::report::writer_json::log_report_json;
24use crate::report::{
25 ErrorKey, FilterRule, LogReport, LogReportMetadata, LogReportPointers, LogReportStyle,
26 OutputStyle, PointedMessage,
27};
28use crate::set;
29use crate::token::{Loc, leak};
30
31static LOG_ONCE: LazyLock<TigerHashSet<ErrorKey>> = LazyLock::new(|| {
33 set!([
34 ErrorKey::MissingFile,
35 ErrorKey::MissingItem,
36 ErrorKey::MissingLocalization,
37 ErrorKey::MissingPerspective,
38 ErrorKey::MissingSound,
39 ])
40});
41
42static ERRORS: LazyLock<Mutex<Errors>> = LazyLock::new(|| Mutex::new(Errors::default()));
43
44#[allow(missing_debug_implementations)]
45#[derive(Default)]
46pub struct Errors<'a> {
47 pub(crate) loaded_mods_labels: Vec<String>,
49
50 pub(crate) loaded_dlcs_labels: Vec<String>,
52
53 pub(crate) cache: Cache,
54
55 pub(crate) filter: ReportFilter,
57
58 pub(crate) styles: OutputStyle,
60
61 pub(crate) suppress: TigerHashMap<SuppressionKey<'a>, Vec<Suppression>>,
62 ignore: TigerHashMap<&'a Path, Vec<IgnoreEntry>>,
65
66 storage: TigerHashMap<LogReportMetadata, TigerHashSet<LogReportPointers>>,
70}
71
72impl Errors<'_> {
73 fn should_suppress(&self, report: &LogReportMetadata, pointers: &LogReportPointers) -> bool {
74 let key = SuppressionKey { key: report.key, message: Cow::Borrowed(&report.msg) };
75 if let Some(v) = self.suppress.get(&key) {
76 for suppression in v {
77 if suppression.len() != pointers.len() {
78 continue;
79 }
80 for (s, p) in suppression.iter().zip(pointers.iter()) {
81 if s.path == p.loc.pathname()
82 && s.tag == p.msg
83 && s.line.as_deref() == self.cache.get_line(p.loc)
84 {
85 return true;
86 }
87 }
88 }
89 }
90 false
91 }
92
93 fn should_ignore(&self, report: &LogReportMetadata, pointers: &LogReportPointers) -> bool {
94 for p in pointers {
95 if let Some(vec) = self.ignore.get(p.loc.pathname()) {
96 for entry in vec {
97 if (entry.start, entry.end).contains(&p.loc.line)
98 && entry.filter.matches(report.key, &report.msg)
99 {
100 return true;
101 }
102 }
103 }
104 }
105 false
106 }
107
108 fn push_report(&mut self, report: LogReportMetadata, pointers: LogReportPointers) {
111 if !self.filter.should_print_report(&report, &pointers)
112 || self.should_suppress(&report, &pointers)
113 {
114 return;
115 }
116 self.storage.entry(report).or_default().insert(pointers);
117 }
118
119 pub fn flatten_reports(
121 &self,
122 consolidate: bool,
123 ) -> Vec<(&LogReportMetadata, Cow<'_, LogReportPointers>, usize)> {
124 let mut reports: Vec<_> = self
125 .storage
126 .iter()
127 .flat_map(|(report, occurrences)| -> Box<dyn Iterator<Item = _>> {
128 let mut iterator =
129 occurrences.iter().filter(|pointers| !self.should_ignore(report, pointers));
130 match report.style {
131 LogReportStyle::Full => {
132 if consolidate && LOG_ONCE.contains(&report.key) {
133 if let Some(initial) = iterator.next() {
134 let (pointers, additional_count) = iterator.fold(
135 (initial, 0usize),
136 |(first_occurrence, count), e| {
137 (
138 min_by(first_occurrence, e, |a, b| {
139 a.iter().map(|e| e.loc).cmp(b.iter().map(|e| e.loc))
140 }),
141 count + 1,
142 )
143 },
144 );
145 Box::new(once((report, Cow::Borrowed(pointers), additional_count)))
146 } else {
147 Box::new(empty())
148 }
149 } else {
150 Box::new(
151 iterator.map(move |pointers| (report, Cow::Borrowed(pointers), 0)),
152 )
153 }
154 }
155 LogReportStyle::Abbreviated => {
156 let mut pointers: Vec<_> = iterator.map(|o| o[0].clone()).collect();
157 pointers.sort_unstable_by_key(|p| p.loc);
158 Box::new(once((report, Cow::Owned(pointers), 0)))
159 }
160 }
161 })
162 .collect();
163 reports.sort_unstable_by(|(a, ap, _), (b, bp, _)| {
164 let mut cmp = b.severity.cmp(&a.severity);
166 if cmp != Ordering::Equal {
167 return cmp;
168 }
169 cmp = b.confidence.cmp(&a.confidence);
171 if cmp != Ordering::Equal {
172 return cmp;
173 }
174 cmp = ap.iter().map(|e| e.loc).cmp(bp.iter().map(|e| e.loc));
176 if cmp == Ordering::Equal {
178 cmp = a.msg.cmp(&b.msg);
179 }
180 cmp
181 });
182 reports
183 }
184
185 pub fn emit_reports<O: Write + Send>(
196 &mut self,
197 output: &mut O,
198 json: bool,
199 consolidate: bool,
200 summary: bool,
201 ) -> bool {
202 let reports = self.flatten_reports(consolidate);
203 let result = !reports.is_empty();
204 if json {
205 _ = writeln!(output, "[");
206 let mut first = true;
207 for (report, pointers, _) in &reports {
208 if !first {
209 _ = writeln!(output, ",");
210 }
211 first = false;
212 log_report_json(self, output, report, pointers);
213 }
214 _ = writeln!(output, "\n]");
215 } else {
216 for (report, pointers, additional) in &reports {
217 log_report(self, output, report, pointers, *additional);
218 }
219 if summary {
220 log_summary(output, &self.styles, &reports);
221 }
222 }
223 self.storage.clear();
224 result
225 }
226
227 pub fn store_source_file(&mut self, fullpath: PathBuf, source: &'static str) {
228 self.cache.filecache.borrow_mut().insert(fullpath, source);
229 }
230
231 pub fn get_mut() -> MutexGuard<'static, Errors<'static>> {
236 ERRORS.lock().unwrap()
237 }
238
239 pub fn get() -> MutexGuard<'static, Errors<'static>> {
247 ERRORS.lock().unwrap()
248 }
249}
250
251#[derive(Debug, Default)]
252pub(crate) struct Cache {
253 filecache: RefCell<TigerHashMap<PathBuf, &'static str>>,
256
257 linecache: RefCell<TigerHashMap<PathBuf, Vec<&'static str>>>,
259}
260
261impl Cache {
262 pub(crate) fn get_line(&self, loc: Loc) -> Option<&'static str> {
264 let mut filecache = self.filecache.borrow_mut();
265 let mut linecache = self.linecache.borrow_mut();
266
267 if loc.line == 0 {
268 return None;
269 }
270 let fullpath = loc.fullpath();
271 if let Some(lines) = linecache.get(fullpath) {
272 return lines.get(loc.line as usize - 1).copied();
273 }
274 if let Some(contents) = filecache.get(fullpath) {
275 let lines: Vec<_> = contents.lines().collect();
276 let line = lines.get(loc.line as usize - 1).copied();
277 linecache.insert(fullpath.to_path_buf(), lines);
278 return line;
279 }
280 let bytes = read(fullpath).ok()?;
281 let contents = match UTF_8.decode(&bytes) {
284 (contents, _, false) => contents,
285 (_, _, true) => WINDOWS_1252.decode(&bytes).0,
286 };
287 let contents = leak(contents.into_owned());
288 filecache.insert(fullpath.to_path_buf(), contents);
289
290 let lines: Vec<_> = contents.lines().collect();
291 let line = lines.get(loc.line as usize - 1).copied();
292 linecache.insert(fullpath.to_path_buf(), lines);
293 line
294 }
295}
296
297#[derive(Debug, Clone)]
298struct IgnoreEntry {
299 start: Bound<u32>,
300 end: Bound<u32>,
301 filter: IgnoreFilter,
302}
303
304pub fn add_loaded_mod_root(label: String) {
307 let mut errors = Errors::get_mut();
308 errors.loaded_mods_labels.push(label);
309}
310
311pub fn add_loaded_dlc_root(label: String) {
314 let mut errors = Errors::get_mut();
315 errors.loaded_dlcs_labels.push(label);
316}
317
318pub fn log((report, pointers): LogReport) {
320 let pointers = pointed_msg_expansion(pointers);
321 Errors::get_mut().push_report(report, pointers);
322}
323
324fn pointed_msg_expansion(pointers: Vec<PointedMessage>) -> Vec<PointedMessage> {
329 pointers
330 .into_iter()
331 .flat_map(|p| {
332 let mut next_loc = Some(p.loc);
333 let mut first = true;
334 std::iter::from_fn(move || match next_loc {
335 Some(mut stack) => {
336 next_loc = stack.link_idx.and_then(|idx| MACRO_MAP.get_loc(idx));
337 stack.link_idx = None;
338 let next = if first {
339 PointedMessage { loc: stack, length: p.length, msg: p.msg.clone() }
340 } else {
341 PointedMessage { loc: stack, length: 1, msg: Some("from here".into()) }
342 };
343 first = false;
344 Some(next)
345 }
346 None => None,
347 })
348 })
349 .collect()
350}
351
352pub fn will_maybe_log<E: ErrorLoc>(eloc: E, key: ErrorKey) -> bool {
354 Errors::get().filter.should_maybe_print(key, eloc.into_loc())
355}
356
357pub fn emit_reports<O: Write + Send>(
366 output: &mut O,
367 json: bool,
368 consolidate: bool,
369 summary: bool,
370) -> bool {
371 Errors::get_mut().emit_reports(output, json, consolidate, summary)
372}
373
374pub fn take_reports() -> TigerHashMap<LogReportMetadata, TigerHashSet<LogReportPointers>> {
379 take(&mut Errors::get_mut().storage)
380}
381
382pub fn store_source_file(fullpath: PathBuf, source: &'static str) {
383 Errors::get_mut().store_source_file(fullpath, source);
384}
385
386pub fn register_ignore_filter<R>(pathname: &'static Path, lines: R, filter: IgnoreFilter)
387where
388 R: RangeBounds<u32>,
389{
390 let start = lines.start_bound().cloned();
391 let end = lines.end_bound().cloned();
392 let entry = IgnoreEntry { start, end, filter };
393 Errors::get_mut().ignore.entry(pathname).or_default().push(entry);
394}
395
396pub fn set_output_style(style: OutputStyle) {
402 Errors::get_mut().styles = style;
403}
404
405pub fn disable_ansi_colors() {
407 Errors::get_mut().styles = OutputStyle::no_color();
408}
409
410pub fn set_show_vanilla(v: bool) {
417 Errors::get_mut().filter.show_vanilla = v;
418}
419
420pub fn set_show_loaded_mods(v: bool) {
423 Errors::get_mut().filter.show_loaded_mods = v;
424}
425
426pub(crate) fn set_predicate(predicate: FilterRule) {
428 Errors::get_mut().filter.predicate = predicate;
429}