From 29c1403efdd7fd218f240ac458fd19bba17e9551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Tue, 17 Aug 2021 22:36:21 -0700 Subject: [PATCH] feat(reporter): Overhauled return type/main/DiagnosticReport experience. Fixes: https://github.com/zkat/miette/issues/13 --- Cargo.toml | 1 + README.md | 65 +++++++++++++++++++++++--------------------- src/error.rs | 2 ++ src/protocol.rs | 10 +++---- src/reporter.rs | 69 +++++++++++++++++++++++++++++++++++++++++------ src/utils.rs | 14 ++++++---- tests/reporter.rs | 17 +++--------- 7 files changed, 116 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e2d1c462..6ceaab12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ edition = "2018" indenter = "0.3.3" thiserror = "1.0.26" miette-derive = { version = "=0.10.0", path = "miette-derive" } +once_cell = "1.8.0" [dev-dependencies] thiserror = "1.0.26" diff --git a/README.md b/README.md index 42ad9e99..806800af 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# miette - you run miette? You run her code like the software? Oh. Oh! Error code for 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 [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] -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 ecosystem that leans on this extra metadata to provide it for others in a way 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. -`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; -#[derive(Error, Diagnostic)] +#[derive(Error, Debug, Diagnostic)] #[error("oops it broke!")] #[diagnostic( code(oops::my::bad), @@ -61,6 +64,7 @@ use thiserror::Error; help("try doing it better next time?"), )] struct MyBad { + // The Source that we're gonna be printing snippets out of. src: String, // Snippets and highlights can be included in the diagnostic! #[snippet(src, "This is the part that broke")] @@ -70,40 +74,39 @@ struct MyBad { } /* -Then, we implement `std::fmt::Debug` using the included `MietteReporter`, -which is able to pretty print diagnostics reasonably well. +Now let's define a function! -You can use any reporter you want here, or no reporter at all, -but `Debug` is required by `std::error::Error`, so you need to at -least derive it. - -Make sure you pull in the `miette::DiagnosticReporter` trait!. +Use this DiagnosticResult type (or its expanded version) as the return type +throughout your app (but NOT your libraries! Those should always return concrete +types!). */ -use std::fmt; +use miette::DiagnosticResult as Result; +fn this_fails() -> Result<()> { + // You can use plain strings as a `Source`, or anything that implements + // the one-method `Source` trait. + let src = "source\n text\n here".to_string(); + let len = src.len(); -use miette::{DiagnosticReporter, MietteReporter}; + Err(MyBad { + src, + snip: ("bad_file.rs", 0, len).into(), + bad_bit: ("this bit here", 9, 4).into(), + })?; -impl fmt::Debug for MyBad { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - MietteReporter.debug(self, f) - } + Ok(()) } /* -Now we can use `miette`~ -*/ -use miette::{MietteError, SourceSpan}; +Now to get everything printed nicely, just return a Result<(), DiagnosticReport> +and you're all set! -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 len = src.len(); +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()?; - Err(MyBad { - src, - snip: ("bad_file.rs", 0, len).into(), - bad_bit: ("this bit here", 9, 3).into(), - }) + Ok(()) } ``` diff --git a/src/error.rs b/src/error.rs index 3c2a51db..2ebafff1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,4 +11,6 @@ pub enum MietteError { IoError(#[from] io::Error), #[error("The given offset is outside the bounds of its Source")] OutOfBounds, + #[error("Failed to install reporter hook")] + ReporterInstallFailed, } diff --git a/src/protocol.rs b/src/protocol.rs index 737a32b1..941ec49f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -9,7 +9,7 @@ use std::{fmt::Display, fs, panic::Location}; 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. */ pub trait Diagnostic: std::error::Error { @@ -20,7 +20,7 @@ pub trait Diagnostic: std::error::Error { /// `E0123` or Enums will work just fine. fn code<'a>(&'a self) -> Box; - /// 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. /// /// 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!) */ -pub trait DiagnosticReporter: core::any::Any + Send + Sync { +pub trait DiagnosticReportPrinter: core::any::Any + Send + Sync { /// Define the report format. fn debug( &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. */ #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -316,7 +316,7 @@ impl SourceOffset { /// Returns an offset for the _file_ location of wherever this function is /// 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 /// as a SourceOffset diff --git a/src/reporter.rs b/src/reporter.rs index 1c51a725..3b8299e7 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -5,18 +5,71 @@ but largely meant to be an example. use std::fmt; use indenter::indented; +use once_cell::sync::OnceCell; use crate::chain::Chain; -use crate::protocol::{Diagnostic, DiagnosticReporter, DiagnosticSnippet, Severity}; +use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity}; +use crate::MietteError; + +static REPORTER: OnceCell> = + 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 = Result; + +/// 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, +} + +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 From for DiagnosticReport { + fn from(diagnostic: T) -> Self { + DiagnosticReport { + diagnostic: Box::new(diagnostic), + } + } +} /** -Reference implementation of the [DiagnosticReporter] trait. This is generally -good enough for simple use-cases, but you might want to implement your own if -you want custom reporting for your tool or app. +Reference implementation of the [DiagnosticReportPrinter] trait. This is generally +good enough for simple use-cases, and is the default one installed with `miette`, +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( &self, 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 { use fmt::Write as _; @@ -155,7 +208,7 @@ impl DiagnosticReporter for MietteReporter { /// Literally what it says on the tin. pub struct JokeReporter; -impl DiagnosticReporter for JokeReporter { +impl DiagnosticReportPrinter for JokeReporter { fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { if f.alternate() { return fmt::Debug::fmt(diagnostic, f); diff --git a/src/utils.rs b/src/utils.rs index 8320eb78..3f3b3961 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,8 +10,15 @@ use crate::Diagnostic; #[error("{}", self.error)] pub struct DiagnosticError { #[source] - pub error: Box, - pub code: String, + error: Box, + 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 { @@ -20,9 +27,6 @@ impl Diagnostic for DiagnosticError { } } -/// Utility Result type for functions that return boxed [Diagnostic]s. -pub type DiagnosticResult = Result>; - pub trait IntoDiagnostic { /// Converts [Result]-like types that return regular errors into a /// `Result` that returns a [Diagnostic]. diff --git a/tests/reporter.rs b/tests/reporter.rs index beecd5a9..7130d8eb 100644 --- a/tests/reporter.rs +++ b/tests/reporter.rs @@ -1,11 +1,7 @@ -use std::fmt; - -use miette::{ - Diagnostic, DiagnosticReporter, DiagnosticSnippet, MietteError, MietteReporter, SourceSpan, -}; +use miette::{Diagnostic, DiagnosticReport, DiagnosticSnippet, MietteError, SourceSpan}; use thiserror::Error; -#[derive(Error)] +#[derive(Debug, Error)] #[error("oops!")] struct MyBad { message: String, @@ -14,12 +10,6 @@ struct MyBad { highlight: SourceSpan, } -impl fmt::Debug for MyBad { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - MietteReporter.debug(self, f) - } -} - impl Diagnostic for MyBad { fn code(&self) -> Box { Box::new(&"oops::my::bad") @@ -52,7 +42,8 @@ fn fancy() -> Result<(), MietteError> { ctx: ("bad_file.rs", 0, len).into(), highlight: ("this bit here", 9, 4).into(), }; - let out = format!("{:?}", err); + let rep: DiagnosticReport = err.into(); + let out = format!("{:?}", rep); // 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); Ok(())