diff --git a/Cargo.toml b/Cargo.toml index ff7dc29..1d70acd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.26" -miette-derive = { version = "=1.1.0", path = "miette-derive" } +miette-derive = { path = "miette-derive", version = "=1.1.0" } once_cell = "1.8.0" owo-colors = "2.0.0" atty = "0.2.14" @@ -22,5 +22,12 @@ ci_info = "0.14.2" [dev-dependencies] semver = "1.0.4" +# Eyre devdeps +futures = { version = "0.3", default-features = false } +indenter = "0.3.0" +rustversion = "1.0" +trybuild = { version = "1.0.19", features = ["diff"] } +syn = { version = "1.0", features = ["full"] } + [workspace] members = ["miette-derive"] diff --git a/Makefile.toml b/Makefile.toml index 55a0bb0..1b83b43 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -8,4 +8,4 @@ args = ["--prepend", "CHANGELOG.md", "-u", "--tag", "${@}"] workspace=false install_crate="cargo-release" command = "cargo" -args = ["release", "--tag-prefix", "", "--workspace", "${@}"] +args = ["release", "--workspace", "${@}"] diff --git a/README.md b/README.md index 14181ec..dd30b5d 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ diagnostic error code: ruget::api::bad_json - Unique error codes on every [Diagnostic]. - Custom links to get more details on error codes. - Super handy derive macro for defining diagnostic metadata. -- Lightweight [`anyhow`](https://docs.rs/anyhow)/[`eyre`](https://docs.rs/eyre)-style error wrapper type, [DiagnosticReport], +- [`anyhow`](https://docs.rs/anyhow)/[`eyre`](https://docs.rs/eyre)-compatible error wrapper type, [Report], which can be returned from `main`. - Generic support for arbitrary [Source]s for snippet data, with default support for `String`s included. -The `miette` crate also comes bundled with a default [DiagnosticReportPrinter] with the following features: +The `miette` crate also comes bundled with a default [ReportHandler] with the following features: - Fancy graphical [diagnostic output](#about), using ANSI/Unicode text - single- and multi-line highlighting support @@ -97,12 +97,12 @@ struct MyBad { /* Now let's define a function! -Use this DiagnosticResult type (or its expanded version) as the return type +Use this Result type (or its expanded version) as the return type throughout your app (but NOT your libraries! Those should always return concrete types!). */ -use miette::{DiagnosticResult, NamedSource}; -fn this_fails() -> DiagnosticResult<()> { +use miette::{Result, NamedSource}; +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(); @@ -118,12 +118,12 @@ fn this_fails() -> DiagnosticResult<()> { } /* -Now to get everything printed nicely, just return a DiagnosticResult<()> +Now to get everything printed nicely, just return a Result<()> 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() -> DiagnosticResult<()> { +fn pretend_this_is_main() -> Result<()> { // kaboom~ this_fails()?; @@ -183,7 +183,7 @@ pub enum MyLibError { Then, return this error type from all your fallible public APIs. It's a best practice to wrap any "external" error types in your error `enum` instead of -using something like [eyre](https://docs.rs/eyre) in a library. +using something like [Report] in a library. ### ... in application code @@ -191,33 +191,50 @@ Application code tends to work a little differently than libraries. You don't always need or care to define dedicated error wrappers for errors coming from external libraries and tools. -For this situation, `miette` includes two tools: [DiagnosticReport] and +For this situation, `miette` includes two tools: [Report] and [IntoDiagnostic]. They work in tandem to make it easy to convert regular `std::error::Error`s into [Diagnostic]s. Additionally, there's a -[DiagnosticResult] type alias that you can use to be more terse: +[Result] type alias that you can use to be more terse. + +When dealing with non-`Diagnostic` types, you'll want to `.into_diagnostic()` +them: ```rust // my_app/lib/my_internal_file.rs -use miette::{IntoDiagnostic, DiagnosticResult}; +use miette::{IntoDiagnostic, Result}; use semver::Version; -pub fn some_tool() -> DiagnosticResult { - Ok("1.2.x".parse().into_diagnostic("my_app::semver::parse_error")?) +pub fn some_tool() -> Result { + Ok("1.2.x".parse().into_diagnostic()?) +} +``` + +`miette` also includes an `anyhow`/`eyre`-style `Context`/`WrapErr` traits that +you can import to add ad-hoc context messages to your `Diagnostic`s, as well, +though you'll still need to use `.into_diagnostic()` to make use of it: + +```rust +// my_app/lib/my_internal_file.rs +use miette::{IntoDiagnostic, Result, WrapErr}; +use semver::Version; + +pub fn some_tool() -> Result { + Ok("1.2.x".parse().into_diagnostic().wrap_err("Parsing this tool's semver version failed.")?) } ``` ### ... in `main()` `main()` is just like any other part of your application-internal code. Use -`DiagnosticResult` as your return value, and it will pretty-print your +`Result` as your return value, and it will pretty-print your diagnostics automatically. ```rust -use miette::{DiagnosticResult, IntoDiagnostic}; +use miette::{Result, IntoDiagnostic}; use semver::Version; -fn pretend_this_is_main() -> DiagnosticResult<()> { - let version: Version = "1.2.x".parse().into_diagnostic("my_app::semver::parse_error")?; +fn pretend_this_is_main() -> Result<()> { + let version: Version = "1.2.x".parse().into_diagnostic()?; println!("{}", version); Ok(()) } @@ -249,7 +266,7 @@ use thiserror::Error; #[diagnostic( code(my_app::my_error), // You can do formatting! - url("https://my_website.com/error_codes#{}", self.code()) + url("https://my_website.com/error_codes#{}", self.code().unwrap()) )] struct MyErr; ``` @@ -330,7 +347,7 @@ pub struct MyErrorType { [`color-eyre`](https://crates.io/crates/color-eyre): these two enormously influential error handling libraries have pushed forward the experience of application-level error handling and error reporting. `miette`'s - `DiagnosticReport` type is an attempt at a very very rough version of their + `Report` type is an attempt at a very very rough version of their `Report` types. - [`thiserror`](https://crates.io/crates/thiserror) for setting the standard for library-level error definitions, and for being the inspiration behind @@ -344,7 +361,7 @@ pub struct MyErrorType { `miette` is released to the Rust community under the [Apache license 2.0](./LICENSE). -It also includes some code taken from [`eyre`](https://github.com/yaahc/eyre), +It also includes code taken from [`eyre`](https://github.com/yaahc/eyre), and some from [`thiserror`](https://github.com/dtolnay/thiserror), also under the Apache License. Some code is taken from [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed. diff --git a/miette-derive/src/code.rs b/miette-derive/src/code.rs index acbacc7..35bc189 100644 --- a/miette-derive/src/code.rs +++ b/miette-derive/src/code.rs @@ -56,29 +56,30 @@ impl Code { }| { match args { DiagnosticDefArgs::Transparent => { - forward_to_single_field_variant(ident, fields, quote! { code() }) + Some(forward_to_single_field_variant(ident, fields, quote! { code() })) } DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { code, .. }) => { - let code = &code.0; - match fields { + let code = &code.as_ref()?.0; + Some(match fields { syn::Fields::Named(_) => { - quote! { Self::#ident { .. } => std::boxed::Box::new(#code), } + quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), } } syn::Fields::Unnamed(_) => { - quote! { Self::#ident(..) => std::boxed::Box::new(#code), } + quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), } } syn::Fields::Unit => { - quote! { Self::#ident => std::boxed::Box::new(#code), } + quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), } } - } + }) } } }, ); Some(quote! { - fn code<'a>(&'a self) -> std::boxed::Box { + fn code<'a>(&'a self) -> std::option::Option> { match self { #(#code_pairs)* + _ => std::option::Option::None, } } }) @@ -87,8 +88,8 @@ impl Code { pub(crate) fn gen_struct(&self) -> Option { let code = &self.0; Some(quote! { - fn code<'a>(&'a self) -> std::boxed::Box { - std::boxed::Box::new(#code) + fn code<'a>(&'a self) -> std::option::Option> { + std::option::Option::Some(std::boxed::Box::new(#code)) } }) } diff --git a/miette-derive/src/diagnostic.rs b/miette-derive/src/diagnostic.rs index f84b861..d5309ab 100644 --- a/miette-derive/src/diagnostic.rs +++ b/miette-derive/src/diagnostic.rs @@ -34,8 +34,9 @@ pub enum DiagnosticDefArgs { Concrete(DiagnosticConcreteArgs), } +#[derive(Default)] pub struct DiagnosticConcreteArgs { - pub code: Code, + pub code: Option, pub severity: Option, pub help: Option, pub snippets: Option, @@ -44,7 +45,7 @@ pub struct DiagnosticConcreteArgs { impl DiagnosticConcreteArgs { fn parse( - ident: &syn::Ident, + _ident: &syn::Ident, fields: &syn::Fields, attr: &syn::Attribute, args: impl Iterator, @@ -75,8 +76,7 @@ impl DiagnosticConcreteArgs { } let snippets = Snippets::from_fields(fields)?; let concrete = DiagnosticConcreteArgs { - code: code - .ok_or_else(|| syn::Error::new(ident.span(), "Diagnostic code is required."))?, + code, help, severity, snippets, @@ -132,11 +132,12 @@ impl Diagnostic { args, } } else { - // Also handle when there's multiple `#[diagnostic]` attrs? - return Err(syn::Error::new( - input.ident.span(), - "#[diagnostic] attribute is required when deriving Diagnostic.", - )); + Diagnostic::Struct { + fields: data_struct.fields, + ident: input.ident, + generics: input.generics, + args: DiagnosticDefArgs::Concrete(Default::default()), + } } } syn::Data::Enum(syn::DataEnum { variants, .. }) => { @@ -206,7 +207,7 @@ impl Diagnostic { quote! { impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { - fn code<'a>(&'a self) -> std::boxed::Box { + fn code<'a>(&'a self) -> std::option::Option> { #matcher #field_name.code() } @@ -230,7 +231,7 @@ impl Diagnostic { } } DiagnosticDefArgs::Concrete(concrete) => { - let code_body = concrete.code.gen_struct(); + let code_body = concrete.code.as_ref().and_then(|x| x.gen_struct()); let help_body = concrete.help.as_ref().and_then(|x| x.gen_struct(fields)); let sev_body = concrete.severity.as_ref().and_then(|x| x.gen_struct()); let snip_body = concrete diff --git a/miette-derive/src/severity.rs b/miette-derive/src/severity.rs index 633e8f7..d828335 100644 --- a/miette-derive/src/severity.rs +++ b/miette-derive/src/severity.rs @@ -91,7 +91,7 @@ impl Severity { fn severity(&self) -> std::option::Option { match self { #(#sev_pairs)* - _ => None, + _ => std::option::Option::None, } } }) diff --git a/src/chain.rs b/src/chain.rs index a71c476..fe4d336 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -10,7 +10,7 @@ use ChainState::*; #[derive(Clone)] #[allow(missing_debug_implementations)] -pub(crate) struct Chain<'a> { +pub struct Chain<'a> { state: crate::chain::ChainState<'a>, } @@ -25,7 +25,7 @@ pub(crate) enum ChainState<'a> { } impl<'a> Chain<'a> { - pub(crate) fn new(head: &'a (dyn StdError + 'static)) -> Self { + pub fn new(head: &'a (dyn StdError + 'static)) -> Self { Chain { state: ChainState::Linked { next: Some(head) }, } diff --git a/src/error.rs b/src/error.rs index bf02d12..76a50a9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,9 +24,9 @@ pub enum MietteError { )] OutOfBounds, - /// Returned when installing a [crate::DiagnosticReportPrinter] failed. + /// Returned when installing a [crate::ReportHandler] failed. /// Typically, this will be because [crate::set_printer] was called twice. - #[error("Failed to install DiagnosticReportPrinter")] + #[error("Failed to install ReportHandler")] #[diagnostic(code(miette::set_printer_failed), url(docsrs))] SetPrinterFailure, } diff --git a/src/eyreish/context.rs b/src/eyreish/context.rs new file mode 100644 index 0000000..a4ae4e9 --- /dev/null +++ b/src/eyreish/context.rs @@ -0,0 +1,159 @@ +use super::error::ContextError; +use super::{Report, WrapErr}; +use core::fmt::{self, Debug, Display, Write}; + +use std::error::Error as StdError; + +use crate::Diagnostic; + +mod ext { + use super::*; + + pub trait Diag { + #[cfg_attr(track_caller, track_caller)] + fn ext_report(self, msg: D) -> Report + where + D: Display + Send + Sync + 'static; + } + + impl Diag for E + where + E: Diagnostic + Send + Sync + 'static, + { + fn ext_report(self, msg: D) -> Report + where + D: Display + Send + Sync + 'static, + { + Report::from_msg(msg, self) + } + } + + impl Diag for Report { + fn ext_report(self, msg: D) -> Report + where + D: Display + Send + Sync + 'static, + { + self.wrap_err(msg) + } + } +} + +impl WrapErr for Result +where + E: ext::Diag + Send + Sync + 'static, +{ + fn wrap_err(self, msg: D) -> Result + where + D: Display + Send + Sync + 'static, + { + match self { + Ok(t) => Ok(t), + Err(e) => Err(e.ext_report(msg)), + } + } + + fn wrap_err_with(self, msg: F) -> Result + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + match self { + Ok(t) => Ok(t), + Err(e) => Err(e.ext_report(msg())), + } + } + + fn context(self, msg: D) -> Result + where + D: Display + Send + Sync + 'static, + { + self.wrap_err(msg) + } + + fn with_context(self, msg: F) -> Result + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + self.wrap_err_with(msg) + } +} + +impl Debug for ContextError +where + D: Display, + E: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Error") + .field("msg", &Quoted(&self.msg)) + .field("source", &self.error) + .finish() + } +} + +impl Display for ContextError +where + D: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.msg, f) + } +} + +impl StdError for ContextError +where + D: Display, + E: StdError + 'static, +{ + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&self.error) + } +} + +impl StdError for ContextError +where + D: Display, +{ + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(self.error.inner.error()) + } +} + +impl Diagnostic for ContextError +where + D: Display, + E: Diagnostic + 'static, +{ +} + +impl Diagnostic for ContextError where D: Display {} + +struct Quoted(D); + +impl Debug for Quoted +where + D: Display, +{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_char('"')?; + Quoted(&mut *formatter).write_fmt(format_args!("{}", self.0))?; + formatter.write_char('"')?; + Ok(()) + } +} + +impl Write for Quoted<&mut fmt::Formatter<'_>> { + fn write_str(&mut self, s: &str) -> fmt::Result { + Display::fmt(&s.escape_debug(), self.0) + } +} + +pub(crate) mod private { + use super::*; + + pub trait Sealed {} + + impl Sealed for Result where E: ext::Diag {} + impl Sealed for Option {} +} diff --git a/src/eyreish/error.rs b/src/eyreish/error.rs new file mode 100644 index 0000000..f9dc525 --- /dev/null +++ b/src/eyreish/error.rs @@ -0,0 +1,723 @@ +use core::any::TypeId; +use core::fmt::{self, Debug, Display}; +use core::mem::{self, ManuallyDrop}; +use core::ptr::{self, NonNull}; +use std::error::Error as StdError; + +use super::Report; +use super::ReportHandler; +use crate::chain::Chain; +use crate::Diagnostic; +use core::ops::{Deref, DerefMut}; + +impl Report { + /// Create a new error object from any error type. + /// + /// The error type must be thread safe and `'static`, so that the `Report` + /// will be as well. + /// + /// If the error type does not provide a backtrace, a backtrace will be + /// created here to ensure that a backtrace exists. + #[cfg_attr(track_caller, track_caller)] + pub fn new(error: E) -> Self + where + E: Diagnostic + Send + Sync + 'static, + { + Report::from_std(error) + } + + /// Create a new error object from a printable error message. + /// + /// If the argument implements std::error::Error, prefer `Report::new` + /// instead which preserves the underlying error's cause chain and + /// backtrace. If the argument may or may not implement std::error::Error + /// now or in the future, use `miette!(err)` which handles either way + /// correctly. + /// + /// `Report::msg("...")` is equivalent to `miette!("...")` but occasionally + /// convenient in places where a function is preferable over a macro, such + /// as iterator or stream combinators: + /// + /// ``` + /// # mod ffi { + /// # pub struct Input; + /// # pub struct Output; + /// # pub async fn do_some_work(_: Input) -> Result { + /// # unimplemented!() + /// # } + /// # } + /// # + /// # use ffi::{Input, Output}; + /// # + /// use miette::{Report, Result}; + /// use futures::stream::{Stream, StreamExt, TryStreamExt}; + /// + /// async fn demo(stream: S) -> Result> + /// where + /// S: Stream, + /// { + /// stream + /// .then(ffi::do_some_work) // returns Result + /// .map_err(Report::msg) + /// .try_collect() + /// .await + /// } + /// ``` + #[cfg_attr(track_caller, track_caller)] + pub fn msg(message: M) -> Self + where + M: Display + Debug + Send + Sync + 'static, + { + Report::from_adhoc(message) + } + + #[cfg_attr(track_caller, track_caller)] + pub(crate) fn from_std(error: E) -> Self + where + E: Diagnostic + Send + Sync + 'static, + { + let vtable = &ErrorVTable { + object_drop: object_drop::, + object_ref: object_ref::, + object_mut: object_mut::, + object_ref_stderr: object_ref_stderr::, + object_boxed: object_boxed::, + object_downcast: object_downcast::, + object_drop_rest: object_drop_front::, + }; + + // Safety: passing vtable that operates on the right type E. + let handler = Some(super::capture_handler(&error)); + + unsafe { Report::construct(error, vtable, handler) } + } + + #[cfg_attr(track_caller, track_caller)] + pub(crate) fn from_adhoc(message: M) -> Self + where + M: Display + Debug + Send + Sync + 'static, + { + use super::wrapper::MessageError; + let error: MessageError = MessageError(message); + let vtable = &ErrorVTable { + object_drop: object_drop::>, + object_ref: object_ref::>, + object_mut: object_mut::>, + object_ref_stderr: object_ref_stderr::>, + object_boxed: object_boxed::>, + object_downcast: object_downcast::, + object_drop_rest: object_drop_front::, + }; + + // Safety: MessageError is repr(transparent) so it is okay for the + // vtable to allow casting the MessageError to M. + let handler = Some(super::capture_handler(&error)); + + unsafe { Report::construct(error, vtable, handler) } + } + + #[cfg_attr(track_caller, track_caller)] + pub(crate) fn from_msg(msg: D, error: E) -> Self + where + D: Display + Send + Sync + 'static, + E: Diagnostic + Send + Sync + 'static, + { + let error: ContextError = ContextError { msg, error }; + + let vtable = &ErrorVTable { + object_drop: object_drop::>, + object_ref: object_ref::>, + object_mut: object_mut::>, + object_ref_stderr: object_ref_stderr::>, + object_boxed: object_boxed::>, + object_downcast: context_downcast::, + object_drop_rest: context_drop_rest::, + }; + + // Safety: passing vtable that operates on the right type. + let handler = Some(super::capture_handler(&error)); + + unsafe { Report::construct(error, vtable, handler) } + } + + #[cfg_attr(track_caller, track_caller)] + pub(crate) fn from_boxed(error: Box) -> Self { + use super::wrapper::BoxedError; + let error = BoxedError(error); + let handler = Some(super::capture_handler(&error)); + + let vtable = &ErrorVTable { + object_drop: object_drop::, + object_ref: object_ref::, + object_mut: object_mut::, + object_ref_stderr: object_ref_stderr::, + object_boxed: object_boxed::, + object_downcast: object_downcast::>, + object_drop_rest: object_drop_front::>, + }; + + // Safety: BoxedError is repr(transparent) so it is okay for the vtable + // to allow casting to Box. + unsafe { Report::construct(error, vtable, handler) } + } + + // Takes backtrace as argument rather than capturing it here so that the + // user sees one fewer layer of wrapping noise in the backtrace. + // + // Unsafe because the given vtable must have sensible behavior on the error + // value of type E. + unsafe fn construct( + error: E, + vtable: &'static ErrorVTable, + handler: Option>, + ) -> Self + where + E: Diagnostic + Send + Sync + 'static, + { + let inner = Box::new(ErrorImpl { + vtable, + handler, + _object: error, + }); + // Erase the concrete type of E from the compile-time type system. This + // is equivalent to the safe unsize coercion from Box> to + // Box> except that the + // result is a thin pointer. The necessary behavior for manipulating the + // underlying ErrorImpl is preserved in the vtable provided by the + // caller rather than a builtin fat pointer vtable. + let erased = mem::transmute::>, Box>>(inner); + let inner = ManuallyDrop::new(erased); + Report { inner } + } + + /// Create a new error from an error message to wrap the existing error. + /// + /// For attaching a higher level error message to a `Result` as it is propagated, the + /// [crate::WrapErr] extension trait may be more convenient than this function. + /// + /// The primary reason to use `error.wrap_err(...)` instead of `result.wrap_err(...)` via the + /// `WrapErr` trait would be if the message needs to depend on some data held by the underlying + /// error: + /// + pub fn wrap_err(mut self, msg: D) -> Self + where + D: Display + Send + Sync + 'static, + { + let handler = self.inner.handler.take(); + let error: ContextError = ContextError { msg, error: self }; + + let vtable = &ErrorVTable { + object_drop: object_drop::>, + object_ref: object_ref::>, + object_mut: object_mut::>, + object_ref_stderr: object_ref_stderr::>, + object_boxed: object_boxed::>, + object_downcast: context_chain_downcast::, + object_drop_rest: context_chain_drop_rest::, + }; + + // Safety: passing vtable that operates on the right type. + unsafe { Report::construct(error, vtable, handler) } + } + + /// An iterator of the chain of source errors contained by this Report. + /// + /// This iterator will visit every error in the cause chain of this error + /// object, beginning with the error that this error object was created + /// from. + /// + /// # Example + /// + /// ``` + /// use miette::Report; + /// use std::io; + /// + /// pub fn underlying_io_error_kind(error: &Report) -> Option { + /// for cause in error.chain() { + /// if let Some(io_error) = cause.downcast_ref::() { + /// return Some(io_error.kind()); + /// } + /// } + /// None + /// } + /// ``` + pub fn chain(&self) -> Chain<'_> { + self.inner.chain() + } + + /// The lowest level cause of this error — this error's cause's + /// cause's cause etc. + /// + /// The root cause is the last error in the iterator produced by + /// [`chain()`][Report::chain]. + pub fn root_cause(&self) -> &(dyn StdError + 'static) { + let mut chain = self.chain(); + let mut root_cause = chain.next().unwrap(); + for cause in chain { + root_cause = cause; + } + root_cause + } + + /// Returns true if `E` is the type held by this error object. + /// + /// For errors constructed from messages, this method returns true if `E` matches the type of + /// the message `D` **or** the type of the error on which the message has been attached. For + /// details about the interaction between message and downcasting, [see here]. + /// + /// [see here]: trait.WrapErr.html#effect-on-downcasting + pub fn is(&self) -> bool + where + E: Display + Debug + Send + Sync + 'static, + { + self.downcast_ref::().is_some() + } + + /// Attempt to downcast the error object to a concrete type. + pub fn downcast(self) -> Result + where + E: Display + Debug + Send + Sync + 'static, + { + let target = TypeId::of::(); + unsafe { + // Use vtable to find NonNull<()> which points to a value of type E + // somewhere inside the data structure. + let addr = match (self.inner.vtable.object_downcast)(&self.inner, target) { + Some(addr) => addr, + None => return Err(self), + }; + + // Prepare to read E out of the data structure. We'll drop the rest + // of the data structure separately so that E is not dropped. + let outer = ManuallyDrop::new(self); + + // Read E from where the vtable found it. + let error = ptr::read(addr.cast::().as_ptr()); + + // Read Box> from self. Can't move it out because + // Report has a Drop impl which we want to not run. + let inner = ptr::read(&outer.inner); + let erased = ManuallyDrop::into_inner(inner); + + // Drop rest of the data structure outside of E. + (erased.vtable.object_drop_rest)(erased, target); + + Ok(error) + } + } + + /// Downcast this error object by reference. + /// + /// # Example + /// + /// ``` + /// # use miette::{Report, miette}; + /// # use std::fmt::{self, Display}; + /// # use std::task::Poll; + /// # + /// # #[derive(Debug)] + /// # enum DataStoreError { + /// # Censored(()), + /// # } + /// # + /// # impl Display for DataStoreError { + /// # fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + /// # unimplemented!() + /// # } + /// # } + /// # + /// # impl std::error::Error for DataStoreError {} + /// # + /// # const REDACTED_CONTENT: () = (); + /// # + /// # let error: Report = miette!("..."); + /// # let root_cause = &error; + /// # + /// # let ret = + /// // If the error was caused by redaction, then return a tombstone instead + /// // of the content. + /// match root_cause.downcast_ref::() { + /// Some(DataStoreError::Censored(_)) => Ok(Poll::Ready(REDACTED_CONTENT)), + /// None => Err(error), + /// } + /// # ; + /// ``` + pub fn downcast_ref(&self) -> Option<&E> + where + E: Display + Debug + Send + Sync + 'static, + { + let target = TypeId::of::(); + unsafe { + // Use vtable to find NonNull<()> which points to a value of type E + // somewhere inside the data structure. + let addr = (self.inner.vtable.object_downcast)(&self.inner, target)?; + Some(&*addr.cast::().as_ptr()) + } + } + + /// Downcast this error object by mutable reference. + pub fn downcast_mut(&mut self) -> Option<&mut E> + where + E: Display + Debug + Send + Sync + 'static, + { + let target = TypeId::of::(); + unsafe { + // Use vtable to find NonNull<()> which points to a value of type E + // somewhere inside the data structure. + let addr = (self.inner.vtable.object_downcast)(&self.inner, target)?; + Some(&mut *addr.cast::().as_ptr()) + } + } + + /// Get a reference to the Handler for this Report. + pub fn handler(&self) -> &dyn ReportHandler { + self.inner.handler.as_ref().unwrap().as_ref() + } + + /// Get a mutable reference to the Handler for this Report. + pub fn handler_mut(&mut self) -> &mut dyn ReportHandler { + self.inner.handler.as_mut().unwrap().as_mut() + } + + /// Get a reference to the Handler for this Report. + #[doc(hidden)] + pub fn context(&self) -> &dyn ReportHandler { + self.inner.handler.as_ref().unwrap().as_ref() + } + + /// Get a mutable reference to the Handler for this Report. + #[doc(hidden)] + pub fn context_mut(&mut self) -> &mut dyn ReportHandler { + self.inner.handler.as_mut().unwrap().as_mut() + } +} + +impl From for Report +where + E: Diagnostic + Send + Sync + 'static, +{ + #[cfg_attr(track_caller, track_caller)] + fn from(error: E) -> Self { + Report::from_std(error) + } +} + +impl Deref for Report { + type Target = dyn Diagnostic + Send + Sync + 'static; + + fn deref(&self) -> &Self::Target { + self.inner.diagnostic() + } +} + +impl DerefMut for Report { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner.diagnostic_mut() + } +} + +impl Display for Report { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.display(formatter) + } +} + +impl Debug for Report { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.debug(formatter) + } +} + +impl Drop for Report { + fn drop(&mut self) { + unsafe { + // Read Box> from self. + let inner = ptr::read(&self.inner); + let erased = ManuallyDrop::into_inner(inner); + + // Invoke the vtable's drop behavior. + (erased.vtable.object_drop)(erased); + } + } +} + +struct ErrorVTable { + object_drop: unsafe fn(Box>), + object_ref: unsafe fn(&ErrorImpl<()>) -> &(dyn Diagnostic + Send + Sync + 'static), + object_mut: unsafe fn(&mut ErrorImpl<()>) -> &mut (dyn Diagnostic + Send + Sync + 'static), + object_ref_stderr: unsafe fn(&ErrorImpl<()>) -> &(dyn StdError + Send + Sync + 'static), + #[allow(clippy::type_complexity)] + object_boxed: unsafe fn(Box>) -> Box, + object_downcast: unsafe fn(&ErrorImpl<()>, TypeId) -> Option>, + object_drop_rest: unsafe fn(Box>, TypeId), +} + +// Safety: requires layout of *e to match ErrorImpl. +unsafe fn object_drop(e: Box>) { + // Cast back to ErrorImpl so that the allocator receives the correct + // Layout to deallocate the Box's memory. + let unerased = mem::transmute::>, Box>>(e); + drop(unerased); +} + +// Safety: requires layout of *e to match ErrorImpl. +unsafe fn object_drop_front(e: Box>, target: TypeId) { + // Drop the fields of ErrorImpl other than E as well as the Box allocation, + // without dropping E itself. This is used by downcast after doing a + // ptr::read to take ownership of the E. + let _ = target; + let unerased = mem::transmute::>, Box>>>(e); + drop(unerased); +} + +// Safety: requires layout of *e to match ErrorImpl. +unsafe fn object_ref(e: &ErrorImpl<()>) -> &(dyn Diagnostic + Send + Sync + 'static) +where + E: Diagnostic + Send + Sync + 'static, +{ + // Attach E's native StdError vtable onto a pointer to self._object. + &(*(e as *const ErrorImpl<()> as *const ErrorImpl))._object +} + +// Safety: requires layout of *e to match ErrorImpl. +unsafe fn object_mut(e: &mut ErrorImpl<()>) -> &mut (dyn Diagnostic + Send + Sync + 'static) +where + E: Diagnostic + Send + Sync + 'static, +{ + // Attach E's native StdError vtable onto a pointer to self._object. + &mut (*(e as *mut ErrorImpl<()> as *mut ErrorImpl))._object +} + +// Safety: requires layout of *e to match ErrorImpl. +unsafe fn object_ref_stderr(e: &ErrorImpl<()>) -> &(dyn StdError + Send + Sync + 'static) +where + E: StdError + Send + Sync + 'static, +{ + // Attach E's native StdError vtable onto a pointer to self._object. + &(*(e as *const ErrorImpl<()> as *const ErrorImpl))._object +} + +// Safety: requires layout of *e to match ErrorImpl. +unsafe fn object_boxed(e: Box>) -> Box +where + E: Diagnostic + Send + Sync + 'static, +{ + // Attach ErrorImpl's native StdError vtable. The StdError impl is below. + mem::transmute::>, Box>>(e) +} + +// Safety: requires layout of *e to match ErrorImpl. +unsafe fn object_downcast(e: &ErrorImpl<()>, target: TypeId) -> Option> +where + E: 'static, +{ + if TypeId::of::() == target { + // Caller is looking for an E pointer and e is ErrorImpl, take a + // pointer to its E field. + let unerased = e as *const ErrorImpl<()> as *const ErrorImpl; + let addr = &(*unerased)._object as *const E as *mut (); + Some(NonNull::new_unchecked(addr)) + } else { + None + } +} + +// Safety: requires layout of *e to match ErrorImpl>. +unsafe fn context_downcast(e: &ErrorImpl<()>, target: TypeId) -> Option> +where + D: 'static, + E: 'static, +{ + if TypeId::of::() == target { + let unerased = e as *const ErrorImpl<()> as *const ErrorImpl>; + let addr = &(*unerased)._object.msg as *const D as *mut (); + Some(NonNull::new_unchecked(addr)) + } else if TypeId::of::() == target { + let unerased = e as *const ErrorImpl<()> as *const ErrorImpl>; + let addr = &(*unerased)._object.error as *const E as *mut (); + Some(NonNull::new_unchecked(addr)) + } else { + None + } +} + +// Safety: requires layout of *e to match ErrorImpl>. +unsafe fn context_drop_rest(e: Box>, target: TypeId) +where + D: 'static, + E: 'static, +{ + // Called after downcasting by value to either the D or the E and doing a + // ptr::read to take ownership of that value. + if TypeId::of::() == target { + let unerased = mem::transmute::< + Box>, + Box, E>>>, + >(e); + drop(unerased); + } else { + let unerased = mem::transmute::< + Box>, + Box>>>, + >(e); + drop(unerased); + } +} + +// Safety: requires layout of *e to match ErrorImpl>. +unsafe fn context_chain_downcast(e: &ErrorImpl<()>, target: TypeId) -> Option> +where + D: 'static, +{ + let unerased = e as *const ErrorImpl<()> as *const ErrorImpl>; + if TypeId::of::() == target { + let addr = &(*unerased)._object.msg as *const D as *mut (); + Some(NonNull::new_unchecked(addr)) + } else { + // Recurse down the context chain per the inner error's vtable. + let source = &(*unerased)._object.error; + (source.inner.vtable.object_downcast)(&source.inner, target) + } +} + +// Safety: requires layout of *e to match ErrorImpl>. +unsafe fn context_chain_drop_rest(e: Box>, target: TypeId) +where + D: 'static, +{ + // Called after downcasting by value to either the D or one of the causes + // and doing a ptr::read to take ownership of that value. + if TypeId::of::() == target { + let unerased = mem::transmute::< + Box>, + Box, Report>>>, + >(e); + // Drop the entire rest of the data structure rooted in the next Report. + drop(unerased); + } else { + let unerased = mem::transmute::< + Box>, + Box>>>, + >(e); + // Read out a ManuallyDrop>> from the next error. + let inner = ptr::read(&unerased._object.error.inner); + drop(unerased); + let erased = ManuallyDrop::into_inner(inner); + // Recursively drop the next error using the same target typeid. + (erased.vtable.object_drop_rest)(erased, target); + } +} + +// repr C to ensure that E remains in the final position. +#[repr(C)] +pub(crate) struct ErrorImpl { + vtable: &'static ErrorVTable, + pub(crate) handler: Option>, + // NOTE: Don't use directly. Use only through vtable. Erased type may have + // different alignment. + _object: E, +} + +// repr C to ensure that ContextError has the same layout as +// ContextError, E> and ContextError>. +#[repr(C)] +pub(crate) struct ContextError { + pub(crate) msg: D, + pub(crate) error: E, +} + +impl ErrorImpl { + fn erase(&self) -> &ErrorImpl<()> { + // Erase the concrete type of E but preserve the vtable in self.vtable + // for manipulating the resulting thin pointer. This is analogous to an + // unsize coercion. + unsafe { &*(self as *const ErrorImpl as *const ErrorImpl<()>) } + } +} + +impl ErrorImpl<()> { + pub(crate) fn error(&self) -> &(dyn StdError + Send + Sync + 'static) { + // Use vtable to attach E's native StdError vtable for the right + // original type E. + unsafe { &*(self.vtable.object_ref_stderr)(self) } + } + + pub(crate) fn diagnostic(&self) -> &(dyn Diagnostic + Send + Sync + 'static) { + // Use vtable to attach E's native StdError vtable for the right + // original type E. + unsafe { &*(self.vtable.object_ref)(self) } + } + + pub(crate) fn diagnostic_mut(&mut self) -> &mut (dyn Diagnostic + Send + Sync + 'static) { + // Use vtable to attach E's native StdError vtable for the right + // original type E. + unsafe { &mut *(self.vtable.object_mut)(self) } + } + + pub(crate) fn chain(&self) -> Chain<'_> { + Chain::new(self.error()) + } +} + +impl StdError for ErrorImpl +where + E: StdError, +{ + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.erase().diagnostic().source() + } +} + +impl Diagnostic for ErrorImpl where E: Diagnostic {} + +impl Debug for ErrorImpl +where + E: Debug, +{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.erase().debug(formatter) + } +} + +impl Display for ErrorImpl +where + E: Display, +{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.erase().diagnostic(), formatter) + } +} + +impl From for Box { + fn from(error: Report) -> Self { + let outer = ManuallyDrop::new(error); + unsafe { + // Read Box> from error. Can't move it out because + // Report has a Drop impl which we want to not run. + let inner = ptr::read(&outer.inner); + let erased = ManuallyDrop::into_inner(inner); + + // Use vtable to attach ErrorImpl's native StdError vtable for + // the right original type E. + (erased.vtable.object_boxed)(erased) + } + } +} + +impl From for Box { + fn from(error: Report) -> Self { + Box::::from(error) + } +} + +impl AsRef for Report { + fn as_ref(&self) -> &(dyn Diagnostic + Send + Sync + 'static) { + &**self + } +} + +impl AsRef for Report { + fn as_ref(&self) -> &(dyn Diagnostic + 'static) { + &**self + } +} diff --git a/src/eyreish/fmt.rs b/src/eyreish/fmt.rs new file mode 100644 index 0000000..bb094ca --- /dev/null +++ b/src/eyreish/fmt.rs @@ -0,0 +1,18 @@ +use super::error::ErrorImpl; +use core::fmt; + +impl ErrorImpl<()> { + pub(crate) fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.handler + .as_ref() + .map(|handler| handler.display(self.error(), f)) + .unwrap_or_else(|| core::fmt::Display::fmt(self.diagnostic(), f)) + } + + pub(crate) fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.handler + .as_ref() + .map(|handler| handler.debug(self.diagnostic(), f)) + .unwrap_or_else(|| core::fmt::Debug::fmt(self.diagnostic(), f)) + } +} diff --git a/src/eyreish/into_diagnostic.rs b/src/eyreish/into_diagnostic.rs new file mode 100644 index 0000000..3e03694 --- /dev/null +++ b/src/eyreish/into_diagnostic.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +use crate::{Diagnostic, Report}; + +/// Convenience [Diagnostic] that can be used as an "anonymous" wrapper for +/// Errors. This is intended to be paired with [IntoDiagnostic]. +#[derive(Debug, Error)] +#[error(transparent)] +struct DiagnosticError(Box); +impl Diagnostic for DiagnosticError {} + +/** +Convenience trait that adds a `.into_diagnostic()` method that converts a type to a `Result`. +*/ +pub trait IntoDiagnostic { + /// Converts [Result]-like types that return regular errors into a + /// `Result` that returns a [Diagnostic]. + fn into_diagnostic(self) -> Result; +} + +impl IntoDiagnostic for Result { + fn into_diagnostic(self) -> Result { + self.map_err(|e| DiagnosticError(Box::new(e)).into()) + } +} diff --git a/src/eyreish/kind.rs b/src/eyreish/kind.rs new file mode 100644 index 0000000..ce60b50 --- /dev/null +++ b/src/eyreish/kind.rs @@ -0,0 +1,111 @@ +#![allow(missing_debug_implementations, missing_docs)] +// Tagged dispatch mechanism for resolving the behavior of `miette!($expr)`. +// +// When miette! is given a single expr argument to turn into miette::Report, we +// want the resulting Report to pick up the input's implementation of source() +// and backtrace() if it has a std::error::Error impl, otherwise require nothing +// more than Display and Debug. +// +// Expressed in terms of specialization, we want something like: +// +// trait EyreNew { +// fn new(self) -> Report; +// } +// +// impl EyreNew for T +// where +// T: Display + Debug + Send + Sync + 'static, +// { +// default fn new(self) -> Report { +// /* no std error impl */ +// } +// } +// +// impl EyreNew for T +// where +// T: std::error::Error + Send + Sync + 'static, +// { +// fn new(self) -> Report { +// /* use std error's source() and backtrace() */ +// } +// } +// +// Since specialization is not stable yet, instead we rely on autoref behavior +// of method resolution to perform tagged dispatch. Here we have two traits +// AdhocKind and TraitKind that both have an miette_kind() method. AdhocKind is +// implemented whether or not the caller's type has a std error impl, while +// TraitKind is implemented only when a std error impl does exist. The ambiguity +// is resolved by AdhocKind requiring an extra autoref so that it has lower +// precedence. +// +// The miette! macro will set up the call in this form: +// +// #[allow(unused_imports)] +// use $crate::private::{AdhocKind, TraitKind}; +// let error = $msg; +// (&error).miette_kind().new(error) + +use super::Report; +use core::fmt::{Debug, Display}; + +use crate::Diagnostic; + +pub struct Adhoc; + +pub trait AdhocKind: Sized { + #[inline] + fn miette_kind(&self) -> Adhoc { + Adhoc + } +} + +impl AdhocKind for &T where T: ?Sized + Display + Debug + Send + Sync + 'static {} + +impl Adhoc { + #[cfg_attr(track_caller, track_caller)] + pub fn new(self, message: M) -> Report + where + M: Display + Debug + Send + Sync + 'static, + { + Report::from_adhoc(message) + } +} + +pub struct Trait; + +pub trait TraitKind: Sized { + #[inline] + fn miette_kind(&self) -> Trait { + Trait + } +} + +impl TraitKind for E where E: Into {} + +impl Trait { + #[cfg_attr(track_caller, track_caller)] + pub fn new(self, error: E) -> Report + where + E: Into, + { + error.into() + } +} + +pub struct Boxed; + +pub trait BoxedKind: Sized { + #[inline] + fn miette_kind(&self) -> Boxed { + Boxed + } +} + +impl BoxedKind for Box {} + +impl Boxed { + #[cfg_attr(track_caller, track_caller)] + pub fn new(self, error: Box) -> Report { + Report::from_boxed(error) + } +} diff --git a/src/eyreish/macros.rs b/src/eyreish/macros.rs new file mode 100644 index 0000000..ff8c1fe --- /dev/null +++ b/src/eyreish/macros.rs @@ -0,0 +1,164 @@ +/// Return early with an error. +/// +/// This macro is equivalent to `return Err(From::from($err))`. +/// +/// # Example +/// +/// ``` +/// # use miette::{bail, Result}; +/// # +/// # fn has_permission(user: usize, resource: usize) -> bool { +/// # true +/// # } +/// # +/// # fn main() -> Result<()> { +/// # let user = 0; +/// # let resource = 0; +/// # +/// if !has_permission(user, resource) { +/// bail!("permission denied for accessing {}", resource); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// ``` +/// # use miette::{bail, Result}; +/// # use thiserror::Error; +/// # +/// # const MAX_DEPTH: usize = 1; +/// # +/// #[derive(Error, Debug)] +/// enum ScienceError { +/// #[error("recursion limit exceeded")] +/// RecursionLimitExceeded, +/// # #[error("...")] +/// # More = (stringify! { +/// ... +/// # }, 1).1, +/// } +/// +/// # fn main() -> Result<()> { +/// # let depth = 0; +/// # let err: &'static dyn std::error::Error = &ScienceError::RecursionLimitExceeded; +/// # +/// if depth > MAX_DEPTH { +/// bail!(ScienceError::RecursionLimitExceeded); +/// } +/// # Ok(()) +/// # } +/// ``` +#[macro_export] +macro_rules! bail { + ($msg:literal $(,)?) => { + return $crate::private::Err($crate::miette!($msg)); + }; + ($err:expr $(,)?) => { + return $crate::private::Err($crate::miette!($err)); + }; + ($fmt:expr, $($arg:tt)*) => { + return $crate::private::Err($crate::miette!($fmt, $($arg)*)); + }; +} + +/// Return early with an error if a condition is not satisfied. +/// +/// This macro is equivalent to `if !$cond { return Err(From::from($err)); }`. +/// +/// Analogously to `assert!`, `ensure!` takes a condition and exits the function +/// if the condition fails. Unlike `assert!`, `ensure!` returns an `Error` +/// rather than panicking. +/// +/// # Example +/// +/// ``` +/// # use miette::{ensure, Result}; +/// # +/// # fn main() -> Result<()> { +/// # let user = 0; +/// # +/// ensure!(user == 0, "only user 0 is allowed"); +/// # Ok(()) +/// # } +/// ``` +/// +/// ``` +/// # use miette::{ensure, Result}; +/// # use thiserror::Error; +/// # +/// # const MAX_DEPTH: usize = 1; +/// # +/// #[derive(Error, Debug)] +/// enum ScienceError { +/// #[error("recursion limit exceeded")] +/// RecursionLimitExceeded, +/// # #[error("...")] +/// # More = (stringify! { +/// ... +/// # }, 1).1, +/// } +/// +/// # fn main() -> Result<()> { +/// # let depth = 0; +/// # +/// ensure!(depth <= MAX_DEPTH, ScienceError::RecursionLimitExceeded); +/// # Ok(()) +/// # } +/// ``` +#[macro_export] +macro_rules! ensure { + ($cond:expr, $msg:literal $(,)?) => { + if !$cond { + return $crate::private::Err($crate::miette!($msg)); + } + }; + ($cond:expr, $err:expr $(,)?) => { + if !$cond { + return $crate::private::Err($crate::miette!($err)); + } + }; + ($cond:expr, $fmt:expr, $($arg:tt)*) => { + if !$cond { + return $crate::private::Err($crate::miette!($fmt, $($arg)*)); + } + }; +} + +/// Construct an ad-hoc error from a string. +/// +/// This evaluates to an `Error`. It can take either just a string, or a format +/// string with arguments. It also can take any custom type which implements +/// `Debug` and `Display`. +/// +/// # Example +/// +/// ``` +/// # type V = (); +/// # +/// use miette::{miette, Result}; +/// +/// fn lookup(key: &str) -> Result { +/// if key.len() != 16 { +/// return Err(miette!("key length must be 16 characters, got {:?}", key)); +/// } +/// +/// // ... +/// # Ok(()) +/// } +/// ``` +#[macro_export] +macro_rules! miette { + ($msg:literal $(,)?) => { + // Handle $:literal as a special case to make cargo-expanded code more + // concise in the common case. + $crate::private::new_adhoc($msg) + }; + ($err:expr $(,)?) => ({ + use $crate::private::kind::*; + let error = $err; + (&error).miette_kind().new(error) + }); + ($fmt:expr, $($arg:tt)*) => { + $crate::private::new_adhoc(format!($fmt, $($arg)*)) + }; +} diff --git a/src/eyreish/mod.rs b/src/eyreish/mod.rs new file mode 100644 index 0000000..8a45e27 --- /dev/null +++ b/src/eyreish/mod.rs @@ -0,0 +1,490 @@ +#![cfg_attr(doc_cfg, feature(doc_cfg))] +#![allow( + clippy::needless_doctest_main, + clippy::new_ret_no_self, + clippy::wrong_self_convention +)] +use core::fmt::Display; +use core::mem::ManuallyDrop; + +use std::error::Error as StdError; + +use atty::Stream; +use once_cell::sync::OnceCell; + +#[allow(unreachable_pub)] +pub use into_diagnostic::*; +#[doc(hidden)] +#[allow(unreachable_pub)] +pub use Report as ErrReport; +/// Compatibility re-export of `Report` for interop with `anyhow` +#[allow(unreachable_pub)] +pub use Report as Error; +#[doc(hidden)] +#[allow(unreachable_pub)] +pub use ReportHandler as EyreContext; +/// Compatibility re-export of `WrapErr` for interop with `anyhow` +#[allow(unreachable_pub)] +pub use WrapErr as Context; + +use crate::{Diagnostic, GraphicalReportHandler, NarratableReportHandler}; +use error::ErrorImpl; + +mod context; +mod error; +mod fmt; +mod into_diagnostic; +mod kind; +mod macros; +mod wrapper; + +/** +Core Diagnostic wrapper type. +*/ +pub struct Report { + inner: ManuallyDrop>>, +} + +type ErrorHook = + Box Box + Sync + Send + 'static>; + +static HOOK: OnceCell = OnceCell::new(); + +/// Error indicating that `set_hook` was unable to install the provided ErrorHook +#[derive(Debug)] +pub struct InstallError; + +impl core::fmt::Display for InstallError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("cannot install provided ErrorHook, a hook has already been installed") + } +} + +impl StdError for InstallError {} +impl Diagnostic for InstallError {} + +/** +Set the hook? +*/ +pub fn set_hook(hook: ErrorHook) -> Result<(), InstallError> { + HOOK.set(hook).map_err(|_| InstallError) +} + +#[cfg_attr(track_caller, track_caller)] +#[cfg_attr(not(track_caller), allow(unused_mut))] +fn capture_handler(error: &(dyn Diagnostic + 'static)) -> Box { + let hook = HOOK.get_or_init(|| Box::new(get_default_printer)).as_ref(); + + #[cfg(track_caller)] + { + let mut handler = hook(error); + handler.track_caller(std::panic::Location::caller()); + handler + } + #[cfg(not(track_caller))] + { + hook(error) + } +} + +fn get_default_printer(_err: &(dyn Diagnostic + 'static)) -> Box { + let fancy = if let Ok(string) = std::env::var("NO_COLOR") { + string == "0" + } else if let Ok(string) = std::env::var("CLICOLOR") { + string != "0" || string == "1" + } else { + atty::is(Stream::Stdout) && atty::is(Stream::Stderr) && !ci_info::is_ci() + }; + if fancy { + Box::new(GraphicalReportHandler::new()) + } else { + Box::new(NarratableReportHandler) + } +} + +impl dyn ReportHandler { + /// + pub fn is(&self) -> bool { + // Get `TypeId` of the type this function is instantiated with. + let t = core::any::TypeId::of::(); + + // Get `TypeId` of the type in the trait object (`self`). + let concrete = self.type_id(); + + // Compare both `TypeId`s on equality. + t == concrete + } + + /// + pub fn downcast_ref(&self) -> Option<&T> { + if self.is::() { + unsafe { Some(&*(self as *const dyn ReportHandler as *const T)) } + } else { + None + } + } + + /// + pub fn downcast_mut(&mut self) -> Option<&mut T> { + if self.is::() { + unsafe { Some(&mut *(self as *mut dyn ReportHandler as *mut T)) } + } else { + None + } + } +} + +/// Error Report Handler trait for customizing `miette::Report` +pub trait ReportHandler: core::any::Any + Send + Sync { + /// Define the report format + /// + /// Used to override the report format of `miette::Report` + /// + /// # Example + /// + /// ```rust + /// use miette::Diagnostic; + /// use miette::ReportHandler; + /// use indenter::indented; + /// + /// pub struct Handler; + /// + /// impl ReportHandler for Handler { + /// fn debug( + /// &self, + /// error: &(dyn Diagnostic + 'static), + /// f: &mut core::fmt::Formatter<'_>, + /// ) -> core::fmt::Result { + /// use core::fmt::Write as _; + /// + /// if f.alternate() { + /// return core::fmt::Debug::fmt(error, f); + /// } + /// + /// write!(f, "{}", error)?; + /// + /// Ok(()) + /// } + /// } + /// ``` + fn debug( + &self, + error: &(dyn Diagnostic + 'static), + f: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result; + + /// Override for the `Display` format + fn display( + &self, + error: &(dyn StdError + 'static), + f: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { + write!(f, "{}", error)?; + + if f.alternate() { + for cause in crate::chain::Chain::new(error).skip(1) { + write!(f, ": {}", cause)?; + } + } + + Ok(()) + } + + /// Store the location of the caller who constructed this error report + #[allow(unused_variables)] + fn track_caller(&mut self, location: &'static std::panic::Location<'static>) {} +} + +/// Iterator of a chain of source errors. +/// +/// This type is the iterator returned by [`Report::chain`]. +/// +/// # Example +/// +/// ``` +/// use miette::Report; +/// use std::io; +/// +/// pub fn underlying_io_error_kind(error: &Report) -> Option { +/// for cause in error.chain() { +/// if let Some(io_error) = cause.downcast_ref::() { +/// return Some(io_error.kind()); +/// } +/// } +/// None +/// } +/// ``` +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct Chain<'a> { + state: crate::chain::ChainState<'a>, +} + +/// type alias for `Result` +/// +/// This is a reasonable return type to use throughout your application but also for `fn main`; if +/// you do, failures will be printed along with a backtrace if one was captured. +/// +/// `miette::Result` may be used with one *or* two type parameters. +/// +/// ```rust +/// use miette::Result; +/// +/// # const IGNORE: &str = stringify! { +/// fn demo1() -> Result {...} +/// // ^ equivalent to std::result::Result +/// +/// fn demo2() -> Result {...} +/// // ^ equivalent to std::result::Result +/// # }; +/// ``` +/// +/// # Example +/// +/// ``` +/// # pub trait Deserialize {} +/// # +/// # mod serde_json { +/// # use super::Deserialize; +/// # use std::io; +/// # +/// # pub fn from_str(json: &str) -> io::Result { +/// # unimplemented!() +/// # } +/// # } +/// # +/// # #[derive(Debug)] +/// # struct ClusterMap; +/// # +/// # impl Deserialize for ClusterMap {} +/// # +/// use miette::{IntoDiagnostic, Result}; +/// +/// fn main() -> Result<()> { +/// # return Ok(()); +/// let config = std::fs::read_to_string("cluster.json").into_diagnostic()?; +/// let map: ClusterMap = serde_json::from_str(&config).into_diagnostic()?; +/// println!("cluster info: {:#?}", map); +/// Ok(()) +/// } +/// ``` +pub type Result = core::result::Result; + +/// Provides the `wrap_err` method for `Result`. +/// +/// This trait is sealed and cannot be implemented for types outside of +/// `miette`. +/// +/// # Example +/// +/// ``` +/// use miette::{WrapErr, IntoDiagnostic, Result}; +/// use std::fs; +/// use std::path::PathBuf; +/// +/// pub struct ImportantThing { +/// path: PathBuf, +/// } +/// +/// impl ImportantThing { +/// # const IGNORE: &'static str = stringify! { +/// pub fn detach(&mut self) -> Result<()> {...} +/// # }; +/// # fn detach(&mut self) -> Result<()> { +/// # unimplemented!() +/// # } +/// } +/// +/// pub fn do_it(mut it: ImportantThing) -> Result> { +/// it.detach().wrap_err("Failed to detach the important thing")?; +/// +/// let path = &it.path; +/// let content = fs::read(path) +/// .into_diagnostic() +/// .wrap_err_with(|| format!("Failed to read instrs from {}", path.display()))?; +/// +/// Ok(content) +/// } +/// ``` +/// +/// When printed, the outermost error would be printed first and the lower +/// level underlying causes would be enumerated below. +/// +/// ```console +/// Error: Failed to read instrs from ./path/to/instrs.json +/// +/// Caused by: +/// No such file or directory (os error 2) +/// ``` +/// +/// # Wrapping Types That Don't impl `Error` (e.g. `&str` and `Box`) +/// +/// Due to restrictions for coherence `Report` cannot impl `From` for types that don't impl +/// `Error`. Attempts to do so will give "this type might implement Error in the future" as an +/// error. As such, `wrap_err`, which uses `From` under the hood, cannot be used to wrap these +/// types. Instead we encourage you to use the combinators provided for `Result` in `std`/`core`. +/// +/// For example, instead of this: +/// +/// ```rust,compile_fail +/// use std::error::Error; +/// use miette::{WrapErr, Report}; +/// +/// fn wrap_example(err: Result<(), Box>) -> Result<(), Report> { +/// err.wrap_err("saw a downstream error") +/// } +/// ``` +/// +/// We encourage you to write this: +/// +/// ```rust +/// use std::error::Error; +/// use miette::{WrapErr, Report, miette}; +/// +/// fn wrap_example(err: Result<(), Box>) -> Result<(), Report> { +/// err.map_err(|e| miette!(e)).wrap_err("saw a downstream error") +/// } +/// ``` +/// +/// # Effect on downcasting +/// +/// After attaching a message of type `D` onto an error of type `E`, the resulting +/// `miette::Error` may be downcast to `D` **or** to `E`. +/// +/// That is, in codebases that rely on downcasting, Eyre's wrap_err supports +/// both of the following use cases: +/// +/// - **Attaching messages whose type is insignificant onto errors whose type +/// is used in downcasts.** +/// +/// In other error libraries whose wrap_err is not designed this way, it can +/// be risky to introduce messages to existing code because new message might +/// break existing working downcasts. In Eyre, any downcast that worked +/// before adding the message will continue to work after you add a message, so +/// you should freely wrap errors wherever it would be helpful. +/// +/// ``` +/// # use miette::bail; +/// # use thiserror::Error; +/// # +/// # #[derive(Error, Debug)] +/// # #[error("???")] +/// # struct SuspiciousError; +/// # +/// # fn helper() -> Result<()> { +/// # bail!(SuspiciousError); +/// # } +/// # +/// use miette::{WrapErr, Result}; +/// +/// fn do_it() -> Result<()> { +/// helper().wrap_err("Failed to complete the work")?; +/// # const IGNORE: &str = stringify! { +/// ... +/// # }; +/// # unreachable!() +/// } +/// +/// fn main() { +/// let err = do_it().unwrap_err(); +/// if let Some(e) = err.downcast_ref::() { +/// // If helper() returned SuspiciousError, this downcast will +/// // correctly succeed even with the message in between. +/// # return; +/// } +/// # panic!("expected downcast to succeed"); +/// } +/// ``` +/// +/// - **Attaching message whose type is used in downcasts onto errors whose +/// type is insignificant.** +/// +/// Some codebases prefer to use machine-readable messages to categorize +/// lower level errors in a way that will be actionable to higher levels of +/// the application. +/// +/// ``` +/// # use miette::bail; +/// # use thiserror::Error; +/// # +/// # #[derive(Error, Debug)] +/// # #[error("???")] +/// # struct HelperFailed; +/// # +/// # fn helper() -> Result<()> { +/// # bail!("no such file or directory"); +/// # } +/// # +/// use miette::{WrapErr, Result}; +/// +/// fn do_it() -> Result<()> { +/// helper().wrap_err(HelperFailed)?; +/// # const IGNORE: &str = stringify! { +/// ... +/// # }; +/// # unreachable!() +/// } +/// +/// fn main() { +/// let err = do_it().unwrap_err(); +/// if let Some(e) = err.downcast_ref::() { +/// // If helper failed, this downcast will succeed because +/// // HelperFailed is the message that has been attached to +/// // that error. +/// # return; +/// } +/// # panic!("expected downcast to succeed"); +/// } +/// ``` +pub trait WrapErr: context::private::Sealed { + /// Wrap the error value with a new adhoc error + #[cfg_attr(track_caller, track_caller)] + fn wrap_err(self, msg: D) -> Result + where + D: Display + Send + Sync + 'static; + + /// Wrap the error value with a new adhoc error that is evaluated lazily + /// only once an error does occur. + #[cfg_attr(track_caller, track_caller)] + fn wrap_err_with(self, f: F) -> Result + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; + + /// Compatibility re-export of wrap_err for interop with `anyhow` + #[cfg_attr(track_caller, track_caller)] + fn context(self, msg: D) -> Result + where + D: Display + Send + Sync + 'static; + + /// Compatibility re-export of wrap_err_with for interop with `anyhow` + #[cfg_attr(track_caller, track_caller)] + fn with_context(self, f: F) -> Result + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; +} + +// Not public API. Referenced by macro-generated code. +#[doc(hidden)] +pub mod private { + use super::Report; + use core::fmt::{Debug, Display}; + + pub use core::result::Result::Err; + + #[doc(hidden)] + pub mod kind { + pub use super::super::kind::{AdhocKind, TraitKind}; + + pub use super::super::kind::BoxedKind; + } + + #[cfg_attr(track_caller, track_caller)] + pub fn new_adhoc(message: M) -> Report + where + M: Display + Debug + Send + Sync + 'static, + { + Report::from_adhoc(message) + } +} diff --git a/src/eyreish/wrapper.rs b/src/eyreish/wrapper.rs new file mode 100644 index 0000000..90e58c1 --- /dev/null +++ b/src/eyreish/wrapper.rs @@ -0,0 +1,88 @@ +use core::fmt::{self, Debug, Display}; + +use std::error::Error as StdError; + +use crate::Diagnostic; + +#[repr(transparent)] +pub(crate) struct DisplayError(pub(crate) M); + +#[repr(transparent)] +pub(crate) struct MessageError(pub(crate) M); + +pub(crate) struct NoneError; + +impl Debug for DisplayError +where + M: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Display for DisplayError +where + M: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl StdError for DisplayError where M: Display + 'static {} +impl Diagnostic for DisplayError where M: Display + 'static {} + +impl Debug for MessageError +where + M: Display + Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl Display for MessageError +where + M: Display + Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl StdError for MessageError where M: Display + Debug + 'static {} +impl Diagnostic for MessageError where M: Display + Debug + 'static {} + +impl Debug for NoneError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt("Option was None", f) + } +} + +impl Display for NoneError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt("Option was None", f) + } +} + +impl StdError for NoneError {} +impl Diagnostic for NoneError {} + +#[repr(transparent)] +pub(crate) struct BoxedError(pub(crate) Box); + +impl Debug for BoxedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl Display for BoxedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl StdError for BoxedError {} +impl Diagnostic for BoxedError {} diff --git a/src/printer/graphical_printer.rs b/src/handlers/graphical.rs similarity index 93% rename from src/printer/graphical_printer.rs rename to src/handlers/graphical.rs index 3625996..b3f9d96 100644 --- a/src/printer/graphical_printer.rs +++ b/src/handlers/graphical.rs @@ -3,34 +3,29 @@ use std::fmt; use owo_colors::{OwoColorize, Style}; use crate::chain::Chain; -use crate::printer::theme::*; -use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity}; -use crate::{SourceSpan, SpanContents}; +use crate::handlers::theme::*; +use crate::protocol::{Diagnostic, DiagnosticSnippet, Severity}; +use crate::{ReportHandler, SourceSpan, SpanContents}; /** -A [DiagnosticReportPrinter] that displays a given [crate::DiagnosticReport] in a quasi-graphical way, using terminal colors, unicode drawing characters, and other such things. +A [ReportHandler] that displays a given [crate::Report] in a quasi-graphical +way, using terminal colors, unicode drawing characters, and other such things. This is the default reporter bundled with `miette`. -This printer can be customized by using `new_themed()` and handing it a [GraphicalTheme] of your own creation (or using one of its own defaults!) +This printer can be customized by using `new_themed()` and handing it a +[GraphicalTheme] of your own creation (or using one of its own defaults!) -See [crate::set_printer] for more details on customizing your global printer. - -## Example - -``` -use miette::{GraphicalReportPrinter, GraphicalTheme}; -miette::set_printer(GraphicalReportPrinter::new_themed(GraphicalTheme::unicode_nocolor())); -``` +See [crate::set_hook] for more details on customizing your global printer. */ #[derive(Debug, Clone)] -pub struct GraphicalReportPrinter { +pub struct GraphicalReportHandler { pub(crate) linkify_code: bool, pub(crate) theme: GraphicalTheme, } -impl GraphicalReportPrinter { - /// Create a new [GraphicalReportPrinter] with the default +impl GraphicalReportHandler { + /// Create a new [GraphicalReportHandler] with the default /// [GraphicalTheme]. This will use both unicode characters and colors. pub fn new() -> Self { Self { @@ -39,7 +34,7 @@ impl GraphicalReportPrinter { } } - ///Create a new [GraphicalReportPrinter] with a given [GraphicalTheme]. + ///Create a new [GraphicalReportHandler] with a given [GraphicalTheme]. pub fn new_themed(theme: GraphicalTheme) -> Self { Self { linkify_code: true, @@ -54,15 +49,15 @@ impl GraphicalReportPrinter { } } -impl Default for GraphicalReportPrinter { +impl Default for GraphicalReportHandler { fn default() -> Self { Self::new() } } -impl GraphicalReportPrinter { +impl GraphicalReportHandler { /// Render a [Diagnostic]. This function is mostly internal and meant to - /// be called by the toplevel [DiagnosticReportPrinter] handler, but is + /// be called by the toplevel [ReportHandler] handler, but is /// made public to make it easier (possible) to test in isolation from /// global state. pub fn render_report( @@ -91,13 +86,18 @@ impl GraphicalReportPrinter { Some(Severity::Advice) => (self.theme.styles.advice, self.theme.characters.point_right), }; write!(f, "{}", self.theme.characters.hbar.to_string().repeat(4))?; - if self.linkify_code && diagnostic.url().is_some() { + if self.linkify_code && diagnostic.url().is_some() && diagnostic.code().is_some() { let url = diagnostic.url().unwrap(); // safe - let code = format!("{} (click for details)", diagnostic.code()); + let code = format!( + "{} (click for details)", + diagnostic + .code() + .expect("MIETTE BUG: already got checked for None") + ); let link = format!("\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", url, code); write!(f, "[{}]", link.style(self.theme.styles.code))?; - } else { - write!(f, "[{}]", diagnostic.code().style(self.theme.styles.code))?; + } else if let Some(code) = diagnostic.code() { + write!(f, "[{}]", code.style(self.theme.styles.code))?; } writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(20),)?; writeln!(f)?; @@ -510,7 +510,7 @@ impl GraphicalReportPrinter { } } -impl DiagnosticReportPrinter for GraphicalReportPrinter { +impl ReportHandler for GraphicalReportHandler { 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/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..521fe50 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,14 @@ +/*! +Reporters included with `miette`. +*/ + +#[allow(unreachable_pub)] +pub use graphical::*; +#[allow(unreachable_pub)] +pub use narratable::*; +#[allow(unreachable_pub)] +pub use theme::*; + +mod graphical; +mod narratable; +mod theme; diff --git a/src/printer/narratable_printer.rs b/src/handlers/narratable.rs similarity index 91% rename from src/printer/narratable_printer.rs rename to src/handlers/narratable.rs index d7ea72c..6603f35 100644 --- a/src/printer/narratable_printer.rs +++ b/src/handlers/narratable.rs @@ -1,34 +1,34 @@ use std::fmt; use crate::chain::Chain; -use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity}; -use crate::{SourceSpan, SpanContents}; +use crate::protocol::{Diagnostic, DiagnosticSnippet, Severity}; +use crate::{ReportHandler, SourceSpan, SpanContents}; /** -[DiagnosticReportPrinter] that renders plain text and avoids extraneous graphics. +[ReportHandler] that renders plain text and avoids extraneous graphics. It's optimized for screen readers and braille users, but is also used in any non-graphical environments, such as non-TTY output. */ #[derive(Debug, Clone)] -pub struct NarratableReportPrinter; +pub struct NarratableReportHandler; -impl NarratableReportPrinter { - /// Create a new [NarratableReportPrinter]. There are no customization +impl NarratableReportHandler { + /// Create a new [NarratableReportHandler]. There are no customization /// options. pub fn new() -> Self { Self } } -impl Default for NarratableReportPrinter { +impl Default for NarratableReportHandler { fn default() -> Self { Self::new() } } -impl NarratableReportPrinter { +impl NarratableReportHandler { /// Render a [Diagnostic]. This function is mostly internal and meant to - /// be called by the toplevel [DiagnosticReportPrinter] handler, but is + /// be called by the toplevel [ReportHandler] handler, but is /// made public to make it easier (possible) to test in isolation from /// global state. pub fn render_report( @@ -75,7 +75,9 @@ impl NarratableReportPrinter { if let Some(help) = diagnostic.help() { writeln!(f, "diagnostic help: {}", help)?; } - writeln!(f, "diagnostic error code: {}", diagnostic.code())?; + if let Some(code) = diagnostic.code() { + writeln!(f, "diagnostic code: {}", code)?; + } if let Some(url) = diagnostic.url() { writeln!(f, "For more details, see {}", url)?; } @@ -192,7 +194,7 @@ impl NarratableReportPrinter { } } -impl DiagnosticReportPrinter for NarratableReportPrinter { +impl ReportHandler for NarratableReportHandler { 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/printer/theme.rs b/src/handlers/theme.rs similarity index 96% rename from src/printer/theme.rs rename to src/handlers/theme.rs index 6f2b464..bd744f0 100644 --- a/src/printer/theme.rs +++ b/src/handlers/theme.rs @@ -2,7 +2,7 @@ use atty::Stream; use owo_colors::Style; /** -Theme used by [crate::GraphicalReportPrinter] to render fancy [crate::Diagnostic] reports. +Theme used by [crate::GraphicalReportHandler] to render fancy [crate::Diagnostic] reports. A theme consists of two things: the set of characters to be used for drawing, and the [owo_colors::Style]s to be used to paint various items. @@ -47,8 +47,8 @@ impl GraphicalTheme { /// A "basic" graphical theme that skips colors and unicode characters and /// just does monochrome ascii art. If you want a completely non-graphical /// rendering of your `Diagnostic`s, check out - /// [crate::NarratableReportPrinter], or write your own - /// [crate::DiagnosticReportPrinter]! + /// [crate::NarratableReportHandler], or write your own + /// [crate::ReportHandler]! pub fn none() -> Self { Self { characters: ThemeCharacters::ascii(), @@ -68,7 +68,7 @@ impl Default for GraphicalTheme { } /** -Styles for various parts of graphical rendering for the [crate::GraphicalReportPrinter]. +Styles for various parts of graphical rendering for the [crate::GraphicalReportHandler]. */ #[derive(Debug, Clone)] pub struct ThemeStyles { @@ -150,7 +150,7 @@ impl ThemeStyles { // Most of these characters were taken from // https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs -/// Characters to be used when drawing when using [crate::GraphicalReportPrinter]. +/// Characters to be used when drawing when using [crate::GraphicalReportHandler]. #[allow(missing_docs)] #[derive(Debug, Clone, Eq, PartialEq)] pub struct ThemeCharacters { diff --git a/src/lib.rs b/src/lib.rs index 1068ba7..6ad4027 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,16 +5,18 @@ pub use miette_derive::*; pub use error::*; -pub use printer::*; +pub use eyreish::*; +pub use handlers::*; +pub use named_source::*; pub use protocol::*; -pub use utils::*; mod chain; mod error; -mod printer; +mod eyreish; +mod handlers; +mod named_source; mod protocol; mod source_impls; -mod utils; #[cfg(doctest)] mod compile_test; diff --git a/src/named_source.rs b/src/named_source.rs new file mode 100644 index 0000000..0a6270d --- /dev/null +++ b/src/named_source.rs @@ -0,0 +1,38 @@ +use crate::Source; + +/// Utility struct for when you have a regular [Source] type, such as a String, +/// that doesn't implement `name`, or if you want to override the `.name()` +/// returned by the `Source`. +#[derive(Debug)] +pub struct NamedSource { + source: Box, + name: String, +} + +impl NamedSource { + /// Create a new [NamedSource] using a regular [Source] and giving it a [Source::name]. + pub fn new(name: impl AsRef, source: impl Source + Send + Sync + 'static) -> Self { + Self { + source: Box::new(source), + name: name.as_ref().to_string(), + } + } + + /// Returns a reference the inner [Source] type for this [NamedSource]. + pub fn inner(&self) -> &(dyn Source + Send + Sync + 'static) { + &*self.source + } +} + +impl Source for NamedSource { + fn read_span<'a>( + &'a self, + span: &crate::SourceSpan, + ) -> Result, crate::MietteError> { + self.source.read_span(span) + } + + fn name(&self) -> Option { + Some(self.name.clone()) + } +} diff --git a/src/printer/mod.rs b/src/printer/mod.rs deleted file mode 100644 index 25f21e1..0000000 --- a/src/printer/mod.rs +++ /dev/null @@ -1,55 +0,0 @@ -/*! -Reporters included with `miette`. -*/ -use atty::Stream; -use once_cell::sync::OnceCell; - -use crate::protocol::DiagnosticReportPrinter; -use crate::MietteError; - -// NOTE(zkat): I don't understand why these three are "unreachable" when -// they're clearly being exported? Maybe a bug? -#[allow(unreachable_pub)] -pub use graphical_printer::*; -#[allow(unreachable_pub)] -pub use narratable_printer::*; -#[allow(unreachable_pub)] -pub use theme::*; - -mod graphical_printer; -mod narratable_printer; -mod theme; - -static REPORTER: OnceCell> = - OnceCell::new(); - -/// Set the global [DiagnosticReportPrinter] that will be used when you report -/// using [crate::DiagnosticReport]. -pub fn set_printer( - reporter: impl DiagnosticReportPrinter + Send + Sync + 'static, -) -> Result<(), MietteError> { - REPORTER - .set(Box::new(reporter)) - .map_err(|_| MietteError::SetPrinterFailure) -} - -/// Used by [crate::DiagnosticReport] to fetch the reporter that will be used to -/// print stuff out. -pub(crate) fn get_printer() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) { - &**REPORTER.get_or_init(get_default_printer) -} - -fn get_default_printer() -> Box { - let fancy = if let Ok(string) = std::env::var("NO_COLOR") { - string == "0" - } else if let Ok(string) = std::env::var("CLICOLOR") { - string != "0" || string == "1" - } else { - atty::is(Stream::Stdout) && atty::is(Stream::Stderr) && !ci_info::is_ci() - }; - if fancy { - Box::new(GraphicalReportPrinter::new()) - } else { - Box::new(NarratableReportPrinter) - } -} diff --git a/src/protocol.rs b/src/protocol.rs index 3c80f9b..5df665d 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -13,7 +13,7 @@ use std::{ use crate::MietteError; /** -Adds rich metadata to your Error that can be used by [DiagnosticReportPrinter] to print +Adds rich metadata to your Error that can be used by [Report] to print really nice and human-friendly error messages. */ pub trait Diagnostic: std::error::Error { @@ -22,9 +22,11 @@ pub trait Diagnostic: std::error::Error { /// the toplevel crate's documentation for easy searching. Rust path /// format (`foo::bar::baz`) is recommended, but more classic codes like /// `E0123` or Enums will work just fine. - fn code<'a>(&'a self) -> Box; + fn code<'a>(&'a self) -> Option> { + None + } - /// Diagnostic severity. This may be used by [DiagnosticReportPrinter]s to change the + /// Diagnostic severity. This may be used by [ReportHandler]s to change the /// display format of this diagnostic. /// /// If `None`, reporters should treat this as [Severity::Error] @@ -72,62 +74,69 @@ impl From for Box, -} - -impl DiagnosticReport { - /// Return a reference to the inner [Diagnostic]. - pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) { - &*self.diagnostic +impl From<&str> for Box { + fn from(s: &str) -> Self { + From::from(String::from(s)) } } -impl std::fmt::Debug for DiagnosticReport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - crate::get_printer().debug(&*self.diagnostic, f) +impl<'a> From<&str> for Box { + fn from(s: &str) -> Self { + From::from(String::from(s)) } } -impl From for DiagnosticReport { - fn from(diagnostic: T) -> Self { - DiagnosticReport { - diagnostic: Box::new(diagnostic), +impl From for Box { + fn from(s: String) -> Self { + let err1: Box = From::from(s); + let err2: Box = err1; + err2 + } +} + +impl From for Box { + fn from(s: String) -> Self { + struct StringError(String); + + impl std::error::Error for StringError {} + impl Diagnostic for StringError {} + + impl Display for StringError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } } + + // Purposefully skip printing "StringError(..)" + impl fmt::Debug for StringError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } + } + + Box::new(StringError(s)) + } +} + +impl From> for Box { + fn from(s: Box) -> Self { + #[derive(thiserror::Error)] + #[error(transparent)] + struct BoxedDiagnostic(Box); + impl fmt::Debug for BoxedDiagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } + } + + impl Diagnostic for BoxedDiagnostic {} + + Box::new(BoxedDiagnostic(s)) } } /** -Protocol for [Diagnostic] handlers, which are responsible for actually printing out Diagnostics. - -Blatantly based on [EyreHandler](https://docs.rs/eyre/0.6.5/eyre/trait.EyreHandler.html) (thanks, Jane!) -*/ -pub trait DiagnosticReportPrinter: core::any::Any + Send + Sync { - /// Define the report format. - fn debug( - &self, - diagnostic: &(dyn Diagnostic), - f: &mut core::fmt::Formatter<'_>, - ) -> core::fmt::Result; - - /// Override for the `Display` format. - fn display( - &self, - diagnostic: &(dyn Diagnostic), - f: &mut core::fmt::Formatter<'_>, - ) -> core::fmt::Result { - write!(f, "{}", diagnostic)?; - Ok(()) - } -} - -/** -[Diagnostic] severity. Intended to be used by [DiagnosticReportPrinter]s to change the +[Diagnostic] severity. Intended to be used by [ReportHandler]s to change the way different Diagnostics are displayed. */ #[derive(Copy, Clone, Debug, Eq, PartialEq)] diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 6b8d4c9..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::fmt; - -use thiserror::Error; - -use crate::{Diagnostic, DiagnosticReport, Source}; - -/// Convenience alias. This is intended to be used as the return type for `main()` -pub type DiagnosticResult = Result; - -/// Convenience [Diagnostic] that can be used as an "anonymous" wrapper for -/// Errors. This is intended to be paired with [IntoDiagnostic]. -#[derive(Debug, Error)] -#[error("{}", self.error)] -pub struct DiagnosticError { - #[source] - 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 { - fn code<'a>(&'a self) -> Box { - Box::new(&self.code) - } -} - -/** -Convenience trait that adds a `.into_diagnostic()` method that converts a type to a `Result`. -*/ -pub trait IntoDiagnostic { - /// Converts [Result]-like types that return regular errors into a - /// `Result` that returns a [Diagnostic]. - fn into_diagnostic(self, code: impl fmt::Display) -> Result; -} - -impl IntoDiagnostic for Result { - fn into_diagnostic(self, code: impl fmt::Display) -> Result { - self.map_err(|e| DiagnosticError { - error: Box::new(e), - code: format!("{}", code), - }) - } -} - -/// Utility struct for when you have a regular [Source] type, such as a String, -/// that doesn't implement `name`, or if you want to override the `.name()` -/// returned by the `Source`. -#[derive(Debug)] -pub struct NamedSource { - source: Box, - name: String, -} - -impl NamedSource { - /// Create a new [NamedSource] using a regular [Source] and giving it a [Source::name]. - pub fn new(name: impl AsRef, source: impl Source + Send + Sync + 'static) -> Self { - Self { - source: Box::new(source), - name: name.as_ref().to_string(), - } - } - - /// Returns a reference the inner [Source] type for this [NamedSource]. - pub fn inner(&self) -> &(dyn Source + Send + Sync + 'static) { - &*self.source - } -} - -impl Source for NamedSource { - fn read_span<'a>( - &'a self, - span: &crate::SourceSpan, - ) -> Result, crate::MietteError> { - self.source.read_span(span) - } - - fn name(&self) -> Option { - Some(self.name.clone()) - } -} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..223810c --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,14 @@ +use miette::{bail, Result}; +use std::io; + +pub fn bail_literal() -> Result<()> { + bail!("oh no!"); +} + +pub fn bail_fmt() -> Result<()> { + bail!("{} {}!", "oh", "no"); +} + +pub fn bail_error() -> Result<()> { + bail!(io::Error::new(io::ErrorKind::Other, "oh no!")); +} diff --git a/tests/compiletest.rs b/tests/compiletest.rs new file mode 100644 index 0000000..f9aea23 --- /dev/null +++ b/tests/compiletest.rs @@ -0,0 +1,6 @@ +#[rustversion::attr(not(nightly), ignore)] +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/tests/derive.rs b/tests/derive.rs index 97c391b..9257650 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -12,7 +12,7 @@ fn basic_struct() { )] struct Foo; - assert_eq!("foo::bar::baz".to_string(), Foo.code().to_string()); + assert_eq!("foo::bar::baz".to_string(), Foo.code().unwrap().to_string()); assert_eq!(Some(Severity::Error), Foo.severity()); @@ -39,11 +39,11 @@ fn basic_enum() { Z { prop: String }, } - assert_eq!("foo::x".to_string(), Foo::X.code().to_string()); - assert_eq!("foo::y".to_string(), Foo::Y(1).code().to_string()); + assert_eq!("foo::x".to_string(), Foo::X.code().unwrap().to_string()); + assert_eq!("foo::y".to_string(), Foo::Y(1).code().unwrap().to_string()); assert_eq!( "foo::z".to_string(), - Foo::Z { prop: "bar".into() }.code().to_string() + Foo::Z { prop: "bar".into() }.code().unwrap().to_string() ); assert_eq!(Some(Severity::Warning), Foo::X.severity()); @@ -57,7 +57,10 @@ fn paren_code() { #[diagnostic(code("foo::bar::baz"))] struct FooStruct; - assert_eq!("foo::bar::baz".to_string(), FooStruct.code().to_string()); + assert_eq!( + "foo::bar::baz".to_string(), + FooStruct.code().unwrap().to_string() + ); #[derive(Debug, Diagnostic, Error)] #[error("welp")] @@ -66,7 +69,7 @@ fn paren_code() { X, } - assert_eq!("foo::x".to_string(), FooEnum::X.code().to_string()); + assert_eq!("foo::x".to_string(), FooEnum::X.code().unwrap().to_string()); } #[test] @@ -76,7 +79,10 @@ fn path_code() { #[diagnostic(code(foo::bar::baz))] struct FooStruct; - assert_eq!("foo::bar::baz".to_string(), FooStruct.code().to_string()); + assert_eq!( + "foo::bar::baz".to_string(), + FooStruct.code().unwrap().to_string() + ); #[derive(Debug, Diagnostic, Error)] #[error("welp")] @@ -85,7 +91,7 @@ fn path_code() { X, } - assert_eq!("foo::x".to_string(), FooEnum::X.code().to_string()); + assert_eq!("foo::x".to_string(), FooEnum::X.code().unwrap().to_string()); } #[test] @@ -341,7 +347,7 @@ impl ForwardsTo { fn check_snippets(diag: &impl Diagnostic) { // check Diagnostic impl forwards all these methods - assert_eq!(diag.code().to_string(), "foo::bar::baz"); + assert_eq!(diag.code().unwrap().to_string(), "foo::bar::baz"); assert_eq!(diag.url().unwrap().to_string(), "https://example.com"); assert_eq!(diag.help().unwrap().to_string(), "help"); assert_eq!(diag.severity().unwrap(), miette::Severity::Warning); @@ -413,7 +419,10 @@ fn test_transparent_enum_named() { check_snippets(&variant); let bar_variant = Enum::BarVariant; - assert_eq!(bar_variant.code().to_string(), "foo::bar::bar_variant"); + assert_eq!( + bar_variant.code().unwrap().to_string(), + "foo::bar::bar_variant" + ); } #[test] diff --git a/tests/drop/mod.rs b/tests/drop/mod.rs new file mode 100644 index 0000000..9a1eabc --- /dev/null +++ b/tests/drop/mod.rs @@ -0,0 +1,55 @@ +use std::error::Error as StdError; +use std::fmt::{self, Display}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::SeqCst; +use std::sync::Arc; + +use miette::Diagnostic; + +#[derive(Debug)] +pub struct Flag { + atomic: Arc, +} + +impl Flag { + pub fn new() -> Self { + Flag { + atomic: Arc::new(AtomicBool::new(false)), + } + } + + pub fn get(&self) -> bool { + self.atomic.load(SeqCst) + } +} + +#[derive(Debug)] +pub struct DetectDrop { + has_dropped: Flag, +} + +impl DetectDrop { + pub fn new(has_dropped: &Flag) -> Self { + DetectDrop { + has_dropped: Flag { + atomic: Arc::clone(&has_dropped.atomic), + }, + } + } +} + +impl StdError for DetectDrop {} +impl Diagnostic for DetectDrop {} + +impl Display for DetectDrop { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "oh no!") + } +} + +impl Drop for DetectDrop { + fn drop(&mut self) { + let already_dropped = self.has_dropped.atomic.swap(true, SeqCst); + assert!(!already_dropped); + } +} diff --git a/tests/narrated.rs b/tests/narrated.rs index 778d3fe..4a30d33 100644 --- a/tests/narrated.rs +++ b/tests/narrated.rs @@ -1,19 +1,19 @@ use miette::{ - Diagnostic, DiagnosticReport, GraphicalReportPrinter, GraphicalTheme, MietteError, NamedSource, - NarratableReportPrinter, SourceSpan, + Diagnostic, GraphicalReportHandler, GraphicalTheme, MietteError, NamedSource, + NarratableReportHandler, Report, SourceSpan, }; use thiserror::Error; -fn fmt_report(diag: DiagnosticReport) -> String { +fn fmt_report(diag: Report) -> String { let mut out = String::new(); // Mostly for dev purposes. if std::env::var("STYLE").is_ok() { - GraphicalReportPrinter::new_themed(GraphicalTheme::unicode()) - .render_report(&mut out, diag.inner()) + GraphicalReportHandler::new_themed(GraphicalTheme::unicode()) + .render_report(&mut out, diag.as_ref()) .unwrap(); } else { - NarratableReportPrinter - .render_report(&mut out, diag.inner()) + NarratableReportHandler + .render_report(&mut out, diag.as_ref()) .unwrap(); }; out @@ -53,7 +53,7 @@ snippet line 2: text snippet line 3: here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -95,7 +95,7 @@ snippet line 2: text snippet line 3: here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -141,7 +141,7 @@ snippet line 2: text text text text text snippet line 3: here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -183,7 +183,7 @@ snippet line 2: text snippet line 3: here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -237,7 +237,7 @@ snippet line 4: line4 snippet line 6: line5 diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -291,7 +291,7 @@ snippet line 4: line4 snippet line 6: line5 diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -338,7 +338,7 @@ snippet line 3: here snippet line 4: more here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -386,7 +386,7 @@ snippet line 2: text snippet line 3: here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -434,7 +434,7 @@ snippet line 2: text snippet line 3: here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start() .to_string(); @@ -469,7 +469,7 @@ Begin snippet starting at line 1, column 1: This is the part that broke snippet line 1: source_text_here diagnostic help: try doing it better next time? -diagnostic error code: oops::my::bad +diagnostic code: oops::my::bad "# .trim_start(); assert_eq!(out, expected); diff --git a/tests/printer.rs b/tests/printer.rs index 9465789..b485c5c 100644 --- a/tests/printer.rs +++ b/tests/printer.rs @@ -1,23 +1,23 @@ use miette::{ - Diagnostic, DiagnosticReport, GraphicalReportPrinter, GraphicalTheme, MietteError, NamedSource, - NarratableReportPrinter, SourceSpan, + Diagnostic, GraphicalReportHandler, GraphicalTheme, MietteError, NamedSource, + NarratableReportHandler, Report, SourceSpan, }; use thiserror::Error; -fn fmt_report(diag: DiagnosticReport) -> String { +fn fmt_report(diag: Report) -> String { let mut out = String::new(); // Mostly for dev purposes. if std::env::var("STYLE").is_ok() { - GraphicalReportPrinter::new_themed(GraphicalTheme::unicode()) - .render_report(&mut out, diag.inner()) + GraphicalReportHandler::new_themed(GraphicalTheme::unicode()) + .render_report(&mut out, diag.as_ref()) .unwrap(); } else if std::env::var("NARRATED").is_ok() { - NarratableReportPrinter - .render_report(&mut out, diag.inner()) + NarratableReportHandler + .render_report(&mut out, diag.as_ref()) .unwrap(); } else { - GraphicalReportPrinter::new_themed(GraphicalTheme::unicode_nocolor()) - .render_report(&mut out, diag.inner()) + GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor()) + .render_report(&mut out, diag.as_ref()) .unwrap(); }; out @@ -481,7 +481,7 @@ fn disable_url_links() -> Result<(), MietteError> { struct MyBad; let err = MyBad; let mut out = String::new(); - GraphicalReportPrinter::new_themed(GraphicalTheme::unicode_nocolor()) + GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor()) .without_code_linking() .render_report(&mut out, &err) .unwrap(); diff --git a/tests/test_autotrait.rs b/tests/test_autotrait.rs new file mode 100644 index 0000000..a4b62f9 --- /dev/null +++ b/tests/test_autotrait.rs @@ -0,0 +1,13 @@ +use miette::Report; + +#[test] +fn test_send() { + fn assert_send() {} + assert_send::(); +} + +#[test] +fn test_sync() { + fn assert_sync() {} + assert_sync::(); +} diff --git a/tests/test_boxed.rs b/tests/test_boxed.rs new file mode 100644 index 0000000..af16db7 --- /dev/null +++ b/tests/test_boxed.rs @@ -0,0 +1,76 @@ +use miette::{miette, Diagnostic, Report}; +use std::error::Error as StdError; +use std::io; +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("outer")] +struct MyError { + source: io::Error, +} +impl Diagnostic for MyError {} + +#[test] +fn test_boxed_str_diagnostic() { + let error = Box::::from("oh no!"); + let error: Report = miette!(error); + assert_eq!("oh no!", error.to_string()); + assert_eq!( + "oh no!", + error + .downcast_ref::>() + .unwrap() + .to_string() + ); +} + +#[test] +fn test_boxed_str_stderr() { + let error = Box::::from("oh no!"); + let error: Report = miette!(error); + assert_eq!("oh no!", error.to_string()); + assert_eq!( + "oh no!", + error + .downcast_ref::>() + .unwrap() + .to_string() + ); +} + +#[test] +fn test_boxed_thiserror() { + let error = MyError { + source: io::Error::new(io::ErrorKind::Other, "oh no!"), + }; + let error: Report = miette!(error); + assert_eq!("oh no!", error.source().unwrap().to_string()); +} + +#[test] +fn test_boxed_miette() { + let error: Report = miette!("oh no!").wrap_err("it failed"); + let error = miette!(error); + assert_eq!("oh no!", error.source().unwrap().to_string()); +} + +#[test] +#[ignore = "I don't know why this isn't working but it needs fixing."] +fn test_boxed_sources() { + let error = MyError { + source: io::Error::new(io::ErrorKind::Other, "oh no!"), + }; + let error = Box::::from(error); + let error: Report = miette!(error).wrap_err("it failed"); + assert_eq!("it failed", error.to_string()); + assert_eq!("outer", error.source().unwrap().to_string()); + assert_eq!( + "oh no!", + error + .source() + .expect("outer") + .source() + .expect("inner") + .to_string() + ); +} diff --git a/tests/test_chain.rs b/tests/test_chain.rs new file mode 100644 index 0000000..3fdd4cd --- /dev/null +++ b/tests/test_chain.rs @@ -0,0 +1,45 @@ +use miette::{miette, Report}; + +fn error() -> Report { + miette!(0).wrap_err(1).wrap_err(2).wrap_err(3) +} + +#[test] +fn test_iter() { + let e = error(); + let mut chain = e.chain(); + assert_eq!("3", chain.next().unwrap().to_string()); + assert_eq!("2", chain.next().unwrap().to_string()); + assert_eq!("1", chain.next().unwrap().to_string()); + assert_eq!("0", chain.next().unwrap().to_string()); + assert!(chain.next().is_none()); + assert!(chain.next_back().is_none()); +} + +#[test] +fn test_rev() { + let e = error(); + let mut chain = e.chain().rev(); + assert_eq!("0", chain.next().unwrap().to_string()); + assert_eq!("1", chain.next().unwrap().to_string()); + assert_eq!("2", chain.next().unwrap().to_string()); + assert_eq!("3", chain.next().unwrap().to_string()); + assert!(chain.next().is_none()); + assert!(chain.next_back().is_none()); +} + +#[test] +fn test_len() { + let e = error(); + let mut chain = e.chain(); + assert_eq!(4, chain.len()); + assert_eq!("3", chain.next().unwrap().to_string()); + assert_eq!(3, chain.len()); + assert_eq!("0", chain.next_back().unwrap().to_string()); + assert_eq!(2, chain.len()); + assert_eq!("2", chain.next().unwrap().to_string()); + assert_eq!(1, chain.len()); + assert_eq!("1", chain.next_back().unwrap().to_string()); + assert_eq!(0, chain.len()); + assert!(chain.next().is_none()); +} diff --git a/tests/test_context.rs b/tests/test_context.rs new file mode 100644 index 0000000..5f78572 --- /dev/null +++ b/tests/test_context.rs @@ -0,0 +1,160 @@ +mod drop; + +use crate::drop::{DetectDrop, Flag}; +use miette::{Diagnostic, IntoDiagnostic, Report, Result, WrapErr}; +use std::fmt::{self, Display}; +use thiserror::Error; + +// https://github.com/dtolnay/miette/issues/18 +#[test] +fn test_inference() -> Result<()> { + let x = "1"; + let y: u32 = x.parse().into_diagnostic().context("...")?; + assert_eq!(y, 1); + Ok(()) +} + +macro_rules! context_type { + ($name:ident) => { + #[derive(Debug)] + struct $name { + message: &'static str, + drop: DetectDrop, + } + + impl Display for $name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.message) + } + } + }; +} + +context_type!(HighLevel); +context_type!(MidLevel); + +#[derive(Diagnostic, Error, Debug)] +#[error("{message}")] +#[diagnostic()] // TODO +struct LowLevel { + message: &'static str, + drop: DetectDrop, +} + +struct Dropped { + low: Flag, + mid: Flag, + high: Flag, +} + +impl Dropped { + fn none(&self) -> bool { + !self.low.get() && !self.mid.get() && !self.high.get() + } + + fn all(&self) -> bool { + self.low.get() && self.mid.get() && self.high.get() + } +} + +fn make_chain() -> (Report, Dropped) { + let dropped = Dropped { + low: Flag::new(), + mid: Flag::new(), + high: Flag::new(), + }; + + let low = LowLevel { + message: "no such file or directory", + drop: DetectDrop::new(&dropped.low), + }; + + // impl Report for Result + let mid = Err::<(), LowLevel>(low) + .wrap_err(MidLevel { + message: "failed to load config", + drop: DetectDrop::new(&dropped.mid), + }) + .unwrap_err(); + + // impl Report for Result + let high = Err::<(), Report>(mid) + .wrap_err(HighLevel { + message: "failed to start server", + drop: DetectDrop::new(&dropped.high), + }) + .unwrap_err(); + + (high, dropped) +} + +#[test] +fn test_downcast_ref() { + let (err, dropped) = make_chain(); + + assert!(!err.is::()); + assert!(err.downcast_ref::().is_none()); + + assert!(err.is::()); + let high = err.downcast_ref::().unwrap(); + assert_eq!(high.to_string(), "failed to start server"); + + assert!(err.is::()); + let mid = err.downcast_ref::().unwrap(); + assert_eq!(mid.to_string(), "failed to load config"); + + assert!(err.is::()); + let low = err.downcast_ref::().unwrap(); + assert_eq!(low.to_string(), "no such file or directory"); + + assert!(dropped.none()); + drop(err); + assert!(dropped.all()); +} + +#[test] +fn test_downcast_high() { + let (err, dropped) = make_chain(); + + let err = err.downcast::().unwrap(); + assert!(!dropped.high.get()); + assert!(dropped.low.get() && dropped.mid.get()); + + drop(err); + assert!(dropped.all()); +} + +#[test] +fn test_downcast_mid() { + let (err, dropped) = make_chain(); + + let err = err.downcast::().unwrap(); + assert!(!dropped.mid.get()); + assert!(dropped.low.get() && dropped.high.get()); + + drop(err); + assert!(dropped.all()); +} + +#[test] +fn test_downcast_low() { + let (err, dropped) = make_chain(); + + let err = err.downcast::().unwrap(); + assert!(!dropped.low.get()); + assert!(dropped.mid.get() && dropped.high.get()); + + drop(err); + assert!(dropped.all()); +} + +#[test] +fn test_unsuccessful_downcast() { + let (err, dropped) = make_chain(); + + let err = err.downcast::().unwrap_err(); + assert!(dropped.none()); + + drop(err); + assert!(dropped.all()); +} diff --git a/tests/test_context_access.rs b/tests/test_context_access.rs new file mode 100644 index 0000000..804fbc4 --- /dev/null +++ b/tests/test_context_access.rs @@ -0,0 +1,7 @@ +#[test] +fn test_context() { + use miette::{miette, Report}; + + let error: Report = miette!("oh no!"); + let _ = error.context(); +} diff --git a/tests/test_convert.rs b/tests/test_convert.rs new file mode 100644 index 0000000..d43190b --- /dev/null +++ b/tests/test_convert.rs @@ -0,0 +1,23 @@ +mod drop; + +use self::drop::{DetectDrop, Flag}; +use miette::{Diagnostic, Report, Result}; + +#[test] +fn test_convert() { + let has_dropped = Flag::new(); + let error: Report = Report::new(DetectDrop::new(&has_dropped)); + let box_dyn = Box::::from(error); + assert_eq!("oh no!", box_dyn.to_string()); + drop(box_dyn); + assert!(has_dropped.get()); +} + +#[test] +fn test_question_mark() -> Result<(), Box> { + fn f() -> Result<()> { + Ok(()) + } + f()?; + Ok(()) +} diff --git a/tests/test_downcast.rs b/tests/test_downcast.rs new file mode 100644 index 0000000..2823177 --- /dev/null +++ b/tests/test_downcast.rs @@ -0,0 +1,107 @@ +mod common; +mod drop; + +use self::common::*; +use self::drop::{DetectDrop, Flag}; +use miette::{Diagnostic, Report}; +use std::error::Error as StdError; +use std::fmt::{self, Display}; +use std::io; + +#[test] +fn test_downcast() { + assert_eq!( + "oh no!", + bail_literal().unwrap_err().downcast::<&str>().unwrap(), + ); + assert_eq!( + "oh no!", + bail_fmt().unwrap_err().downcast::().unwrap(), + ); + assert_eq!( + "oh no!", + bail_error() + .unwrap_err() + .downcast::() + .unwrap() + .to_string(), + ); +} + +#[test] +fn test_downcast_ref() { + assert_eq!( + "oh no!", + *bail_literal().unwrap_err().downcast_ref::<&str>().unwrap(), + ); + assert_eq!( + "oh no!", + bail_fmt().unwrap_err().downcast_ref::().unwrap(), + ); + assert_eq!( + "oh no!", + bail_error() + .unwrap_err() + .downcast_ref::() + .unwrap() + .to_string(), + ); +} + +#[test] +fn test_downcast_mut() { + assert_eq!( + "oh no!", + *bail_literal().unwrap_err().downcast_mut::<&str>().unwrap(), + ); + assert_eq!( + "oh no!", + bail_fmt().unwrap_err().downcast_mut::().unwrap(), + ); + assert_eq!( + "oh no!", + bail_error() + .unwrap_err() + .downcast_mut::() + .unwrap() + .to_string(), + ); +} + +#[test] +fn test_drop() { + let has_dropped = Flag::new(); + let error: Report = Report::new(DetectDrop::new(&has_dropped)); + drop(error.downcast::().unwrap()); + assert!(has_dropped.get()); +} + +#[test] +fn test_large_alignment() { + #[repr(align(64))] + #[derive(Debug)] + struct LargeAlignedError(&'static str); + + impl Display for LargeAlignedError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.0) + } + } + + impl StdError for LargeAlignedError {} + impl Diagnostic for LargeAlignedError {} + + let error = Report::new(LargeAlignedError("oh no!")); + assert_eq!( + "oh no!", + error.downcast_ref::().unwrap().0 + ); +} + +#[test] +fn test_unsuccessful_downcast() { + let mut error = bail_error().unwrap_err(); + assert!(error.downcast_ref::<&str>().is_none()); + assert!(error.downcast_mut::<&str>().is_none()); + assert!(error.downcast::<&str>().is_err()); +} diff --git a/tests/test_fmt.rs b/tests/test_fmt.rs new file mode 100644 index 0000000..8f18216 --- /dev/null +++ b/tests/test_fmt.rs @@ -0,0 +1,95 @@ +use miette::{bail, Result, WrapErr}; +use std::io; + +fn f() -> Result<()> { + bail!(io::Error::new(io::ErrorKind::PermissionDenied, "oh no!")); +} + +fn g() -> Result<()> { + f().wrap_err("f failed") +} + +fn h() -> Result<()> { + g().wrap_err("g failed") +} + +const EXPECTED_ALTDISPLAY_F: &str = "oh no!"; + +const EXPECTED_ALTDISPLAY_G: &str = "f failed: oh no!"; + +const EXPECTED_ALTDISPLAY_H: &str = "g failed: f failed: oh no!"; + +const EXPECTED_DEBUG_F: &str = "oh no!"; + +const EXPECTED_DEBUG_G: &str = "\ +f failed + +Caused by: + oh no!\ +"; + +const EXPECTED_DEBUG_H: &str = "\ +g failed + +Caused by: + 0: f failed + 1: oh no!\ +"; + +const EXPECTED_ALTDEBUG_F: &str = "\ +Custom { + kind: PermissionDenied, + error: \"oh no!\", +}\ +"; + +const EXPECTED_ALTDEBUG_G: &str = "\ +Error { + msg: \"f failed\", + source: Custom { + kind: PermissionDenied, + error: \"oh no!\", + }, +}\ +"; + +const EXPECTED_ALTDEBUG_H: &str = "\ +Error { + msg: \"g failed\", + source: Error { + msg: \"f failed\", + source: Custom { + kind: PermissionDenied, + error: \"oh no!\", + }, + }, +}\ +"; + +#[test] +fn test_display() { + assert_eq!("g failed", h().unwrap_err().to_string()); +} + +#[test] +fn test_altdisplay() { + assert_eq!(EXPECTED_ALTDISPLAY_F, format!("{:#}", f().unwrap_err())); + assert_eq!(EXPECTED_ALTDISPLAY_G, format!("{:#}", g().unwrap_err())); + assert_eq!(EXPECTED_ALTDISPLAY_H, format!("{:#}", h().unwrap_err())); +} + +#[test] +#[ignore = "not really gonna work with the current printers"] +#[cfg_attr(track_caller, ignore)] +fn test_debug() { + assert_eq!(EXPECTED_DEBUG_F, format!("{:?}", f().unwrap_err())); + assert_eq!(EXPECTED_DEBUG_G, format!("{:?}", g().unwrap_err())); + assert_eq!(EXPECTED_DEBUG_H, format!("{:?}", h().unwrap_err())); +} + +#[test] +fn test_altdebug() { + assert_eq!(EXPECTED_ALTDEBUG_F, format!("{:#?}", f().unwrap_err())); + assert_eq!(EXPECTED_ALTDEBUG_G, format!("{:#?}", g().unwrap_err())); + assert_eq!(EXPECTED_ALTDEBUG_H, format!("{:#?}", h().unwrap_err())); +} diff --git a/tests/test_location.rs b/tests/test_location.rs new file mode 100644 index 0000000..07cd754 --- /dev/null +++ b/tests/test_location.rs @@ -0,0 +1,104 @@ +use std::panic::Location; + +use miette::{Diagnostic, IntoDiagnostic, WrapErr}; + +struct LocationHandler { + actual: Option<&'static str>, + expected: &'static str, +} + +impl LocationHandler { + fn new(expected: &'static str) -> Self { + LocationHandler { + actual: None, + expected, + } + } +} + +impl miette::ReportHandler for LocationHandler { + fn debug( + &self, + _error: &(dyn Diagnostic + 'static), + _f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + // we assume that if the compiler is new enough to support + // `track_caller` that we will always have `actual` be `Some`, so we can + // safely skip the assertion if the location is `None` which should only + // happen in older rust versions. + if let Some(actual) = self.actual { + assert_eq!(self.expected, actual); + } + + Ok(()) + } + + fn track_caller(&mut self, location: &'static Location<'static>) { + dbg!(location); + self.actual = Some(location.file()); + } +} + +#[test] +fn test_wrap_err() { + let _ = miette::set_hook(Box::new(|_e| { + let expected_location = file!(); + Box::new(LocationHandler::new(expected_location)) + })); + + let err = std::fs::read_to_string("totally_fake_path") + .into_diagnostic() + .wrap_err("oopsie") + .unwrap_err(); + + // should panic if the location isn't in our crate + println!("{:?}", err); +} + +#[test] +fn test_wrap_err_with() { + let _ = miette::set_hook(Box::new(|_e| { + let expected_location = file!(); + Box::new(LocationHandler::new(expected_location)) + })); + + let err = std::fs::read_to_string("totally_fake_path") + .into_diagnostic() + .wrap_err_with(|| "oopsie") + .unwrap_err(); + + // should panic if the location isn't in our crate + println!("{:?}", err); +} + +#[test] +fn test_context() { + let _ = miette::set_hook(Box::new(|_e| { + let expected_location = file!(); + Box::new(LocationHandler::new(expected_location)) + })); + + let err = std::fs::read_to_string("totally_fake_path") + .into_diagnostic() + .context("oopsie") + .unwrap_err(); + + // should panic if the location isn't in our crate + println!("{:?}", err); +} + +#[test] +fn test_with_context() { + let _ = miette::set_hook(Box::new(|_e| { + let expected_location = file!(); + Box::new(LocationHandler::new(expected_location)) + })); + + let err = std::fs::read_to_string("totally_fake_path") + .into_diagnostic() + .with_context(|| "oopsie") + .unwrap_err(); + + // should panic if the location isn't in our crate + println!("{:?}", err); +} diff --git a/tests/test_macros.rs b/tests/test_macros.rs new file mode 100644 index 0000000..22bf8cc --- /dev/null +++ b/tests/test_macros.rs @@ -0,0 +1,34 @@ +#![allow(clippy::eq_op)] +mod common; + +use self::common::*; +use miette::{ensure, Result}; + +#[test] +fn test_messages() { + assert_eq!("oh no!", bail_literal().unwrap_err().to_string()); + assert_eq!("oh no!", bail_fmt().unwrap_err().to_string()); + assert_eq!("oh no!", bail_error().unwrap_err().to_string()); +} + +#[test] +fn test_ensure() { + let f = || -> Result<()> { + ensure!(1 + 1 == 2, "This is correct"); + Ok(()) + }; + assert!(f().is_ok()); + + let v = 1; + let f = || -> Result<()> { + ensure!(v + v == 2, "This is correct, v: {}", v); + Ok(()) + }; + assert!(f().is_ok()); + + let f = || -> Result<()> { + ensure!(v + v == 1, "This is not correct, v: {}", v); + Ok(()) + }; + assert!(f().is_err()); +} diff --git a/tests/test_repr.rs b/tests/test_repr.rs new file mode 100644 index 0000000..4978fe5 --- /dev/null +++ b/tests/test_repr.rs @@ -0,0 +1,32 @@ +mod drop; + +use self::drop::{DetectDrop, Flag}; +use miette::Report; +use std::marker::Unpin; +use std::mem; + +#[test] +fn test_error_size() { + assert_eq!(mem::size_of::(), mem::size_of::()); +} + +#[test] +fn test_null_pointer_optimization() { + assert_eq!( + mem::size_of::>(), + mem::size_of::() + ); +} + +#[test] +fn test_autotraits() { + fn assert() {} + assert::(); +} + +#[test] +fn test_drop() { + let has_dropped = Flag::new(); + drop(Report::new(DetectDrop::new(&has_dropped))); + assert!(has_dropped.get()); +} diff --git a/tests/test_source.rs b/tests/test_source.rs new file mode 100644 index 0000000..9511bdb --- /dev/null +++ b/tests/test_source.rs @@ -0,0 +1,63 @@ +use miette::{miette, Report}; +use std::error::Error as StdError; +use std::fmt::{self, Display}; +use std::io; + +#[derive(Debug)] +enum TestError { + Io(io::Error), +} + +impl Display for TestError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + TestError::Io(e) => Display::fmt(e, formatter), + } + } +} + +impl StdError for TestError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + TestError::Io(io) => Some(io), + } + } +} + +#[test] +fn test_literal_source() { + let error: Report = miette!("oh no!"); + assert!(error.source().is_none()); +} + +#[test] +fn test_variable_source() { + let msg = "oh no!"; + let error = miette!(msg); + assert!(error.source().is_none()); + + let msg = msg.to_owned(); + let error: Report = miette!(msg); + assert!(error.source().is_none()); +} + +#[test] +fn test_fmt_source() { + let error: Report = miette!("{} {}!", "oh", "no"); + assert!(error.source().is_none()); +} + +#[test] +#[ignore = "Again with the io::Error source issue?"] +fn test_io_source() { + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error: Report = miette!(TestError::Io(io)); + assert_eq!("oh no!", error.source().unwrap().to_string()); +} + +#[test] +fn test_miette_from_miette() { + let error: Report = miette!("oh no!").wrap_err("context"); + let error = miette!(error); + assert_eq!("oh no!", error.source().unwrap().to_string()); +}