use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp::{Ordering, min_by};
use std::fs::read;
use std::io::Write;
use std::iter::{empty, once};
use std::mem::take;
use std::ops::{Bound, RangeBounds};
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex, MutexGuard};
use encoding_rs::{UTF_8, WINDOWS_1252};
use crate::helpers::{TigerHashMap, TigerHashSet};
use crate::macros::MACRO_MAP;
use crate::parse::ignore::IgnoreFilter;
use crate::report::error_loc::ErrorLoc;
use crate::report::filter::ReportFilter;
use crate::report::suppress::{Suppression, SuppressionKey};
use crate::report::writer::{log_report, log_summary};
use crate::report::writer_json::log_report_json;
use crate::report::{
ErrorKey, FilterRule, LogReport, LogReportMetadata, LogReportPointers, LogReportStyle,
OutputStyle, PointedMessage,
};
use crate::set;
use crate::token::Loc;
static LOG_ONCE: LazyLock<TigerHashSet<ErrorKey>> = LazyLock::new(|| {
set!([
ErrorKey::MissingFile,
ErrorKey::MissingItem,
ErrorKey::MissingLocalization,
ErrorKey::MissingPerspective,
ErrorKey::MissingSound,
])
});
static ERRORS: LazyLock<Mutex<Errors>> = LazyLock::new(|| Mutex::new(Errors::default()));
#[allow(missing_debug_implementations)]
#[derive(Default)]
pub struct Errors<'a> {
pub(crate) loaded_mods_labels: Vec<String>,
pub(crate) loaded_dlcs_labels: Vec<String>,
pub(crate) cache: Cache,
pub(crate) filter: ReportFilter,
pub(crate) styles: OutputStyle,
pub(crate) suppress: TigerHashMap<SuppressionKey<'a>, Vec<Suppression>>,
ignore: TigerHashMap<&'a Path, Vec<IgnoreEntry>>,
storage: TigerHashMap<LogReportMetadata, TigerHashSet<LogReportPointers>>,
}
impl Errors<'_> {
fn should_suppress(&self, report: &LogReportMetadata, pointers: &LogReportPointers) -> bool {
let key = SuppressionKey { key: report.key, message: Cow::Borrowed(&report.msg) };
if let Some(v) = self.suppress.get(&key) {
for suppression in v {
if suppression.len() != pointers.len() {
continue;
}
for (s, p) in suppression.iter().zip(pointers.iter()) {
if s.path == p.loc.pathname()
&& s.tag == p.msg
&& s.line.as_deref() == self.cache.get_line(p.loc)
{
return true;
}
}
}
}
false
}
fn should_ignore(&self, report: &LogReportMetadata, pointers: &LogReportPointers) -> bool {
for p in pointers {
if let Some(vec) = self.ignore.get(p.loc.pathname()) {
for entry in vec {
if (entry.start, entry.end).contains(&p.loc.line)
&& entry.filter.matches(report.key, &report.msg)
{
return true;
}
}
}
}
false
}
fn push_report(&mut self, report: LogReportMetadata, pointers: LogReportPointers) {
if !self.filter.should_print_report(&report, &pointers)
|| self.should_suppress(&report, &pointers)
{
return;
}
self.storage.entry(report).or_default().insert(pointers);
}
pub fn flatten_reports(
&self,
consolidate: bool,
) -> Vec<(&LogReportMetadata, Cow<'_, LogReportPointers>, usize)> {
let mut reports: Vec<_> = self
.storage
.iter()
.flat_map(|(report, occurrences)| -> Box<dyn Iterator<Item = _>> {
let mut iterator =
occurrences.iter().filter(|pointers| !self.should_ignore(report, pointers));
match report.style {
LogReportStyle::Full => {
if consolidate && LOG_ONCE.contains(&report.key) {
if let Some(initial) = iterator.next() {
let (pointers, additional_count) = iterator.fold(
(initial, 0usize),
|(first_occurrence, count), e| {
(
min_by(first_occurrence, e, |a, b| {
a.iter().map(|e| e.loc).cmp(b.iter().map(|e| e.loc))
}),
count + 1,
)
},
);
Box::new(once((report, Cow::Borrowed(pointers), additional_count)))
} else {
Box::new(empty())
}
} else {
Box::new(
iterator.map(move |pointers| (report, Cow::Borrowed(pointers), 0)),
)
}
}
LogReportStyle::Abbreviated => {
let mut pointers: Vec<_> = iterator.map(|o| o[0].clone()).collect();
pointers.sort_unstable_by_key(|p| p.loc);
Box::new(once((report, Cow::Owned(pointers), 0)))
}
}
})
.collect();
reports.sort_unstable_by(|(a, ap, _), (b, bp, _)| {
let mut cmp = b.severity.cmp(&a.severity);
if cmp != Ordering::Equal {
return cmp;
}
cmp = b.confidence.cmp(&a.confidence);
if cmp != Ordering::Equal {
return cmp;
}
cmp = ap.iter().map(|e| e.loc).cmp(bp.iter().map(|e| e.loc));
if cmp == Ordering::Equal {
cmp = a.msg.cmp(&b.msg);
}
cmp
});
reports
}
pub fn emit_reports<O: Write + Send>(
&mut self,
output: &mut O,
json: bool,
consolidate: bool,
summary: bool,
) -> bool {
let reports = self.flatten_reports(consolidate);
let result = !reports.is_empty();
if json {
_ = writeln!(output, "[");
let mut first = true;
for (report, pointers, _) in &reports {
if !first {
_ = writeln!(output, ",");
}
first = false;
log_report_json(self, output, report, pointers);
}
_ = writeln!(output, "\n]");
} else {
for (report, pointers, additional) in &reports {
log_report(self, output, report, pointers, *additional);
}
if summary {
log_summary(output, &self.styles, &reports);
}
}
self.storage.clear();
result
}
pub fn store_source_file(&mut self, fullpath: PathBuf, source: &'static str) {
self.cache.filecache.borrow_mut().insert(fullpath, source);
}
pub fn get_mut() -> MutexGuard<'static, Errors<'static>> {
ERRORS.lock().unwrap()
}
pub fn get() -> MutexGuard<'static, Errors<'static>> {
ERRORS.lock().unwrap()
}
}
#[derive(Debug, Default)]
pub(crate) struct Cache {
filecache: RefCell<TigerHashMap<PathBuf, &'static str>>,
linecache: RefCell<TigerHashMap<PathBuf, Vec<&'static str>>>,
}
impl Cache {
pub(crate) fn get_line(&self, loc: Loc) -> Option<&'static str> {
let mut filecache = self.filecache.borrow_mut();
let mut linecache = self.linecache.borrow_mut();
if loc.line == 0 {
return None;
}
let fullpath = loc.fullpath();
if let Some(lines) = linecache.get(fullpath) {
return lines.get(loc.line as usize - 1).copied();
}
if let Some(contents) = filecache.get(fullpath) {
let lines: Vec<_> = contents.lines().collect();
let line = lines.get(loc.line as usize - 1).copied();
linecache.insert(fullpath.to_path_buf(), lines);
return line;
}
let bytes = read(fullpath).ok()?;
let contents = match UTF_8.decode(&bytes) {
(contents, _, false) => contents,
(_, _, true) => WINDOWS_1252.decode(&bytes).0,
};
let contents = contents.into_owned().leak();
filecache.insert(fullpath.to_path_buf(), contents);
let lines: Vec<_> = contents.lines().collect();
let line = lines.get(loc.line as usize - 1).copied();
linecache.insert(fullpath.to_path_buf(), lines);
line
}
}
#[derive(Debug, Clone)]
struct IgnoreEntry {
start: Bound<u32>,
end: Bound<u32>,
filter: IgnoreFilter,
}
pub fn add_loaded_mod_root(label: String) {
let mut errors = Errors::get_mut();
errors.loaded_mods_labels.push(label);
}
pub fn add_loaded_dlc_root(label: String) {
let mut errors = Errors::get_mut();
errors.loaded_dlcs_labels.push(label);
}
pub fn log((report, pointers): LogReport) {
let pointers = pointed_msg_expansion(pointers);
Errors::get_mut().push_report(report, pointers);
}
fn pointed_msg_expansion(pointers: Vec<PointedMessage>) -> Vec<PointedMessage> {
pointers
.into_iter()
.flat_map(|p| {
let mut next_loc = Some(p.loc);
let mut first = true;
std::iter::from_fn(move || match next_loc {
Some(mut stack) => {
next_loc = stack.link_idx.and_then(|idx| MACRO_MAP.get_loc(idx));
stack.link_idx = None;
let next = if first {
PointedMessage { loc: stack, length: p.length, msg: p.msg.clone() }
} else {
PointedMessage { loc: stack, length: 1, msg: Some("from here".into()) }
};
first = false;
Some(next)
}
None => None,
})
})
.collect()
}
pub fn will_maybe_log<E: ErrorLoc>(eloc: E, key: ErrorKey) -> bool {
Errors::get().filter.should_maybe_print(key, eloc.into_loc())
}
pub fn emit_reports<O: Write + Send>(
output: &mut O,
json: bool,
consolidate: bool,
summary: bool,
) -> bool {
Errors::get_mut().emit_reports(output, json, consolidate, summary)
}
pub fn take_reports() -> TigerHashMap<LogReportMetadata, TigerHashSet<LogReportPointers>> {
take(&mut Errors::get_mut().storage)
}
pub fn store_source_file(fullpath: PathBuf, source: &'static str) {
Errors::get_mut().store_source_file(fullpath, source);
}
pub fn register_ignore_filter<R>(pathname: &'static Path, lines: R, filter: IgnoreFilter)
where
R: RangeBounds<u32>,
{
let start = lines.start_bound().cloned();
let end = lines.end_bound().cloned();
let entry = IgnoreEntry { start, end, filter };
Errors::get_mut().ignore.entry(pathname).or_default().push(entry);
}
pub fn set_output_style(style: OutputStyle) {
Errors::get_mut().styles = style;
}
pub fn disable_ansi_colors() {
Errors::get_mut().styles = OutputStyle::no_color();
}
pub fn set_show_vanilla(v: bool) {
Errors::get_mut().filter.show_vanilla = v;
}
pub fn set_show_loaded_mods(v: bool) {
Errors::get_mut().filter.show_loaded_mods = v;
}
pub(crate) fn set_predicate(predicate: FilterRule) {
Errors::get_mut().filter.predicate = predicate;
}