1use std::borrow::Cow;
4use std::cell::RefCell;
5use std::cmp::{min_by, Ordering};
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::{leak, Loc};
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, mut pointers): LogReport) {
320 let mut vec = Vec::new();
321 pointers.drain(..).for_each(|pointer| {
322 let index = vec.len();
323 recursive_pointed_msg_expansion(&mut vec, &pointer);
324 vec.insert(index, pointer);
325 });
326 pointers.extend(vec);
327 Errors::get_mut().push_report(report, pointers);
328}
329
330fn recursive_pointed_msg_expansion(vec: &mut LogReportPointers, pointer: &PointedMessage) {
335 if let Some(link) = pointer.loc.link_idx {
336 let from_here = PointedMessage {
337 loc: MACRO_MAP.get_loc(link).unwrap(),
338 length: 1,
339 msg: Some("from here".to_owned()),
340 };
341 let index = vec.len();
342 recursive_pointed_msg_expansion(vec, &from_here);
343 vec.insert(index, from_here);
344 }
345}
346
347pub fn will_maybe_log<E: ErrorLoc>(eloc: E, key: ErrorKey) -> bool {
349 Errors::get().filter.should_maybe_print(key, eloc.into_loc())
350}
351
352pub fn emit_reports<O: Write + Send>(
361 output: &mut O,
362 json: bool,
363 consolidate: bool,
364 summary: bool,
365) -> bool {
366 Errors::get_mut().emit_reports(output, json, consolidate, summary)
367}
368
369pub fn take_reports() -> TigerHashMap<LogReportMetadata, TigerHashSet<LogReportPointers>> {
374 take(&mut Errors::get_mut().storage)
375}
376
377pub fn store_source_file(fullpath: PathBuf, source: &'static str) {
378 Errors::get_mut().store_source_file(fullpath, source);
379}
380
381pub fn register_ignore_filter<R>(pathname: &'static Path, lines: R, filter: IgnoreFilter)
382where
383 R: RangeBounds<u32>,
384{
385 let start = lines.start_bound().cloned();
386 let end = lines.end_bound().cloned();
387 let entry = IgnoreEntry { start, end, filter };
388 Errors::get_mut().ignore.entry(pathname).or_default().push(entry);
389}
390
391pub fn set_output_style(style: OutputStyle) {
397 Errors::get_mut().styles = style;
398}
399
400pub fn disable_ansi_colors() {
402 Errors::get_mut().styles = OutputStyle::no_color();
403}
404
405pub fn set_show_vanilla(v: bool) {
412 Errors::get_mut().filter.show_vanilla = v;
413}
414
415pub fn set_show_loaded_mods(v: bool) {
418 Errors::get_mut().filter.show_loaded_mods = v;
419}
420
421pub(crate) fn set_predicate(predicate: FilterRule) {
423 Errors::get_mut().filter.predicate = predicate;
424}