use std::cell::RefCell;
use std::cmp::Ordering;
use std::fs::{read, File};
use std::io::{stdout, Write};
use std::mem::take;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
use anyhow::Result;
use encoding::all::{UTF_8, WINDOWS_1252};
use encoding::{DecoderTrap, Encoding};
use fnv::{FnvHashMap, FnvHashSet};
use once_cell::sync::Lazy;
use crate::fileset::FileKind;
use crate::report::error_loc::ErrorLoc;
use crate::report::filter::ReportFilter;
use crate::report::writer::log_report;
use crate::report::writer_json::log_report_json;
use crate::report::{
err, tips, warn, ErrorKey, FilterRule, LogReport, OutputStyle, PointedMessage,
};
use crate::token::Loc;
static ERRORS: Lazy<Mutex<Errors>> = Lazy::new(|| Mutex::new(Errors::default()));
#[allow(missing_debug_implementations)]
pub struct Errors {
pub(crate) output: RefCell<Box<dyn Write + Send>>,
vanilla_root: PathBuf,
clausewitz_root: PathBuf,
jomini_root: PathBuf,
mod_root: PathBuf,
loaded_mods: Vec<PathBuf>,
pub(crate) loaded_mods_labels: Vec<String>,
filecache: FnvHashMap<PathBuf, String>,
pub(crate) filter: ReportFilter,
pub(crate) styles: OutputStyle,
pub(crate) max_line_length: Option<usize>,
storage: FnvHashSet<LogReport>,
}
impl Default for Errors {
fn default() -> Self {
Errors {
output: RefCell::new(Box::new(stdout())),
vanilla_root: PathBuf::default(),
clausewitz_root: PathBuf::default(),
jomini_root: PathBuf::default(),
mod_root: PathBuf::default(),
loaded_mods: Vec::default(),
loaded_mods_labels: Vec::default(),
filecache: FnvHashMap::default(),
filter: ReportFilter::default(),
styles: OutputStyle::default(),
max_line_length: None,
storage: FnvHashSet::default(),
}
}
}
impl Errors {
pub(crate) fn get_fullpath(&mut self, kind: FileKind, path: &Path) -> PathBuf {
match kind {
FileKind::Internal => path.to_path_buf(),
FileKind::Clausewitz => self.clausewitz_root.join(path),
FileKind::Jomini => self.jomini_root.join(path),
FileKind::Vanilla => self.vanilla_root.join(path),
FileKind::LoadedMod(idx) => self.loaded_mods[idx as usize].join(path),
FileKind::Mod => self.mod_root.join(path),
}
}
pub(crate) fn get_line(&mut self, loc: &Loc) -> Option<String> {
if loc.line == 0 {
return None;
}
let pathname = self.get_fullpath(loc.kind, loc.pathname());
if let Some(contents) = self.filecache.get(&pathname) {
return contents.lines().nth(loc.line as usize - 1).map(str::to_string);
}
let bytes = read(&pathname).ok()?;
let contents = match UTF_8.decode(&bytes, DecoderTrap::Strict) {
Ok(contents) => contents,
Err(_) => WINDOWS_1252.decode(&bytes, DecoderTrap::Strict).ok()?,
};
let line = contents.lines().nth(loc.line as usize - 1).map(str::to_string);
self.filecache.insert(pathname, contents);
line
}
fn push_report(&mut self, report: LogReport) {
if !self.filter.should_print_report(&report) {
return;
}
self.storage.insert(report);
}
pub fn log_abbreviated(&mut self, loc: &Loc, key: ErrorKey) {
if loc.line == 0 {
_ = writeln!(self.output.get_mut(), "({key}) {}", loc.pathname().to_string_lossy());
} else if let Some(line) = self.get_line(loc) {
_ = writeln!(self.output.get_mut(), "({key}) {line}");
}
}
pub fn push_abbreviated<E: ErrorLoc>(&mut self, eloc: E, key: ErrorKey) {
let loc = eloc.into_loc();
self.log_abbreviated(&loc, key);
}
pub fn push_header(&mut self, _key: ErrorKey, msg: &str) {
_ = writeln!(self.output.get_mut(), "{msg}");
}
pub fn emit_reports(&mut self, json: bool) {
let mut reports: Vec<LogReport> = take(&mut self.storage).into_iter().collect();
reports.sort_unstable_by(|a, b| {
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;
}
for (a, b) in a.pointers.iter().zip(b.pointers.iter()) {
cmp = a.loc.cmp(&b.loc);
if cmp != Ordering::Equal {
return cmp;
}
}
cmp = b.pointers.len().cmp(&a.pointers.len());
if cmp != Ordering::Equal {
return cmp;
}
if cmp == Ordering::Equal {
cmp = a.msg.cmp(&b.msg);
}
cmp
});
if json {
_ = writeln!(self.output.get_mut(), "[");
let mut first = true;
for report in &reports {
if !first {
_ = writeln!(self.output.get_mut(), ",");
}
first = false;
log_report_json(self, report);
}
_ = writeln!(self.output.get_mut(), "\n]");
} else {
for report in &reports {
log_report(self, report);
}
}
}
pub fn get_mut() -> MutexGuard<'static, Errors> {
ERRORS.lock().unwrap()
}
pub fn get() -> MutexGuard<'static, Errors> {
ERRORS.lock().unwrap()
}
}
pub fn set_vanilla_dir(dir: PathBuf) {
let mut game = dir.clone();
game.push("game");
Errors::get_mut().vanilla_root = game;
let mut clausewitz = dir.clone();
clausewitz.push("clausewitz");
Errors::get_mut().clausewitz_root = clausewitz;
let mut jomini = dir;
jomini.push("jomini");
Errors::get_mut().jomini_root = jomini;
}
pub fn set_mod_root(root: PathBuf) {
Errors::get_mut().mod_root = root;
}
pub fn add_loaded_mod_root(label: String, root: PathBuf) {
let mut errors = Errors::get_mut();
errors.loaded_mods_labels.push(label);
errors.loaded_mods.push(root);
}
pub fn set_output_file(file: &Path) -> Result<()> {
let file = File::create(file)?;
Errors::get_mut().output = RefCell::new(Box::new(file));
Ok(())
}
pub fn log(mut report: LogReport) {
let mut vec = Vec::new();
report.pointers.drain(..).for_each(|pointer| {
let index = vec.len();
recursive_pointed_msg_expansion(&mut vec, &pointer);
vec.insert(index, pointer);
});
report.pointers.extend(vec);
Errors::get_mut().push_report(report);
}
fn recursive_pointed_msg_expansion(vec: &mut Vec<PointedMessage>, pointer: &PointedMessage) {
if let Some(link) = &pointer.loc.link {
let from_here = PointedMessage {
loc: link.as_ref().into_loc(),
length: 0,
msg: Some("from here".to_owned()),
};
let index = vec.len();
recursive_pointed_msg_expansion(vec, &from_here);
vec.insert(index, from_here);
}
}
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(json: bool) {
Errors::get_mut().emit_reports(json);
}
pub fn error<E: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str) {
err(key).msg(msg).loc(eloc).push();
}
pub fn error_info<E: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str, info: &str) {
err(key).msg(msg).info(info).loc(eloc).push();
}
pub fn old_warn<E: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str) {
warn(key).msg(msg).loc(eloc).push();
}
pub fn warn2<E: ErrorLoc, F: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str, eloc2: F, msg2: &str) {
warn(key).msg(msg).loc(eloc).loc(eloc2, msg2).push();
}
pub fn warn3<E: ErrorLoc, E2: ErrorLoc, E3: ErrorLoc>(
eloc: E,
key: ErrorKey,
msg: &str,
eloc2: E2,
msg2: &str,
eloc3: E3,
msg3: &str,
) {
warn(key).msg(msg).loc(eloc).loc(eloc2, msg2).loc(eloc3, msg3).push();
}
pub fn warn_info<E: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str, info: &str) {
warn(key).msg(msg).info(info).loc(eloc).push();
}
pub fn advice<E: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str) {
tips(key).msg(msg).loc(eloc).push();
}
pub fn advice2<E: ErrorLoc, F: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str, eloc2: F, msg2: &str) {
tips(key).msg(msg).loc(eloc).loc(eloc2, msg2).push();
}
pub fn advice_info<E: ErrorLoc>(eloc: E, key: ErrorKey, msg: &str, info: &str) {
tips(key).msg(msg).info(info).loc(eloc).push();
}
pub fn warn_header(key: ErrorKey, msg: &str) {
Errors::get_mut().push_header(key, msg);
}
pub fn warn_abbreviated<E: ErrorLoc>(eloc: E, key: ErrorKey) {
Errors::get_mut().push_abbreviated(eloc, key);
}
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_max_line_length(max_line_length: usize) {
Errors::get_mut().max_line_length =
if max_line_length == 0 { None } else { Some(max_line_length) };
}
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 fn set_predicate(predicate: FilterRule) {
Errors::get_mut().filter.predicate = predicate;
}