From 7b0f53cb9661383a25b5f419be0b991a7674eb9f Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Tue, 20 Sep 2022 22:36:44 -0400 Subject: [PATCH] Add Report for user-friendly error output [todo] [todo] rewrite feature flag docs [todo] chain rust feature flag to lower version --- Cargo.toml | 8 +- snafu-derive/Cargo.toml | 1 + snafu-derive/src/lib.rs | 8 ++ snafu-derive/src/report.rs | 70 +++++++++++ src/guide/compatibility.md | 12 ++ src/guide/feature_flags.md | 12 ++ src/lib.rs | 8 ++ src/report.md | 76 ++++++++++++ src/report.rs | 237 +++++++++++++++++++++++++++++++++++++ tests/report.rs | 75 ++++++++++++ 10 files changed, 506 insertions(+), 1 deletion(-) create mode 100644 snafu-derive/src/report.rs create mode 100644 src/report.md create mode 100644 src/report.rs create mode 100644 tests/report.rs diff --git a/Cargo.toml b/Cargo.toml index 5d51c550..d32a5a85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ exclude = [ features = [ "std", "backtraces", "futures", "guide" ] [features] -default = ["std", "rust_1_46"] +default = ["std", "rust_1_46", "rust_1_61"] # Implement the `std::error::Error` trait. std = [] @@ -36,6 +36,9 @@ unstable-core-error = [] # Add support for `#[track_caller]` rust_1_46 = ["snafu-derive/rust_1_46"] +# Add support for `Termination` for `Report` +rust_1_61 = ["snafu-derive/rust_1_61"] + # Makes the backtrace type live backtraces = ["std", "backtrace"] @@ -49,6 +52,9 @@ unstable-backtraces-impl-std = ["backtraces", "snafu-derive/unstable-backtraces- # The std::error::Error provider API will be implemented. unstable-provider-api = ["snafu-derive/unstable-provider-api"] +# Add support for `FromResidual` for `Report` +unstable-try-trait = [] + # The standard library's implementation of futures futures = ["futures-core-crate", "pin-project"] diff --git a/snafu-derive/Cargo.toml b/snafu-derive/Cargo.toml index b4a3b035..b2a57a8f 100644 --- a/snafu-derive/Cargo.toml +++ b/snafu-derive/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT OR Apache-2.0" [features] rust_1_46 = [] +rust_1_61 = [] unstable-backtraces-impl-std = [] unstable-provider-api = [] diff --git a/snafu-derive/src/lib.rs b/snafu-derive/src/lib.rs index d2e3307e..8d753765 100644 --- a/snafu-derive/src/lib.rs +++ b/snafu-derive/src/lib.rs @@ -19,6 +19,14 @@ pub fn snafu_derive(input: TokenStream) -> TokenStream { impl_snafu_macro(ast) } +mod report; +#[proc_macro_attribute] +pub fn report(attr: TokenStream, item: TokenStream) -> TokenStream { + report::body(attr, item) + .unwrap_or_else(|e| e.into_compile_error()) + .into() +} + type MultiSynResult = std::result::Result>; /// Some arbitrary tokens we treat as a black box diff --git a/snafu-derive/src/report.rs b/snafu-derive/src/report.rs new file mode 100644 index 00000000..767c2a68 --- /dev/null +++ b/snafu-derive/src/report.rs @@ -0,0 +1,70 @@ +use quote::quote; +use syn::{spanned::Spanned, Item}; + +pub fn body( + _attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> syn::Result { + let item = syn::parse::(item)?; + + match item { + Item::Fn(f) => { + let syn::ItemFn { + attrs, + vis, + sig, + block, + } = f; + + let syn::Signature { + constness, + asyncness, + unsafety, + abi, + fn_token, + ident, + generics, + paren_token: _, + inputs, + variadic, + output, + } = sig; + + let output = match output { + syn::ReturnType::Default => quote! {}, + syn::ReturnType::Type(_, ty) => { + if cfg!(feature = "rust_1_61") { + quote! { + -> ::snafu::Report<<#ty as ::snafu::__InternalExtractErrorType>::Err> + } + } else { + quote! { + -> ::core::result::Result<(), snafu::Report<<#ty as ::snafu::__InternalExtractErrorType>::Err>> + } + } + } + }; + + Ok(quote! { + #(#attrs)* + #vis + #constness + #asyncness + #unsafety + #abi + #fn_token + #ident + #generics + (#inputs #variadic) + #output + { + snafu::Report::capture(|| #block) + } + }) + } + _ => Err(syn::Error::new( + item.span(), + "`#[snafu::report]` may only be used on functions", + )), + } +} diff --git a/src/guide/compatibility.md b/src/guide/compatibility.md index 90c99573..fc8df06b 100644 --- a/src/guide/compatibility.md +++ b/src/guide/compatibility.md @@ -11,3 +11,15 @@ When enabled, SNAFU will assume that it's safe to target features available in Rust 1.46. Notably, the `#[track_caller]` feature is needed to allow [`Location`][crate::Location] to automatically discern the source code location. + +## `rust_1_61` + +**default**: enabled + +When enabled, SNAFU will assume that it's safe to target features +available in Rust 1.61. Notably, the [`Termination`][] trait is +implemented for [`Report`][] to allow it to be returned from `main` +and test functions. + +[`Termination`]: std::process::Termination +[`Report`]: crate::Report diff --git a/src/guide/feature_flags.md b/src/guide/feature_flags.md index 9cc0f74c..151c8a0e 100644 --- a/src/guide/feature_flags.md +++ b/src/guide/feature_flags.md @@ -12,6 +12,7 @@ cases: - [`unstable-backtraces-impl-std`](#unstable-backtraces-impl-std) - [`unstable-provider-api`](#unstable-provider-api) - [`futures`](#futures) +- [`unstable-try-trait`](#unstable-try-trait) [controlling compatibility]: super::guide::compatibility [feature flags]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#choosing-features @@ -102,3 +103,14 @@ and streams returning `Result`s. [`futures::TryFutureExt`]: crate::futures::TryFutureExt [`futures::TryStreamExt`]: crate::futures::TryStreamExt + +## `unstable-try-trait` + +**default**: disabled + +When enabled, the `?` operator can be used on [`Result`][] values in +functions where a [`Report`][] type is returned. + +It is recommended that only applications make use of this feature. + +[`Report`]: crate::Report diff --git a/src/lib.rs b/src/lib.rs index f2bf3b95..1c9eea55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -267,11 +267,19 @@ pub mod futures; mod error_chain; pub use crate::error_chain::*; +mod report; +pub use report::{Report, __InternalExtractErrorType}; + doc_comment::doc_comment! { include_str!("Snafu.md"), pub use snafu_derive::Snafu; } +doc_comment::doc_comment! { + include_str!("report.md"), + pub use snafu_derive::report; +} + macro_rules! generate_guide { (pub mod $name:ident { $($children:tt)* } $($rest:tt)*) => { generate_guide!(@gen ".", pub mod $name { $($children)* } $($rest)*); diff --git a/src/report.md b/src/report.md new file mode 100644 index 00000000..ed7fcba8 --- /dev/null +++ b/src/report.md @@ -0,0 +1,76 @@ +Adapts a function to provide user-friendly error output for `main` +functions and tests. + +```rust,no_run +use snafu::prelude::*; + +#[snafu::report] +fn main() -> Result<(), ApplicationError> { + let _v = may_fail_with_application_error()?; + + Ok(()) +} + +#[derive(Debug, Snafu)] +#[snafu(display("Unable to frobnicate the mumbletypeg"))] +struct ApplicationError { + source: SpecificError +} + +#[derive(Debug, Snafu)] +#[snafu(display("The mumbletypeg could not be found"))] +struct SpecificError { + backtrace: snafu::Backtrace, +} + +fn may_fail_with_application_error() -> Result { + may_fail_with_specific_error().context(ApplicationSnafu) +} + +fn may_fail_with_specific_error() -> Result { + SpecificSnafu.fail() +} +``` + +When using `#[snafu::report]`, the output of running this program +may look like (backtrace edited for clarity and brevity): + +```text +Unable to frobnicate the mumbletypeg + +Caused by: + 0: The mumbletypeg could not be found + +Backtrace: + + 4 ::generate::h57208e368a5db816 + /snafu/src/backtrace_shim.rs:15 + example::SpecificSnafu::build::hff0254e61866cabb + example.rs:18 + 5 example::SpecificSnafu::fail::hec8a0999ec6ee527 + example.rs:18 + 6 example::may_fail_with_specific_error::h271b4f6daa5d46aa + example.rs:29 + 7 example::may_fail_with_application_error::h20cbd2f6aead9af3 + example.rs:25 + 8 example::main::{{closure}}::hc62fd2b706cb40f3 + example.rs:7 + 9 snafu::report::Report::capture::h81d9883cf934c7bb + /snafu/src/report.rs:132 +10 example::main::hf53e4b9f8a221d64 + example.rs:5 +``` + +Contrast this to the default output produced when returning a +`Result`: + +```text +Error: ApplicationError { source: DetailedError { backtrace: Backtrace(...same backtrace as above...) } } +``` + +This macro is syntax sugar for using [`snafu::Report`][]; please read +its documentation for detailed information, especially if you wish to +[see backtraces][] in the output. + +[`snafu::Report`]: crate::Report +[see backtraces]: crate::Report#interaction-with-the-provider-api diff --git a/src/report.rs b/src/report.rs new file mode 100644 index 00000000..1e5010e6 --- /dev/null +++ b/src/report.rs @@ -0,0 +1,237 @@ +use crate::ChainCompat; +use core::fmt; + +#[cfg(all(feature = "std", feature = "rust_1_61"))] +use std::process::{ExitCode, Termination}; + +/// Opinionated solution to format an error in a user-friendly +/// way. Useful as the return type from `main` and test functions. +/// +/// Most users will use the [`snafu::report`][] procedural macro +/// instead of directly using this type, but you can if you do not +/// wish to use the macro. +/// +/// [`snafu::report`]: macro@crate::report +/// +/// ## Rust 1.61 and up +/// +/// Change the return type of the function to [`Report`][] and wrap +/// the body of your function with [`Report::capture`][]. +/// +/// ## Rust before 1.61 +/// +/// Use [`Report`][] as the error type inside of [`Result`][] and then +/// call either [`Report::capture_into_result`][] or +/// [`Report::from_error`][]. +/// +/// ## Nightly Rust +/// +/// Enabling the [`unstable-try-trait` feature flag][try-ff] will +/// allow you to use the `?` operator directly: +/// +/// ```rust,ignore +/// use snafu::{prelude::*, Report}; +/// +/// fn main() -> Report { +/// let _v = may_fail_with_placeholder_error()?; +/// +/// Report::ok() +/// } +/// # #[derive(Debug, Snafu)] +/// # struct PlaceholderError; +/// # fn may_fail_with_placeholder_error() -> Result { Ok(42) } +/// ``` +/// +/// [try-ff]: crate::guide::feature_flags#unstable-try-trait +/// +/// ## Interaction with the Provider API +/// +/// If you return a [`Report`][] from your function and enable the +/// [`unstable-provider-api` feature flag][provider-ff], additional +/// capabilities will be added: +/// +/// 1. If provided, a [`Backtrace`][] will be included in the output. +/// 1. If provided, a [`ExitCode`][] will be used as the return value. +/// +/// [provider-ff]: crate::guide::feature_flags#unstable-provider-api +/// [`Backtrace`]: crate::Backtrace +pub struct Report(Result<(), E>); + +impl Report { + /// Convert an error into a [`Report`][]. + /// + /// Recommended if you support versions of Rust before 1.61. + /// + /// ```rust + /// use snafu::{prelude::*, Report}; + /// + /// #[derive(Debug, Snafu)] + /// struct PlaceholderError; + /// + /// fn main() -> Result<(), Report> { + /// let _v = may_fail_with_placeholder_error().map_err(Report::from_error)?; + /// Ok(()) + /// } + /// + /// fn may_fail_with_placeholder_error() -> Result { + /// Ok(42) + /// } + /// ``` + pub fn from_error(error: E) -> Self { + Self(Err(error)) + } + + /// Executes a closure that returns a [`Result`][], converting the + /// error variant into a [`Report`][]. + /// + /// Recommended if you support versions of Rust before 1.61. + /// + /// ```rust + /// use snafu::{prelude::*, Report}; + /// + /// #[derive(Debug, Snafu)] + /// struct PlaceholderError; + /// + /// fn main() -> Result<(), Report> { + /// Report::capture_into_result(|| { + /// let _v = may_fail_with_placeholder_error()?; + /// + /// Ok(()) + /// }) + /// } + /// + /// fn may_fail_with_placeholder_error() -> Result { + /// Ok(42) + /// } + /// ``` + pub fn capture_into_result(body: impl FnOnce() -> Result) -> Result { + body().map_err(Self::from_error) + } + + /// Executes a closure that returns a [`Result`][], converting any + /// error to a [`Report`][]. + /// + /// Recommended if you only support Rust version 1.61 or above. + /// + /// ```rust + /// use snafu::{prelude::*, Report}; + /// + /// #[derive(Debug, Snafu)] + /// struct PlaceholderError; + /// + /// fn main() -> Report { + /// Report::capture(|| { + /// let _v = may_fail_with_placeholder_error()?; + /// + /// Ok(()) + /// }) + /// } + /// + /// fn may_fail_with_placeholder_error() -> Result { + /// Ok(42) + /// } + /// ``` + pub fn capture(body: impl FnOnce() -> Result<(), E>) -> Self { + Self(body()) + } + + /// A [`Report`][] that indicates no error occurred. + pub const fn ok() -> Self { + Self(Ok(())) + } +} + +impl fmt::Debug for Report +where + E: crate::Error, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for Report +where + E: crate::Error, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + Err(e) => fmt::Display::fmt(&ReportFormatter(e), f), + _ => Ok(()), + } + } +} + +#[cfg(all(feature = "std", feature = "rust_1_61"))] +impl Termination for Report +where + E: crate::Error, +{ + fn report(self) -> ExitCode { + match self.0 { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("{}", ReportFormatter(&e)); + + #[cfg(feature = "unstable-provider-api")] + { + use core::any; + + any::request_value::(&e) + .or_else(|| any::request_ref::(&e).copied()) + .unwrap_or(ExitCode::FAILURE) + } + + #[cfg(not(feature = "unstable-provider-api"))] + { + ExitCode::FAILURE + } + } + } + } +} + +#[cfg(feature = "unstable-try-trait")] +impl core::ops::FromResidual> for Report { + fn from_residual(residual: Result) -> Self { + Self(residual.map(drop)) + } +} + +struct ReportFormatter<'a>(&'a dyn crate::Error); + +impl<'a> fmt::Display for ReportFormatter<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{}", self.0)?; + + let mut chain = ChainCompat::new(self.0).skip(1).peekable(); + if chain.peek().is_some() { + writeln!(f, "\nCaused by:")?; + + for (i, source) in chain.enumerate() { + writeln!(f, "{:3}: {}", i, source)?; + } + } + + #[cfg(feature = "unstable-provider-api")] + { + use core::any; + + if let Some(bt) = any::request_ref::(self.0) { + writeln!(f, "\nBacktrace:\n")?; + fmt::Display::fmt(bt, f)?; + } + } + + Ok(()) + } +} + +#[doc(hidden)] +pub trait __InternalExtractErrorType { + type Err; +} + +impl __InternalExtractErrorType for core::result::Result { + type Err = E; +} diff --git a/tests/report.rs b/tests/report.rs new file mode 100644 index 00000000..f0475616 --- /dev/null +++ b/tests/report.rs @@ -0,0 +1,75 @@ +use snafu::{prelude::*, Report, IntoError}; + +// #[derive(Debug, Snafu)] +// #[snafu(display("This is the inner expected error"))] +// struct InnerError; + +// #[derive(Debug, Snafu)] +// #[snafu(display("This is the outer expected error"))] +// struct OuterError { +// source: InnerError, +// } + +// fn inner_ok() -> Result { +// Ok(42) +// } + +// fn inner_err() -> Result { +// InnerSnafu.fail().context(OuterSnafu) +// } + +#[test] +#[ignore] +fn works_with_box_error() { + #[derive(Debug, Snafu)] + struct Error; + + let e: Box = Box::new(Error); + + use snafu::AsErrorSource; + let _this_is_ok: &dyn snafu::Error = e.as_error_source(); + let _this_is_ok: &dyn snafu::Error = (&e).as_error_source(); + + let _r: Report> = Report::from_error(e); + // let message = r.to_string(); + + // let _: &dyn std::fmt::Display = &r; +} + +#[test] +fn procedural_macro_works_with_result_return_type() { + #[derive(Debug, Snafu)] + struct Error; + + #[snafu::report] + fn mainlike_result() -> Result<(), Error> { + Ok(()) + } + + let _: Report = mainlike_result(); +} + +#[derive(Debug, Snafu)] +struct TestFunctionError; + +#[test] +#[snafu::report] +fn procedural_macro_works_with_test_functions() -> Result<(), TestFunctionError> { + Ok(()) +} + +// error contains display +// contains sources + +// debug prints +// display prints +// term prints + +// ensure works with Box +// feature flag test for try trait +// feature flag for provide backtrace; exitcode + +// works with tokio::main? + +// attribute on unit type return +// attribute before 1.61 (works; returns Result)