From 81b942b5b64a6084978a191628e72b9471c7c74b 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 --- .cirrus.yml | 24 ++ Cargo.toml | 11 +- .../compile-fail/tests/ui/report.rs | 14 + .../compile-fail/tests/ui/report.stderr | 23 ++ .../report-provider-api/Cargo.toml | 9 + .../report-provider-api/rust-toolchain | 1 + .../report-provider-api/src/lib.rs | 66 +++++ .../report-try-trait/Cargo.toml | 9 + .../report-try-trait/rust-toolchain | 1 + .../report-try-trait/src/lib.rs | 18 ++ compatibility-tests/v1_34/src/lib.rs | 13 + compatibility-tests/v1_61/Cargo.toml | 9 + compatibility-tests/v1_61/rust-toolchain | 1 + compatibility-tests/v1_61/src/lib.rs | 46 ++++ snafu-derive/Cargo.toml | 2 + snafu-derive/src/lib.rs | 8 + snafu-derive/src/report.rs | 101 +++++++ snafu-derive/src/report/no_async.rs | 8 + snafu-derive/src/report/yes_async.rs | 19 ++ src/guide/compatibility.md | 21 ++ src/guide/feature_flags.md | 12 + src/lib.rs | 9 + src/report.md | 115 ++++++++ src/report.rs | 249 ++++++++++++++++++ tests/report.rs | 87 ++++++ 25 files changed, 875 insertions(+), 1 deletion(-) create mode 100644 compatibility-tests/compile-fail/tests/ui/report.rs create mode 100644 compatibility-tests/compile-fail/tests/ui/report.stderr create mode 100644 compatibility-tests/report-provider-api/Cargo.toml create mode 100644 compatibility-tests/report-provider-api/rust-toolchain create mode 100644 compatibility-tests/report-provider-api/src/lib.rs create mode 100644 compatibility-tests/report-try-trait/Cargo.toml create mode 100644 compatibility-tests/report-try-trait/rust-toolchain create mode 100644 compatibility-tests/report-try-trait/src/lib.rs create mode 100644 compatibility-tests/v1_61/Cargo.toml create mode 100644 compatibility-tests/v1_61/rust-toolchain create mode 100644 compatibility-tests/v1_61/src/lib.rs create mode 100644 snafu-derive/src/report.rs create mode 100644 snafu-derive/src/report/no_async.rs create mode 100644 snafu-derive/src/report/yes_async.rs create mode 100644 src/report.md create mode 100644 src/report.rs create mode 100644 tests/report.rs diff --git a/.cirrus.yml b/.cirrus.yml index ef9cec2f..580388d1 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -137,6 +137,14 @@ nightly_test_task: - cd compatibility-tests/provider-api/ - rustc --version - cargo test + report_provider_api_test_script: + - cd compatibility-tests/report-provider-api/ + - rustc --version + - cargo test + report_try_trait_test_script: + - cd compatibility-tests/report-try-trait/ + - rustc --version + - cargo test before_cache_script: rm -rf $CARGO_HOME/registry/index unstable_std_backtraces_test_task: @@ -188,3 +196,19 @@ v1_34_test_task: - rustc --version - cargo test before_cache_script: rm -rf $CARGO_HOME/registry/index + +v1_61_test_task: + name: "Rust 1.61" + container: + image: rust:1.61 + cpu: 1 + memory: 2Gi + cargo_cache: + folder: $CARGO_HOME/registry + fingerprint_script: cat Cargo.toml + primary_test_script: + - rustup self update + - cd compatibility-tests/v1_61/ + - rustc --version + - cargo test + before_cache_script: rm -rf $CARGO_HOME/registry/index diff --git a/Cargo.toml b/Cargo.toml index 80b4f1ab..f8b1b309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,14 @@ std = [] # Implement the `core::error::Error` trait. unstable-core-error = [] +# Add support for `async` / `.await` +rust_1_39 = ["snafu-derive/rust_1_39"] + # Add support for `#[track_caller]` -rust_1_46 = ["snafu-derive/rust_1_46"] +rust_1_46 = ["rust_1_39", "snafu-derive/rust_1_46"] + +# Add support for `Termination` for `Report` +rust_1_61 = ["rust_1_46", "snafu-derive/rust_1_61"] # Makes the backtrace type live backtraces = ["std", "backtrace"] @@ -51,6 +57,9 @@ backtraces-impl-std = [] # 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/compatibility-tests/compile-fail/tests/ui/report.rs b/compatibility-tests/compile-fail/tests/ui/report.rs new file mode 100644 index 00000000..23e9f2e3 --- /dev/null +++ b/compatibility-tests/compile-fail/tests/ui/report.rs @@ -0,0 +1,14 @@ +use std::process::ExitCode; + +#[snafu::report] +const NOT_HERE: u8 = 42; + +#[snafu::report] +fn cannot_add_report_macro_with_no_return_value() {} + +#[snafu::report] +fn cannot_add_report_macro_with_non_result_return_value() -> ExitCode { + ExitCode::SUCCESS +} + +fn main() {} diff --git a/compatibility-tests/compile-fail/tests/ui/report.stderr b/compatibility-tests/compile-fail/tests/ui/report.stderr new file mode 100644 index 00000000..2f30c4b5 --- /dev/null +++ b/compatibility-tests/compile-fail/tests/ui/report.stderr @@ -0,0 +1,23 @@ +error: `#[snafu::report]` may only be used on functions + --> tests/ui/report.rs:4:1 + | +4 | const NOT_HERE: u8 = 42; + | ^^^^^ + +error[E0277]: the trait bound `(): __InternalExtractErrorType` is not satisfied + --> tests/ui/report.rs:6:1 + | +6 | #[snafu::report] + | ^^^^^^^^^^^^^^^^ the trait `__InternalExtractErrorType` is not implemented for `()` + | + = help: the trait `__InternalExtractErrorType` is implemented for `Result` + = note: this error originates in the attribute macro `snafu::report` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `ExitCode: __InternalExtractErrorType` is not satisfied + --> tests/ui/report.rs:9:1 + | +9 | #[snafu::report] + | ^^^^^^^^^^^^^^^^ the trait `__InternalExtractErrorType` is not implemented for `ExitCode` + | + = help: the trait `__InternalExtractErrorType` is implemented for `Result` + = note: this error originates in the attribute macro `snafu::report` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/compatibility-tests/report-provider-api/Cargo.toml b/compatibility-tests/report-provider-api/Cargo.toml new file mode 100644 index 00000000..2c7250d8 --- /dev/null +++ b/compatibility-tests/report-provider-api/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "report-provider-api" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +snafu = { path = "../..", features = ["backtraces-impl-std", "unstable-provider-api"] } diff --git a/compatibility-tests/report-provider-api/rust-toolchain b/compatibility-tests/report-provider-api/rust-toolchain new file mode 100644 index 00000000..bf867e0a --- /dev/null +++ b/compatibility-tests/report-provider-api/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/compatibility-tests/report-provider-api/src/lib.rs b/compatibility-tests/report-provider-api/src/lib.rs new file mode 100644 index 00000000..6a4b14dc --- /dev/null +++ b/compatibility-tests/report-provider-api/src/lib.rs @@ -0,0 +1,66 @@ +#![cfg(test)] +#![feature(error_generic_member_access, provide_any)] + +use snafu::{prelude::*, Report}; +use std::process::ExitCode; + +#[test] +fn provided_exit_code_is_returned() { + use std::process::Termination; + + #[derive(Debug, Snafu)] + enum TwoKindError { + #[snafu(provide(ExitCode => ExitCode::from(2)))] + Mild, + #[snafu(provide(ExitCode => ExitCode::from(3)))] + Extreme, + } + + let mild = Report::from_error(MildSnafu.build()).report(); + let expected_mild = ExitCode::from(2); + + assert!( + nasty_hack_exit_code_eq(mild, expected_mild), + "Wanted {:?} but got {:?}", + expected_mild, + mild, + ); + + let extreme = Report::from_error(ExtremeSnafu.build()).report(); + let expected_extreme = ExitCode::from(3); + + assert!( + nasty_hack_exit_code_eq(extreme, expected_extreme), + "Wanted {:?} but got {:?}", + expected_extreme, + extreme, + ); +} + +#[test] +fn provided_backtrace_is_printed() { + #[derive(Debug, Snafu)] + struct Error { + backtrace: snafu::Backtrace, + } + + let r = Report::from_error(Snafu.build()); + let msg = r.to_string(); + + let this_function = "::provided_backtrace_is_printed"; + assert!( + msg.contains(this_function), + "Expected {msg:?} to contain {this_function:?}" + ); +} + +fn nasty_hack_exit_code_eq(left: ExitCode, right: ExitCode) -> bool { + use std::mem; + + let (left, right): (u8, u8) = unsafe { + assert_eq!(mem::size_of::(), mem::size_of::()); + (mem::transmute(left), mem::transmute(right)) + }; + + left == right +} diff --git a/compatibility-tests/report-try-trait/Cargo.toml b/compatibility-tests/report-try-trait/Cargo.toml new file mode 100644 index 00000000..2fe1f879 --- /dev/null +++ b/compatibility-tests/report-try-trait/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "report-try-trait" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +snafu = { path = "../..", features = ["unstable-try-trait"] } diff --git a/compatibility-tests/report-try-trait/rust-toolchain b/compatibility-tests/report-try-trait/rust-toolchain new file mode 100644 index 00000000..bf867e0a --- /dev/null +++ b/compatibility-tests/report-try-trait/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/compatibility-tests/report-try-trait/src/lib.rs b/compatibility-tests/report-try-trait/src/lib.rs new file mode 100644 index 00000000..0b3e16e6 --- /dev/null +++ b/compatibility-tests/report-try-trait/src/lib.rs @@ -0,0 +1,18 @@ +#![cfg(test)] +#![feature(try_trait_v2)] + +use snafu::{prelude::*, Report}; + +#[test] +fn can_be_used_with_the_try_operator() { + #[derive(Debug, Snafu)] + struct ExampleError; + + fn mainlike() -> Report { + ExampleSnafu.fail()?; + + Report::ok() + } + + let _: Report = mainlike(); +} diff --git a/compatibility-tests/v1_34/src/lib.rs b/compatibility-tests/v1_34/src/lib.rs index 6049df54..daa86538 100644 --- a/compatibility-tests/v1_34/src/lib.rs +++ b/compatibility-tests/v1_34/src/lib.rs @@ -95,3 +95,16 @@ mod opaque_style { let _ = create(); } } + +mod report { + use snafu::prelude::*; + + #[derive(Debug, Snafu)] + struct Error; + + #[test] + #[snafu::report] + fn it_works() -> Result<(), Error> { + Ok(()) + } +} diff --git a/compatibility-tests/v1_61/Cargo.toml b/compatibility-tests/v1_61/Cargo.toml new file mode 100644 index 00000000..43fbf954 --- /dev/null +++ b/compatibility-tests/v1_61/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "v1_61" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +snafu = { path = "../..", features = ["rust_1_61"] } diff --git a/compatibility-tests/v1_61/rust-toolchain b/compatibility-tests/v1_61/rust-toolchain new file mode 100644 index 00000000..4213d88d --- /dev/null +++ b/compatibility-tests/v1_61/rust-toolchain @@ -0,0 +1 @@ +1.61 diff --git a/compatibility-tests/v1_61/src/lib.rs b/compatibility-tests/v1_61/src/lib.rs new file mode 100644 index 00000000..b2ba35d7 --- /dev/null +++ b/compatibility-tests/v1_61/src/lib.rs @@ -0,0 +1,46 @@ +#![cfg(test)] + +use snafu::{prelude::*, Report}; +use std::process::ExitCode; + +#[test] +fn termination_returns_failure_code() { + use std::process::Termination; + + #[derive(Debug, Snafu)] + struct Error; + + let r = Report::from_error(Error); + let code: ExitCode = r.report(); + + assert!( + nasty_hack_exit_code_eq(code, ExitCode::FAILURE), + "Wanted {:?} but got {:?}", + ExitCode::FAILURE, + code, + ); +} + +#[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(); +} + +fn nasty_hack_exit_code_eq(left: ExitCode, right: ExitCode) -> bool { + use std::mem; + + let (left, right): (u8, u8) = unsafe { + assert_eq!(mem::size_of::(), mem::size_of::()); + (mem::transmute(left), mem::transmute(right)) + }; + + left == right +} diff --git a/snafu-derive/Cargo.toml b/snafu-derive/Cargo.toml index b4a3b035..4603f098 100644 --- a/snafu-derive/Cargo.toml +++ b/snafu-derive/Cargo.toml @@ -11,7 +11,9 @@ repository = "https://github.com/shepmaster/snafu" license = "MIT OR Apache-2.0" [features] +rust_1_39 = [] 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..0da67fcf --- /dev/null +++ b/snafu-derive/src/report.rs @@ -0,0 +1,101 @@ +use quote::quote; +use syn::{spanned::Spanned, Item, ItemFn, ReturnType, Signature}; + +// In versions of Rust before 1.39, we can't use the `.await` keyword +// in *our* source code, even if we never generate it in the *output* +// source code. Hiding it behind a conditionally-compiled module +// works. +#[cfg(not(feature = "rust_1_39"))] +mod no_async; +#[cfg(not(feature = "rust_1_39"))] +use no_async::async_body; +#[cfg(feature = "rust_1_39")] +mod yes_async; +#[cfg(feature = "rust_1_39")] +use yes_async::async_body; + +pub fn body( + _attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> syn::Result { + let item = syn::parse::(item)?; + + let f = match item { + Item::Fn(f) => f, + _ => { + return Err(syn::Error::new( + item.span(), + "`#[snafu::report]` may only be used on functions", + )) + } + }; + + let ItemFn { + attrs, + vis, + sig, + block, + } = f; + + let Signature { + constness, + asyncness, + unsafety, + abi, + fn_token, + ident, + generics, + paren_token: _, + inputs, + variadic, + output, + } = sig; + + let output_ty = match output { + ReturnType::Default => quote! { () }, + ReturnType::Type(_, ty) => quote! { #ty }, + }; + + let error_ty = quote! { <#output_ty as ::snafu::__InternalExtractErrorType>::Err }; + + let output = if cfg!(feature = "rust_1_61") { + quote! { -> ::snafu::Report<#error_ty> } + } else { + quote! { -> ::core::result::Result<(), ::snafu::Report<#error_ty>> } + }; + + let block = if asyncness.is_some() { + async_body(block)? + } else { + if cfg!(feature = "rust_1_61") { + quote! { + { + let __snafu_body = || #block; + <::snafu::Report<_> as ::core::convert::From<_>>::from(__snafu_body()) + } + } + } else { + quote! { + { + let __snafu_body = || #block; + ::core::result::Result::map_err(__snafu_body(), ::snafu::Report::from_error) + } + } + } + }; + + Ok(quote! { + #(#attrs)* + #vis + #constness + #asyncness + #unsafety + #abi + #fn_token + #ident + #generics + (#inputs #variadic) + #output + #block + }) +} diff --git a/snafu-derive/src/report/no_async.rs b/snafu-derive/src/report/no_async.rs new file mode 100644 index 00000000..723e4943 --- /dev/null +++ b/snafu-derive/src/report/no_async.rs @@ -0,0 +1,8 @@ +use syn::spanned::Spanned; + +pub fn async_body(block: Box) -> syn::Result { + Err(syn::Error::new( + block.span(), + "`#[snafu::report]` cannot be used with async functions in this version of Rust", + )) +} diff --git a/snafu-derive/src/report/yes_async.rs b/snafu-derive/src/report/yes_async.rs new file mode 100644 index 00000000..4ff45e7b --- /dev/null +++ b/snafu-derive/src/report/yes_async.rs @@ -0,0 +1,19 @@ +use quote::quote; + +pub fn async_body(block: Box) -> syn::Result { + if cfg!(feature = "rust_1_61") { + Ok(quote! { + { + let __snafu_body = async #block; + <::snafu::Report<_> as ::core::convert::From<_>>::from(__snafu_body.await) + } + }) + } else { + Ok(quote! { + { + let __snafu_body = async #block; + ::core::result::Result::map_err(__snafu_body.await, ::snafu::Report::from_error) + } + }) + } +} diff --git a/src/guide/compatibility.md b/src/guide/compatibility.md index c77ca26e..7ceb0cec 100644 --- a/src/guide/compatibility.md +++ b/src/guide/compatibility.md @@ -23,3 +23,24 @@ 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
+
Implies
+
+ +[`rust_1_46`](#rust_1_46) + +
+
+ +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 4f4e89b7..fcdfe939 100644 --- a/src/guide/feature_flags.md +++ b/src/guide/feature_flags.md @@ -13,6 +13,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 @@ -165,3 +166,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 83989fa6..582383d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ feature = "unstable-provider-api", feature(error_generic_member_access, provide_any) )] +#![cfg_attr(feature = "unstable-try-trait", feature(try_trait_v2))] //! # SNAFU //! @@ -267,11 +268,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..0f535e1e --- /dev/null +++ b/src/report.md @@ -0,0 +1,115 @@ +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<()> { + let _v = frobnicate_the_mumbletypeg()?; + + Ok(()) +} + +fn frobnicate_the_mumbletypeg() -> Result { + api::contact_frobnicate_api().context(FrobnicateSnafu) +} + +#[derive(Debug, Snafu)] +#[snafu(display("Unable to frobnicate the mumbletypeg"))] +struct FrobnicateError { + source: api::ContactFrobnicateApiError, +} + +type Result = std::result::Result; + +mod api { + use crate::config; + use snafu::prelude::*; + + pub fn contact_frobnicate_api() -> Result { + config::load_password().context(ContactFrobnicateApiSnafu) + } + + #[derive(Debug, Snafu)] + #[snafu(display("Could not contact the mumbletypeg API"))] + pub struct ContactFrobnicateApiError { + source: crate::config::MissingPasswordError, + } + + pub type Result = std::result::Result; +} + +mod config { + use snafu::prelude::*; + + pub fn load_password() -> Result { + MissingPasswordSnafu.fail() + } + + #[derive(Debug, Snafu)] + #[snafu(display("The configuration has no password"))] + pub struct MissingPasswordError { + backtrace: snafu::Backtrace, + } + + pub type Result = std::result::Result; +} +``` + +When using `#[snafu::report]`, the output of running this program +may look like (backtrace edited for clarity and brevity): + +```text +Error: Unable to frobnicate the mumbletypeg + +Caused by these errors (recent errors listed first): + 1: Could not contact the mumbletypeg API + 2: The configuration has no password + +Backtrace: + [... output edited ...] + 3: ::generate + at crates/snafu/src/lib.rs:1210:9 + 4: backtrace_example::config::MissingPasswordSnafu::build + at ./src/main.rs:48:21 + 5: backtrace_example::config::MissingPasswordSnafu::fail + at ./src/main.rs:48:21 + 6: backtrace_example::config::load_password + at ./src/main.rs:45:9 + 7: backtrace_example::api::contact_frobnicate_api + at ./src/main.rs:29:9 + 8: backtrace_example::frobnicate_the_mumbletypeg + at ./src/main.rs:13:5 + 9: backtrace_example::main::{{closure}} + at ./src/main.rs:7:14 + 10: backtrace_example::main + at ./src/main.rs:5:1 + [... output edited ...] +``` + +Contrast this to the default output produced when returning a +`Result`: + +```text +Error: FrobnicateError { source: ContactFrobnicateApiError { source: MissingPasswordError { backtrace: Backtrace [...2000+ bytes of backtrace...] } } } +``` + +This macro is syntax sugar for using [`Report`][]; please read +its documentation for detailed information, especially if you wish to +[see backtraces][] in the output. + +[see backtraces]: crate::Report#interaction-with-the-provider-api + +## Usage with other procedural macros + +This macro should work with other common procedural macros. It has been tested with + +- `tokio::main` +- `tokio::test` +- `async_std::main` +- `async_std::test` + +Depending on the implementation details of each procedural macro, you +may need to experiment by placing `snafu::report` before or after +other macro invocations. diff --git a/src/report.rs b/src/report.rs new file mode 100644 index 00000000..a7e57a64 --- /dev/null +++ b/src/report.rs @@ -0,0 +1,249 @@ +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 +/// [`ExitCode`]: std::process::ExitCode +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 From> for Report { + fn from(other: Result<(), E>) -> Self { + Self(other) + } +} + +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 sources = ChainCompat::new(self.0).skip(1); + let plurality = sources.clone().take(2).count(); + + match plurality { + 0 => {} + 1 => writeln!(f, "\nCaused by this error:")?, + _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?, + } + + for (i, source) in sources.enumerate() { + // Let's use 1-based indexing for presentation + let i = i + 1; + 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{}", bt)?; + } + } + + 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..476fc624 --- /dev/null +++ b/tests/report.rs @@ -0,0 +1,87 @@ +use snafu::{prelude::*, IntoError, Report}; + +#[test] +fn includes_the_error_display_text() { + #[derive(Debug, Snafu)] + #[snafu(display("This is my Display text!"))] + struct Error; + + let r = Report::from_error(Error); + let msg = r.to_string(); + + let expected = "This is my Display text!"; + assert!( + msg.contains(expected), + "Expected {:?} to include {:?}", + msg, + expected, + ); +} + +#[test] +fn includes_the_source_display_text() { + #[derive(Debug, Snafu)] + #[snafu(display("This is my inner Display"))] + struct InnerError; + + #[derive(Debug, Snafu)] + #[snafu(display("This is my outer Display"))] + struct OuterError { + source: InnerError, + } + + let e = OuterSnafu.into_error(InnerError); + let r = Report::from_error(e); + let msg = r.to_string(); + + let expected = "This is my inner Display"; + assert!( + msg.contains(expected), + "Expected {:?} to include {:?}", + msg, + expected, + ); +} + +#[test] +fn debug_and_display_are_the_same() { + #[derive(Debug, Snafu)] + #[snafu(display("This is my inner Display"))] + struct InnerError; + + #[derive(Debug, Snafu)] + #[snafu(display("This is my outer Display"))] + struct OuterError { + source: InnerError, + } + + let e = OuterSnafu.into_error(InnerError); + let r = Report::from_error(e); + + let display = format!("{}", r); + let debug = format!("{:?}", r); + + assert_eq!(display, debug); +} + +#[test] +fn procedural_macro_works_with_result_return_type() { + #[derive(Debug, Snafu)] + struct Error; + + #[snafu::report] + fn mainlike_result() -> Result<(), Error> { + Ok(()) + } + + let _: Result<(), Report> = mainlike_result(); +} + +#[derive(Debug, Snafu)] +struct TestFunctionError; + +#[test] +#[snafu::report] +fn procedural_macro_works_with_test_functions() -> Result<(), TestFunctionError> { + Ok(()) +}