use codespan_reporting::diagnostic::{Diagnostic, Label, Severity};
use globetrotter_model::diagnostics::Span;
use std::path::PathBuf;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Tally {
pub errors: usize,
pub warnings: usize,
}
impl Tally {
pub fn record(&mut self, severity: Severity) {
match severity {
Severity::Bug | Severity::Error => self.errors += 1,
Severity::Warning => self.warnings += 1,
Severity::Note | Severity::Help => {}
}
}
#[must_use]
pub fn has_errors(self) -> bool {
self.errors > 0
}
#[must_use]
pub fn has_issues(self) -> bool {
self.errors > 0 || self.warnings > 0
}
pub fn fail_on_errors(self) -> Result<Self, FailedWithErrors> {
if self.has_errors() {
Err(FailedWithErrors(self))
} else {
Ok(self)
}
}
}
impl std::ops::AddAssign for Tally {
fn add_assign(&mut self, other: Self) {
self.errors += other.errors;
self.warnings += other.warnings;
}
}
impl std::fmt::Display for Tally {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.errors > 0 {
write!(f, "{} {} and ", self.errors, plural(self.errors, "error"))?;
}
write!(f, "{} {}", self.warnings, plural(self.warnings, "warning"))
}
}
fn plural(count: usize, noun: &'static str) -> String {
if count == 1 {
noun.to_string()
} else {
format!("{noun}s")
}
}
#[derive(thiserror::Error, Debug)]
#[error("{path}: {inner}")]
pub struct IoError {
pub path: PathBuf,
pub inner: std::io::Error,
}
impl IoError {
pub fn new(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self {
inner: source,
path: path.into(),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum OutputError {
#[error("failed to generate JSON output")]
Json(#[from] crate::json::JsonOutputError),
#[cfg(feature = "typescript")]
#[error("failed to generate typescript output")]
Typescript(#[from] crate::target::TypescriptOutputError),
#[cfg(feature = "rust")]
#[error("failed to generate rust output")]
Rust(#[from] crate::target::RustOutputError),
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("invalid glob pattern {path:?}")]
Pattern {
#[source]
source: glob::PatternError,
path: String,
},
#[error("failed to glob for pattern {path}")]
Glob {
#[source]
source: glob::GlobError,
path: String,
},
#[error(transparent)]
Io(#[from] IoError),
#[error(transparent)]
Output(#[from] OutputError),
#[error(transparent)]
Toml(#[from] crate::model::toml::Error),
#[error(transparent)]
Failed(#[from] FailedWithErrors),
#[error(transparent)]
Task(#[from] tokio::task::JoinError),
#[error("failed to emit diagnostic")]
Diagnostic(#[from] codespan_reporting::files::Error),
#[cfg(feature = "llm-judge")]
#[error(transparent)]
LlmJudge(#[from] globetrotter_llm_judge::Error),
}
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error("globetrotter failed with {0}")]
pub struct FailedWithErrors(pub Tally);
#[derive(thiserror::Error, Debug)]
#[error("duplicate key {key:?}")]
pub struct DuplicateKeyError<F: Copy + PartialEq> {
pub key: String,
pub occurrences: Vec<(Span, F)>,
}
impl<F> DuplicateKeyError<F>
where
F: Copy + PartialEq,
{
#[must_use]
pub fn to_diagnostics(&self, all: bool) -> Vec<Diagnostic<F>> {
let mut labels = vec![];
match self.occurrences.split_last() {
None => {
}
Some((last, rest)) => {
if all {
labels.extend(rest.iter().map(|(span, file_id)| {
Label::secondary(*file_id, span.clone())
.with_message(format!("previous use of key `{}`", self.key))
}));
} else if let Some((span, file_id)) = rest.last() {
let label = Label::secondary(*file_id, span.clone()).with_message(format!(
"first use of key `{}`{}",
self.key,
if rest.len() > 1 {
format!(" (duplicated {} more time)", rest.len() - 1)
} else {
String::new()
},
));
labels.push(label);
}
let (span, file_id) = last;
labels.push(
Label::primary(*file_id, span.clone())
.with_message("cannot set the same key twice"),
);
}
}
vec![
Diagnostic::error()
.with_message(format!("duplicate key `{}`", self.key))
.with_labels(labels),
]
}
}
#[cfg(test)]
mod tests {
use super::Tally;
use codespan_reporting::diagnostic::Severity;
#[test_util::test]
fn tally_counts_and_pluralizes() {
let mut tally = Tally::default();
for severity in [
Severity::Error,
Severity::Warning,
Severity::Warning,
Severity::Note,
Severity::Help,
Severity::Bug,
] {
tally.record(severity);
}
assert_eq!(tally.to_string(), "2 errors and 2 warnings");
assert!(tally.has_errors());
assert!(tally.fail_on_errors().is_err());
let one = Tally {
errors: 1,
warnings: 0,
};
assert_eq!(one.to_string(), "1 error and 0 warnings");
let warnings_only = Tally {
errors: 0,
warnings: 1,
};
assert_eq!(warnings_only.to_string(), "1 warning");
assert!(warnings_only.has_issues());
assert_eq!(warnings_only.fail_on_errors(), Ok(warnings_only));
}
}