feat(reporter): Overhauled return type/main/DiagnosticReport experience.

Fixes: https://github.com/zkat/miette/issues/13
This commit is contained in:
Kat Marchán 2021-08-17 22:36:21 -07:00
parent 4d45884362
commit 29c1403efd
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
7 changed files with 117 additions and 63 deletions

View File

@ -14,6 +14,7 @@ edition = "2018"
indenter = "0.3.3" indenter = "0.3.3"
thiserror = "1.0.26" thiserror = "1.0.26"
miette-derive = { version = "=0.10.0", path = "miette-derive" } miette-derive = { version = "=0.10.0", path = "miette-derive" }
once_cell = "1.8.0"
[dev-dependencies] [dev-dependencies]
thiserror = "1.0.26" thiserror = "1.0.26"

View File

@ -1,5 +1,3 @@
# miette
you run miette? You run her code like the software? Oh. Oh! Error code for you run miette? You run her code like the software? Oh. Oh! Error code for
coder! Error code for One Thousand Lines! coder! Error code for One Thousand Lines!
@ -28,8 +26,13 @@ adds various facilities like [Severity], error codes that could be looked up
by users, and snippet display with support for multiline reports, arbitrary by users, and snippet display with support for multiline reports, arbitrary
[Source]s, and pretty printing. [Source]s, and pretty printing.
`miette` also includes a (lightweight) `anyhow`/`eyre`-style
[DiagnosticReport] type which can be returned from application-internal
functions to make the `?` experience nicer. It's extra easy to use when using
[DiagnosticResult]!
While the `miette` crate bundles some baseline implementations for [Source] While the `miette` crate bundles some baseline implementations for [Source]
and [DiagnosticReporter], it's intended to define a protocol that other crates and [DiagnosticReportPrinter], it's intended to define a protocol that other crates
can build on top of to provide rich error reporting, and encourage an can build on top of to provide rich error reporting, and encourage an
ecosystem that leans on this extra metadata to provide it for others in a way ecosystem that leans on this extra metadata to provide it for others in a way
that's compatible with [std::error::Error] that's compatible with [std::error::Error]
@ -48,12 +51,12 @@ $ cargo add miette
/* /*
You can derive a Diagnostic from any `std::error::Error` type. You can derive a Diagnostic from any `std::error::Error` type.
`thiserror` is a great way to define them so, and plays extremely nicely with `miette`! `thiserror` is a great way to define them, and plays nicely with `miette`!
*/ */
use miette::Diagnostic; use miette::{Diagnostic, SourceSpan};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Diagnostic)] #[derive(Error, Debug, Diagnostic)]
#[error("oops it broke!")] #[error("oops it broke!")]
#[diagnostic( #[diagnostic(
code(oops::my::bad), code(oops::my::bad),
@ -61,6 +64,7 @@ use thiserror::Error;
help("try doing it better next time?"), help("try doing it better next time?"),
)] )]
struct MyBad { struct MyBad {
// The Source that we're gonna be printing snippets out of.
src: String, src: String,
// Snippets and highlights can be included in the diagnostic! // Snippets and highlights can be included in the diagnostic!
#[snippet(src, "This is the part that broke")] #[snippet(src, "This is the part that broke")]
@ -70,40 +74,39 @@ struct MyBad {
} }
/* /*
Then, we implement `std::fmt::Debug` using the included `MietteReporter`, Now let's define a function!
which is able to pretty print diagnostics reasonably well.
You can use any reporter you want here, or no reporter at all, Use this DiagnosticResult type (or its expanded version) as the return type
but `Debug` is required by `std::error::Error`, so you need to at throughout your app (but NOT your libraries! Those should always return concrete
least derive it. types!).
Make sure you pull in the `miette::DiagnosticReporter` trait!.
*/ */
use std::fmt; use miette::DiagnosticResult as Result;
fn this_fails() -> Result<()> {
use miette::{DiagnosticReporter, MietteReporter}; // You can use plain strings as a `Source`, or anything that implements
// the one-method `Source` trait.
impl fmt::Debug for MyBad {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
MietteReporter.debug(self, f)
}
}
/*
Now we can use `miette`~
*/
use miette::{MietteError, SourceSpan};
fn pretend_this_is_main() -> Result<(), MyBad> {
// You can use plain strings as a `Source`, bu the protocol is fully extensible!
let src = "source\n text\n here".to_string(); let src = "source\n text\n here".to_string();
let len = src.len(); let len = src.len();
Err(MyBad { Err(MyBad {
src, src,
snip: ("bad_file.rs", 0, len).into(), snip: ("bad_file.rs", 0, len).into(),
bad_bit: ("this bit here", 9, 3).into(), bad_bit: ("this bit here", 9, 4).into(),
}) })?;
Ok(())
}
/*
Now to get everything printed nicely, just return a Result<(), DiagnosticReport>
and you're all set!
Note: You can swap out the default reporter for a custom one using `miette::set_reporter()`
*/
fn pretend_this_is_main() -> Result<()> {
// kaboom~
this_fails()?;
Ok(())
} }
``` ```

View File

@ -11,4 +11,6 @@ pub enum MietteError {
IoError(#[from] io::Error), IoError(#[from] io::Error),
#[error("The given offset is outside the bounds of its Source")] #[error("The given offset is outside the bounds of its Source")]
OutOfBounds, OutOfBounds,
#[error("Failed to install reporter hook")]
ReporterInstallFailed,
} }

View File

@ -9,7 +9,7 @@ use std::{fmt::Display, fs, panic::Location};
use crate::MietteError; use crate::MietteError;
/** /**
Adds rich metadata to your Error that can be used by [DiagnosticReporter] to print Adds rich metadata to your Error that can be used by [DiagnosticReportPrinter] to print
really nice and human-friendly error messages. really nice and human-friendly error messages.
*/ */
pub trait Diagnostic: std::error::Error { pub trait Diagnostic: std::error::Error {
@ -20,7 +20,7 @@ pub trait Diagnostic: std::error::Error {
/// `E0123` or Enums will work just fine. /// `E0123` or Enums will work just fine.
fn code<'a>(&'a self) -> Box<dyn Display + 'a>; fn code<'a>(&'a self) -> Box<dyn Display + 'a>;
/// Diagnostic severity. This may be used by [DiagnosticReporter]s to change the /// Diagnostic severity. This may be used by [DiagnosticReportPrinter]s to change the
/// display format of this diagnostic. /// display format of this diagnostic.
/// ///
/// If `None`, reporters should treat this as [Severity::Error] /// If `None`, reporters should treat this as [Severity::Error]
@ -68,7 +68,7 @@ Protocol for [Diagnostic] handlers, which are responsible for actually printing
Blatantly based on [EyreHandler](https://docs.rs/eyre/0.6.5/eyre/trait.EyreHandler.html) (thanks, Jane!) Blatantly based on [EyreHandler](https://docs.rs/eyre/0.6.5/eyre/trait.EyreHandler.html) (thanks, Jane!)
*/ */
pub trait DiagnosticReporter: core::any::Any + Send + Sync { pub trait DiagnosticReportPrinter: core::any::Any + Send + Sync {
/// Define the report format. /// Define the report format.
fn debug( fn debug(
&self, &self,
@ -88,7 +88,7 @@ pub trait DiagnosticReporter: core::any::Any + Send + Sync {
} }
/** /**
[Diagnostic] severity. Intended to be used by [DiagnosticReporter] to change the [Diagnostic] severity. Intended to be used by [DiagnosticReportPrinter]s to change the
way different Diagnostics are displayed. way different Diagnostics are displayed.
*/ */
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
@ -316,7 +316,7 @@ impl SourceOffset {
/// Returns an offset for the _file_ location of wherever this function is /// Returns an offset for the _file_ location of wherever this function is
/// called. If you want to get _that_ caller's location, mark this /// called. If you want to get _that_ caller's location, mark this
/// function's caller with #[track_caller] (and so on and so forth). /// function's caller with `#[track_caller]` (and so on and so forth).
/// ///
/// Returns both the filename that was given and the offset of the caller /// Returns both the filename that was given and the offset of the caller
/// as a SourceOffset /// as a SourceOffset

View File

@ -5,18 +5,71 @@ but largely meant to be an example.
use std::fmt; use std::fmt;
use indenter::indented; use indenter::indented;
use once_cell::sync::OnceCell;
use crate::chain::Chain; use crate::chain::Chain;
use crate::protocol::{Diagnostic, DiagnosticReporter, DiagnosticSnippet, Severity}; use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
use crate::MietteError;
static REPORTER: OnceCell<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
OnceCell::new();
/// Set the global [DiagnosticReportPrinter] that will be used when you report
/// using [DiagnosticReport].
pub fn set_reporter(
reporter: impl DiagnosticReportPrinter + Send + Sync + 'static,
) -> Result<(), MietteError> {
REPORTER
.set(Box::new(reporter))
.map_err(|_| MietteError::ReporterInstallFailed)
}
/// Used by [DiagnosticReport] to fetch the reporter that will be used to
/// print stuff out.
pub fn get_reporter() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) {
&**REPORTER.get_or_init(|| Box::new(DefaultReportPrinter))
}
/// Convenience alias. This is intended to be used as the return type for `main()`
pub type DiagnosticResult<T> = Result<T, DiagnosticReport>;
/// When used with `?`/`From`, this will wrap any Diagnostics and, when
/// formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
/// use it to format the inner [Diagnostic].
pub struct DiagnosticReport {
diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
}
impl DiagnosticReport {
/// Return a reference to the inner [Diagnostic].
pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
&*self.diagnostic
}
}
impl std::fmt::Debug for DiagnosticReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
get_reporter().debug(&*self.diagnostic, f)
}
}
impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
fn from(diagnostic: T) -> Self {
DiagnosticReport {
diagnostic: Box::new(diagnostic),
}
}
}
/** /**
Reference implementation of the [DiagnosticReporter] trait. This is generally Reference implementation of the [DiagnosticReportPrinter] trait. This is generally
good enough for simple use-cases, but you might want to implement your own if good enough for simple use-cases, and is the default one installed with `miette`,
you want custom reporting for your tool or app. but you might want to implement your own if you want custom reporting for your
tool or app.
*/ */
pub struct MietteReporter; pub struct DefaultReportPrinter;
impl MietteReporter { impl DefaultReportPrinter {
fn render_snippet( fn render_snippet(
&self, &self,
f: &mut fmt::Formatter<'_>, f: &mut fmt::Formatter<'_>,
@ -102,7 +155,7 @@ impl MietteReporter {
} }
} }
impl DiagnosticReporter for MietteReporter { impl DiagnosticReportPrinter for DefaultReportPrinter {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write as _; use fmt::Write as _;
@ -155,7 +208,7 @@ impl DiagnosticReporter for MietteReporter {
/// Literally what it says on the tin. /// Literally what it says on the tin.
pub struct JokeReporter; pub struct JokeReporter;
impl DiagnosticReporter for JokeReporter { impl DiagnosticReportPrinter for JokeReporter {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() { if f.alternate() {
return fmt::Debug::fmt(diagnostic, f); return fmt::Debug::fmt(diagnostic, f);

View File

@ -10,8 +10,15 @@ use crate::Diagnostic;
#[error("{}", self.error)] #[error("{}", self.error)]
pub struct DiagnosticError { pub struct DiagnosticError {
#[source] #[source]
pub error: Box<dyn std::error::Error + Send + Sync + 'static>, error: Box<dyn std::error::Error + Send + Sync + 'static>,
pub code: String, code: String,
}
impl DiagnosticError {
/// Return a reference to the inner Error type.
pub fn inner(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
&*self.error
}
} }
impl Diagnostic for DiagnosticError { impl Diagnostic for DiagnosticError {
@ -20,9 +27,6 @@ impl Diagnostic for DiagnosticError {
} }
} }
/// Utility Result type for functions that return boxed [Diagnostic]s.
pub type DiagnosticResult<T> = Result<T, Box<dyn Diagnostic + Send + Sync + 'static>>;
pub trait IntoDiagnostic<T, E> { pub trait IntoDiagnostic<T, E> {
/// Converts [Result]-like types that return regular errors into a /// Converts [Result]-like types that return regular errors into a
/// `Result` that returns a [Diagnostic]. /// `Result` that returns a [Diagnostic].

View File

@ -1,11 +1,7 @@
use std::fmt; use miette::{Diagnostic, DiagnosticReport, DiagnosticSnippet, MietteError, SourceSpan};
use miette::{
Diagnostic, DiagnosticReporter, DiagnosticSnippet, MietteError, MietteReporter, SourceSpan,
};
use thiserror::Error; use thiserror::Error;
#[derive(Error)] #[derive(Debug, Error)]
#[error("oops!")] #[error("oops!")]
struct MyBad { struct MyBad {
message: String, message: String,
@ -14,12 +10,6 @@ struct MyBad {
highlight: SourceSpan, highlight: SourceSpan,
} }
impl fmt::Debug for MyBad {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
MietteReporter.debug(self, f)
}
}
impl Diagnostic for MyBad { impl Diagnostic for MyBad {
fn code(&self) -> Box<dyn std::fmt::Display> { fn code(&self) -> Box<dyn std::fmt::Display> {
Box::new(&"oops::my::bad") Box::new(&"oops::my::bad")
@ -52,7 +42,8 @@ fn fancy() -> Result<(), MietteError> {
ctx: ("bad_file.rs", 0, len).into(), ctx: ("bad_file.rs", 0, len).into(),
highlight: ("this bit here", 9, 4).into(), highlight: ("this bit here", 9, 4).into(),
}; };
let out = format!("{:?}", err); let rep: DiagnosticReport = err.into();
let out = format!("{:?}", rep);
// println!("{}", out); // println!("{}", out);
assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 | source\n 2 | text\n ⫶ | ^^^^ this bit here\n 3 | here\n\n﹦try doing it better next time?\n".to_string(), out); assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 | source\n 2 | text\n ⫶ | ^^^^ this bit here\n 3 | here\n\n﹦try doing it better next time?\n".to_string(), out);
Ok(()) Ok(())