feat(report): anyhow-ify DiagnosticReport (#35)

This PR overhauls the toplevel/main experience for `miette`. It adds a new `Report` type based on `eyre::Report` and overhauls various types to fit into this model, as well as prepare for some [future changes in Rust](https://github.com/nrc/rfcs/pull/1) that will make it possible to integrate `miette` directly with crates like `eyre` instead of having to use this specific `Report`.

As such, this PR is a major breaking change, especially for anyone using `DiagnosticReport` and company.

BREAKING CHANGES:
* `DiagnosticReport` is now just `Report`, and is a different, `eyre::Report`-like type.
* `DiagnosticResult` is now just `Result`.
* `.into_diagnostic()` now just transforms the error into a `Report`.
* `DiagnosticReportPrinter` has been replaced with `ReportHandler`
* `set_printer` has been replaced by `set_hook`
* `code` is now optional.
* `.into_diagnostic()` no longer takes a `code` argument.
* `#[diagnostic]` is now optional when deriving `Diagnostic`.
This commit is contained in:
Kat Marchán 2021-09-04 21:22:46 -07:00 committed by GitHub
parent 0427c9f966
commit 3f9da04b86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2890 additions and 319 deletions

View File

@ -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"]

View File

@ -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", "${@}"]

View File

@ -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<Version> {
Ok("1.2.x".parse().into_diagnostic("my_app::semver::parse_error")?)
pub fn some_tool() -> Result<Version> {
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<Version> {
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.

View File

@ -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<dyn std::fmt::Display + 'a> {
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
match self {
#(#code_pairs)*
_ => std::option::Option::None,
}
}
})
@ -87,8 +88,8 @@ impl Code {
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
let code = &self.0;
Some(quote! {
fn code<'a>(&'a self) -> std::boxed::Box<dyn std::fmt::Display + 'a> {
std::boxed::Box::new(#code)
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
std::option::Option::Some(std::boxed::Box::new(#code))
}
})
}

View File

@ -34,8 +34,9 @@ pub enum DiagnosticDefArgs {
Concrete(DiagnosticConcreteArgs),
}
#[derive(Default)]
pub struct DiagnosticConcreteArgs {
pub code: Code,
pub code: Option<Code>,
pub severity: Option<Severity>,
pub help: Option<Help>,
pub snippets: Option<Snippets>,
@ -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<Item = DiagnosticArg>,
@ -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<dyn std::fmt::Display + 'a> {
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#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

View File

@ -91,7 +91,7 @@ impl Severity {
fn severity(&self) -> std::option::Option<miette::Severity> {
match self {
#(#sev_pairs)*
_ => None,
_ => std::option::Option::None,
}
}
})

View File

@ -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) },
}

View File

@ -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,
}

159
src/eyreish/context.rs Normal file
View File

@ -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<D>(self, msg: D) -> Report
where
D: Display + Send + Sync + 'static;
}
impl<E> Diag for E
where
E: Diagnostic + Send + Sync + 'static,
{
fn ext_report<D>(self, msg: D) -> Report
where
D: Display + Send + Sync + 'static,
{
Report::from_msg(msg, self)
}
}
impl Diag for Report {
fn ext_report<D>(self, msg: D) -> Report
where
D: Display + Send + Sync + 'static,
{
self.wrap_err(msg)
}
}
}
impl<T, E> WrapErr<T, E> for Result<T, E>
where
E: ext::Diag + Send + Sync + 'static,
{
fn wrap_err<D>(self, msg: D) -> Result<T, Report>
where
D: Display + Send + Sync + 'static,
{
match self {
Ok(t) => Ok(t),
Err(e) => Err(e.ext_report(msg)),
}
}
fn wrap_err_with<D, F>(self, msg: F) -> Result<T, Report>
where
D: Display + Send + Sync + 'static,
F: FnOnce() -> D,
{
match self {
Ok(t) => Ok(t),
Err(e) => Err(e.ext_report(msg())),
}
}
fn context<D>(self, msg: D) -> Result<T, Report>
where
D: Display + Send + Sync + 'static,
{
self.wrap_err(msg)
}
fn with_context<D, F>(self, msg: F) -> Result<T, Report>
where
D: Display + Send + Sync + 'static,
F: FnOnce() -> D,
{
self.wrap_err_with(msg)
}
}
impl<D, E> Debug for ContextError<D, E>
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<D, E> Display for ContextError<D, E>
where
D: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.msg, f)
}
}
impl<D, E> StdError for ContextError<D, E>
where
D: Display,
E: StdError + 'static,
{
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.error)
}
}
impl<D> StdError for ContextError<D, Report>
where
D: Display,
{
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.error.inner.error())
}
}
impl<D, E> Diagnostic for ContextError<D, E>
where
D: Display,
E: Diagnostic + 'static,
{
}
impl<D> Diagnostic for ContextError<D, Report> where D: Display {}
struct Quoted<D>(D);
impl<D> Debug for Quoted<D>
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<T, E> Sealed for Result<T, E> where E: ext::Diag {}
impl<T> Sealed for Option<T> {}
}

723
src/eyreish/error.rs Normal file
View File

@ -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<E>(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<Output, &'static str> {
/// # unimplemented!()
/// # }
/// # }
/// #
/// # use ffi::{Input, Output};
/// #
/// use miette::{Report, Result};
/// use futures::stream::{Stream, StreamExt, TryStreamExt};
///
/// async fn demo<S>(stream: S) -> Result<Vec<Output>>
/// where
/// S: Stream<Item = Input>,
/// {
/// stream
/// .then(ffi::do_some_work) // returns Result<Output, &str>
/// .map_err(Report::msg)
/// .try_collect()
/// .await
/// }
/// ```
#[cfg_attr(track_caller, track_caller)]
pub fn msg<M>(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<E>(error: E) -> Self
where
E: Diagnostic + Send + Sync + 'static,
{
let vtable = &ErrorVTable {
object_drop: object_drop::<E>,
object_ref: object_ref::<E>,
object_mut: object_mut::<E>,
object_ref_stderr: object_ref_stderr::<E>,
object_boxed: object_boxed::<E>,
object_downcast: object_downcast::<E>,
object_drop_rest: object_drop_front::<E>,
};
// 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<M>(message: M) -> Self
where
M: Display + Debug + Send + Sync + 'static,
{
use super::wrapper::MessageError;
let error: MessageError<M> = MessageError(message);
let vtable = &ErrorVTable {
object_drop: object_drop::<MessageError<M>>,
object_ref: object_ref::<MessageError<M>>,
object_mut: object_mut::<MessageError<M>>,
object_ref_stderr: object_ref_stderr::<MessageError<M>>,
object_boxed: object_boxed::<MessageError<M>>,
object_downcast: object_downcast::<M>,
object_drop_rest: object_drop_front::<M>,
};
// Safety: MessageError is repr(transparent) so it is okay for the
// vtable to allow casting the MessageError<M> 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<D, E>(msg: D, error: E) -> Self
where
D: Display + Send + Sync + 'static,
E: Diagnostic + Send + Sync + 'static,
{
let error: ContextError<D, E> = ContextError { msg, error };
let vtable = &ErrorVTable {
object_drop: object_drop::<ContextError<D, E>>,
object_ref: object_ref::<ContextError<D, E>>,
object_mut: object_mut::<ContextError<D, E>>,
object_ref_stderr: object_ref_stderr::<ContextError<D, E>>,
object_boxed: object_boxed::<ContextError<D, E>>,
object_downcast: context_downcast::<D, E>,
object_drop_rest: context_drop_rest::<D, E>,
};
// 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<dyn Diagnostic + Send + Sync>) -> Self {
use super::wrapper::BoxedError;
let error = BoxedError(error);
let handler = Some(super::capture_handler(&error));
let vtable = &ErrorVTable {
object_drop: object_drop::<BoxedError>,
object_ref: object_ref::<BoxedError>,
object_mut: object_mut::<BoxedError>,
object_ref_stderr: object_ref_stderr::<BoxedError>,
object_boxed: object_boxed::<BoxedError>,
object_downcast: object_downcast::<Box<dyn Diagnostic + Send + Sync>>,
object_drop_rest: object_drop_front::<Box<dyn Diagnostic + Send + Sync>>,
};
// Safety: BoxedError is repr(transparent) so it is okay for the vtable
// to allow casting to Box<dyn StdError + Send + Sync>.
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<E>(
error: E,
vtable: &'static ErrorVTable,
handler: Option<Box<dyn ReportHandler>>,
) -> 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<ErrorImpl<E>> to
// Box<ErrorImpl<dyn StdError + Send + Sync + 'static>> except that the
// result is a thin pointer. The necessary behavior for manipulating the
// underlying ErrorImpl<E> is preserved in the vtable provided by the
// caller rather than a builtin fat pointer vtable.
let erased = mem::transmute::<Box<ErrorImpl<E>>, Box<ErrorImpl<()>>>(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<D>(mut self, msg: D) -> Self
where
D: Display + Send + Sync + 'static,
{
let handler = self.inner.handler.take();
let error: ContextError<D, Report> = ContextError { msg, error: self };
let vtable = &ErrorVTable {
object_drop: object_drop::<ContextError<D, Report>>,
object_ref: object_ref::<ContextError<D, Report>>,
object_mut: object_mut::<ContextError<D, Report>>,
object_ref_stderr: object_ref_stderr::<ContextError<D, Report>>,
object_boxed: object_boxed::<ContextError<D, Report>>,
object_downcast: context_chain_downcast::<D>,
object_drop_rest: context_chain_drop_rest::<D>,
};
// 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<io::ErrorKind> {
/// for cause in error.chain() {
/// if let Some(io_error) = cause.downcast_ref::<io::Error>() {
/// return Some(io_error.kind());
/// }
/// }
/// None
/// }
/// ```
pub fn chain(&self) -> Chain<'_> {
self.inner.chain()
}
/// The lowest level cause of this error &mdash; 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<E>(&self) -> bool
where
E: Display + Debug + Send + Sync + 'static,
{
self.downcast_ref::<E>().is_some()
}
/// Attempt to downcast the error object to a concrete type.
pub fn downcast<E>(self) -> Result<E, Self>
where
E: Display + Debug + Send + Sync + 'static,
{
let target = TypeId::of::<E>();
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::<E>().as_ptr());
// Read Box<ErrorImpl<()>> 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::<DataStoreError>() {
/// Some(DataStoreError::Censored(_)) => Ok(Poll::Ready(REDACTED_CONTENT)),
/// None => Err(error),
/// }
/// # ;
/// ```
pub fn downcast_ref<E>(&self) -> Option<&E>
where
E: Display + Debug + Send + Sync + 'static,
{
let target = TypeId::of::<E>();
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::<E>().as_ptr())
}
}
/// Downcast this error object by mutable reference.
pub fn downcast_mut<E>(&mut self) -> Option<&mut E>
where
E: Display + Debug + Send + Sync + 'static,
{
let target = TypeId::of::<E>();
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::<E>().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<E> From<E> 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<ErrorImpl<()>> 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<ErrorImpl<()>>),
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<ErrorImpl<()>>) -> Box<dyn Diagnostic + Send + Sync + 'static>,
object_downcast: unsafe fn(&ErrorImpl<()>, TypeId) -> Option<NonNull<()>>,
object_drop_rest: unsafe fn(Box<ErrorImpl<()>>, TypeId),
}
// Safety: requires layout of *e to match ErrorImpl<E>.
unsafe fn object_drop<E>(e: Box<ErrorImpl<()>>) {
// Cast back to ErrorImpl<E> so that the allocator receives the correct
// Layout to deallocate the Box's memory.
let unerased = mem::transmute::<Box<ErrorImpl<()>>, Box<ErrorImpl<E>>>(e);
drop(unerased);
}
// Safety: requires layout of *e to match ErrorImpl<E>.
unsafe fn object_drop_front<E>(e: Box<ErrorImpl<()>>, 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<ErrorImpl<()>>, Box<ErrorImpl<ManuallyDrop<E>>>>(e);
drop(unerased);
}
// Safety: requires layout of *e to match ErrorImpl<E>.
unsafe fn object_ref<E>(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<E>))._object
}
// Safety: requires layout of *e to match ErrorImpl<E>.
unsafe fn object_mut<E>(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<E>))._object
}
// Safety: requires layout of *e to match ErrorImpl<E>.
unsafe fn object_ref_stderr<E>(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<E>))._object
}
// Safety: requires layout of *e to match ErrorImpl<E>.
unsafe fn object_boxed<E>(e: Box<ErrorImpl<()>>) -> Box<dyn Diagnostic + Send + Sync + 'static>
where
E: Diagnostic + Send + Sync + 'static,
{
// Attach ErrorImpl<E>'s native StdError vtable. The StdError impl is below.
mem::transmute::<Box<ErrorImpl<()>>, Box<ErrorImpl<E>>>(e)
}
// Safety: requires layout of *e to match ErrorImpl<E>.
unsafe fn object_downcast<E>(e: &ErrorImpl<()>, target: TypeId) -> Option<NonNull<()>>
where
E: 'static,
{
if TypeId::of::<E>() == target {
// Caller is looking for an E pointer and e is ErrorImpl<E>, take a
// pointer to its E field.
let unerased = e as *const ErrorImpl<()> as *const ErrorImpl<E>;
let addr = &(*unerased)._object as *const E as *mut ();
Some(NonNull::new_unchecked(addr))
} else {
None
}
}
// Safety: requires layout of *e to match ErrorImpl<ContextError<D, E>>.
unsafe fn context_downcast<D, E>(e: &ErrorImpl<()>, target: TypeId) -> Option<NonNull<()>>
where
D: 'static,
E: 'static,
{
if TypeId::of::<D>() == target {
let unerased = e as *const ErrorImpl<()> as *const ErrorImpl<ContextError<D, E>>;
let addr = &(*unerased)._object.msg as *const D as *mut ();
Some(NonNull::new_unchecked(addr))
} else if TypeId::of::<E>() == target {
let unerased = e as *const ErrorImpl<()> as *const ErrorImpl<ContextError<D, E>>;
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<ContextError<D, E>>.
unsafe fn context_drop_rest<D, E>(e: Box<ErrorImpl<()>>, 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::<D>() == target {
let unerased = mem::transmute::<
Box<ErrorImpl<()>>,
Box<ErrorImpl<ContextError<ManuallyDrop<D>, E>>>,
>(e);
drop(unerased);
} else {
let unerased = mem::transmute::<
Box<ErrorImpl<()>>,
Box<ErrorImpl<ContextError<D, ManuallyDrop<E>>>>,
>(e);
drop(unerased);
}
}
// Safety: requires layout of *e to match ErrorImpl<ContextError<D, Report>>.
unsafe fn context_chain_downcast<D>(e: &ErrorImpl<()>, target: TypeId) -> Option<NonNull<()>>
where
D: 'static,
{
let unerased = e as *const ErrorImpl<()> as *const ErrorImpl<ContextError<D, Report>>;
if TypeId::of::<D>() == 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<ContextError<D, Report>>.
unsafe fn context_chain_drop_rest<D>(e: Box<ErrorImpl<()>>, 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::<D>() == target {
let unerased = mem::transmute::<
Box<ErrorImpl<()>>,
Box<ErrorImpl<ContextError<ManuallyDrop<D>, Report>>>,
>(e);
// Drop the entire rest of the data structure rooted in the next Report.
drop(unerased);
} else {
let unerased = mem::transmute::<
Box<ErrorImpl<()>>,
Box<ErrorImpl<ContextError<D, ManuallyDrop<Report>>>>,
>(e);
// Read out a ManuallyDrop<Box<ErrorImpl<()>>> 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<E> {
vtable: &'static ErrorVTable,
pub(crate) handler: Option<Box<dyn ReportHandler>>,
// NOTE: Don't use directly. Use only through vtable. Erased type may have
// different alignment.
_object: E,
}
// repr C to ensure that ContextError<D, E> has the same layout as
// ContextError<ManuallyDrop<D>, E> and ContextError<D, ManuallyDrop<E>>.
#[repr(C)]
pub(crate) struct ContextError<D, E> {
pub(crate) msg: D,
pub(crate) error: E,
}
impl<E> ErrorImpl<E> {
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<E> 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<E> StdError for ErrorImpl<E>
where
E: StdError,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.erase().diagnostic().source()
}
}
impl<E> Diagnostic for ErrorImpl<E> where E: Diagnostic {}
impl<E> Debug for ErrorImpl<E>
where
E: Debug,
{
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.erase().debug(formatter)
}
}
impl<E> Display for ErrorImpl<E>
where
E: Display,
{
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.erase().diagnostic(), formatter)
}
}
impl From<Report> for Box<dyn Diagnostic + Send + Sync + 'static> {
fn from(error: Report) -> Self {
let outer = ManuallyDrop::new(error);
unsafe {
// Read Box<ErrorImpl<()>> 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<E>'s native StdError vtable for
// the right original type E.
(erased.vtable.object_boxed)(erased)
}
}
}
impl From<Report> for Box<dyn Diagnostic + 'static> {
fn from(error: Report) -> Self {
Box::<dyn Diagnostic + Send + Sync>::from(error)
}
}
impl AsRef<dyn Diagnostic + Send + Sync> for Report {
fn as_ref(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
&**self
}
}
impl AsRef<dyn Diagnostic> for Report {
fn as_ref(&self) -> &(dyn Diagnostic + 'static) {
&**self
}
}

18
src/eyreish/fmt.rs Normal file
View File

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

View File

@ -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<dyn std::error::Error + Send + Sync + 'static>);
impl Diagnostic for DiagnosticError {}
/**
Convenience trait that adds a `.into_diagnostic()` method that converts a type to a `Result<T, DiagnosticError>`.
*/
pub trait IntoDiagnostic<T, E> {
/// Converts [Result]-like types that return regular errors into a
/// `Result` that returns a [Diagnostic].
fn into_diagnostic(self) -> Result<T, Report>;
}
impl<T, E: std::error::Error + Send + Sync + 'static> IntoDiagnostic<T, E> for Result<T, E> {
fn into_diagnostic(self) -> Result<T, Report> {
self.map_err(|e| DiagnosticError(Box::new(e)).into())
}
}

111
src/eyreish/kind.rs Normal file
View File

@ -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<T> EyreNew for T
// where
// T: Display + Debug + Send + Sync + 'static,
// {
// default fn new(self) -> Report {
// /* no std error impl */
// }
// }
//
// impl<T> 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<T> AdhocKind for &T where T: ?Sized + Display + Debug + Send + Sync + 'static {}
impl Adhoc {
#[cfg_attr(track_caller, track_caller)]
pub fn new<M>(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<E> TraitKind for E where E: Into<Report> {}
impl Trait {
#[cfg_attr(track_caller, track_caller)]
pub fn new<E>(self, error: E) -> Report
where
E: Into<Report>,
{
error.into()
}
}
pub struct Boxed;
pub trait BoxedKind: Sized {
#[inline]
fn miette_kind(&self) -> Boxed {
Boxed
}
}
impl BoxedKind for Box<dyn Diagnostic + Send + Sync> {}
impl Boxed {
#[cfg_attr(track_caller, track_caller)]
pub fn new(self, error: Box<dyn Diagnostic + Send + Sync>) -> Report {
Report::from_boxed(error)
}
}

164
src/eyreish/macros.rs Normal file
View File

@ -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<V> {
/// 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)*))
};
}

490
src/eyreish/mod.rs Normal file
View File

@ -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<Box<ErrorImpl<()>>>,
}
type ErrorHook =
Box<dyn Fn(&(dyn Diagnostic + 'static)) -> Box<dyn ReportHandler> + Sync + Send + 'static>;
static HOOK: OnceCell<ErrorHook> = 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<dyn ReportHandler> {
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<dyn ReportHandler + 'static> {
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<T: ReportHandler>(&self) -> bool {
// Get `TypeId` of the type this function is instantiated with.
let t = core::any::TypeId::of::<T>();
// 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<T: ReportHandler>(&self) -> Option<&T> {
if self.is::<T>() {
unsafe { Some(&*(self as *const dyn ReportHandler as *const T)) }
} else {
None
}
}
///
pub fn downcast_mut<T: ReportHandler>(&mut self) -> Option<&mut T> {
if self.is::<T>() {
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<io::ErrorKind> {
/// for cause in error.chain() {
/// if let Some(io_error) = cause.downcast_ref::<io::Error>() {
/// 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<T, Report>`
///
/// 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<T> {...}
/// // ^ equivalent to std::result::Result<T, miette::Error>
///
/// fn demo2() -> Result<T, OtherError> {...}
/// // ^ equivalent to std::result::Result<T, OtherError>
/// # };
/// ```
///
/// # Example
///
/// ```
/// # pub trait Deserialize {}
/// #
/// # mod serde_json {
/// # use super::Deserialize;
/// # use std::io;
/// #
/// # pub fn from_str<T: Deserialize>(json: &str) -> io::Result<T> {
/// # 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<T, E = Report> = core::result::Result<T, E>;
/// 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<Vec<u8>> {
/// 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<dyn Error>`)
///
/// 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<dyn Error + Send + Sync + 'static>>) -> 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<dyn Error + Send + Sync + 'static>>) -> 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::<SuspiciousError>() {
/// // 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::<HelperFailed>() {
/// // 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<T, E>: context::private::Sealed {
/// Wrap the error value with a new adhoc error
#[cfg_attr(track_caller, track_caller)]
fn wrap_err<D>(self, msg: D) -> Result<T, Report>
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<D, F>(self, f: F) -> Result<T, Report>
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<D>(self, msg: D) -> Result<T, Report>
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<D, F>(self, f: F) -> Result<T, Report>
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<M>(message: M) -> Report
where
M: Display + Debug + Send + Sync + 'static,
{
Report::from_adhoc(message)
}
}

88
src/eyreish/wrapper.rs Normal file
View File

@ -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<M>(pub(crate) M);
#[repr(transparent)]
pub(crate) struct MessageError<M>(pub(crate) M);
pub(crate) struct NoneError;
impl<M> Debug for DisplayError<M>
where
M: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<M> Display for DisplayError<M>
where
M: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<M> StdError for DisplayError<M> where M: Display + 'static {}
impl<M> Diagnostic for DisplayError<M> where M: Display + 'static {}
impl<M> Debug for MessageError<M>
where
M: Display + Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.0, f)
}
}
impl<M> Display for MessageError<M>
where
M: Display + Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<M> StdError for MessageError<M> where M: Display + Debug + 'static {}
impl<M> Diagnostic for MessageError<M> 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<dyn Diagnostic + Send + Sync>);
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 {}

View File

@ -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);

14
src/handlers/mod.rs Normal file
View File

@ -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;

View File

@ -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);

View File

@ -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 {

View File

@ -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;

38
src/named_source.rs Normal file
View File

@ -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<dyn Source + Send + Sync + 'static>,
name: String,
}
impl NamedSource {
/// Create a new [NamedSource] using a regular [Source] and giving it a [Source::name].
pub fn new(name: impl AsRef<str>, 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<Box<dyn crate::SpanContents + 'a>, crate::MietteError> {
self.source.read_span(span)
}
fn name(&self) -> Option<String> {
Some(self.name.clone())
}
}

View File

@ -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<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
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<dyn DiagnosticReportPrinter + Send + Sync + 'static> {
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)
}
}

View File

@ -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<dyn Display + 'a>;
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
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<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + 'st
}
}
/**
When used with `?`/`From`, this will wrap any Diagnostics and, when
formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
use it to format the inner [Diagnostic].
*/
pub struct DiagnosticReport {
diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
}
impl DiagnosticReport {
/// Return a reference to the inner [Diagnostic].
pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
&*self.diagnostic
impl From<&str> for Box<dyn Diagnostic> {
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<dyn Diagnostic + Send + Sync + 'a> {
fn from(s: &str) -> Self {
From::from(String::from(s))
}
}
impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
fn from(diagnostic: T) -> Self {
DiagnosticReport {
diagnostic: Box::new(diagnostic),
impl From<String> for Box<dyn Diagnostic> {
fn from(s: String) -> Self {
let err1: Box<dyn Diagnostic + Send + Sync> = From::from(s);
let err2: Box<dyn Diagnostic> = err1;
err2
}
}
impl From<String> for Box<dyn Diagnostic + Send + Sync> {
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<Box<dyn std::error::Error + Send + Sync>> for Box<dyn Diagnostic + Send + Sync> {
fn from(s: Box<dyn std::error::Error + Send + Sync>) -> Self {
#[derive(thiserror::Error)]
#[error(transparent)]
struct BoxedDiagnostic(Box<dyn std::error::Error + Send + Sync>);
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)]

View File

@ -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<T> = Result<T, DiagnosticReport>;
/// 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<dyn std::error::Error + Send + Sync + 'static>,
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<dyn std::fmt::Display + 'a> {
Box::new(&self.code)
}
}
/**
Convenience trait that adds a `.into_diagnostic()` method that converts a type to a `Result<T, DiagnosticError>`.
*/
pub trait IntoDiagnostic<T, E> {
/// Converts [Result]-like types that return regular errors into a
/// `Result` that returns a [Diagnostic].
fn into_diagnostic(self, code: impl fmt::Display) -> Result<T, DiagnosticError>;
}
impl<T, E: std::error::Error + Send + Sync + 'static> IntoDiagnostic<T, E> for Result<T, E> {
fn into_diagnostic(self, code: impl fmt::Display) -> Result<T, DiagnosticError> {
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<dyn Source + Send + Sync + 'static>,
name: String,
}
impl NamedSource {
/// Create a new [NamedSource] using a regular [Source] and giving it a [Source::name].
pub fn new(name: impl AsRef<str>, 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<Box<dyn crate::SpanContents + 'a>, crate::MietteError> {
self.source.read_span(span)
}
fn name(&self) -> Option<String> {
Some(self.name.clone())
}
}

14
tests/common/mod.rs Normal file
View File

@ -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!"));
}

6
tests/compiletest.rs Normal file
View File

@ -0,0 +1,6 @@
#[rustversion::attr(not(nightly), ignore)]
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}

View File

@ -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]

55
tests/drop/mod.rs Normal file
View File

@ -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<AtomicBool>,
}
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);
}
}

View File

@ -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);

View File

@ -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();

13
tests/test_autotrait.rs Normal file
View File

@ -0,0 +1,13 @@
use miette::Report;
#[test]
fn test_send() {
fn assert_send<T: Send>() {}
assert_send::<Report>();
}
#[test]
fn test_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<Report>();
}

76
tests/test_boxed.rs Normal file
View File

@ -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::<dyn Diagnostic + Send + Sync>::from("oh no!");
let error: Report = miette!(error);
assert_eq!("oh no!", error.to_string());
assert_eq!(
"oh no!",
error
.downcast_ref::<Box<dyn Diagnostic + Send + Sync>>()
.unwrap()
.to_string()
);
}
#[test]
fn test_boxed_str_stderr() {
let error = Box::<dyn StdError + Send + Sync>::from("oh no!");
let error: Report = miette!(error);
assert_eq!("oh no!", error.to_string());
assert_eq!(
"oh no!",
error
.downcast_ref::<Box<dyn StdError + Send + Sync>>()
.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::<dyn Diagnostic + Send + Sync>::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()
);
}

45
tests/test_chain.rs Normal file
View File

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

160
tests/test_context.rs Normal file
View File

@ -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<T, E>
let mid = Err::<(), LowLevel>(low)
.wrap_err(MidLevel {
message: "failed to load config",
drop: DetectDrop::new(&dropped.mid),
})
.unwrap_err();
// impl Report for Result<T, Error>
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::<String>());
assert!(err.downcast_ref::<String>().is_none());
assert!(err.is::<HighLevel>());
let high = err.downcast_ref::<HighLevel>().unwrap();
assert_eq!(high.to_string(), "failed to start server");
assert!(err.is::<MidLevel>());
let mid = err.downcast_ref::<MidLevel>().unwrap();
assert_eq!(mid.to_string(), "failed to load config");
assert!(err.is::<LowLevel>());
let low = err.downcast_ref::<LowLevel>().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::<HighLevel>().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::<MidLevel>().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::<LowLevel>().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::<String>().unwrap_err();
assert!(dropped.none());
drop(err);
assert!(dropped.all());
}

View File

@ -0,0 +1,7 @@
#[test]
fn test_context() {
use miette::{miette, Report};
let error: Report = miette!("oh no!");
let _ = error.context();
}

23
tests/test_convert.rs Normal file
View File

@ -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::<dyn Diagnostic + Send + Sync>::from(error);
assert_eq!("oh no!", box_dyn.to_string());
drop(box_dyn);
assert!(has_dropped.get());
}
#[test]
fn test_question_mark() -> Result<(), Box<dyn Diagnostic>> {
fn f() -> Result<()> {
Ok(())
}
f()?;
Ok(())
}

107
tests/test_downcast.rs Normal file
View File

@ -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::<String>().unwrap(),
);
assert_eq!(
"oh no!",
bail_error()
.unwrap_err()
.downcast::<io::Error>()
.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::<String>().unwrap(),
);
assert_eq!(
"oh no!",
bail_error()
.unwrap_err()
.downcast_ref::<io::Error>()
.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::<String>().unwrap(),
);
assert_eq!(
"oh no!",
bail_error()
.unwrap_err()
.downcast_mut::<io::Error>()
.unwrap()
.to_string(),
);
}
#[test]
fn test_drop() {
let has_dropped = Flag::new();
let error: Report = Report::new(DetectDrop::new(&has_dropped));
drop(error.downcast::<DetectDrop>().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::<LargeAlignedError>().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());
}

95
tests/test_fmt.rs Normal file
View File

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

104
tests/test_location.rs Normal file
View File

@ -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);
}

34
tests/test_macros.rs Normal file
View File

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

32
tests/test_repr.rs Normal file
View File

@ -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::<Report>(), mem::size_of::<usize>());
}
#[test]
fn test_null_pointer_optimization() {
assert_eq!(
mem::size_of::<Result<(), Report>>(),
mem::size_of::<usize>()
);
}
#[test]
fn test_autotraits() {
fn assert<E: Unpin + Send + Sync + 'static>() {}
assert::<Report>();
}
#[test]
fn test_drop() {
let has_dropped = Flag::new();
drop(Report::new(DetectDrop::new(&has_dropped)));
assert!(has_dropped.get());
}

63
tests/test_source.rs Normal file
View File

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