Skip to content

Commit

Permalink
feat(reporter): dummy reporter implementation + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
zkat committed Aug 3, 2021
1 parent c3f41b9 commit a437f44
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ readme = "README.md"
edition = "2018"

[dependencies]
indenter = "0.3.3"

[dev-dependencies]
thiserror = "1.0.26"
99 changes: 99 additions & 0 deletions src/chain.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Item> {
match &mut self.state {
Linked { next } => {
let error = (*next)?;
*next = error.source();
Some(error)
}
Buffered { rest } => rest.next(),
}
}

fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.len();
(len, Some(len))
}
}

impl DoubleEndedIterator for Chain<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
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(),
},
}
}
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 4 additions & 1 deletion src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ 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<Box<dyn Read>>;
}

/**
Details and additional context to be displayed.
*/
#[derive(Debug)]
pub struct DiagnosticDetail {
/// Explanation of this specific diagnostic detail.
pub message: Option<String>,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
89 changes: 89 additions & 0 deletions src/reporter.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
79 changes: 79 additions & 0 deletions tests/reporter.rs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDetail>,
}

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(())
}

0 comments on commit a437f44

Please sign in to comment.