use crate::{Code, Path, Span};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Severity {
Error,
Warning,
}
impl core::fmt::Display for Severity {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Self::Error => "error",
Self::Warning => "warning",
})
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Label {
pub span: Span,
pub message: String,
}
impl Label {
#[must_use]
pub fn new(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Applicability {
MachineApplicable,
MaybeIncorrect,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Replacement {
pub span: Span,
pub text: String,
}
impl Replacement {
#[must_use]
pub fn new(span: Span, text: impl Into<String>) -> Self {
Self {
span,
text: text.into(),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Suggestion {
pub message: String,
pub edits: Vec<Replacement>,
pub applicability: Applicability,
}
impl Suggestion {
#[must_use]
pub fn new(
message: impl Into<String>,
edits: Vec<Replacement>,
applicability: Applicability,
) -> Self {
Self {
message: message.into(),
edits,
applicability,
}
}
#[must_use]
pub fn apply(&self, source: &str) -> Option<String> {
let mut out = String::with_capacity(source.len());
let mut at = 0usize;
for edit in &self.edits {
if edit.span.start < at || edit.span.end < edit.span.start {
return None;
}
out.push_str(source.get(at..edit.span.start)?);
source.get(edit.span.start..edit.span.end)?;
out.push_str(&edit.text);
at = edit.span.end;
}
out.push_str(source.get(at..)?);
Some(out)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Diagnostic {
pub code: Code,
pub severity: Severity,
pub message: String,
pub primary: Label,
pub secondary: Vec<Label>,
pub help: Option<String>,
pub path: Option<Path>,
pub suggestions: Vec<Suggestion>,
}
impl Diagnostic {
#[must_use]
pub fn new(code: Code, severity: Severity, message: impl Into<String>, primary: Label) -> Self {
Self {
code,
severity,
message: message.into(),
primary,
secondary: Vec::new(),
help: None,
path: None,
suggestions: Vec::new(),
}
}
#[must_use]
pub fn error(code: Code, message: impl Into<String>, primary: Label) -> Self {
Self::new(code, Severity::Error, message, primary)
}
#[must_use]
pub fn warning(code: Code, message: impl Into<String>, primary: Label) -> Self {
Self::new(code, Severity::Warning, message, primary)
}
#[must_use]
pub fn with_secondary(mut self, label: Label) -> Self {
self.secondary.push(label);
self
}
#[must_use]
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
#[must_use]
pub fn with_path(mut self, path: Path) -> Self {
self.path = Some(path);
self
}
#[must_use]
pub fn with_suggestion(mut self, suggestion: Suggestion) -> Self {
self.suggestions.push(suggestion);
self
}
}
impl core::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}[{}]: {}", self.severity, self.code, self.message)?;
if let Some(path) = &self.path {
write!(f, " at {path}")?;
}
Ok(())
}
}
impl core::error::Error for Diagnostic {}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn builders_and_display() {
let code = Code::new(1003).unwrap();
let d = Diagnostic::warning(code, "ignored", Label::new(Span::new(0, 4), "here"))
.with_secondary(Label::new(Span::new(5, 6), "also"))
.with_help("remove it")
.with_path(Path::new().with_key("a").with_index(2));
assert_eq!(d.severity, Severity::Warning);
assert_eq!(d.secondary.len(), 1);
assert_eq!(d.help.as_deref(), Some("remove it"));
assert_eq!(d.to_string(), "warning[Y1003]: ignored at a[2]");
let e = Diagnostic::error(code, "bad", Label::new(Span::empty(0), ""));
assert_eq!(e.severity, Severity::Error);
assert_eq!(e.path, None);
assert_eq!(e.to_string(), "error[Y1003]: bad");
assert_eq!(
e,
Diagnostic::new(code, Severity::Error, "bad", Label::new(Span::empty(0), ""))
);
assert!(e.suggestions.is_empty());
}
#[test]
fn suggestions_apply() {
let src = "a: b: c\nd: é\n";
let quote = Suggestion::new(
"quote the value",
vec![Replacement::new(Span::new(3, 7), "'b: c'")],
Applicability::MachineApplicable,
);
assert_eq!(quote.apply(src).unwrap(), "a: 'b: c'\nd: é\n");
let multi = Suggestion::new(
"insert and delete",
vec![
Replacement::new(Span::empty(0), "# x\n"),
Replacement::new(Span::new(8, 13), ""),
],
Applicability::MaybeIncorrect,
);
assert_eq!(multi.apply(src).unwrap(), "# x\na: b: c\n\n");
for bad in [
vec![
Replacement::new(Span::new(0, 4), ""),
Replacement::new(Span::new(3, 5), ""),
],
vec![Replacement::new(Span::new(5, 4), "")],
vec![Replacement::new(Span::new(0, 99), "")],
vec![Replacement::new(Span::new(0, 12), "")],
] {
let s = Suggestion::new("bad", bad, Applicability::MaybeIncorrect);
assert_eq!(s.apply(src), None);
}
let d = Diagnostic::error(Code::new(6).unwrap(), "x", Label::new(Span::empty(0), ""))
.with_suggestion(quote.clone());
assert_eq!(d.suggestions, vec![quote]);
}
}