From a437f44511768e52cfedd856b5b1432c0716f378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Tue, 3 Aug 2021 00:52:10 -0700 Subject: [PATCH] feat(reporter): dummy reporter implementation + tests --- Cargo.toml | 4 ++ src/chain.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ src/protocol.rs | 5 ++- src/reporter.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ tests/reporter.rs | 79 +++++++++++++++++++++++++++++++++++++ 6 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 src/chain.rs create mode 100644 src/reporter.rs create mode 100644 tests/reporter.rs diff --git a/Cargo.toml b/Cargo.toml index 49dea92..ff45104 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,7 @@ readme = "README.md" edition = "2018" [dependencies] +indenter = "0.3.3" + +[dev-dependencies] +thiserror = "1.0.26" diff --git a/src/chain.rs b/src/chain.rs new file mode 100644 index 0000000..74895c0 --- /dev/null +++ b/src/chain.rs @@ -0,0 +1,99 @@ +/*! +Iterate over error `.source()` chains. + +NOTE: This module is taken wholesale from https://crates.io/crates/eyre. +*/ +use std::error::Error as StdError; +use std::vec; + +use ChainState::*; + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct Chain<'a> { + state: crate::chain::ChainState<'a>, +} + + +#[derive(Clone)] +pub(crate) enum ChainState<'a> { + Linked { + next: Option<&'a (dyn StdError + 'static)>, + }, + Buffered { + rest: vec::IntoIter<&'a (dyn StdError + 'static)>, + }, +} + +impl<'a> Chain<'a> { + pub fn new(head: &'a (dyn StdError + 'static)) -> Self { + Chain { + state: ChainState::Linked { next: Some(head) }, + } + } +} + +impl<'a> Iterator for Chain<'a> { + type Item = &'a (dyn StdError + 'static); + + fn next(&mut self) -> Option { + match &mut self.state { + Linked { next } => { + let error = (*next)?; + *next = error.source(); + Some(error) + } + Buffered { rest } => rest.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} + +impl DoubleEndedIterator for Chain<'_> { + fn next_back(&mut self) -> Option { + match &mut self.state { + Linked { mut next } => { + let mut rest = Vec::new(); + while let Some(cause) = next { + next = cause.source(); + rest.push(cause); + } + let mut rest = rest.into_iter(); + let last = rest.next_back(); + self.state = Buffered { rest }; + last + } + Buffered { rest } => rest.next_back(), + } + } +} + +impl ExactSizeIterator for Chain<'_> { + fn len(&self) -> usize { + match &self.state { + Linked { mut next } => { + let mut len = 0; + while let Some(cause) = next { + next = cause.source(); + len += 1; + } + len + } + Buffered { rest } => rest.len(), + } + } +} + +impl Default for Chain<'_> { + fn default() -> Self { + Chain { + state: ChainState::Buffered { + rest: Vec::new().into_iter(), + }, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 791d244..cd9b2af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ #![doc = include_str!("../README.md")] +pub use chain::*; pub use protocol::*; +pub use reporter::*; +mod chain; mod source_impls; mod protocol; +mod reporter; diff --git a/src/protocol.rs b/src/protocol.rs index 9ac9650..a797507 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -71,7 +71,7 @@ pub enum Severity { /** Represents a readable source of some sort: a source file, a String, etc. */ -pub trait Source { +pub trait Source: std::fmt::Debug + Send + Sync + 'static { /// Get a `Read`er from a given [Source]. fn open(&self) -> io::Result>; } @@ -79,6 +79,7 @@ pub trait Source { /** Details and additional context to be displayed. */ +#[derive(Debug)] pub struct DiagnosticDetail { /// Explanation of this specific diagnostic detail. pub message: Option, @@ -95,6 +96,7 @@ pub struct DiagnosticDetail { /** Span within a [Source] with an associated message. */ +#[derive(Debug)] pub struct SourceSpan { /// A name for the thing this SourceSpan is actually pointing to. pub label: String, @@ -107,6 +109,7 @@ pub struct SourceSpan { /** Specific location in a [SourceSpan] */ +#[derive(Debug)] pub struct SourceLocation { /// 0-indexed column of location. pub column: usize, diff --git a/src/reporter.rs b/src/reporter.rs new file mode 100644 index 0000000..2419cd3 --- /dev/null +++ b/src/reporter.rs @@ -0,0 +1,89 @@ +/*! +Basic reporter for Diagnostics. Probably good enough for most use-cases, +but largely meant to be an example. +*/ +use indenter::indented; + +use crate::chain::Chain; +use crate::protocol::{Diagnostic, DiagnosticDetail, DiagnosticReporter, Severity}; + +pub struct Reporter; + +impl DiagnosticReporter for Reporter { + fn debug( + &self, + diagnostic: &(dyn Diagnostic), + f: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { + use core::fmt::Write as _; + + if f.alternate() { + return core::fmt::Debug::fmt(diagnostic, f); + } + + let sev = match diagnostic.severity() { + Severity::Error => "Error", + Severity::Warning => "Warning", + Severity::Advice => "Advice", + }; + write!(f, "{}[{}]: {}", sev, diagnostic.code(), diagnostic)?; + + if let Some(cause) = diagnostic.source() { + write!(f, "\n\nCaused by:")?; + let multiple = cause.source().is_some(); + + for (n, error) in Chain::new(cause).enumerate() { + writeln!(f)?; + if multiple { + write!(indented(f).ind(n), "{}", error)?; + } else { + write!(indented(f), "{}", error)?; + } + } + } + + if let Some(details) = diagnostic.details() { + writeln!(f)?; + for DiagnosticDetail { + source_name, + message, + span, + other_spans, + .. + } in details + { + write!(f, "\n[{}]", source_name)?; + if let Some(msg) = message { + write!(f, " {}:", msg)?; + } + writeln!( + indented(f), + "\n\n({}) @ line {}, col {} ", + span.label, + span.start.line + 1, + span.start.column + 1 + )?; + if let Some(other_spans) = other_spans { + for span in other_spans { + writeln!( + indented(f), + "\n{} @ line {}, col {} ", + span.label, + span.start.line + 1, + span.start.column + 1 + )?; + } + } + } + } + + if let Some(help) = diagnostic.help() { + writeln!(f)?; + for msg in help { + writeln!(f, "﹦{}", msg)?; + } + } + + Ok(()) + } +} diff --git a/tests/reporter.rs b/tests/reporter.rs new file mode 100644 index 0000000..ed7cb78 --- /dev/null +++ b/tests/reporter.rs @@ -0,0 +1,79 @@ +use std::{fmt, io}; + +use miette::{ + Diagnostic, DiagnosticDetail, DiagnosticReporter, Reporter, Severity, SourceLocation, + SourceSpan, +}; +use thiserror::Error; + +#[derive(Error)] +#[error("oops!")] +struct MyBad { + details: Vec, +} + +impl fmt::Debug for MyBad { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Reporter.debug(self, f) + } +} + +impl Diagnostic for MyBad { + fn code(&self) -> &(dyn std::fmt::Display + 'static) { + &"oops::my::bad" + } + + fn severity(&self) -> Severity { + Severity::Error + } + + fn help(&self) -> Option<&[&str]> { + Some(&["try doing it better next time?"]) + } + + fn details(&self) -> Option<&[DiagnosticDetail]> { + Some(&self.details) + } +} + +#[test] +fn basic() -> io::Result<()> { + let err = MyBad { + details: Vec::new(), + }; + let out = format!("{:?}", err); + assert_eq!( + "Error[oops::my::bad]: oops!\n\n﹦try doing it better next time?\n".to_string(), + out + ); + Ok(()) +} + +#[test] +fn fancy() -> io::Result<()> { + let err = MyBad { + details: vec![DiagnosticDetail { + message: Some("This is the part that broke".into()), + source_name: "bad_file.rs".into(), + source: Box::new("source_text".to_string()), + other_spans: None, + span: SourceSpan { + label: "this thing here is bad".into(), + start: SourceLocation { + line: 0, + column: 0, + offset: 0, + }, + end: SourceLocation { + line: 0, + column: 4, + offset: 4, + }, + }, + }], + }; + let out = format!("{:?}", err); + // println!("{}", out); + assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n (this thing here is bad) @ line 1, col 1 \n\n﹦try doing it better next time?\n".to_string(), out); + Ok(()) +}