From dd527069b8c43f37f8a353e1c88218d990c4b084 Mon Sep 17 00:00:00 2001 From: Frando Date: Fri, 24 Oct 2025 14:56:32 +0200 Subject: [PATCH 01/11] refactor: various changes after review - improved error printing: hide transparent errors in default view, but do display them in the debug view when locations are printed - add `try_e`, `try_any`, `ensure_e`, `bail`, `bail_any` macros - add context to anyerr macro - deprecate `whatever` macro, use `try_e` or `try_any` instead - deprecate `Err` macro, use `Err(e!(..))` insetad --- n0-error-macros/src/lib.rs | 137 +++++++++++++++++++++++-------------- src/any.rs | 16 +++-- src/error.rs | 112 +++++++++++++++--------------- src/macros.rs | 122 +++++++++++++++++++++++++++------ src/meta.rs | 22 +++++- src/tests.rs | 108 +++++++++++++++++++++++++---- 6 files changed, 368 insertions(+), 149 deletions(-) diff --git a/n0-error-macros/src/lib.rs b/n0-error-macros/src/lib.rs index fdba160..a1c706d 100644 --- a/n0-error-macros/src/lib.rs +++ b/n0-error-macros/src/lib.rs @@ -1,5 +1,6 @@ use darling::FromAttributes; use proc_macro::TokenStream; +use proc_macro2::Span; use quote::{quote, ToTokens}; use syn::{ parse_macro_input, parse_quote, punctuated::Punctuated, Attribute, DeriveInput, Expr, Field, @@ -215,25 +216,29 @@ enum FieldIdent<'a> { Unnamed(usize), } -impl<'a> VariantInfo<'a> { - fn transparent(&self) -> Option<&Ident> { - let source = self.source.as_ref()?; - if !source.transparent { - None - } else { - match source.field.ident { - FieldIdent::Named(ident) => Some(ident), - FieldIdent::Unnamed(_) => None, +impl<'a> FieldIdent<'a> { + fn self_expr(&self) -> proc_macro2::TokenStream { + match self { + FieldIdent::Named(ident) => { + quote!(self.#ident) + } + FieldIdent::Unnamed(num) => { + quote!(self.#num) } } } +} + +impl<'a> VariantInfo<'a> { + fn transparent(&self) -> Option<&FieldInfo<'_>> { + let source = self.source.as_ref()?; + source.transparent.then_some(&source.field) + } fn field_binding_idents(&self) -> impl Iterator + '_ { self.fields.iter().map(|f| match f.ident { FieldIdent::Named(ident) => ident.clone(), - FieldIdent::Unnamed(i) => { - syn::Ident::new(&format!("_{}", i), proc_macro2::Span::call_site()) - } + FieldIdent::Unnamed(i) => syn::Ident::new(&format!("_{}", i), Span::call_site()), }) } @@ -248,9 +253,7 @@ impl<'a> VariantInfo<'a> { fn field_idents_without_meta(&self) -> impl Iterator + '_ { self.fields_without_meta().map(|f| match f.ident { FieldIdent::Named(ident) => ident.clone(), - FieldIdent::Unnamed(i) => { - syn::Ident::new(&format!("_{}", i), proc_macro2::Span::call_site()) - } + FieldIdent::Unnamed(i) => syn::Ident::new(&format!("_{}", i), Span::call_site()), }) } @@ -260,6 +263,15 @@ impl<'a> VariantInfo<'a> { attrs: &[Attribute], top: &TopAttrs, ) -> Result, syn::Error> { + let variant_attrs = VariantAttrs::from_attributes(attrs)?; + let display = get_doc_or_display(&attrs)?; + // TODO: enable this but only for #[display] not for doc commments + // if display.is_some() && variant_attrs.transparent { + // return Err(err( + // ident, + // "#[display] and #[error(transparent)] are mutually exclusive", + // )); + // } let (kind, fields): (Kind, Vec) = match fields { Fields::Named(ref fields) => ( Kind::Named, @@ -295,10 +307,15 @@ impl<'a> VariantInfo<'a> { FieldIdent::Named(name) => name == "source", _ => false, }), - Kind::Tuple => None, + Kind::Tuple => { + if variant_attrs.transparent { + fields.first() + } else { + None + } + } }); - let variant_attrs = VariantAttrs::from_attributes(attrs)?; if variant_attrs.transparent && source_field.is_none() { return Err(err( ident, @@ -334,7 +351,7 @@ impl<'a> VariantInfo<'a> { ident: ident.clone(), fields, kind, - display: get_doc_or_display(&attrs)?, + display, source, from: from_field, meta: meta_field, @@ -401,10 +418,10 @@ impl<'a> VariantInfo<'a> { } FieldIdent::Unnamed(_) => unreachable!(), }); - quote! { Self::#v_ident { #( #pairs ),* } } + quote! { #[allow(unused)] Self::#v_ident { #( #pairs ),* } } } Kind::Tuple => { - quote! { Self::#v_ident ( #( #binds ),* ) } + quote! { #[allow(unused)] Self::#v_ident ( #( #binds ),* ) } } } } @@ -419,7 +436,7 @@ fn generate_enum_impls( // StackError impl pieces let match_meta_arms = variants.iter().map(|vi| { if let Some(meta_field) = &vi.meta { - let bind = syn::Ident::new("__meta", proc_macro2::Span::call_site()); + let bind = syn::Ident::new("__meta", Span::call_site()); let pat = vi.spread_field(meta_field, &bind); quote! { #pat => Some(&#bind) } } else { @@ -430,7 +447,7 @@ fn generate_enum_impls( let match_source_arms = variants.iter().map(|vi| match &vi.source { Some(src) => { - let bind = syn::Ident::new("__src", proc_macro2::Span::call_site()); + let bind = syn::Ident::new("__src", Span::call_site()); let pat = vi.spread_field(&src.field, &bind); let expr = src.expr_error_ref(&bind); quote! { #pat => #expr, } @@ -443,7 +460,7 @@ fn generate_enum_impls( let match_std_source_arms = variants.iter().map(|vi| match &vi.source { Some(src) => { - let bind = syn::Ident::new("__src", proc_macro2::Span::call_site()); + let bind = syn::Ident::new("__src", Span::call_site()); let pat = vi.spread_field(&src.field, &bind); let expr = src.expr_error_std(&bind); quote! { #pat => #expr, } @@ -462,14 +479,15 @@ fn generate_enum_impls( let match_fmt_message_arms = variants.iter().map(|vi| { let v_ident = &vi.ident; - if let Some(trans_ident) = vi.transparent() { - return quote! { Self::#v_ident { #trans_ident, .. } => { write!(f, "{}", #trans_ident) } }; - } + // if vi.transparent().is_some() { + // let pat = vi.spread_empty(); + // quote! { #pat => ::n0_error::StackError::source(self).unwrap().fmt_message(f) } + // } else { match &vi.display { Some(expr) => { let binds: Vec = vi.field_binding_idents().collect(); let pat = vi.spread_all(&binds); - quote! { #[allow(unused)] #pat => { #expr } } + quote! { #pat => { #expr } } } None => { let text = v_ident.to_string(); @@ -477,6 +495,7 @@ fn generate_enum_impls( quote! { #pat => write!(f, #text) } } } + // } }); let match_debug_arms = variants.iter().map(|vi| { @@ -484,8 +503,21 @@ fn generate_enum_impls( let v_name = v_ident.to_string(); let binds: Vec = vi.field_binding_idents().collect(); let pat = vi.spread_all(&binds); - let labels: Vec = vi.fields.iter().enumerate().map(|(i, f)| match f.ident { FieldIdent::Named(id) => id.to_string(), FieldIdent::Unnamed(_) => i.to_string() }).collect(); - quote! { #pat => { let mut dbg = f.debug_struct(#v_name); #( dbg.field(#labels, &#binds); )*; dbg.finish()?; } } + let labels: Vec = vi + .fields + .iter() + .enumerate() + .map(|(i, f)| match f.ident { + FieldIdent::Named(id) => id.to_string(), + FieldIdent::Unnamed(_) => i.to_string(), + }) + .collect(); + quote! { + #pat => { + let mut dbg = f.debug_struct(#v_name); + #( dbg.field(#labels, &#binds); )*; dbg.finish()?; + } + } }); // From impls for variants marked with #[from] @@ -533,6 +565,12 @@ fn generate_enum_impls( self } + fn fmt_message(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #( #match_fmt_message_arms, )* + } + } + fn meta(&self) -> Option<&::n0_error::Meta> { match self { #( #match_meta_arms, )* @@ -559,14 +597,9 @@ fn generate_enum_impls( impl ::std::fmt::Display for #enum_ident #generics { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - use ::n0_error::{SourceFormat, StackError, StackErrorExt}; - match self { - #( #match_fmt_message_arms, )* - }?; - if f.alternate() { - self.report().fmt_sources(f, SourceFormat::OneLine)?; - } - Ok(()) + use ::n0_error::{SourceFormat, StackError}; + let sources = f.alternate().then_some(SourceFormat::OneLine); + write!(f, "{}", self.report().sources(sources)) } } @@ -578,8 +611,8 @@ fn generate_enum_impls( #(#match_debug_arms)* } } else { - use ::n0_error::{StackError}; - self.report().full().format(f)?; + use ::n0_error::StackError; + write!(f, "{}", self.report().full())?; } Ok(()) } @@ -607,7 +640,7 @@ fn generate_struct_impl( match f.ident { FieldIdent::Named(ident) => quote! { #ident: #ty }, FieldIdent::Unnamed(i) => { - let arg = syn::Ident::new(&format!("_{}", i), proc_macro2::Span::call_site()); + let arg = syn::Ident::new(&format!("_{}", i), Span::call_site()); quote! { #arg: #ty } } } @@ -644,7 +677,7 @@ fn generate_struct_impl( let get_error_source = match &info.source { Some(src) => { - let bind = syn::Ident::new("__src", proc_macro2::Span::call_site()); + let bind = syn::Ident::new("__src", Span::call_site()); let getter = match src.field.ident { FieldIdent::Named(id) => quote! { let #bind = &self.#id; }, FieldIdent::Unnamed(i) => { @@ -660,7 +693,7 @@ fn generate_struct_impl( let get_std_source = match &info.source { Some(src) => { - let bind = syn::Ident::new("__src", proc_macro2::Span::call_site()); + let bind = syn::Ident::new("__src", Span::call_site()); let getter = match src.field.ident { FieldIdent::Named(id) => quote! { let #bind = &self.#id; }, FieldIdent::Unnamed(i) => { @@ -677,8 +710,9 @@ fn generate_struct_impl( let is_transparent = info.transparent().is_some(); let get_display = { - if let Some(ident) = info.transparent() { - quote! { write!(f, "{}", self.#ident) } + if let Some(field) = info.transparent() { + let expr = field.ident.self_expr(); + quote! { write!(f, "{}", #expr) } } else { match &info.display { Some(expr) => { @@ -791,6 +825,10 @@ fn generate_struct_impl( fn is_transparent(&self) -> bool { #is_transparent } + + fn fmt_message(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + #get_display + } } @@ -803,11 +841,8 @@ fn generate_struct_impl( impl ::std::fmt::Display for #item_ident #generics { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { use ::n0_error::{SourceFormat, StackError}; - #get_display?; - if f.alternate() { - self.report().fmt_sources(f, SourceFormat::OneLine)?; - } - Ok(()) + let sources = f.alternate().then_some(SourceFormat::OneLine); + write!(f, "{}", self.report().sources(sources)) } } @@ -816,8 +851,8 @@ fn generate_struct_impl( if f.alternate() { #get_debug } else { - use ::n0_error::{StackError}; - self.report().full().format(f)?; + use ::n0_error::StackError; + write!(f, "{}", self.report().full())?; } Ok(()) } diff --git a/src/any.rs b/src/any.rs index f828b48..d364e72 100644 --- a/src/any.rs +++ b/src/any.rs @@ -100,11 +100,8 @@ impl AnyError { impl fmt::Display for AnyError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_ref())?; - if f.alternate() { - self.report().fmt_sources(f, SourceFormat::OneLine)?; - } - Ok(()) + let sources = f.alternate().then_some(SourceFormat::OneLine); + write!(f, "{}", self.report().sources(sources)) } } @@ -120,7 +117,7 @@ impl fmt::Debug for AnyError { } } } else { - self.report().full().format(f) + write!(f, "{}", self.report().full()) } } } @@ -158,6 +155,13 @@ impl StackError for AnyError { Inner::Std(error, meta) => ErrorRef::std_with_meta(error.as_ref(), meta), } } + + fn fmt_message(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + Inner::Stack(error) => error.fmt_message(f), + Inner::Std(error, _) => fmt::Display::fmt(error, f), + } + } } impl std::error::Error for AnyError { diff --git a/src/error.rs b/src/error.rs index 581661a..fa5d96f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -31,6 +31,9 @@ pub trait StackError: fmt::Display + fmt::Debug + Send + Sync { /// Returns the next source in the chain, if any. fn source(&self) -> Option>; + /// Returns the next source in the chain, if any. + fn fmt_message(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; + /// Returns whether this error is transparent and should be skipped in reports. fn is_transparent(&self) -> bool; @@ -139,6 +142,14 @@ impl<'a> ErrorRef<'a> { pub(crate) fn fmt_location(&self, f: &mut Formatter) -> fmt::Result { fmt_location(self.meta().and_then(|m| m.location()), f) } + + /// Formats the error message. + pub fn fmt_message(&self, f: &mut Formatter) -> fmt::Result { + match self { + ErrorRef::Std(error, _) => fmt::Display::fmt(error, f), + ErrorRef::Stack(error) => error.fmt_message(f), + } + } } impl<'a> fmt::Display for ErrorRef<'a> { @@ -160,9 +171,10 @@ pub struct Report<'a> { impl<'a> Report<'a> { pub(crate) fn new(inner: &'a dyn StackError) -> Self { + let location = backtrace_enabled(); Self { inner, - location: false, + location, sources: None, } } @@ -185,50 +197,55 @@ impl<'a> Report<'a> { } /// Prints the error's sources. - pub fn sources(mut self, format: SourceFormat) -> Self { - self.sources = Some(format); + pub fn sources(mut self, format: Option) -> Self { + self.sources = format; self } +} - /// Formats the report via a [`Formatter`]. - pub fn format(&self, f: &mut Formatter) -> fmt::Result { - fmt::Display::fmt(self, f) - } - - /// Formats only the location. - pub fn fmt_location(&'a self, f: &mut Formatter) -> fmt::Result { - match self.inner.as_dyn().meta().and_then(|m| m.location()) { - None => Ok(()), - Some(location) => { - write!(f, " (at {})", location) +impl<'a> fmt::Display for Report<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.sources { + None => { + let item = self + .inner + .stack() + .find(|item| !item.is_transparent()) + .unwrap_or_else(|| self.inner.as_ref()); + item.fmt_message(f)?; } - } - } - - /// Formats only the sources. - pub fn fmt_sources(&'a self, f: &mut Formatter, format: SourceFormat) -> fmt::Result { - let chain = self.inner.as_dyn().sources(); - let mut chain = chain - // We skip errors marked as transparent. - .filter(|s| !s.is_transparent()) - .enumerate() - .peekable(); - if chain.peek().is_some() && matches!(format, SourceFormat::MultiLine { .. }) { - writeln!(f, "\nCaused by:")?; - } - while let Some((i, item)) = chain.next() { - match format { - SourceFormat::OneLine => { - write!(f, ": {item}")?; - } - SourceFormat::MultiLine { location } => { - write!(f, " {i}: {item}")?; - if location { - item.fmt_location(f)?; - } - if chain.peek().is_some() { - writeln!(f)?; + Some(format) => { + let mut stack = self + .inner + .stack() + .filter(|s| self.location || !s.is_transparent()) + .peekable(); + let mut is_first = true; + while let Some(item) = stack.next() { + match format { + SourceFormat::OneLine => { + if !is_first { + write!(f, ": ")?; + } + item.fmt_message(f)?; + } + SourceFormat::MultiLine { location } => { + if !is_first { + write!(f, " ")?; + } + item.fmt_message(f)?; + if location { + item.fmt_location(f)?; + } + if stack.peek().is_some() { + writeln!(f)?; + if is_first { + writeln!(f, "Caused by:")?; + } + } + } } + is_first = false; } } } @@ -236,22 +253,9 @@ impl<'a> Report<'a> { } } -impl<'a> fmt::Display for Report<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.inner)?; - if self.location { - self.fmt_location(f)?; - } - if let Some(format) = self.sources { - self.fmt_sources(f, format)?; - } - Ok(()) - } -} - fn fmt_location(location: Option<&Location>, f: &mut Formatter) -> fmt::Result { if let Some(location) = location { - write!(f, " (at {})", location)?; + write!(f, " ({})", location)?; } Ok(()) } diff --git a/src/macros.rs b/src/macros.rs index fd61e90..a97293e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -24,6 +24,7 @@ macro_rules! e { /// Constructs an error enum/struct value and wraps it in `Err(err)`. /// /// See [`e`] for supported syntax. +#[deprecated] #[macro_export] macro_rules! Err { ($($tt:tt)*) => { @@ -35,6 +36,7 @@ macro_rules! Err { /// /// - `whatever!("msg")` returns `Err(format_err!(...))`. /// - `whatever!(source, "msg {x}", x)` unwraps `source` or returns with context. +#[deprecated] #[macro_export] macro_rules! whatever { ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { @@ -42,8 +44,8 @@ macro_rules! whatever { $crate::format_err!($fmt$(, $($arg),*)*) }); }; - ($source:expr, $fmt:literal$(, $($arg:expr),* $(,)?)*) => { - match $source { + ($result:expr, $fmt:literal$(, $($arg:expr),* $(,)?)*) => { + match $result { ::core::result::Result::Ok(v) => v, ::core::result::Result::Err(e) => { let context = ::std::format!($fmt$(, $($arg),*)*); @@ -55,6 +57,38 @@ macro_rules! whatever { }; } +/// Unwraps a result, returning in the error case while converting the error. +/// +/// If the result is the error variant, this will construct a new error with [`e`] +/// that takes the result's error as its source. +#[macro_export] +macro_rules! try_e { + ($result:expr, $($tt:tt)*) => { + match $result { + ::core::result::Result::Ok(v) => v, + ::core::result::Result::Err(e) => { + return ::core::result::Result::Err($crate::e!($($tt)*, e)); + } + } + }; +} + +/// Unwraps a result, returning in the error case while adding context to the error. +/// +/// If the result is the error variant, this will construct a new error with [`anyerr`] +/// from the result's error while providing additional context. +#[macro_export] +macro_rules! try_any { + ($result:expr, $($context:tt)*) => { + match $result { + ::core::result::Result::Ok(v) => v, + ::core::result::Result::Err(e) => { + return ::core::result::Result::Err($crate::anyerr!(e, $($context)*)); + } + } + }; +} + /// Formats a message into an [`AnyError`]. #[macro_export] macro_rules! format_err { @@ -65,49 +99,87 @@ macro_rules! format_err { }; } +/// Ensures a condition, otherwise returns the error constructed with [`e`] from the remaining args. +/// +/// This macro takes an expression as its first argument. If the expression evaluates +/// to `false`, the macro expands to returning an error result. +/// +/// The error will be constructed by passing all remaining arguments to [`e`]. +/// See its docs for details on accepted forms. +#[macro_export] +macro_rules! ensure_e { + ($predicate:expr, $($tt:tt)*) => { + if !$predicate { + $crate::bail!($($tt)*) + } + }; +} + /// Ensures a condition, otherwise returns the given error. +/// +/// The error will be converted into the function's expected error return type +/// with `into`. #[macro_export] macro_rules! ensure { ($predicate:expr, $err:expr $(,)?) => { if !$predicate { - return Err(::core::convert::Into::into($err)); + return Err(::std::convert::Into::into($err)); } }; } /// Ensures a condition, otherwise returns an [`AnyError`]. +/// +/// This macro takes an expression as its first argument. If the expression evaluates +/// to `false`, the macro expands to returning an error result. The error will be constructed +/// by passing the remaining arguments after the expression to [`anyerr`]. See its docs for +/// supported forms. #[macro_export] macro_rules! ensure_any { - ($cond:expr, $fmt:literal) => { - if !$cond{ - return Err($crate::anyerr!($fmt)); + ($cond:expr, $($tt:tt)*) => { + if !$cond { + $crate::bail_any!($($tt)*) } }; +} - ($cond:expr, $fmt:literal$(, $($arg:expr),* $(,)?)?) => { - if !$cond{ - return Err($crate::anyerr!($fmt, $($arg),*)); - } - }; +/// Returns an error result by constructing an error with [`e`]. +/// +/// This macro accepts the same forms as [`e`], but wraps the error into `Err` and +/// expands to returning the result from the current function. +#[macro_export] +macro_rules! bail { + ($($tt:tt)*) => { + return ::core::result::Result::Err($crate::e!($($tt)*)) + } +} - ($cond:expr, $err:expr) => { - if !$cond{ - return Err($crate::anyerr!($err)); - } +/// Returns an error result by constructing an error with [`anyerr`]. +/// +/// This macro accepts the same forms as [`anyerr`], but wraps the error into `Err` and +/// expands to returning the result from the current function. +#[macro_export] +macro_rules! bail_any { + ($($tt:tt)*) => { + return core::result::Result::Err($crate::anyerr!($($tt)*)) } } +/// Reexport `spez` for use in the [`anyerr`] macro. #[doc(hidden)] pub use spez as __spez; -/// Converts a value into [`AnyError`], or formats a message. +/// Converts a value into [`AnyError`]. /// -/// - `anyerr!("msg")` formats a message. -/// - `anyerr!(value)` converts `StackError`, std error, or `Display` in [`AnyError`]. +/// - `anyerr!("msg")` creates an error with just a message. +/// - `anyerr!("this failed at {a} with {}", b)` creates an error with a formatted message +/// - `anyerr!(value)` converts any `impl StackError`, `impl std::error::Error`, or `impl Display` into [`AnyError`]. +/// - `anyerr!(value, "context string") works as above, but adds "context string" as [`context`](Anyerr::context). +/// - `anyerr!(value, "contex {}", foo) works as above, but with a formatted string as context /// -/// This uses *autoref specialization* to use the most details available: if given a [`StackError`] -/// it uses [`AnyError::from_stack`], if given a std error, uses [`AnyError::from_std`], if given a value -/// that impls `Display` it uses [`AnyError::from_display`] - in this order. +/// The forms that take `value` use *autoref specialization* to keep the most details possible: +/// if given a [`StackError`] it uses [`AnyError::from_stack`], if given a std error, uses [`AnyError::from_std`], +/// if given a value that impls `Display` it uses [`AnyError::from_display`] - in this order. #[macro_export] macro_rules! anyerr { ($fmt:literal) => { @@ -118,6 +190,14 @@ macro_rules! anyerr { $crate::format_err!($fmt$(, $($arg),*)*) }; + ($err:expr, $fmt:literal) => { + $crate::anyerr!($err).context($fmt) + }; + + ($err:expr, $fmt:literal$(, $($arg:expr),* $(,)?)?) => { + $crate::anyerr!($err).context(format!($fmt$(, $($arg),*)*)) + }; + ($err:expr) => { $crate::__spez::spez! { for err = $err; diff --git a/src/meta.rs b/src/meta.rs index 55960a1..6286175 100644 --- a/src/meta.rs +++ b/src/meta.rs @@ -1,4 +1,4 @@ -use std::sync::OnceLock; +use std::{fmt, sync::OnceLock}; /// Wrapper around `std::panic::Location` used for display in reports. #[derive(derive_more::Debug, derive_more::Display, Clone, Copy)] @@ -8,11 +8,29 @@ pub struct Location(&'static std::panic::Location<'static>); /// Captured metadata for an error creation site. /// /// Currently this only contains the call-site [`Location`]. -#[derive(Debug)] pub struct Meta { location: Option, } +impl fmt::Display for Meta { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(location) = self.location.as_ref() { + write!(f, "{location}")?; + } + Ok(()) + } +} + +impl fmt::Debug for Meta { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Meta")?; + if let Some(location) = self.location.as_ref() { + write!(f, "({location})")?; + } + Ok(()) + } +} + /// Creates new [`Meta`] capturing the caller location. #[track_caller] pub fn meta() -> Meta { diff --git a/src/tests.rs b/src/tests.rs index 1843ffd..e1a7bd6 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,7 +3,7 @@ use std::io; use self::util::wait_sequential; use crate::{ AnyError, Error, Result, StackError, StackErrorExt, StackResultExt, StdResultExt, add_meta, - anyerr, e, ensure_any, format_err, meta, + anyerr, e, ensure_any, format_err, meta, set_backtrace_enabled, }; mod util; @@ -27,7 +27,7 @@ fn test_whatever() { let _guard = wait_sequential(); fn fail() -> Result { - n0_error::whatever!("sad face"); + n0_error::bail_any!("sad face"); } fn fail_my_error() -> Result<(), MyError> { @@ -35,12 +35,12 @@ fn test_whatever() { } fn fail_whatever() -> Result { - n0_error::whatever!(fail(), "sad"); + n0_error::try_any!(fail(), "sad"); Ok(()) } fn fail_whatever_my_error() -> Result { - n0_error::whatever!(fail_my_error(), "sad"); + n0_error::try_any!(fail_my_error(), "sad"); Ok(()) } @@ -61,12 +61,12 @@ fn test_whatever() { assert_eq!( format!("{:?}", fail_whatever().unwrap_err()), - "sad\nCaused by:\n 0: sad face" + "sad\nCaused by:\n sad face" ); assert_eq!( format!("{:?}", fail_whatever()), - "Err(sad\nCaused by:\n 0: sad face)" + "Err(sad\nCaused by:\n sad face)" ); n0_error::set_backtrace_enabled(true); @@ -76,7 +76,7 @@ fn test_whatever() { assert_eq!(format!("{:#}", fail_my_error().unwrap_err()), "A failure"); assert_eq!( format!("{:?}", fail_my_error().unwrap_err()), - format!("A failure (at src/tests.rs:34:32)") + format!("A failure (src/tests.rs:34:32)") ); // let expected = r#"A { // location: Some( @@ -180,8 +180,8 @@ fn test_sources() { &fmt, r#"read error Caused by: - 0: failed to read foo.txt - 1: file not found"# + failed to read foo.txt + file not found"# ); let fmt = format!("{err:#?}"); @@ -208,10 +208,10 @@ Caused by: println!("debug :\n{fmt}\n"); assert_eq!( &fmt, - r#"read error (at src/tests.rs:195:34) + r#"read error (src/tests.rs:195:34) Caused by: - 0: failed to read foo.txt (at src/tests.rs:194:39) - 1: file not found (at src/tests.rs:194:39)"# + failed to read foo.txt (src/tests.rs:194:39) + file not found (src/tests.rs:194:39)"# ); let fmt = format!("{err:#?}"); println!("debug alternate:\n{fmt}\n"); @@ -289,7 +289,7 @@ fn test_structs_location() { let res = fail_some_error(); let err = res.unwrap_err(); assert_eq!(format!("{err}"), "SomeErrorLoc"); - assert_eq!(format!("{err:?}"), "SomeErrorLoc (at src/tests.rs:282:13)"); + assert_eq!(format!("{err:?}"), "SomeErrorLoc (src/tests.rs:282:13)"); let err2 = err.context("bad"); assert_eq!(format!("{err2:#}"), "bad: SomeErrorLoc"); let res = fail_some_error_fields(); @@ -300,9 +300,9 @@ fn test_structs_location() { println!("{err2:?}"); assert_eq!( format!("{err2:?}"), - r#"bad (at src/tests.rs:298:20) + r#"bad (src/tests.rs:298:20) Caused by: - 0: fail (22) (at src/tests.rs:286:13)"# + fail (22) (src/tests.rs:286:13)"# ); } @@ -392,3 +392,81 @@ fn test_tuple_enum_source_and_meta() { let src = std::error::Error::source(&e).unwrap(); assert_eq!(src.to_string(), "oops"); } + +// TODO: turn into actual test +#[test] +pub fn test_skip_transparent_errors() { + #[add_meta] + #[derive(Error)] + #[error(from_sources)] + enum ErrorA { + #[display("failure at b")] + ErrorB { source: ErrorB }, + } + + #[add_meta] + #[derive(Error)] + #[error(std_sources)] + enum ErrorB { + #[error(transparent)] + IoTransparent { source: io::Error }, + #[display("io error")] + Io { source: io::Error }, + } + + fn err_a(transparent: bool) -> Result<(), ErrorA> { + err_b(transparent)?; + Ok(()) + } + + fn err_b(transparent: bool) -> Result<(), ErrorB> { + if transparent { + io().map_err(|err| e!(ErrorB::IoTransparent, err)) + } else { + io().map_err(|err| e!(ErrorB::Io, err)) + } + } + + fn io() -> io::Result<()> { + Err(io::Error::other("bad")) + } + + let _guard = wait_sequential(); + set_backtrace_enabled(false); + println!("no bt, transparent"); + println!("{:?}", err_a(true).unwrap_err()); + set_backtrace_enabled(true); + println!("bt, transparent"); + println!("{:?}", err_a(true).unwrap_err()); + println!("{:#?}", err_a(true).unwrap_err()); + set_backtrace_enabled(false); + println!("no bt, not transparent"); + println!("{:?}", err_a(false).unwrap_err()); + set_backtrace_enabled(true); + println!("bt, transparent"); + println!("{:?}", err_a(true).unwrap_err()); + println!("bt, not transparent"); + println!("{:?}", err_a(false).unwrap_err()); + println!("==="); + println!("==="); + println!("==="); + set_backtrace_enabled(true); + println!("bt, transparent, display alt"); + println!("{:#}", err_a(true).unwrap_err()); + println!("bt, not transparent, display alt"); + println!("{:#}", err_a(false).unwrap_err()); + println!("bt, transparent, display"); + println!("{}", err_a(true).unwrap_err()); + println!("bt, not transparent, display"); + println!("{}", err_a(false).unwrap_err()); + println!("==="); + set_backtrace_enabled(false); + println!("no bt, transparent, display alt"); + println!("{:#}", err_a(true).unwrap_err()); + println!("no bt, not transparent, display alt"); + println!("{:#}", err_a(false).unwrap_err()); + println!("no bt, transparent, display"); + println!("{}", err_a(true).unwrap_err()); + println!("no bt, not transparent, display"); + println!("{}", err_a(false).unwrap_err()); +} From 1e30e84098e442ac42e06b47c746da8bf4f855e6 Mon Sep 17 00:00:00 2001 From: Frando Date: Fri, 24 Oct 2025 15:13:14 +0200 Subject: [PATCH 02/11] refactor: change formatting if no display is provided --- examples/simple.rs | 4 ++-- n0-error-macros/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 83d3c89..26b1d87 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,6 +1,6 @@ use std::io; -use n0_error::{Err, StackError, e, meta}; +use n0_error::{StackError, e, meta}; use self::error::CopyError; use crate::error::{InvalidArgsError, OperationError}; @@ -29,7 +29,7 @@ fn main() { fn _some_fn() -> Result<(), CopyError> { // Err! macro works like e! but wraps in Err - Err!(CopyError::Read, io::Error::other("yada")) + Err(e!(CopyError::Read, io::Error::other("yada"))) } fn operation() -> Result<(), OperationError> { diff --git a/n0-error-macros/src/lib.rs b/n0-error-macros/src/lib.rs index a1c706d..0586452 100644 --- a/n0-error-macros/src/lib.rs +++ b/n0-error-macros/src/lib.rs @@ -490,7 +490,7 @@ fn generate_enum_impls( quote! { #pat => { #expr } } } None => { - let text = v_ident.to_string(); + let text = format!("{}::{}", enum_ident, v_ident); let pat = vi.spread_empty(); quote! { #pat => write!(f, #text) } } From 6b46dafc04efe0d8a4b9bdd0889cae6c0890a72d Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 09:26:09 +0100 Subject: [PATCH 03/11] more tests --- src/tests.rs | 54 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/tests.rs b/src/tests.rs index e1a7bd6..4a2d4ae 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -376,9 +376,12 @@ fn test_tuple_struct_basic() { #[add_meta] #[derive(n0_error::Error)] +#[error(from_sources)] enum TupleEnum { #[display("io failed")] Io(#[error(source, std_err)] io::Error), + #[error(transparent)] + Transparent(MyError), } #[test] @@ -391,6 +394,18 @@ fn test_tuple_enum_source_and_meta() { // Std source is the inner io::Error let src = std::error::Error::source(&e).unwrap(); assert_eq!(src.to_string(), "oops"); + + let err = e!(MyError::A); + let err = TupleEnum::from(err); + assert_eq!(format!("{err}"), "A failure"); + assert_eq!(format!("{err:?}"), "A failure"); + n0_error::set_backtrace_enabled(true); + let err = e!(MyError::A); + let err = TupleEnum::from(err); + assert_eq!( + format!("{err:?}"), + "TupleEnum::Transparent (src/tests.rs:404:15)\nCaused by:\n A failure (src/tests.rs:403:15)" + ); } // TODO: turn into actual test @@ -409,7 +424,7 @@ pub fn test_skip_transparent_errors() { #[error(std_sources)] enum ErrorB { #[error(transparent)] - IoTransparent { source: io::Error }, + IoTransparent(io::Error), #[display("io error")] Io { source: io::Error }, } @@ -421,7 +436,7 @@ pub fn test_skip_transparent_errors() { fn err_b(transparent: bool) -> Result<(), ErrorB> { if transparent { - io().map_err(|err| e!(ErrorB::IoTransparent, err)) + io().map_err(|err| ErrorB::IoTransparent(err, meta())) } else { io().map_err(|err| e!(ErrorB::Io, err)) } @@ -433,40 +448,37 @@ pub fn test_skip_transparent_errors() { let _guard = wait_sequential(); set_backtrace_enabled(false); - println!("no bt, transparent"); + println!("#### no bt, transparent"); println!("{:?}", err_a(true).unwrap_err()); set_backtrace_enabled(true); - println!("bt, transparent"); + println!("#### bt, transparent"); println!("{:?}", err_a(true).unwrap_err()); - println!("{:#?}", err_a(true).unwrap_err()); + set_backtrace_enabled(false); - println!("no bt, not transparent"); + println!("#### no bt, not transparent"); println!("{:?}", err_a(false).unwrap_err()); + set_backtrace_enabled(true); - println!("bt, transparent"); + println!("#### bt, transparent"); println!("{:?}", err_a(true).unwrap_err()); - println!("bt, not transparent"); - println!("{:?}", err_a(false).unwrap_err()); - println!("==="); - println!("==="); - println!("==="); + set_backtrace_enabled(true); - println!("bt, transparent, display alt"); + println!("#### bt, transparent, display alt"); println!("{:#}", err_a(true).unwrap_err()); - println!("bt, not transparent, display alt"); + println!("#### bt, not transparent, display alt"); println!("{:#}", err_a(false).unwrap_err()); - println!("bt, transparent, display"); + println!("#### bt, transparent, display"); println!("{}", err_a(true).unwrap_err()); - println!("bt, not transparent, display"); + println!("#### bt, not transparent, display"); println!("{}", err_a(false).unwrap_err()); - println!("==="); + set_backtrace_enabled(false); - println!("no bt, transparent, display alt"); + println!("#### no bt, transparent, display alt"); println!("{:#}", err_a(true).unwrap_err()); - println!("no bt, not transparent, display alt"); + println!("#### no bt, not transparent, display alt"); println!("{:#}", err_a(false).unwrap_err()); - println!("no bt, transparent, display"); + println!("#### no bt, transparent, display"); println!("{}", err_a(true).unwrap_err()); - println!("no bt, not transparent, display"); + println!("#### no bt, not transparent, display"); println!("{}", err_a(false).unwrap_err()); } From 74db31b779f2c1d6a748d3a76494d76792892564 Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 09:43:20 +0100 Subject: [PATCH 04/11] wip --- src/macros.rs | 6 +++--- src/tests.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index a97293e..e94316d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -138,7 +138,7 @@ macro_rules! ensure { macro_rules! ensure_any { ($cond:expr, $($tt:tt)*) => { if !$cond { - $crate::bail_any!($($tt)*) + $crate::bail!($($tt)*) } }; } @@ -148,7 +148,7 @@ macro_rules! ensure_any { /// This macro accepts the same forms as [`e`], but wraps the error into `Err` and /// expands to returning the result from the current function. #[macro_export] -macro_rules! bail { +macro_rules! bail_e { ($($tt:tt)*) => { return ::core::result::Result::Err($crate::e!($($tt)*)) } @@ -159,7 +159,7 @@ macro_rules! bail { /// This macro accepts the same forms as [`anyerr`], but wraps the error into `Err` and /// expands to returning the result from the current function. #[macro_export] -macro_rules! bail_any { +macro_rules! bail { ($($tt:tt)*) => { return core::result::Result::Err($crate::anyerr!($($tt)*)) } diff --git a/src/tests.rs b/src/tests.rs index 4a2d4ae..29c8b5f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -27,7 +27,7 @@ fn test_whatever() { let _guard = wait_sequential(); fn fail() -> Result { - n0_error::bail_any!("sad face"); + n0_error::bail!("sad face"); } fn fail_my_error() -> Result<(), MyError> { From f886f3f9360e501a720cca5a9c4b11732bbb9507 Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 10:09:42 +0100 Subject: [PATCH 05/11] fix: remove deprecated macros --- src/macros.rs | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index e94316d..70473e2 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -21,42 +21,6 @@ macro_rules! e { }; } -/// Constructs an error enum/struct value and wraps it in `Err(err)`. -/// -/// See [`e`] for supported syntax. -#[deprecated] -#[macro_export] -macro_rules! Err { - ($($tt:tt)*) => { - ::core::result::Result::Err($crate::e!($($tt)*)) - } -} - -/// Propagates an error, adding formatted context. -/// -/// - `whatever!("msg")` returns `Err(format_err!(...))`. -/// - `whatever!(source, "msg {x}", x)` unwraps `source` or returns with context. -#[deprecated] -#[macro_export] -macro_rules! whatever { - ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { - return ::core::result::Result::Err({ - $crate::format_err!($fmt$(, $($arg),*)*) - }); - }; - ($result:expr, $fmt:literal$(, $($arg:expr),* $(,)?)*) => { - match $result { - ::core::result::Result::Ok(v) => v, - ::core::result::Result::Err(e) => { - let context = ::std::format!($fmt$(, $($arg),*)*); - return ::core::result::Result::Err( - $crate::anyerr!(e).context(context) - ); - } - } - }; -} - /// Unwraps a result, returning in the error case while converting the error. /// /// If the result is the error variant, this will construct a new error with [`e`] From 0424d0c766e2cb51f47abf8f4e4d051442aa595c Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 10:24:24 +0100 Subject: [PATCH 06/11] cleanup --- n0-error-macros/src/lib.rs | 58 +++++++++++++++++++------------------- src/macros.rs | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/n0-error-macros/src/lib.rs b/n0-error-macros/src/lib.rs index 0586452..ab8c79f 100644 --- a/n0-error-macros/src/lib.rs +++ b/n0-error-macros/src/lib.rs @@ -265,7 +265,7 @@ impl<'a> VariantInfo<'a> { ) -> Result, syn::Error> { let variant_attrs = VariantAttrs::from_attributes(attrs)?; let display = get_doc_or_display(&attrs)?; - // TODO: enable this but only for #[display] not for doc commments + // TODO: enable this but only for #[display] not for doc comments // if display.is_some() && variant_attrs.transparent { // return Err(err( // ident, @@ -460,7 +460,7 @@ fn generate_enum_impls( let match_std_source_arms = variants.iter().map(|vi| match &vi.source { Some(src) => { - let bind = syn::Ident::new("__src", Span::call_site()); + let bind = syn::Ident::new("source", Span::call_site()); let pat = vi.spread_field(&src.field, &bind); let expr = src.expr_error_std(&bind); quote! { #pat => #expr, } @@ -477,39 +477,29 @@ fn generate_enum_impls( quote! { #pat => #value } }); - let match_fmt_message_arms = variants.iter().map(|vi| { - let v_ident = &vi.ident; - // if vi.transparent().is_some() { - // let pat = vi.spread_empty(); - // quote! { #pat => ::n0_error::StackError::source(self).unwrap().fmt_message(f) } - // } else { - match &vi.display { - Some(expr) => { - let binds: Vec = vi.field_binding_idents().collect(); - let pat = vi.spread_all(&binds); - quote! { #pat => { #expr } } - } - None => { - let text = format!("{}::{}", enum_ident, v_ident); - let pat = vi.spread_empty(); - quote! { #pat => write!(f, #text) } - } + let match_fmt_message_arms = variants.iter().map(|vi| match &vi.display { + Some(expr) => { + let binds: Vec = vi.field_binding_idents().collect(); + let pat = vi.spread_all(&binds); + quote! { #pat => { #expr } } + } + None => { + let text = format!("{}::{}", enum_ident, vi.ident); + let pat = vi.spread_empty(); + quote! { #pat => write!(f, #text) } } - // } }); let match_debug_arms = variants.iter().map(|vi| { - let v_ident = &vi.ident; - let v_name = v_ident.to_string(); + let v_name = vi.ident.to_string(); let binds: Vec = vi.field_binding_idents().collect(); let pat = vi.spread_all(&binds); let labels: Vec = vi .fields .iter() - .enumerate() - .map(|(i, f)| match f.ident { + .map(|f| match f.ident { FieldIdent::Named(id) => id.to_string(), - FieldIdent::Unnamed(_) => i.to_string(), + FieldIdent::Unnamed(i) => i.to_string(), }) .collect(); quote! { @@ -520,7 +510,7 @@ fn generate_enum_impls( } }); - // From impls for variants marked with #[from] + // From impls for variant fields marked with #[from] let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let from_impls = variants.iter().filter_map(|vi| vi.from.as_ref().map(|field| (vi, field))).map(|(vi, field)| { let v_ident = &vi.ident; @@ -677,7 +667,7 @@ fn generate_struct_impl( let get_error_source = match &info.source { Some(src) => { - let bind = syn::Ident::new("__src", Span::call_site()); + let bind = syn::Ident::new("source", Span::call_site()); let getter = match src.field.ident { FieldIdent::Named(id) => quote! { let #bind = &self.#id; }, FieldIdent::Unnamed(i) => { @@ -749,10 +739,20 @@ fn generate_struct_impl( .collect(); match info.kind { Kind::Named => { - quote! { let Self { #( #binds ),* } = self; let mut dbg = f.debug_struct(#item_name); #( dbg.field(#labels, &#binds); )*; dbg.finish()?; } + quote! { + let Self { #( #binds ),* } = self; + let mut dbg = f.debug_struct(#item_name); + #( dbg.field(#labels, &#binds); )*; + dbg.finish()?; + } } Kind::Tuple => { - quote! { let Self( #( #binds ),* ) = self; let mut dbg = f.debug_struct(#item_name); #( dbg.field(#labels, &#binds); )*; dbg.finish()?; } + quote! { + let Self( #( #binds ),* ) = self; + let mut dbg = f.debug_struct(#item_name); + #( dbg.field(#labels, &#binds); )*; + dbg.finish()?; + } } } }; diff --git a/src/macros.rs b/src/macros.rs index 70473e2..abf730c 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -139,7 +139,7 @@ pub use spez as __spez; /// - `anyerr!("this failed at {a} with {}", b)` creates an error with a formatted message /// - `anyerr!(value)` converts any `impl StackError`, `impl std::error::Error`, or `impl Display` into [`AnyError`]. /// - `anyerr!(value, "context string") works as above, but adds "context string" as [`context`](Anyerr::context). -/// - `anyerr!(value, "contex {}", foo) works as above, but with a formatted string as context +/// - `anyerr!(value, "context {}", foo) works as above, but with a formatted string as context /// /// The forms that take `value` use *autoref specialization* to keep the most details possible: /// if given a [`StackError`] it uses [`AnyError::from_stack`], if given a std error, uses [`AnyError::from_std`], From 38505e4aadc691a5a374dd67ad69f10a03f93f4c Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 10:27:40 +0100 Subject: [PATCH 07/11] fixup --- src/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/macros.rs b/src/macros.rs index abf730c..232b351 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -74,7 +74,7 @@ macro_rules! format_err { macro_rules! ensure_e { ($predicate:expr, $($tt:tt)*) => { if !$predicate { - $crate::bail!($($tt)*) + $crate::bail_e!($($tt)*) } }; } From 52da5e34a1d791ddb9e4588814bad6be1427b50e Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 10:40:37 +0100 Subject: [PATCH 08/11] fixup --- src/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/macros.rs b/src/macros.rs index 232b351..466c73e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -114,7 +114,7 @@ macro_rules! ensure_any { #[macro_export] macro_rules! bail_e { ($($tt:tt)*) => { - return ::core::result::Result::Err($crate::e!($($tt)*)) + return ::core::result::Result::Err($crate::e!($($tt)*).into()) } } From 967eefdcf718532ff20479b1f9bc8372b6a43e90 Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 11:22:24 +0100 Subject: [PATCH 09/11] add new form to e! macro --- src/macros.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/macros.rs b/src/macros.rs index 466c73e..6dc7b15 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -15,6 +15,11 @@ macro_rules! e { $($err)::+ { source: $source, meta: $crate::Meta::default() } }; + // Fields and values plus source + ($($err:tt)::+ { $($body:tt)* }, $source:expr) => { + $($err)::+ { meta: $crate::Meta::default(), source: $source, $($body)* } + }; + // Fields and values ($($err:tt)::+ { $($body:tt)* }) => { $($err)::+ { meta: $crate::Meta::default(), $($body)* } From b63e3d4b278652f32aab677244a1ebc1b23101a5 Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 11:46:55 +0100 Subject: [PATCH 10/11] add more StackError impls --- src/error.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/macros.rs | 13 -------- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/error.rs b/src/error.rs index fa5d96f..691fc25 100644 --- a/src/error.rs +++ b/src/error.rs @@ -298,3 +298,92 @@ impl<'a> Iterator for Chain<'a> { } } } + +macro_rules! impl_stack_error_for_std_error { + ($ty:ty) => { + impl StackError for $ty { + fn as_std(&self) -> &(dyn std::error::Error + Send + Sync + 'static) { + self + } + + fn as_dyn(&self) -> &dyn StackError { + self + } + + fn meta(&self) -> Option<&Meta> { + None + } + + fn source(&self) -> Option> { + std::error::Error::source(self).map(ErrorRef::std) + } + + fn fmt_message(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } + + fn is_transparent(&self) -> bool { + false + } + } + }; +} + +impl_stack_error_for_std_error!(std::io::Error); +impl_stack_error_for_std_error!(std::fmt::Error); +impl_stack_error_for_std_error!(std::str::Utf8Error); +impl_stack_error_for_std_error!(std::string::FromUtf8Error); +impl_stack_error_for_std_error!(std::net::AddrParseError); +impl_stack_error_for_std_error!(std::array::TryFromSliceError); + +impl StackError for Box { + fn as_std(&self) -> &(dyn std::error::Error + Send + Sync + 'static) { + StackError::as_std(&**self) + } + + fn as_dyn(&self) -> &dyn StackError { + StackError::as_dyn(&**self) + } + + fn meta(&self) -> Option<&Meta> { + StackError::meta(&**self) + } + + fn source(&self) -> Option> { + StackError::source(&**self) + } + + fn fmt_message(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + StackError::fmt_message(&**self, f) + } + + fn is_transparent(&self) -> bool { + StackError::is_transparent(&**self) + } +} + +impl StackError for std::sync::Arc { + fn as_std(&self) -> &(dyn std::error::Error + Send + Sync + 'static) { + StackError::as_std(&**self) + } + + fn as_dyn(&self) -> &dyn StackError { + StackError::as_dyn(&**self) + } + + fn meta(&self) -> Option<&Meta> { + StackError::meta(&**self) + } + + fn source(&self) -> Option> { + StackError::source(&**self) + } + + fn fmt_message(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + StackError::fmt_message(&**self, f) + } + + fn is_transparent(&self) -> bool { + StackError::is_transparent(&**self) + } +} diff --git a/src/macros.rs b/src/macros.rs index 6dc7b15..e18c35d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -84,19 +84,6 @@ macro_rules! ensure_e { }; } -/// Ensures a condition, otherwise returns the given error. -/// -/// The error will be converted into the function's expected error return type -/// with `into`. -#[macro_export] -macro_rules! ensure { - ($predicate:expr, $err:expr $(,)?) => { - if !$predicate { - return Err(::std::convert::Into::into($err)); - } - }; -} - /// Ensures a condition, otherwise returns an [`AnyError`]. /// /// This macro takes an expression as its first argument. If the expression evaluates From 652923bf05d9d27ec368c9d86d265ac258c5aa4a Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 12:10:17 +0100 Subject: [PATCH 11/11] fix: generic errors --- n0-error-macros/src/lib.rs | 24 ++++++++++++------------ src/tests.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/n0-error-macros/src/lib.rs b/n0-error-macros/src/lib.rs index ab8c79f..1b08b78 100644 --- a/n0-error-macros/src/lib.rs +++ b/n0-error-macros/src/lib.rs @@ -546,7 +546,7 @@ fn generate_enum_impls( }); quote! { - impl ::n0_error::StackError for #enum_ident #generics { + impl #impl_generics ::n0_error::StackError for #enum_ident #ty_generics #where_clause { fn as_std(&self) -> &(dyn ::std::error::Error + ::std::marker::Send + ::std::marker::Sync + 'static) { self } @@ -579,13 +579,13 @@ fn generate_enum_impls( } } - impl #impl_generics ::core::convert::From<#enum_ident> for ::n0_error::AnyError #ty_generics #where_clause { - fn from(value: #enum_ident) -> ::n0_error::AnyError { + impl #impl_generics ::core::convert::From<#enum_ident #ty_generics> for ::n0_error::AnyError #where_clause { + fn from(value: #enum_ident #ty_generics) -> ::n0_error::AnyError { ::n0_error::AnyError::from_stack(value) } } - impl ::std::fmt::Display for #enum_ident #generics { + impl #impl_generics ::std::fmt::Display for #enum_ident #ty_generics #where_clause { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { use ::n0_error::{SourceFormat, StackError}; let sources = f.alternate().then_some(SourceFormat::OneLine); @@ -593,7 +593,7 @@ fn generate_enum_impls( } } - impl ::std::fmt::Debug for #enum_ident #generics { + impl #impl_generics ::std::fmt::Debug for #enum_ident #ty_generics #where_clause { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { use ::n0_error::StackErrorExt; if f.alternate() { @@ -608,7 +608,7 @@ fn generate_enum_impls( } } - impl ::std::error::Error for #enum_ident #generics { + impl #impl_generics ::std::error::Error for #enum_ident #ty_generics #where_clause { fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> { match self { #( #match_std_source_arms )* @@ -807,7 +807,7 @@ fn generate_struct_impl( #constructor } - impl ::n0_error::StackError for #item_ident #generics { + impl #impl_generics ::n0_error::StackError for #item_ident #ty_generics #where_clause { fn as_std(&self) -> &(dyn ::std::error::Error + ::std::marker::Send + ::std::marker::Sync + 'static) { self } @@ -832,13 +832,13 @@ fn generate_struct_impl( } - impl #impl_generics ::core::convert::From<#item_ident> for ::n0_error::AnyError #ty_generics #where_clause { - fn from(value: #item_ident) -> ::n0_error::AnyError { + impl #impl_generics ::core::convert::From<#item_ident #ty_generics> for ::n0_error::AnyError #where_clause { + fn from(value: #item_ident #ty_generics) -> ::n0_error::AnyError { ::n0_error::AnyError::from_stack(value) } } - impl ::std::fmt::Display for #item_ident #generics { + impl #impl_generics ::std::fmt::Display for #item_ident #ty_generics #where_clause { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { use ::n0_error::{SourceFormat, StackError}; let sources = f.alternate().then_some(SourceFormat::OneLine); @@ -846,7 +846,7 @@ fn generate_struct_impl( } } - impl ::std::fmt::Debug for #item_ident #generics { + impl #impl_generics ::std::fmt::Debug for #item_ident #ty_generics #where_clause { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { if f.alternate() { #get_debug @@ -858,7 +858,7 @@ fn generate_struct_impl( } } - impl ::std::error::Error for #item_ident #generics { + impl #impl_generics ::std::error::Error for #item_ident #ty_generics #where_clause { fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> { #get_std_source } diff --git a/src/tests.rs b/src/tests.rs index 29c8b5f..156da95 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -482,3 +482,33 @@ pub fn test_skip_transparent_errors() { println!("#### no bt, not transparent, display"); println!("{}", err_a(false).unwrap_err()); } + +#[test] +fn test_generics() { + #[add_meta] + #[derive(Error)] + #[display("failed at {}", list.iter().map(|e| e.to_string()).collect::>().join(", "))] + struct GenericError { + list: Vec, + } + + #[add_meta] + #[derive(Error)] + enum GenericEnumError { + Foo, + Bar { + list: Vec, + }, + #[display("failed at {other}")] + Baz { + other: Box, + }, + } + + let err = GenericError::new(vec!["foo", "bar"]); + assert_eq!(format!("{err}"), "failed at foo, bar"); + let err = e!(GenericEnumError::Baz { + other: Box::new("foo") + }); + assert_eq!(format!("{err}"), "failed at foo"); +}