From 13e2536a43bc9feb13f74f8107b3745ca4ee34c3 Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 27 Oct 2025 23:56:05 +0100 Subject: [PATCH 1/6] wip: refactor macros --- n0-error-macros/src/lib.rs | 308 ++++++++++++++++++++++--------------- src/lib.rs | 2 +- src/meta.rs | 1 + 3 files changed, 188 insertions(+), 123 deletions(-) diff --git a/n0-error-macros/src/lib.rs b/n0-error-macros/src/lib.rs index 61d7764..8a73a05 100644 --- a/n0-error-macros/src/lib.rs +++ b/n0-error-macros/src/lib.rs @@ -7,6 +7,62 @@ use syn::{ Fields, FieldsNamed, Ident, }; +#[proc_macro_attribute] +pub fn stack_error(args: TokenStream, item: TokenStream) -> TokenStream { + match stack_error_inner(args, parse_macro_input!(item as syn::Item)) { + Err(err) => err.to_compile_error().into(), + Ok(tokens) => tokens.into(), + } +} + +fn stack_error_inner( + args: TokenStream, + mut input: syn::Item, +) -> Result { + let args: StackErrAttrArgs = syn::parse(args.clone())?; + match &mut input { + syn::Item::Enum(item) => { + if args.add_meta { + for variant in item.variants.iter_mut() { + add_meta_field(&mut variant.fields)?; + } + } + modify_attrs(&args, &mut item.attrs)?; + Ok(quote! { #item }) + } + syn::Item::Struct(item) => { + if args.add_meta { + add_meta_field(&mut item.fields)?; + } + modify_attrs(&args, &mut item.attrs)?; + Ok(quote! { #item }) + } + _ => Err(err( + &input, + "#[stack_error] only supports enums and structs", + )), + } +} + +fn modify_attrs(args: &StackErrAttrArgs, attrs: &mut Vec) -> Result<(), syn::Error> { + attrs.retain(|attr| !attr.path().is_ident("stackerr")); + if args.derive { + attrs.insert(0, parse_quote!(#[derive(::n0_error::StackError)])); + } + let error_args: Vec<_> = [ + args.from_sources.then(|| quote!(from_sources)), + args.std_sources.then(|| quote!(then_sources)), + ] + .into_iter() + .flatten() + .collect(); + if !error_args.is_empty() { + attrs.push(parse_quote!(#[error(#(#error_args),*)])) + } + + Ok(()) +} + /// Attribute macro that adds a `meta: ::n0_error::Meta` field to a struct or /// to all named-field variants of an enum. Does nothing else. /// @@ -82,7 +138,7 @@ fn add_meta_field(fields: &mut Fields) -> Result<(), syn::Error> { /// - `#[error(from)]`: Creates a `From` impl for the field's type to the error type. /// - `#[error(std_err)]`: Marks the error as a `std` error. Without this attribute, errors are expected to implement `StackError`. Only applicable to source fields. #[proc_macro_derive(StackError, attributes(error))] -pub fn derive_error(input: TokenStream) -> TokenStream { +pub fn derive_stack_error(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as syn::DeriveInput); match derive_error_inner(input) { Err(tokens) => tokens.write_errors().into(), @@ -93,7 +149,7 @@ pub fn derive_error(input: TokenStream) -> TokenStream { fn derive_error_inner(input: DeriveInput) -> Result { match &input.data { syn::Data::Enum(item_enum) => { - let top = TopAttrs::from_attributes(&input.attrs)?; + let top = EnumAttrArgs::from_attributes(&input.attrs)?; let infos = item_enum .variants .iter() @@ -102,7 +158,7 @@ fn derive_error_inner(input: DeriveInput) -> Result { - let top = TopAttrs::default(); + let top = EnumAttrArgs::default(); let info = VariantInfo::parse(&input.ident, &item.fields, &input.attrs, &top)?; Ok(generate_struct_impl(&input.ident, &input.generics, info)) } @@ -140,30 +196,37 @@ enum SourceKind { Std, } +#[derive(Default, Clone, Copy, darling::FromMeta)] +#[darling(default, derive_syn_parse)] +struct StackErrAttrArgs { + add_meta: bool, + derive: bool, + from_sources: bool, + std_sources: bool, +} + #[derive(Default, Clone, Copy, FromAttributes)] #[darling(default, attributes(error, stackerr))] -struct TopAttrs { +struct EnumAttrArgs { from_sources: bool, std_sources: bool, } #[derive(Default, Clone, Copy, FromAttributes)] #[darling(default, attributes(error))] -struct FieldAttrs { +struct FieldAttrArgs { source: bool, from: bool, std_err: bool, stack_err: bool, - /// Marks this field as the metadata field. meta: bool, } -// For each variant, capture doc comment text or #[error] attr struct VariantInfo<'a> { ident: Ident, fields: Vec>, kind: Kind, - display: Display, + display: DisplayArgs, source: Option>, /// The field that is used for From<..> impls from: Option>, @@ -174,37 +237,35 @@ struct VariantInfo<'a> { #[derive(Clone)] struct FieldInfo<'a> { field: &'a Field, - opts: FieldAttrs, + args: FieldAttrArgs, ident: FieldIdent<'a>, } impl<'a> FieldInfo<'a> { fn from_named(field: &'a Field) -> Result { - let opts = FieldAttrs::from_attributes(&field.attrs)?; - let ident = FieldIdent::Named(field.ident.as_ref().unwrap()); - Ok(Self { field, opts, ident }) + Ok(Self { + args: FieldAttrArgs::from_attributes(&field.attrs)?, + ident: FieldIdent::Named(field.ident.as_ref().unwrap()), + field, + }) } fn from_unnamed(index: usize, field: &'a Field) -> Result { - let opts = FieldAttrs::from_attributes(&field.attrs)?; - let ident = FieldIdent::Unnamed(index); - Ok(Self { field, opts, ident }) + Ok(Self { + args: FieldAttrArgs::from_attributes(&field.attrs)?, + ident: FieldIdent::Unnamed(index), + field, + }) } + fn is_meta(&self) -> bool { - if self.opts.meta { - return true; - } - if let FieldIdent::Named(ident) = self.ident { - if ident == "meta" { - return true; - } - } - false + self.args.meta || matches!(self.ident, FieldIdent::Named(ident) if ident == "meta") } } #[derive(Clone, Copy, PartialEq, Eq)] enum Kind { Named, + Unit, Tuple, } @@ -215,6 +276,12 @@ enum FieldIdent<'a> { } impl<'a> FieldIdent<'a> { + fn named(&self) -> Option<&'a Ident> { + match self { + FieldIdent::Named(ident) => Some(ident), + _ => None, + } + } fn self_expr(&self) -> proc_macro2::TokenStream { match self { FieldIdent::Named(ident) => { @@ -230,7 +297,7 @@ impl<'a> FieldIdent<'a> { impl<'a> VariantInfo<'a> { fn transparent(&self) -> Option<&FieldInfo<'_>> { match self.display { - Display::Transparent => self.source.as_ref().map(|s| &s.field), + DisplayArgs::Transparent => self.source.as_ref().map(|s| &s.field), _ => None, } } @@ -261,9 +328,9 @@ impl<'a> VariantInfo<'a> { ident: &Ident, fields: &'a Fields, attrs: &[Attribute], - top: &TopAttrs, + top: &EnumAttrArgs, ) -> Result, syn::Error> { - let display = get_display(&attrs)?; + let display = DisplayArgs::parse(&attrs)?; let (kind, fields): (Kind, Vec) = match fields { Fields::Named(ref fields) => ( Kind::Named, @@ -273,7 +340,7 @@ impl<'a> VariantInfo<'a> { .map(FieldInfo::from_named) .collect::>()?, ), - Fields::Unit => (Kind::Named, Vec::new()), + Fields::Unit => (Kind::Unit, Vec::new()), Fields::Unnamed(ref fields) => ( Kind::Tuple, fields @@ -285,7 +352,7 @@ impl<'a> VariantInfo<'a> { ), }; - if fields.iter().filter(|f| f.opts.source).count() > 1 { + if fields.iter().filter(|f| f.args.source).count() > 1 { return Err(err( ident, "Only one field per variant may have #[error(source)]", @@ -293,7 +360,7 @@ impl<'a> VariantInfo<'a> { } let source_field = fields .iter() - .find(|f| f.opts.source) + .find(|f| f.args.source) .or_else(|| match kind { Kind::Named => fields.iter().find(|f| match f.ident { FieldIdent::Named(name) => name == "source", @@ -306,6 +373,7 @@ impl<'a> VariantInfo<'a> { None } } + Kind::Unit => None, }); if display.is_transparent() && source_field.is_none() { @@ -319,7 +387,7 @@ impl<'a> VariantInfo<'a> { let source = match source_field.as_ref() { None => None, Some(field) => { - let kind = if field.opts.std_err || (top.std_sources && !field.opts.stack_err) { + let kind = if field.args.std_err || (top.std_sources && !field.args.stack_err) { SourceKind::Std } else { SourceKind::Stack @@ -331,7 +399,7 @@ impl<'a> VariantInfo<'a> { let from_field = fields .iter() - .find(|f| f.opts.from) + .find(|f| f.args.from) .or_else(|| top.from_sources.then(|| source_field).flatten()); let from_field = from_field.cloned(); let meta_field = fields.iter().find(|f| f.is_meta()).cloned(); @@ -354,64 +422,53 @@ impl<'a> VariantInfo<'a> { quote! { Self::#v_ident { #ident: #token, .. } } } (Kind::Tuple, FieldIdent::Unnamed(idx)) => { - let mut pats: Vec = Vec::with_capacity(self.fields.len()); - for i in 0..self.fields.len() { + let pats = (0..self.fields.len()).map(|i| { if i == idx { - pats.push(quote! { #token }); + quote! { #token } } else { - pats.push(quote! { _ }); + quote! { _ } } - } + }); quote! { Self::#v_ident ( #(#pats),* ) } } - (Kind::Named, FieldIdent::Unnamed(_)) | (Kind::Tuple, FieldIdent::Named(_)) => { - unreachable!() - } + _ => unreachable!("Invalid call to spread_field"), } } fn spread_empty(&self) -> proc_macro2::TokenStream { let v_ident = &self.ident; - if self.fields.is_empty() { - quote! { Self::#v_ident } - } else { - match self.kind { - Kind::Named => quote! { Self::#v_ident { .. } }, - Kind::Tuple => { - let pats = std::iter::repeat(quote! { _ }).take(self.fields.len()); - quote! { Self::#v_ident ( #(#pats),* ) } - } + match self.kind { + Kind::Unit => quote! { Self::#v_ident }, + Kind::Named => quote! { Self::#v_ident { .. } }, + Kind::Tuple => { + let pats = std::iter::repeat(quote! { _ }).take(self.fields.len()); + quote! { Self::#v_ident ( #(#pats),* ) } } } } fn spread_all(&self, binds: &[Ident]) -> proc_macro2::TokenStream { let v_ident = &self.ident; - if self.fields.is_empty() { - quote! { Self::#v_ident } - } else { - match self.kind { - Kind::Named => { - let pairs = self - .fields - .iter() - .zip(binds.iter()) - .map(|(f, b)| match f.ident { - FieldIdent::Named(id) => { - if *id == *b { - quote! { #id } - } else { - quote! { #id: #b } - } - } - FieldIdent::Unnamed(_) => unreachable!(), - }); - quote! { #[allow(unused)] Self::#v_ident { #( #pairs ),* } } - } - Kind::Tuple => { - quote! { #[allow(unused)] Self::#v_ident ( #( #binds ),* ) } - } + match self.kind { + Kind::Named => { + let pairs = self + .fields + .iter() + .flat_map(|f| f.ident.named()) + .zip(binds.iter()) + .map(|(ident, bind)| { + if *ident == *bind { + quote! { #ident } + } else { + quote! { #ident: #bind } + } + }); + quote! { #[allow(unused)] Self::#v_ident { #( #pairs ),* } } + } + Kind::Tuple => { + quote! { #[allow(unused)] Self::#v_ident ( #( #binds ),* ) } } + Kind::Unit => quote! { Self::#v_ident }, } } } @@ -466,12 +523,12 @@ fn generate_enum_impls( }); let match_fmt_message_arms = variants.iter().map(|vi| match &vi.display { - Display::Format(expr) => { + DisplayArgs::Format(expr) => { let binds: Vec = vi.field_binding_idents().collect(); let pat = vi.spread_all(&binds); quote! { #pat => { #expr } } } - Display::Default | Display::Transparent => { + DisplayArgs::Default | DisplayArgs::Transparent => { let text = format!("{}::{}", enum_ident, vi.ident); let pat = vi.spread_empty(); quote! { #pat => write!(f, #text) } @@ -632,6 +689,7 @@ fn generate_struct_impl( let comma = (info.has_meta() && info.fields.len() > 1).then(|| quote!(,)); let doc = format!("Creates a new [`{}`] error.", item_ident); let construct = match info.kind { + Kind::Unit => quote! { Self }, Kind::Named => quote! { Self { #(#names),* #comma #meta_named } }, Kind::Tuple => quote! { Self( #(#names),* #comma #meta_tuple ) }, }; @@ -671,7 +729,7 @@ fn generate_struct_impl( let get_std_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) => { @@ -693,9 +751,10 @@ fn generate_struct_impl( quote! { write!(f, "{}", #expr) } } else { match &info.display { - Display::Format(expr) => { + DisplayArgs::Format(expr) => { let binds: Vec = info.field_binding_idents().collect(); match info.kind { + Kind::Unit => quote! { #expr }, Kind::Named => { quote! { #[allow(unused)] let Self { #( #binds ),* , .. } = self; #expr } } @@ -704,7 +763,7 @@ fn generate_struct_impl( } } } - Display::Default | Display::Transparent => { + DisplayArgs::Default | DisplayArgs::Transparent => { // Fallback to struct name let text = info.ident.to_string(); quote! { write!(f, #text) } @@ -715,30 +774,32 @@ fn generate_struct_impl( let get_debug = { let item_name = info.ident.to_string(); - let binds: Vec = info.field_binding_idents().collect(); - let labels: Vec = info - .fields - .iter() - .enumerate() - .map(|(i, f)| match f.ident { - FieldIdent::Named(id) => id.to_string(), - FieldIdent::Unnamed(_) => i.to_string(), - }) - .collect(); match info.kind { + Kind::Unit => { + quote! { + write!(f, #item_name)?; + } + } Kind::Named => { + let fields = info + .fields + .iter() + .filter_map(|f| f.ident.named()) + .map(|ident| { + let ident_s = ident.to_string(); + quote! { dbg.field(#ident_s, &self.#ident) } + }); quote! { - let Self { #( #binds ),* } = self; let mut dbg = f.debug_struct(#item_name); - #( dbg.field(#labels, &#binds); )*; + #(#fields);*; dbg.finish()?; } } Kind::Tuple => { + let binds = (0..info.fields.len()).map(|i| syn::Index::from(i)); quote! { - let Self( #( #binds ),* ) = self; - let mut dbg = f.debug_struct(#item_name); - #( dbg.field(#labels, &#binds); )*; + let mut dbg = f.debug_tuple(#item_name); + #( dbg.field(&self.#binds); )*; dbg.finish()?; } } @@ -856,49 +917,48 @@ fn generate_struct_impl( } } -enum Display { +enum DisplayArgs { Default, Transparent, Format(proc_macro2::TokenStream), } -impl Display { +impl DisplayArgs { fn is_transparent(&self) -> bool { matches!(self, Self::Transparent) } -} -fn get_display(attrs: &[Attribute]) -> Result { - // Only consider #[error(...)] - let Some(attr) = attrs.iter().find(|a| a.path().is_ident("error")) else { - return Ok(Display::Default); - }; + fn parse(attrs: &[Attribute]) -> Result { + // Only consider #[error(...)] + let Some(attr) = attrs.iter().find(|a| a.path().is_ident("error")) else { + return Ok(DisplayArgs::Default); + }; - // syn 2: parse args inside the attribute's parentheses - let args: Punctuated = - attr.parse_args_with(Punctuated::::parse_terminated)?; + // syn 2: parse args inside the attribute's parentheses + let args: Punctuated = + attr.parse_args_with(Punctuated::::parse_terminated)?; - if args.is_empty() { - return Err(err( - attr, - "#[error(..)] requires arguments: a format string or `transparent`", - )); - } + if args.is_empty() { + return Err(err( + attr, + "#[error(..)] requires arguments: a format string or `transparent`", + )); + } - // #[error(transparent)] - if args.len() == 1 { - if let Expr::Path(p) = &args[0] { - if p.path.is_ident("transparent") { - return Ok(Display::Transparent); + // #[error(transparent)] + if args.len() == 1 { + if let Expr::Path(p) = &args[0] { + if p.path.is_ident("transparent") { + return Ok(DisplayArgs::Transparent); + } } } - } - // #[error("...", args...)] - let mut it = args.into_iter(); - let first = it.next().unwrap(); + // #[error("...", args...)] + let mut it = args.into_iter(); + let first = it.next().unwrap(); - let fmt_lit = match first { + let fmt_lit = match first { Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => s, other => { return Err(err( @@ -908,9 +968,13 @@ fn get_display(attrs: &[Attribute]) -> Result { } }; - let rest: Vec = it.collect(); - Ok(Display::Format(quote! { write!(f, #fmt_lit #(, #rest)* ) })) + let rest: Vec = it.collect(); + Ok(DisplayArgs::Format( + quote! { write!(f, #fmt_lit #(, #rest)* ) }, + )) + } } + fn err(ident: impl ToTokens, err: impl ToString) -> syn::Error { syn::Error::new_spanned(ident, err.to_string()) } diff --git a/src/lib.rs b/src/lib.rs index 6909ca7..4b4f4ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ #![doc = include_str!("../README.md")] #![deny(missing_docs)] -pub use n0_error_macros::{StackError, add_meta}; +pub use n0_error_macros::{StackError, add_meta, stack_error}; extern crate self as n0_error; diff --git a/src/meta.rs b/src/meta.rs index 6286175..9545d19 100644 --- a/src/meta.rs +++ b/src/meta.rs @@ -8,6 +8,7 @@ pub struct Location(&'static std::panic::Location<'static>); /// Captured metadata for an error creation site. /// /// Currently this only contains the call-site [`Location`]. +#[derive(Clone)] pub struct Meta { location: Option, } From 108bdf5f90d7722fbdc44cf43eaf3456a0cadbd3 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 28 Oct 2025 09:02:23 +0100 Subject: [PATCH 2/6] refactor: rename try to try_or, simplify macros --- src/macros.rs | 32 ++++++++++++++++++++++---------- src/tests.rs | 4 ++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index cb807bc..d110ae7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -31,7 +31,7 @@ macro_rules! e { /// 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 { +macro_rules! try_or { ($result:expr, $($tt:tt)*) => { match $result { ::core::result::Result::Ok(v) => v, @@ -47,7 +47,7 @@ macro_rules! try_e { /// 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 { +macro_rules! try_or_any { ($result:expr, $($context:tt)*) => { match $result { ::core::result::Result::Ok(v) => v, @@ -138,18 +138,10 @@ pub use spez as __spez; /// if given a value that impls `Display` it uses [`AnyError::from_display`] - in this order. #[macro_export] macro_rules! anyerr { - ($fmt:literal) => { - $crate::format_err!($fmt) - }; - ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { $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),*)*)) }; @@ -171,4 +163,24 @@ macro_rules! anyerr { } } }; + + ($($err:tt)::+) => { + $($err)::+ { meta: $crate::Meta::default() } + }; + + // Single expression: treat as source + ($($err:tt)::+ , $source:expr) => { + $($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)* } + }; + } diff --git a/src/tests.rs b/src/tests.rs index 1b51363..da2d587 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -35,12 +35,12 @@ fn test_whatever() { } fn fail_whatever() -> Result { - n0_error::try_any!(fail(), "sad"); + n0_error::try_or_any!(fail(), "sad"); Ok(()) } fn fail_whatever_my_error() -> Result { - n0_error::try_any!(fail_my_error(), "sad"); + n0_error::try_or_any!(fail_my_error(), "sad"); Ok(()) } From faddd3fc573a09401a6505a89daad4d3992842cc Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 28 Oct 2025 11:05:47 +0100 Subject: [PATCH 3/6] refactor: improve macro, use stack_error --- README.md | 56 ++-- examples/simple.rs | 23 +- n0-error-macros/src/lib.rs | 647 +++++++++++++++---------------------- src/ext.rs | 8 +- src/lib.rs | 2 +- src/macros.rs | 26 +- src/tests.rs | 38 +-- 7 files changed, 318 insertions(+), 482 deletions(-) diff --git a/README.md b/README.md index 798ba83..4efc562 100644 --- a/README.md +++ b/README.md @@ -5,27 +5,28 @@ A error library that supports tracking the call-site location of errors. Also features an anyhow-style `AnyError`. ```rust -use n0_error::{e, add_meta, StackError, Result, StackResultExt, StdResultExt}; +use n0_error::{err, stack_error, StackError, Result, StackResultExt, StdResultExt}; -// Adds a `meta` field to all variants to track call-site error location. -#[add_meta] -// Derives the various impls for our error. -#[derive(StackError)] -// Automatically create From impls from the error sources -#[error(from_sources)] +/// The `stack_error` macro controls how to turn our enum into a `StackError`. +/// +/// * `add_meta` adds a field to all variants to track the call-site error location +/// * `derive` adds `#[derive(StackError)]` +/// * `from_sources` creates `From` impls for the error sources +#[stack_error(derive, add_meta, from_sources)] enum MyError { - // A custom validation error - #[error("bad input: {count}")] + /// We can define the error message with the `error` attribute + #[error("bad input ({count})")] BadInput { count: usize }, - // Wrap a std::io::Error as a source (std error) + /// Or we can define a variant as `transparent`, which forwards the Display impl to the error source #[error("IO error")] Io { + /// For sources that do not implement `StackError`, we have to mark te source as `std_err`. #[error(std_err)] source: std::io::Error, }, } -// A function that returns a std io::Error +// A function that returns a std::io::Error fn fail_io() -> std::io::Result<()> { Err(std::io::Error::other("io failed")) } @@ -33,13 +34,14 @@ fn fail_io() -> std::io::Result<()> { // An outer function returning our custom MyError fn some_fn(count: usize) -> Result<(), MyError> { if count == 13 { - return Err(e!(MyError::BadInput { count })); + // The `err` macro constructs a `StackError` while automatically adding the `meta` field. + return Err(err!(MyError::BadInput { count })); } - // We have a From impl for std::io::Error on our error. + // We have a `From` impl for `std::io::Error` on our error. fail_io()?; // Without the From impl, we'd need to forward the error manually. - // The `e` macro can assist here, so that we don't have to declare the `meta` field manually. - fail_io().map_err(|source| e!(MyError::Io, source))?; + // The `err` macro can assist here, so that we don't have to declare the `meta` field manually. + fail_io().map_err(|source| err!(MyError::Io, source))?; Ok(()) } @@ -54,27 +56,21 @@ fn run() -> Result<()> { } fn main() -> Result<()> { - let res = run(); - if let Err(err) = run() { - println!("{err}"); - // This prints: - // Error: failed at some_fn - // Caused by: - // 0: bad input: 0 - } + let err = run().unwrap_err(); + assert_eq!( + format!("{err:#}"), + "failed at some_fn: bad input (13)" + ); Ok(()) } -// You can also use the macros with tuple structs or enums. -// In this case the meta field will be added as the last field. - -#[add_meta] -#[derive(StackError)] +/// You can also use the macros with tuple structs or enums. +/// In this case the meta field will be added as the last field. +#[stack_error(derive, add_meta)] #[error("tuple fail ({_0})")] struct TupleStruct(u32); -#[add_meta] -#[derive(StackError)] +#[stack_error(derive, add_meta)] enum TupleEnum { #[error("io failed")] Io(#[error(source, std_err)] std::io::Error), diff --git a/examples/simple.rs b/examples/simple.rs index 8d620cd..cdeca27 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,6 +1,6 @@ use std::io; -use n0_error::{StackError, e, meta}; +use n0_error::{StackError, err, meta}; use self::error::CopyError; use crate::error::{InvalidArgsError, OperationError}; @@ -19,17 +19,17 @@ fn main() { print(err); println!("### InvalidArgs"); - let err = e!(InvalidArgsError::FailedToParse); - let err = e!(CopyError::InvalidArgs, err); + let err = err!(InvalidArgsError::FailedToParse); + let err = err!(CopyError::InvalidArgs, err); // let err = e!(CopyError::InvalidArgs { source: err }); // let err = CopyError!(InvalidArgs { source: err }); - let err = e!(OperationError::Copy { source: err }); + let err = err!(OperationError::Copy { source: err }); print(err); } fn _some_fn() -> Result<(), CopyError> { // Err! macro works like e! but wraps in Err - Err(e!(CopyError::Read, io::Error::other("yada"))) + Err(err!(CopyError::Read, io::Error::other("yada"))) } fn operation() -> Result<(), OperationError> { @@ -39,7 +39,7 @@ fn operation() -> Result<(), OperationError> { } fn copy() -> Result<(), CopyError> { - read().map_err(|err| e!(CopyError::Read { source: err }))?; + read().map_err(|err| err!(CopyError::Read { source: err }))?; Ok(()) } @@ -62,19 +62,16 @@ fn print(err: impl StackError) { } pub mod error { - use n0_error::{StackError, add_meta}; + use n0_error::{StackError, stack_error}; use std::io; - #[add_meta] - #[derive(StackError)] - #[error(from_sources)] + #[stack_error(derive, add_meta, from_sources)] pub enum OperationError { /// Failed to copy Copy { source: CopyError }, } - #[add_meta] - #[derive(StackError)] + #[stack_error(derive, add_meta)] pub enum CopyError { /// Read error Read { @@ -102,7 +99,7 @@ pub mod error { Foo, } - #[add_meta] + #[stack_error(add_meta)] #[derive(StackError)] pub enum InvalidArgsError { /// Failed to parse arguments diff --git a/n0-error-macros/src/lib.rs b/n0-error-macros/src/lib.rs index 8a73a05..750b330 100644 --- a/n0-error-macros/src/lib.rs +++ b/n0-error-macros/src/lib.rs @@ -24,7 +24,7 @@ fn stack_error_inner( syn::Item::Enum(item) => { if args.add_meta { for variant in item.variants.iter_mut() { - add_meta_field(&mut variant.fields)?; + add_meta_field(&mut variant.fields); } } modify_attrs(&args, &mut item.attrs)?; @@ -32,7 +32,7 @@ fn stack_error_inner( } syn::Item::Struct(item) => { if args.add_meta { - add_meta_field(&mut item.fields)?; + add_meta_field(&mut item.fields); } modify_attrs(&args, &mut item.attrs)?; Ok(quote! { #item }) @@ -63,62 +63,25 @@ fn modify_attrs(args: &StackErrAttrArgs, attrs: &mut Vec) -> Result<( Ok(()) } -/// Attribute macro that adds a `meta: ::n0_error::Meta` field to a struct or -/// to all named-field variants of an enum. Does nothing else. -/// -/// If the struct or an enum variant is currently a unit kind, it will be converted -/// to a named-field kind. -/// -/// Tuple structs or variants are supported: for tuple items, the `meta` -/// field is appended as the last unnamed field. -#[proc_macro_attribute] -pub fn add_meta(_attr: TokenStream, item: TokenStream) -> TokenStream { - match add_meta_inner(parse_macro_input!(item as syn::Item)) { - Err(err) => err.to_compile_error().into(), - Ok(tokens) => tokens.into(), - } -} - -fn add_meta_inner(mut input: syn::Item) -> Result { - match &mut input { - syn::Item::Enum(item_enum) => { - for variant in item_enum.variants.iter_mut() { - add_meta_field(&mut variant.fields)?; - } - Ok(quote! { #item_enum }) - } - syn::Item::Struct(item_struct) => { - add_meta_field(&mut item_struct.fields)?; - Ok(quote! { #item_struct }) - } - _ => Err(err(&input, "#[add_meta] only supports enums and structs")), - } -} - -fn add_meta_field(fields: &mut Fields) -> Result<(), syn::Error> { +fn add_meta_field(fields: &mut Fields) { + let doc = "Captured call-site metadata"; match fields { Fields::Named(fields) => { - let field: syn::Field = - parse_quote! { #[doc = "Captured call-site metadata"] meta: ::n0_error::Meta }; + let field: syn::Field = parse_quote! { #[doc = #doc] meta: ::n0_error::Meta }; fields.named.push(field); - Ok(()) } Fields::Unit => { let mut named = FieldsNamed { brace_token: Default::default(), named: Default::default(), }; - let field: syn::Field = - parse_quote! { #[doc = "Captured call-site metadata"] meta: ::n0_error::Meta }; + let field: syn::Field = parse_quote! { #[doc = #doc] meta: ::n0_error::Meta }; named.named.push(field); *fields = Fields::Named(named); - Ok(()) } Fields::Unnamed(fields) => { - // Append meta as last unnamed field for tuple items and mark it explicitly - let field: syn::Field = parse_quote! { #[doc = "Captured call-site metadata"] #[error(meta)] ::n0_error::Meta }; + let field: syn::Field = parse_quote! { #[doc = #doc] #[error(meta)] ::n0_error::Meta }; fields.unnamed.push(field); - Ok(()) } } } @@ -148,18 +111,21 @@ pub fn derive_stack_error(input: TokenStream) -> TokenStream { fn derive_error_inner(input: DeriveInput) -> Result { match &input.data { - syn::Data::Enum(item_enum) => { - let top = EnumAttrArgs::from_attributes(&input.attrs)?; - let infos = item_enum + syn::Data::Enum(item) => { + let args = EnumAttrArgs::from_attributes(&input.attrs)?; + let infos = item .variants .iter() - .map(|v| VariantInfo::parse(&v.ident, &v.fields, &v.attrs, &top)) + .map(|v| { + let ident = VariantIdent::Variant(&input.ident, &v.ident); + VariantInfo::parse(ident, &v.fields, &v.attrs, &args) + }) .collect::, _>>()?; Ok(generate_enum_impls(&input.ident, &input.generics, infos)) } syn::Data::Struct(item) => { - let top = EnumAttrArgs::default(); - let info = VariantInfo::parse(&input.ident, &item.fields, &input.attrs, &top)?; + let ident = VariantIdent::Struct(&input.ident); + let info = VariantInfo::parse(ident, &item.fields, &input.attrs, &Default::default())?; Ok(generate_struct_impl(&input.ident, &input.generics, info)) } _ => Err(err( @@ -176,17 +142,17 @@ struct SourceField<'a> { } impl<'a> SourceField<'a> { - fn expr_error_ref(&self, ident: &Ident) -> proc_macro2::TokenStream { + fn expr_error_ref(&self, expr: proc_macro2::TokenStream) -> proc_macro2::TokenStream { match self.kind { - SourceKind::Std => quote! { Some(::n0_error::ErrorRef::std(#ident)) }, - SourceKind::Stack => quote! { Some(::n0_error::ErrorRef::stack(#ident)) }, + SourceKind::Std => quote! { Some(::n0_error::ErrorRef::std(#expr)) }, + SourceKind::Stack => quote! { Some(::n0_error::ErrorRef::stack(#expr)) }, } } - fn expr_error_std(&self, ident: &Ident) -> proc_macro2::TokenStream { + fn expr_error_std(&self, expr: proc_macro2::TokenStream) -> proc_macro2::TokenStream { match self.kind { - SourceKind::Std => quote! { Some(#ident as &dyn ::std::error::Error) }, - SourceKind::Stack => quote! { Some(::n0_error::StackError::as_std(#ident)) }, + SourceKind::Std => quote! { Some(#expr as &dyn ::std::error::Error) }, + SourceKind::Stack => quote! { Some(::n0_error::StackError::as_std(#expr)) }, } } } @@ -222,43 +188,31 @@ struct FieldAttrArgs { meta: bool, } -struct VariantInfo<'a> { - ident: Ident, - fields: Vec>, - kind: Kind, - display: DisplayArgs, - source: Option>, - /// The field that is used for From<..> impls - from: Option>, - /// The meta field if present. - meta: Option>, -} - -#[derive(Clone)] -struct FieldInfo<'a> { - field: &'a Field, - args: FieldAttrArgs, - ident: FieldIdent<'a>, +#[derive(Debug, Clone, Copy)] +enum VariantIdent<'a> { + Struct(&'a Ident), + Variant(&'a Ident, &'a Ident), } -impl<'a> FieldInfo<'a> { - fn from_named(field: &'a Field) -> Result { - Ok(Self { - args: FieldAttrArgs::from_attributes(&field.attrs)?, - ident: FieldIdent::Named(field.ident.as_ref().unwrap()), - field, - }) +impl<'a> VariantIdent<'a> { + fn self_ident(&self) -> proc_macro2::TokenStream { + match self { + VariantIdent::Struct(_) => quote!(Self), + VariantIdent::Variant(_, variant) => quote!(Self::#variant), + } } - fn from_unnamed(index: usize, field: &'a Field) -> Result { - Ok(Self { - args: FieldAttrArgs::from_attributes(&field.attrs)?, - ident: FieldIdent::Unnamed(index), - field, - }) + fn item_ident(&self) -> &Ident { + match self { + VariantIdent::Struct(ident) => &ident, + VariantIdent::Variant(ident, _) => &ident, + } } - fn is_meta(&self) -> bool { - self.args.meta || matches!(self.ident, FieldIdent::Named(ident) if ident == "meta") + fn inner(&self) -> &Ident { + match self { + VariantIdent::Struct(ident) => &ident, + VariantIdent::Variant(_, ident) => &ident, + } } } @@ -269,66 +223,24 @@ enum Kind { Tuple, } -#[derive(Clone, Copy)] -enum FieldIdent<'a> { - Named(&'a Ident), - Unnamed(usize), -} - -impl<'a> FieldIdent<'a> { - fn named(&self) -> Option<&'a Ident> { - match self { - FieldIdent::Named(ident) => Some(ident), - _ => None, - } - } - fn self_expr(&self) -> proc_macro2::TokenStream { - match self { - FieldIdent::Named(ident) => { - quote!(self.#ident) - } - FieldIdent::Unnamed(num) => { - quote!(self.#num) - } - } - } +struct VariantInfo<'a> { + ident: VariantIdent<'a>, + fields: Vec>, + kind: Kind, + display: DisplayArgs, + source: Option>, + /// The field that is used for From<..> impls + from: Option>, + /// The meta field if present. + meta: Option>, } impl<'a> VariantInfo<'a> { - fn transparent(&self) -> Option<&FieldInfo<'_>> { - match self.display { - DisplayArgs::Transparent => self.source.as_ref().map(|s| &s.field), - _ => None, - } - } - - 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), Span::call_site()), - }) - } - - fn has_meta(&self) -> bool { - self.meta.is_some() - } - - fn fields_without_meta(&self) -> impl Iterator> { - self.fields.iter().filter(|f| !f.is_meta()) - } - - 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), Span::call_site()), - }) - } - fn parse( - ident: &Ident, + ident: VariantIdent<'a>, fields: &'a Fields, attrs: &[Attribute], - top: &EnumAttrArgs, + args: &EnumAttrArgs, ) -> Result, syn::Error> { let display = DisplayArgs::parse(&attrs)?; let (kind, fields): (Kind, Vec) = match fields { @@ -354,7 +266,7 @@ impl<'a> VariantInfo<'a> { if fields.iter().filter(|f| f.args.source).count() > 1 { return Err(err( - ident, + ident.inner(), "Only one field per variant may have #[error(source)]", )); } @@ -362,113 +274,185 @@ impl<'a> VariantInfo<'a> { .iter() .find(|f| f.args.source) .or_else(|| match kind { - Kind::Named => fields.iter().find(|f| match f.ident { - FieldIdent::Named(name) => name == "source", - _ => false, - }), - Kind::Tuple => { - if display.is_transparent() { - fields.first() - } else { - None - } - } - Kind::Unit => None, + Kind::Named => fields + .iter() + .find(|f| matches!(f.ident, FieldIdent::Named(name) if name == "source")), + Kind::Tuple if display.is_transparent() => fields.first(), + _ => None, }); if display.is_transparent() && source_field.is_none() { return Err(err( - ident, + ident.inner(), "Variants with #[error(transparent)] require a source field", )); } // Determine source kind and optional From based on field options and top-level switches - let source = match source_field.as_ref() { - None => None, - Some(field) => { - let kind = if field.args.std_err || (top.std_sources && !field.args.stack_err) { - SourceKind::Std - } else { - SourceKind::Stack - }; - let field = (*field).clone(); - Some(SourceField { kind, field }) + let source = source_field.as_ref().map(|field| { + let kind = if field.args.std_err || (args.std_sources && !field.args.stack_err) { + SourceKind::Std + } else { + SourceKind::Stack + }; + SourceField { + kind, + field: (*field).clone(), } - }; + }); let from_field = fields .iter() .find(|f| f.args.from) - .or_else(|| top.from_sources.then(|| source_field).flatten()); - let from_field = from_field.cloned(); + .or_else(|| args.from_sources.then(|| source_field).flatten()); let meta_field = fields.iter().find(|f| f.is_meta()).cloned(); Ok(VariantInfo { ident: ident.clone(), - fields, kind, display, - source, - from: from_field, + from: from_field.cloned(), meta: meta_field, + source, + fields, }) } - // Pattern helpers - fn spread_field(&self, field: &FieldInfo<'a>, token: &Ident) -> proc_macro2::TokenStream { - let v_ident = &self.ident; - match (self.kind, field.ident) { - (Kind::Named, FieldIdent::Named(ident)) => { - quote! { Self::#v_ident { #ident: #token, .. } } - } - (Kind::Tuple, FieldIdent::Unnamed(idx)) => { - let pats = (0..self.fields.len()).map(|i| { - if i == idx { - quote! { #token } + fn transparent(&self) -> Option<&FieldInfo<'_>> { + match self.display { + DisplayArgs::Transparent => self.source.as_ref().map(|s| &s.field), + _ => None, + } + } + + fn spread_field(&self, field: &FieldInfo<'a>, bind: &Ident) -> proc_macro2::TokenStream { + let slf = self.ident.self_ident(); + match field.ident { + FieldIdent::Named(ident) => { + quote! { #slf { #ident: #bind, .. } } + } + FieldIdent::Unnamed(_) => { + let pats = self.fields.iter().map(|f| { + if f.ident == field.ident { + quote!(#bind) } else { - quote! { _ } + quote!(_) } }); - quote! { Self::#v_ident ( #(#pats),* ) } + quote! { #slf ( #(#pats),* ) } } - _ => unreachable!("Invalid call to spread_field"), } } fn spread_empty(&self) -> proc_macro2::TokenStream { - let v_ident = &self.ident; + let slf = self.ident.self_ident(); match self.kind { - Kind::Unit => quote! { Self::#v_ident }, - Kind::Named => quote! { Self::#v_ident { .. } }, + Kind::Unit => quote! { #slf }, + Kind::Named => quote! { #slf { .. } }, Kind::Tuple => { let pats = std::iter::repeat(quote! { _ }).take(self.fields.len()); - quote! { Self::#v_ident ( #(#pats),* ) } + quote! { #slf ( #(#pats),* ) } } } } - fn spread_all(&self, binds: &[Ident]) -> proc_macro2::TokenStream { - let v_ident = &self.ident; + fn spread_all(&self) -> proc_macro2::TokenStream { + let binds = self.fields.iter().map(|f| f.ident.as_ident()); + self.spread(binds.map(|i| quote!(#i))) + } + + fn spread( + &self, + fields: impl Iterator, + ) -> proc_macro2::TokenStream { + let slf = self.ident.self_ident(); match self.kind { - Kind::Named => { - let pairs = self - .fields - .iter() - .flat_map(|f| f.ident.named()) - .zip(binds.iter()) - .map(|(ident, bind)| { - if *ident == *bind { - quote! { #ident } - } else { - quote! { #ident: #bind } - } - }); - quote! { #[allow(unused)] Self::#v_ident { #( #pairs ),* } } - } - Kind::Tuple => { - quote! { #[allow(unused)] Self::#v_ident ( #( #binds ),* ) } + Kind::Unit => quote! { #slf }, + Kind::Named => quote! { #slf { #(#fields),* } }, + Kind::Tuple => quote! { #slf ( #(#fields),* ) }, + } + } + + fn from_impl(&self) -> Option<(&syn::Type, proc_macro2::TokenStream)> { + self.from.as_ref().map(|from_field| { + let ty = &from_field.field.ty; + let fields = self.fields.iter().map(|field| { + if field.ident == from_field.ident { + field.ident.init(quote!(source)) + } else if field.is_meta() { + field.ident.init(quote!(::n0_error::Meta::new())) + } else { + field.ident.init(quote!(::std::default::Default::default())) + } + }); + let construct = self.spread(fields); + (ty, construct) + }) + } +} + +#[derive(Clone)] +struct FieldInfo<'a> { + field: &'a Field, + args: FieldAttrArgs, + ident: FieldIdent<'a>, +} + +impl<'a> FieldInfo<'a> { + fn from_named(field: &'a Field) -> Result { + Ok(Self { + args: FieldAttrArgs::from_attributes(&field.attrs)?, + ident: FieldIdent::Named(field.ident.as_ref().unwrap()), + field, + }) + } + fn from_unnamed(index: usize, field: &'a Field) -> Result { + Ok(Self { + args: FieldAttrArgs::from_attributes(&field.attrs)?, + ident: FieldIdent::Unnamed(index), + field, + }) + } + + fn is_meta(&self) -> bool { + self.args.meta || matches!(self.ident, FieldIdent::Named(ident) if ident == "meta") + } +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum FieldIdent<'a> { + Named(&'a Ident), + Unnamed(usize), +} + +impl<'a> FieldIdent<'a> { + fn named(&self) -> Option<&'a Ident> { + match self { + FieldIdent::Named(ident) => Some(ident), + _ => None, + } + } + + fn as_ident(&self) -> Ident { + match self { + FieldIdent::Named(ident) => (*ident).clone(), + FieldIdent::Unnamed(i) => syn::Ident::new(&format!("_{}", i), Span::call_site()), + } + } + + fn self_expr(&self) -> proc_macro2::TokenStream { + match self { + FieldIdent::Named(ident) => quote!(self.#ident), + FieldIdent::Unnamed(i) => { + let idx = syn::Index::from(*i); + quote!(self.#idx) } - Kind::Unit => quote! { Self::#v_ident }, + } + } + + fn init(&self, expr: proc_macro2::TokenStream) -> proc_macro2::TokenStream { + match self { + FieldIdent::Named(ident) => quote!(#ident: #expr), + FieldIdent::Unnamed(_) => quote!(#expr), } } } @@ -478,7 +462,6 @@ fn generate_enum_impls( generics: &syn::Generics, variants: Vec, ) -> proc_macro2::TokenStream { - // StackError impl pieces let match_meta_arms = variants.iter().map(|vi| { if let Some(meta_field) = &vi.meta { let bind = syn::Ident::new("__meta", Span::call_site()); @@ -492,9 +475,9 @@ fn generate_enum_impls( let match_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_ref(&bind); + let expr = src.expr_error_ref(quote!(#bind)); quote! { #pat => #expr, } } None => { @@ -505,9 +488,9 @@ fn generate_enum_impls( let match_std_source_arms = variants.iter().map(|vi| match &vi.source { Some(src) => { - let bind = syn::Ident::new("source", 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); + let expr = src.expr_error_std(quote!(#bind)); quote! { #pat => #expr, } } None => { @@ -524,21 +507,23 @@ fn generate_enum_impls( let match_fmt_message_arms = variants.iter().map(|vi| match &vi.display { DisplayArgs::Format(expr) => { - let binds: Vec = vi.field_binding_idents().collect(); - let pat = vi.spread_all(&binds); - quote! { #pat => { #expr } } + let pat = vi.spread_all(); + quote! { + #[allow(unused)] + #pat => { #expr } + } } DisplayArgs::Default | DisplayArgs::Transparent => { - let text = format!("{}::{}", enum_ident, vi.ident); + let text = format!("{}::{}", vi.ident.item_ident(), vi.ident.inner()); let pat = vi.spread_empty(); quote! { #pat => write!(f, #text) } } }); let match_debug_arms = variants.iter().map(|vi| { - let v_name = vi.ident.to_string(); - let binds: Vec = vi.field_binding_idents().collect(); - let pat = vi.spread_all(&binds); + let v_name = vi.ident.inner().to_string(); + let binds = vi.fields.iter().map(|f| f.ident.as_ident()); + let pat = vi.spread_all(); let labels: Vec = vi .fields .iter() @@ -557,35 +542,13 @@ fn generate_enum_impls( // 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; - let ty = &field.field.ty; - let construct = match (vi.kind, field.ident) { - (Kind::Named, FieldIdent::Named(ident)) => { - let meta = vi.meta.as_ref().map(|_| quote!( meta: ::n0_error::Meta::new() )); - let comma = (meta.is_some() && vi.fields.len() > 1).then(|| quote!(,)); - quote! { Self::#v_ident { #ident: source #comma #meta } } - } - (Kind::Tuple, FieldIdent::Unnamed(src_idx)) => { - let total = vi.fields.len(); - let mut exprs: Vec = Vec::with_capacity(total); - for i in 0..total { - if i == src_idx { exprs.push(quote!{ source }); } - else if let Some(meta_field) = &vi.meta { - if let FieldIdent::Unnamed(mi) = meta_field.ident { if mi == i { exprs.push(quote!{ ::n0_error::Meta::new() }); continue; } } - exprs.push(quote!{ ::core::compile_error!("unsupported From impl: extra fields present") }); - } else { - exprs.push(quote!{ ::core::compile_error!("unsupported From impl: extra fields present") }); - } - } - quote! { Self::#v_ident( #(#exprs),* ) } - } - _ => unreachable!(), - }; + let from_impls = variants.iter().filter_map(|vi| vi.from_impl()).map(|(ty, construct)| { quote! { impl #impl_generics ::core::convert::From<#ty> for #enum_ident #ty_generics #where_clause { #[track_caller] - fn from(source: #ty) -> Self { #construct } + fn from(source: #ty) -> Self { + #construct + } } } }); @@ -599,19 +562,16 @@ fn generate_enum_impls( fn as_dyn(&self) -> &(dyn ::n0_error::StackError) { 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, )* } } - fn source(&self) -> Option<::n0_error::ErrorRef<'_>> { match self { #( #match_source_arms )* @@ -632,22 +592,19 @@ fn generate_enum_impls( 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); - write!(f, "{}", self.report().sources(sources)) + let sources = f.alternate().then_some(::n0_error::SourceFormat::OneLine); + write!(f, "{}", ::n0_error::StackError::report(self).sources(sources)) } } 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() { match self { #(#match_debug_arms)* } } else { - use ::n0_error::StackError; - write!(f, "{}", self.report().full())?; + write!(f, "{}", ::n0_error::StackError::report(self).full())?; } Ok(()) } @@ -670,116 +627,67 @@ fn generate_struct_impl( info: VariantInfo, ) -> proc_macro2::TokenStream { let constructor = { - let params = info.fields_without_meta().map(|f| { + let params = info.fields.iter().filter(|f| !f.is_meta()).map(|f| { let ty = &f.field.ty; - match f.ident { - FieldIdent::Named(ident) => quote! { #ident: #ty }, - FieldIdent::Unnamed(i) => { - let arg = syn::Ident::new(&format!("_{}", i), Span::call_site()); - quote! { #arg: #ty } - } + let ident = f.ident.as_ident(); + quote! { #ident: #ty } + }); + let fields = info.fields.iter().map(|f| { + if f.is_meta() { + f.ident.init(quote!(::n0_error::Meta::new())) + } else { + let ident = f.ident.as_ident(); + quote!(#ident) } }); - let names = info.field_idents_without_meta(); - let meta_named = info - .meta - .as_ref() - .map(|_| quote!(meta: ::n0_error::Meta::new())); - let meta_tuple = info.meta.as_ref().map(|_| quote!(::n0_error::Meta::new())); - let comma = (info.has_meta() && info.fields.len() > 1).then(|| quote!(,)); + let construct = info.spread(fields); let doc = format!("Creates a new [`{}`] error.", item_ident); - let construct = match info.kind { - Kind::Unit => quote! { Self }, - Kind::Named => quote! { Self { #(#names),* #comma #meta_named } }, - Kind::Tuple => quote! { Self( #(#names),* #comma #meta_tuple ) }, - }; quote! { #[doc = #doc] #[track_caller] pub fn new(#(#params),*) -> Self { #construct } } }; - let get_meta = if let Some(meta_field) = &info.meta { - match meta_field.ident { - FieldIdent::Named(ident) => quote!(Some(&self.#ident)), - FieldIdent::Unnamed(i) => { - let idx = syn::Index::from(i); - quote!(Some(&self.#idx)) - } - } + let get_meta = if let Some(field) = &info.meta { + let get_expr = field.ident.self_expr(); + quote! { Some(&#get_expr) } } else { quote! { None } }; let get_error_source = match &info.source { - Some(src) => { - 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) => { - let idx = syn::Index::from(i); - quote! { let #bind = &self.#idx; } - } - }; - let expr = src.expr_error_ref(&bind); - quote! {{ #getter #expr }} - } + Some(src) => src.expr_error_ref(src.field.ident.self_expr()), None => quote! { None }, }; let get_std_source = match &info.source { - Some(src) => { - 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) => { - let idx = syn::Index::from(i); - quote! { let #bind = &self.#idx; } - } - }; - let expr = src.expr_error_std(&bind); - quote! {{ #getter #expr }} - } + Some(src) => src.expr_error_std(src.field.ident.self_expr()), None => quote! { None }, }; let is_transparent = info.transparent().is_some(); - let get_display = { - if let Some(field) = info.transparent() { - let expr = field.ident.self_expr(); - quote! { write!(f, "{}", #expr) } - } else { - match &info.display { - DisplayArgs::Format(expr) => { - let binds: Vec = info.field_binding_idents().collect(); - match info.kind { - Kind::Unit => quote! { #expr }, - Kind::Named => { - quote! { #[allow(unused)] let Self { #( #binds ),* , .. } = self; #expr } - } - Kind::Tuple => { - quote! { #[allow(unused)] let Self( #( #binds ),* ) = self; #expr } - } - } - } - DisplayArgs::Default | DisplayArgs::Transparent => { - // Fallback to struct name - let text = info.ident.to_string(); - quote! { write!(f, #text) } + let get_fmt_message = { + match &info.display { + DisplayArgs::Format(expr) => { + let pat = info.spread_all(); + quote! { + #[allow(unused)] + let #pat = self; + #expr } } + DisplayArgs::Default | DisplayArgs::Transparent => { + let text = info.ident.item_ident().to_string(); + quote! { write!(f, #text) } + } } }; let get_debug = { - let item_name = info.ident.to_string(); + let item_name = info.ident.item_ident().to_string(); match info.kind { - Kind::Unit => { - quote! { - write!(f, #item_name)?; - } - } + Kind::Unit => quote!(write!(f, #item_name)?;), Kind::Named => { let fields = info .fields @@ -808,48 +716,14 @@ fn generate_struct_impl( // From impls for fields marked with #[error(from)] (or inferred via from_sources) let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let get_from = if let Some(field) = &info.from { - let ty = &field.field.ty; - let construct = match (info.kind, field.ident) { - (Kind::Named, FieldIdent::Named(ident)) => { - let meta = info - .meta - .as_ref() - .map(|_| quote!( meta: ::n0_error::Meta::new() )); - let comma = (meta.is_some() && info.fields.len() > 1).then(|| quote!(,)); - quote! { Self { #ident: source #comma #meta } } - } - (Kind::Tuple, FieldIdent::Unnamed(src_idx)) => { - let total = info.fields.len(); - let mut exprs: Vec = Vec::with_capacity(total); - for i in 0..total { - if i == src_idx { - exprs.push(quote! { source }); - } else if let Some(meta_field) = &info.meta { - if let FieldIdent::Unnamed(mi) = meta_field.ident { - if mi == i { - exprs.push(quote! { ::n0_error::Meta::new() }); - continue; - } - } - exprs.push(quote!{ ::core::compile_error!("unsupported From impl: extra fields present") }); - } else { - exprs.push(quote!{ ::core::compile_error!("unsupported From impl: extra fields present") }); - } - } - quote! { Self( #(#exprs),* ) } - } - _ => unreachable!(), - }; - Some(quote! { + let from_impl = info.from_impl().map(|(ty, construct)| { + quote! { impl #impl_generics ::core::convert::From<#ty> for #item_ident #ty_generics #where_clause { #[track_caller] fn from(source: #ty) -> Self { #construct } } - }) - } else { - None - }; + } + }); quote! { impl #impl_generics #item_ident #ty_generics #where_clause { @@ -860,11 +734,9 @@ fn generate_struct_impl( fn as_std(&self) -> &(dyn ::std::error::Error + ::std::marker::Send + ::std::marker::Sync + 'static) { self } - fn as_dyn(&self) -> &(dyn ::n0_error::StackError) { self } - fn meta(&self) -> Option<&::n0_error::Meta> { #get_meta } @@ -874,9 +746,8 @@ 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 + #get_fmt_message } } @@ -889,9 +760,8 @@ fn generate_struct_impl( 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); - write!(f, "{}", self.report().sources(sources)) + let sources = f.alternate().then_some(::n0_error::SourceFormat::OneLine); + write!(f, "{}", ::n0_error::StackError::report(self).sources(sources)) } } @@ -900,8 +770,7 @@ fn generate_struct_impl( if f.alternate() { #get_debug } else { - use ::n0_error::StackError; - write!(f, "{}", self.report().full())?; + write!(f, "{}", ::n0_error::StackError::report(self).full())?; } Ok(()) } @@ -913,7 +782,7 @@ fn generate_struct_impl( } } - #get_from + #from_impl } } @@ -934,7 +803,6 @@ impl DisplayArgs { return Ok(DisplayArgs::Default); }; - // syn 2: parse args inside the attribute's parentheses let args: Punctuated = attr.parse_args_with(Punctuated::::parse_terminated)?; @@ -957,16 +825,13 @@ impl DisplayArgs { // #[error("...", args...)] let mut it = args.into_iter(); let first = it.next().unwrap(); - let fmt_lit = match first { - Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => s, - other => { - return Err(err( + Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => s, + other => return Err(err( other, "first argument to #[error(\"...\")] must be a string literal, or use #[error(transparent)]", )) - } - }; + }; let rest: Vec = it.collect(); Ok(DisplayArgs::Format( diff --git a/src/ext.rs b/src/ext.rs index 10e53af..30220dd 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::{AnyError, StackError, StackErrorExt, add_meta, meta}; +use crate::{AnyError, StackError, StackErrorExt, meta, stack_error}; /// Provides extension methods to add context to [`StackError`]s. pub trait StackResultExt { @@ -147,14 +147,12 @@ impl StackResultExt for Option { } /// Error returned when converting [`Option`]s to an error. -#[add_meta] -#[derive(crate::StackError)] +#[stack_error(derive, add_meta)] #[error("Expected some, found none")] pub struct NoneError {} /// A simple string error, providing a message and optionally a source. -#[add_meta] -#[derive(crate::StackError)] +#[stack_error(derive, add_meta)] pub(crate) enum FromString { #[error("{message}")] WithSource { message: String, source: AnyError }, diff --git a/src/lib.rs b/src/lib.rs index 4b4f4ba..082b9ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ #![doc = include_str!("../README.md")] #![deny(missing_docs)] -pub use n0_error_macros::{StackError, add_meta, stack_error}; +pub use n0_error_macros::{StackError, stack_error}; extern crate self as n0_error; diff --git a/src/macros.rs b/src/macros.rs index d110ae7..b7a9a9d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -4,7 +4,7 @@ /// - `e!(MyError::Variant, source)` constructs `MyError::Variant { source, meta: Meta::default() }`. /// - `e!(MyError::Variant { field: value, other })` constructs `MyError::Variant { field: value, other, meta: Meta::default() }` #[macro_export] -macro_rules! e { +macro_rules! err { // No fields ($($err:tt)::+) => { $($err)::+ { meta: $crate::Meta::default() } @@ -36,7 +36,7 @@ macro_rules! try_or { match $result { ::core::result::Result::Ok(v) => v, ::core::result::Result::Err(e) => { - return ::core::result::Result::Err($crate::e!($($tt)*, e)); + return ::core::result::Result::Err($crate::err!($($tt)*, e)); } } }; @@ -139,7 +139,7 @@ pub use spez as __spez; #[macro_export] macro_rules! anyerr { ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { - $crate::format_err!($fmt$(, $($arg),*)*) + $crate::anyerr!(::std::format!($fmt$(, $($arg),*)*)) }; ($err:expr, $fmt:literal$(, $($arg:expr),* $(,)?)?) => { @@ -163,24 +163,4 @@ macro_rules! anyerr { } } }; - - ($($err:tt)::+) => { - $($err)::+ { meta: $crate::Meta::default() } - }; - - // Single expression: treat as source - ($($err:tt)::+ , $source:expr) => { - $($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)* } - }; - } diff --git a/src/tests.rs b/src/tests.rs index da2d587..64fcad4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2,8 +2,8 @@ use std::io; use self::util::wait_sequential; use crate::{ - AnyError, Result, StackError, StackErrorExt, StackResultExt, StdResultExt, add_meta, anyerr, e, - ensure_any, format_err, meta, set_backtrace_enabled, + AnyError, Result, StackError, StackErrorExt, StackResultExt, StdResultExt, anyerr, ensure_any, + err, format_err, meta, set_backtrace_enabled, stack_error, }; mod util; @@ -16,7 +16,7 @@ fn test_anyhow_compat() -> Result { ok().map_err(AnyError::from_anyhow) } -#[add_meta] +#[stack_error(add_meta)] #[derive(StackError)] enum MyError { #[error("A failure")] @@ -263,11 +263,11 @@ fn test_structs() { assert_eq!(format!("{err2:#}"), "bad: fail (22)"); } -#[add_meta] +#[stack_error(add_meta)] #[derive(StackError)] struct SomeErrorLoc; -#[add_meta] +#[stack_error(add_meta)] #[derive(StackError)] #[error("fail ({code})")] struct SomeErrorLocFields { @@ -308,7 +308,7 @@ Caused by: #[test] fn test_context() { - #[add_meta] + #[stack_error(add_meta)] #[derive(StackError)] enum AppError { My { source: MyError }, @@ -330,13 +330,13 @@ fn test_context() { println!("---"); println!( "{:?}", - fail_a().map_err(|s| e!(AppError::My, s)).unwrap_err() + fail_a().map_err(|s| err!(AppError::My, s)).unwrap_err() ); println!("---"); println!( "{:?}", fail_a() - .map_err(|source| e!(AppError::Baz { source, count: 32 })) + .map_err(|source| err!(AppError::Baz { source, count: 32 })) .unwrap_err() ); println!("---"); @@ -353,7 +353,7 @@ fn test_any() { // let x = foo(); let x = anyerr!("foo"); println!("{x:?}"); - let x = anyerr!(e!(MyError::A)); + let x = anyerr!(err!(MyError::A)); println!("{x:?}"); let x = anyerr!(io::Error::other("foo")); println!("{x:?}"); @@ -361,7 +361,7 @@ fn test_any() { // --- tuple support tests --- -#[add_meta] +#[stack_error(add_meta)] #[derive(StackError)] #[error("tuple fail ({_0})")] struct TupleStruct(u32); @@ -374,7 +374,7 @@ fn test_tuple_struct_basic() { assert_eq!(format!("{err}"), "tuple fail (7)"); } -#[add_meta] +#[stack_error(add_meta)] #[derive(StackError)] #[error(from_sources)] enum TupleEnum { @@ -395,12 +395,12 @@ fn test_tuple_enum_source_and_meta() { let src = std::error::Error::source(&e).unwrap(); assert_eq!(src.to_string(), "oops"); - let err = e!(MyError::A); + let err = err!(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 = err!(MyError::A); let err = TupleEnum::from(err); assert_eq!( format!("{err:?}"), @@ -411,7 +411,7 @@ fn test_tuple_enum_source_and_meta() { // TODO: turn into actual test #[test] pub fn test_skip_transparent_errors() { - #[add_meta] + #[stack_error(add_meta)] #[derive(StackError)] #[error(from_sources)] enum ErrorA { @@ -419,7 +419,7 @@ pub fn test_skip_transparent_errors() { ErrorB { source: ErrorB }, } - #[add_meta] + #[stack_error(add_meta)] #[derive(StackError)] #[error(std_sources)] enum ErrorB { @@ -438,7 +438,7 @@ pub fn test_skip_transparent_errors() { if transparent { io().map_err(|err| ErrorB::IoTransparent(err, meta())) } else { - io().map_err(|err| e!(ErrorB::Io, err)) + io().map_err(|err| err!(ErrorB::Io, err)) } } @@ -485,14 +485,14 @@ pub fn test_skip_transparent_errors() { #[test] fn test_generics() { - #[add_meta] + #[stack_error(add_meta)] #[derive(StackError)] #[error("failed at {}", list.iter().map(|e| e.to_string()).collect::>().join(", "))] struct GenericError { list: Vec, } - #[add_meta] + #[stack_error(add_meta)] #[derive(StackError)] enum GenericEnumError { Foo, @@ -507,7 +507,7 @@ fn test_generics() { let err = GenericError::new(vec!["foo", "bar"]); assert_eq!(format!("{err}"), "failed at foo, bar"); - let err = e!(GenericEnumError::Baz { + let err = err!(GenericEnumError::Baz { other: Box::new("foo") }); assert_eq!(format!("{err}"), "failed at foo"); From b34a29a06cf8197d5677d97da7a66f9a739ad634 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 28 Oct 2025 11:13:52 +0100 Subject: [PATCH 4/6] fixup --- README.md | 4 ++-- examples/simple.rs | 12 ++++++------ src/macros.rs | 4 ++-- src/tests.rs | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4efc562..7775d4c 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ enum MyError { #[error("bad input ({count})")] BadInput { count: usize }, /// Or we can define a variant as `transparent`, which forwards the Display impl to the error source - #[error("IO error")] + #[error(transparent)] Io { - /// For sources that do not implement `StackError`, we have to mark te source as `std_err`. + /// For sources that do not implement `StackError`, we have to mark the source as `std_err`. #[error(std_err)] source: std::io::Error, }, diff --git a/examples/simple.rs b/examples/simple.rs index cdeca27..d0a3680 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,6 +1,6 @@ use std::io; -use n0_error::{StackError, err, meta}; +use n0_error::{StackError, e, meta}; use self::error::CopyError; use crate::error::{InvalidArgsError, OperationError}; @@ -19,17 +19,17 @@ fn main() { print(err); println!("### InvalidArgs"); - let err = err!(InvalidArgsError::FailedToParse); - let err = err!(CopyError::InvalidArgs, err); + let err = e!(InvalidArgsError::FailedToParse); + let err = e!(CopyError::InvalidArgs, err); // let err = e!(CopyError::InvalidArgs { source: err }); // let err = CopyError!(InvalidArgs { source: err }); - let err = err!(OperationError::Copy { source: err }); + let err = e!(OperationError::Copy { source: err }); print(err); } fn _some_fn() -> Result<(), CopyError> { // Err! macro works like e! but wraps in Err - Err(err!(CopyError::Read, io::Error::other("yada"))) + Err(e!(CopyError::Read, io::Error::other("yada"))) } fn operation() -> Result<(), OperationError> { @@ -39,7 +39,7 @@ fn operation() -> Result<(), OperationError> { } fn copy() -> Result<(), CopyError> { - read().map_err(|err| err!(CopyError::Read { source: err }))?; + read().map_err(|err| e!(CopyError::Read { source: err }))?; Ok(()) } diff --git a/src/macros.rs b/src/macros.rs index b7a9a9d..34f7297 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -4,7 +4,7 @@ /// - `e!(MyError::Variant, source)` constructs `MyError::Variant { source, meta: Meta::default() }`. /// - `e!(MyError::Variant { field: value, other })` constructs `MyError::Variant { field: value, other, meta: Meta::default() }` #[macro_export] -macro_rules! err { +macro_rules! e { // No fields ($($err:tt)::+) => { $($err)::+ { meta: $crate::Meta::default() } @@ -36,7 +36,7 @@ macro_rules! try_or { match $result { ::core::result::Result::Ok(v) => v, ::core::result::Result::Err(e) => { - return ::core::result::Result::Err($crate::err!($($tt)*, e)); + return ::core::result::Result::Err($crate::e!($($tt)*, e)); } } }; diff --git a/src/tests.rs b/src/tests.rs index 64fcad4..784c125 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,7 +3,7 @@ use std::io; use self::util::wait_sequential; use crate::{ AnyError, Result, StackError, StackErrorExt, StackResultExt, StdResultExt, anyerr, ensure_any, - err, format_err, meta, set_backtrace_enabled, stack_error, + e, format_err, meta, set_backtrace_enabled, stack_error, }; mod util; @@ -330,13 +330,13 @@ fn test_context() { println!("---"); println!( "{:?}", - fail_a().map_err(|s| err!(AppError::My, s)).unwrap_err() + fail_a().map_err(|s| e!(AppError::My, s)).unwrap_err() ); println!("---"); println!( "{:?}", fail_a() - .map_err(|source| err!(AppError::Baz { source, count: 32 })) + .map_err(|source| e!(AppError::Baz { source, count: 32 })) .unwrap_err() ); println!("---"); @@ -353,7 +353,7 @@ fn test_any() { // let x = foo(); let x = anyerr!("foo"); println!("{x:?}"); - let x = anyerr!(err!(MyError::A)); + let x = anyerr!(e!(MyError::A)); println!("{x:?}"); let x = anyerr!(io::Error::other("foo")); println!("{x:?}"); @@ -395,12 +395,12 @@ fn test_tuple_enum_source_and_meta() { let src = std::error::Error::source(&e).unwrap(); assert_eq!(src.to_string(), "oops"); - let err = err!(MyError::A); + 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 = err!(MyError::A); + let err = e!(MyError::A); let err = TupleEnum::from(err); assert_eq!( format!("{err:?}"), @@ -438,7 +438,7 @@ pub fn test_skip_transparent_errors() { if transparent { io().map_err(|err| ErrorB::IoTransparent(err, meta())) } else { - io().map_err(|err| err!(ErrorB::Io, err)) + io().map_err(|err| e!(ErrorB::Io, err)) } } @@ -507,7 +507,7 @@ fn test_generics() { let err = GenericError::new(vec!["foo", "bar"]); assert_eq!(format!("{err}"), "failed at foo, bar"); - let err = err!(GenericEnumError::Baz { + let err = e!(GenericEnumError::Baz { other: Box::new("foo") }); assert_eq!(format!("{err}"), "failed at foo"); From 76ae43e909926b49f6c988c0162a4528988be472 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 28 Oct 2025 11:18:32 +0100 Subject: [PATCH 5/6] fixup --- n0-error-macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/n0-error-macros/src/lib.rs b/n0-error-macros/src/lib.rs index 750b330..48f8108 100644 --- a/n0-error-macros/src/lib.rs +++ b/n0-error-macros/src/lib.rs @@ -51,7 +51,7 @@ fn modify_attrs(args: &StackErrAttrArgs, attrs: &mut Vec) -> Result<( } let error_args: Vec<_> = [ args.from_sources.then(|| quote!(from_sources)), - args.std_sources.then(|| quote!(then_sources)), + args.std_sources.then(|| quote!(std_sources)), ] .into_iter() .flatten() From 1198276e78a319ff73dbd8335de6674473f20105 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 28 Oct 2025 11:39:03 +0100 Subject: [PATCH 6/6] cleanup macros --- src/macros.rs | 144 +++++++++++++++++++++++--------------------------- src/tests.rs | 8 +-- 2 files changed, 71 insertions(+), 81 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 34f7297..bbc947d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -5,12 +5,12 @@ /// - `e!(MyError::Variant { field: value, other })` constructs `MyError::Variant { field: value, other, meta: Meta::default() }` #[macro_export] macro_rules! e { - // No fields + // No source, no fields ($($err:tt)::+) => { $($err)::+ { meta: $crate::Meta::default() } }; - // Single expression: treat as source + // Source, no fields ($($err:tt)::+ , $source:expr) => { $($err)::+ { source: $source, meta: $crate::Meta::default() } }; @@ -26,45 +26,47 @@ macro_rules! e { }; } -/// Unwraps a result, returning in the error case while converting the error. +/// Reexport `spez` for use in the [`anyerr`] macro. +#[doc(hidden)] +pub use spez as __spez; + +/// Converts a value into [`AnyError`]. /// -/// If the result is the error variant, this will construct a new error with [`e`] -/// that takes the result's error as its source. +/// - `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, "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`], +/// if given a value that impls `Display` it uses [`AnyError::from_display`] - in this order. #[macro_export] -macro_rules! try_or { - ($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)); - } - } +macro_rules! anyerr { + ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { + $crate::anyerr!(::std::format!($fmt$(, $($arg),*)*)) }; -} -/// 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_or_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)*)); - } - } + ($err:expr, $fmt:literal$(, $($arg:expr),* $(,)?)?) => { + $crate::anyerr!($err).context(format!($fmt$(, $($arg),*)*)) }; -} -/// Formats a message into an [`AnyError`]. -#[macro_export] -macro_rules! format_err { - ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { - $crate::AnyError::from_display( - ::std::format!($fmt$(, $($arg),*)*), - ) + ($err:expr) => { + $crate::__spez::spez! { + for err = $err; + match $crate::AnyError -> $crate::AnyError { + err + } + match T -> $crate::AnyError { + $crate::AnyError::from_stack(err) + } + match T -> $crate::AnyError { + $crate::AnyError::from_std(err) + } + match T -> $crate::AnyError { + $crate::AnyError::from_display(err) + } + } }; } @@ -79,7 +81,7 @@ macro_rules! format_err { macro_rules! ensure { ($predicate:expr, $($tt:tt)*) => { if !$predicate { - $crate::bail_e!($($tt)*) + $crate::bail!($($tt)*) } }; } @@ -94,72 +96,60 @@ macro_rules! ensure { macro_rules! ensure_any { ($cond:expr, $($tt:tt)*) => { if !$cond { - $crate::bail!($($tt)*) + $crate::bail_any!($($tt)*) } }; } -/// Returns an error result by constructing an error with [`e`]. +/// Returns an error result by constructing a `StackError` 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_e { +macro_rules! bail { ($($tt:tt)*) => { return ::core::result::Result::Err($crate::e!($($tt)*).into()) } } -/// Returns an error result by constructing an error with [`anyerr`]. +/// Returns an error result by constructing an [`AnyError`] 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 { +macro_rules! bail_any { ($($tt:tt)*) => { - return core::result::Result::Err($crate::anyerr!($($tt)*)) + return ::core::result::Result::Err($crate::anyerr!($($tt)*).into()) } } -/// Reexport `spez` for use in the [`anyerr`] macro. -#[doc(hidden)] -pub use spez as __spez; - -/// Converts a value into [`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, "context {}", foo) works as above, but with a formatted string as context +/// Unwraps a result, returning in the error case while converting the error. /// -/// 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. +/// 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! anyerr { - ($fmt:literal$(, $($arg:expr),* $(,)?)?) => { - $crate::anyerr!(::std::format!($fmt$(, $($arg),*)*)) - }; - - ($err:expr, $fmt:literal$(, $($arg:expr),* $(,)?)?) => { - $crate::anyerr!($err).context(format!($fmt$(, $($arg),*)*)) +macro_rules! try_or { + ($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)); + } + } }; +} - ($err:expr) => { - $crate::__spez::spez! { - for err = $err; - match $crate::AnyError -> $crate::AnyError { - err - } - match T -> $crate::AnyError { - $crate::AnyError::from_stack(err) - } - match T -> $crate::AnyError { - $crate::AnyError::from_std(err) - } - match T -> $crate::AnyError { - $crate::AnyError::from_display(err) +/// 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_or_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)*)); } } }; diff --git a/src/tests.rs b/src/tests.rs index 784c125..9e8ea03 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2,8 +2,8 @@ use std::io; use self::util::wait_sequential; use crate::{ - AnyError, Result, StackError, StackErrorExt, StackResultExt, StdResultExt, anyerr, ensure_any, - e, format_err, meta, set_backtrace_enabled, stack_error, + AnyError, Result, StackError, StackErrorExt, StackResultExt, StdResultExt, anyerr, e, + ensure_any, meta, set_backtrace_enabled, stack_error, }; mod util; @@ -27,7 +27,7 @@ fn test_whatever() { let _guard = wait_sequential(); fn fail() -> Result { - n0_error::bail!("sad face"); + n0_error::bail_any!("sad face"); } fn fail_my_error() -> Result<(), MyError> { @@ -102,7 +102,7 @@ fn test_context_none() { #[test] fn test_format_err() { fn fail() -> Result { - Err(format_err!("sad: {}", 12)) + Err(anyerr!("sad: {}", 12)) } assert!(fail().is_err());