diff --git a/derive/src/expand.rs b/derive/src/expand.rs index e352190..c8fb767 100644 --- a/derive/src/expand.rs +++ b/derive/src/expand.rs @@ -694,6 +694,24 @@ pub fn derive_macro(input: &DeriveInput) -> Result { Ok(generated) } +pub fn derive_report_debug(input: &DeriveInput) -> Result { + let input_type = input.ident.clone(); + + // 1. Delegate to `Debug` impl as the backtrace provided by the error + // could be different than where panic happens. + // 2. Passthrough the `alternate` flag. + let generated = quote!( + impl ::std::fmt::Debug for #input_type { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + use ::thiserror_ext::AsReport; + ::std::fmt::Debug::fmt(&self.as_report(), f) + } + } + ); + + Ok(generated) +} + fn big_camel_case_to_snake_case(input: &str) -> String { let mut output = String::new(); diff --git a/derive/src/lib.rs b/derive/src/lib.rs index da210f2..a7432f7 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(rustdoc::broken_intra_doc_links)] + //! Procedural macros for `thiserror_ext`. use expand::{DeriveCtorType, DeriveNewType}; @@ -298,3 +300,47 @@ pub fn derive_arc(input: TokenStream) -> TokenStream { .unwrap_or_else(|err| err.to_compile_error()) .into() } + +/// Generates the [`Debug`] implementation that delegates to the [`Report`] of +/// an error. +/// +/// Generally, the [`Debug`] representation of an error should not be used in +/// user-facing scenarios. However, if [`Result::unwrap`] or [`Result::expect`] +/// is called, or an error is used as [`Termination`], the standard library +/// will format the error with [`Debug`]. By delegating to [`Report`], we ensure +/// that the error is still formatted in a user-friendly way and the source +/// chain can be kept in these cases. +/// +/// # Example +/// ```no_run +/// #[derive(thiserror::Error, thiserror_ext::ReportDebug)] +/// #[error("inner")] +/// struct Inner; +/// +/// #[derive(thiserror::Error, thiserror_ext::ReportDebug)] +/// #[error("outer")] +/// struct Outer { +/// #[source] +/// inner: Inner, +/// } +/// +/// let error = Outer { inner: Inner }; +/// println!("{:?}", error); +/// ``` +/// +/// [`Report`]: thiserror_ext::Report +/// [`Termination`]: std::process::Termination +/// +/// # New type +/// +/// Since the new type delegates its [`Debug`] implementation to the original +/// error type, if the original error type derives [`ReportDebug`], the new type +/// will also behave the same. +#[proc_macro_derive(ReportDebug)] +pub fn derive_report_debug(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + expand::derive_report_debug(&input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/tests/report_debug.rs b/tests/report_debug.rs new file mode 100644 index 0000000..0ef20ef --- /dev/null +++ b/tests/report_debug.rs @@ -0,0 +1,42 @@ +use thiserror::Error; +use thiserror_ext::ReportDebug; + +#[derive(Error, ReportDebug, Default)] +#[error("inner")] +struct Inner; + +#[derive(Error, ReportDebug, Default)] +#[error("outer")] +struct Outer { + #[source] + inner: Inner, +} + +#[test] +fn test_report_debug() { + let error = Outer::default(); + + expect_test::expect!["outer: inner"].assert_eq(&format!("{:?}", error)); + + expect_test::expect![[r#" + outer + + Caused by: + inner +"#]] + .assert_eq(&format!("{:#?}", error)); +} + +#[test] +#[should_panic] +fn test_unwrap() { + let error = Outer::default(); + let _ = Err::<(), _>(error).unwrap(); +} + +#[test] +#[should_panic] +fn test_expect() { + let error = Outer::default(); + let _ = Err::<(), _>(error).expect("intentional panic"); +}