use std::{
borrow::Cow,
error::Error,
fmt::{Debug, Display, Write},
ops::Deref,
};
use annotate_snippets::{Group, Level, Renderer, Title, renderer::DecorStyle};
use device_driver_common::span::Span;
pub mod errors;
#[derive(Debug)]
pub struct Diagnostics {
diagnostics: Vec<Box<dyn Diagnostic>>,
}
impl Default for Diagnostics {
fn default() -> Self {
Self::new()
}
}
impl Diagnostics {
#[must_use]
pub fn new() -> Self {
Self {
diagnostics: Vec::new(),
}
}
pub fn add(&mut self, diagnostic: impl Diagnostic + 'static) {
self.diagnostics.push(Box::new(diagnostic));
}
#[must_use]
pub fn has_error(&self) -> bool {
self.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity() == Severity::Error)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &dyn Diagnostic> {
self.diagnostics.iter().map(Box::deref)
}
pub fn render_for_each<E>(
&self,
metadata: Metadata<'_>,
mut f: impl FnMut(&dyn Diagnostic, String) -> Result<(), E>,
) -> Result<(), E> {
let renderer = metadata.get_renderer();
for diagnostic in self.iter() {
let mut rendered =
renderer.render(&diagnostic.as_report(metadata.source, metadata.source_path));
if !metadata.ansi {
rendered = strip_ansi_urls(&rendered);
}
f(diagnostic, rendered)?;
}
Ok(())
}
pub fn print_to<W: std::io::Write>(
self,
mut writer: W,
metadata: Metadata<'_>,
) -> std::io::Result<()> {
self.render_for_each(metadata, |_, rendered| writeln!(writer, "{rendered}\n"))
}
pub fn print_to_fmt<W: std::fmt::Write>(
self,
mut writer: W,
metadata: Metadata<'_>,
) -> std::fmt::Result {
self.render_for_each(metadata, |_, rendered| writeln!(writer, "{rendered}\n"))
}
}
pub struct Metadata<'s> {
pub source: &'s str,
pub source_path: &'s str,
pub term_width: Option<usize>,
pub ansi: bool,
pub unicode: bool,
pub anonymized_line_numbers: bool,
}
impl Metadata<'_> {
fn get_renderer(&self) -> Renderer {
if self.ansi {
Renderer::styled()
} else {
Renderer::plain()
}
.term_width(
self.term_width
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
)
.decor_style(if self.unicode {
DecorStyle::Unicode
} else {
DecorStyle::Ascii
})
.anonymized_snippet_line_numbers(self.anonymized_line_numbers)
.anonymized_origin_line_numbers(self.anonymized_line_numbers)
}
}
pub fn encode_ansi_url(link: &str, name: &str) -> String {
format!("\x1b]8;;{link}\x1b\\{name}\x1b]8;;\x1b\\")
}
fn strip_ansi_urls(text: &str) -> String {
enum UrlStage {
None,
Start,
End,
}
let mut output = String::new();
let mut url_stage = UrlStage::None;
for split in text.split("\x1b]8;;") {
match url_stage {
UrlStage::None => {
output += split;
url_stage = UrlStage::Start;
}
UrlStage::Start => {
if let Some((link, name)) = split.split_once("\x1b\\") {
output += name;
let _ = write!(output, " ({link})");
url_stage = UrlStage::End;
} else {
return text.into();
}
}
UrlStage::End => {
output += split.trim_start_matches("\x1b\\");
url_stage = UrlStage::Start;
}
}
}
output
}
pub trait Diagnostic: Debug + Send {
fn severity(&self) -> Severity;
fn as_report<'a>(&'a self, source: &'a str, path: &'a str) -> Vec<Group<'a>>;
fn primary_span(&self) -> Span;
fn title(&self) -> Cow<'static, str>;
fn title_snippet(&self) -> Title<'static> {
match self.severity() {
Severity::Error => Level::ERROR,
Severity::Warning => Level::WARNING,
Severity::Info => Level::INFO,
Severity::Note => Level::NOTE,
Severity::Help => Level::HELP,
}
.primary_title(self.title())
}
}
impl<E: Error + Send> Diagnostic for E {
fn severity(&self) -> Severity {
Severity::Error
}
fn as_report<'a>(&'a self, _source: &'a str, _file_path: &'a str) -> Vec<Group<'a>> {
let mut sources = Vec::new();
let mut source = self.source();
while let Some(current_source) = source {
sources.push(Level::NOTE.message(current_source.to_string()));
source = current_source.source();
}
vec![Group::with_title(Level::ERROR.primary_title(self.to_string())).elements(sources)]
}
fn primary_span(&self) -> Span {
Span::empty()
}
fn title(&self) -> Cow<'static, str> {
self.to_string().into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Severity {
Error,
Warning,
Info,
Note,
Help,
}
#[derive(Debug)]
pub struct DynError {
source: Option<Box<dyn Error + Send + 'static>>,
message: String,
}
impl DynError {
pub fn new(message: impl Display) -> Self {
Self {
source: None,
message: message.to_string(),
}
}
pub fn to_report_string(&self) -> String {
let mut string = String::new();
let report = self.as_report("", "");
let output = annotate_snippets::Renderer::styled().render(&report);
write!(&mut string, "{output}").unwrap();
string
}
}
impl Display for DynError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.message.as_str(), self.source()) {
("", Some(source)) => write!(f, "{source}"),
("", None) => unreachable!(),
(message, _) if !f.alternate() => write!(f, "{message}"),
(message, None) => write!(f, "{message}"),
(message, Some(source)) => write!(f, "{message}\n| {source:#}"),
}
}
}
impl Error for DynError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
if self.message.is_empty() {
None
} else {
self.source.as_ref().map(|e| {
let x: &(dyn Error + Send) = Box::as_ref(e);
let y: &dyn Error = x;
y
})
}
}
}
pub trait ErrorExt: Error + Sized + Send + 'static {
fn with_message(self, message: impl Display) -> DynError {
DynError {
source: Some(Box::new(self)),
message: message.to_string(),
}
}
fn into_dyn_error(self) -> DynError {
DynError {
source: Some(Box::new(self)),
message: String::new(),
}
}
}
impl<E: Error + Send + Sized + 'static> ErrorExt for E {}
pub trait ResultExt<T, E: ErrorExt> {
fn with_message<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError>;
fn into_dyn_result(self) -> Result<T, DynError>;
}
impl<T, E: ErrorExt> ResultExt<T, E> for Result<T, E> {
fn with_message<D: Display>(self, f: impl FnOnce() -> D) -> Result<T, DynError> {
self.map_err(|e| e.with_message(f()))
}
fn into_dyn_result(self) -> Result<T, DynError> {
self.map_err(ErrorExt::into_dyn_error)
}
}
#[cfg(test)]
mod tests {
use std::fmt::Display;
use super::*;
#[derive(Debug)]
struct DummyError;
impl Display for DummyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Something went wrong!")
}
}
impl Error for DummyError {}
#[derive(Debug)]
struct DummyErrorWithSource(usize);
impl Display for DummyErrorWithSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "@level {} - Something deep down went wrong!", self.0)
}
}
impl Error for DummyErrorWithSource {
fn source(&self) -> Option<&(dyn Error + 'static)> {
if self.0 == 0 {
Some(&DummyError)
} else {
Some(Box::leak(Box::new(DummyErrorWithSource(self.0 - 1))))
}
}
}
#[test]
fn error_into_report() {
let report = DummyError.as_report("", "");
let output = annotate_snippets::Renderer::plain().render(&report);
pretty_assertions::assert_str_eq!("error: Something went wrong!", output);
let report = DummyErrorWithSource(5).as_report("", "");
let output = annotate_snippets::Renderer::plain().render(&report);
pretty_assertions::assert_str_eq!(
"error: @level 5 - Something deep down went wrong!
|
= note: @level 4 - Something deep down went wrong!
= note: @level 3 - Something deep down went wrong!
= note: @level 2 - Something deep down went wrong!
= note: @level 1 - Something deep down went wrong!
= note: @level 0 - Something deep down went wrong!
= note: Something went wrong!",
output
);
}
#[test]
fn dyn_error_context() {
let error = DummyError.with_message("Here's some context!");
let report = error.as_report("", "");
let output = annotate_snippets::Renderer::plain().render(&report);
pretty_assertions::assert_str_eq!(
"error: Here's some context!
|
= note: Something went wrong!",
output
);
}
}