From 48fc630c43cc9281e5df43da88a5af5c1a1d136e Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:49:43 +0700 Subject: [PATCH] Infer default source/backtrace fields --- CHANGELOG.md | 11 +++ Cargo.lock | 4 +- Cargo.toml | 4 +- README.md | 14 ++-- masterror-derive/Cargo.toml | 2 +- masterror-derive/src/error_trait.rs | 7 +- masterror-derive/src/from_impl.rs | 4 +- masterror-derive/src/input.rs | 86 ++++++++++++++++++----- tests/error_derive.rs | 101 ++++++++++++++++++++++++++++ 9 files changed, 199 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a8624..ec7bc67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file. ### Added - _Nothing yet._ +## [0.5.8] - 2025-09-30 + +### Changed +- `masterror::Error` now infers sources named `source` and backtrace fields of + type `std::backtrace::Backtrace`/`Option` even + without explicit attributes, matching `thiserror`'s ergonomics. + +### Tests +- Expanded derive tests to cover implicit `source`/`backtrace` detection across + structs and enums. + ## [0.5.7] - 2025-09-29 ### Added diff --git a/Cargo.lock b/Cargo.lock index 92f93b5..63dd565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,7 +1527,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.5.7" +version = "0.5.8" dependencies = [ "actix-web", "axum", @@ -1557,7 +1557,7 @@ dependencies = [ [[package]] name = "masterror-derive" -version = "0.1.3" +version = "0.1.4" dependencies = [ "masterror-template", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index a246b48..8c57a76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.5.7" +version = "0.5.8" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" @@ -49,7 +49,7 @@ turnkey = [] openapi = ["dep:utoipa"] [workspace.dependencies] -masterror-derive = { version = "0.1.3", path = "masterror-derive" } +masterror-derive = { version = "0.1.4", path = "masterror-derive" } masterror-template = { version = "0.1.2", path = "masterror-template" } [dependencies] diff --git a/README.md b/README.md index 51b21a6..efe6679 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.5.7", default-features = false } +masterror = { version = "0.5.8", default-features = false } # or with features: -# masterror = { version = "0.5.7", features = [ +# masterror = { version = "0.5.8", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +66,10 @@ masterror = { version = "0.5.7", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.5.7", default-features = false } +masterror = { version = "0.5.8", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.5.7", features = [ +# masterror = { version = "0.5.8", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -342,13 +342,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.5.7", default-features = false } +masterror = { version = "0.5.8", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.5.7", features = [ +masterror = { version = "0.5.8", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -357,7 +357,7 @@ masterror = { version = "0.5.7", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.5.7", features = [ +masterror = { version = "0.5.8", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/masterror-derive/Cargo.toml b/masterror-derive/Cargo.toml index f8d2ecb..c67ee28 100644 --- a/masterror-derive/Cargo.toml +++ b/masterror-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "masterror-derive" rust-version = "1.90" -version = "0.1.3" +version = "0.1.4" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/RAprogramm/masterror" diff --git a/masterror-derive/src/error_trait.rs b/masterror-derive/src/error_trait.rs index 4782ac0..ec07553 100644 --- a/masterror-derive/src/error_trait.rs +++ b/masterror-derive/src/error_trait.rs @@ -82,7 +82,7 @@ fn struct_source_body(fields: &Fields, display: &DisplaySpec) -> TokenStream { } } DisplaySpec::Template(_) => { - if let Some(field) = fields.iter().find(|field| field.attrs.source.is_some()) { + if let Some(field) = fields.iter().find(|field| field.attrs.has_source()) { let member = &field.member; field_source_expr(quote!(self.#member), quote!(&self.#member), &field.ty) } else { @@ -135,10 +135,7 @@ fn variant_transparent_source(variant: &VariantData) -> TokenStream { fn variant_template_source(variant: &VariantData) -> TokenStream { let variant_ident = &variant.ident; - let source_field = variant - .fields - .iter() - .find(|field| field.attrs.source.is_some()); + let source_field = variant.fields.iter().find(|field| field.attrs.has_source()); match (&variant.fields, source_field) { (Fields::Unit, _) => quote! { Self::#variant_ident => None }, diff --git a/masterror-derive/src/from_impl.rs b/masterror-derive/src/from_impl.rs index 7dc873e..7518d01 100644 --- a/masterror-derive/src/from_impl.rs +++ b/masterror-derive/src/from_impl.rs @@ -127,11 +127,11 @@ fn field_value_expr(field: &Field, from_field: &Field) -> Result Option<&Field> { - self.iter().find(|field| field.attrs.backtrace.is_some()) + self.iter().find(|field| field.attrs.has_backtrace()) } } @@ -134,7 +134,7 @@ impl Field { None => syn::Member::Unnamed(syn::Index::from(index)) }; - let attrs = FieldAttrs::from_attrs(&field.attrs, errors); + let attrs = FieldAttrs::from_attrs(&field.attrs, ident.as_ref(), &field.ty, errors); Self { ident, @@ -149,13 +149,20 @@ impl Field { #[derive(Debug, Default)] pub struct FieldAttrs { - pub from: Option, - pub source: Option, - pub backtrace: Option + pub from: Option, + pub source: Option, + pub backtrace: Option, + inferred_source: bool, + inferred_backtrace: bool } impl FieldAttrs { - fn from_attrs(attrs: &[Attribute], errors: &mut Vec) -> Self { + fn from_attrs( + attrs: &[Attribute], + ident: Option<&Ident>, + ty: &syn::Type, + errors: &mut Vec + ) -> Self { let mut result = FieldAttrs::default(); for attr in attrs { @@ -198,8 +205,42 @@ impl FieldAttrs { result.source = Some(attr.clone()); } + if result.source.is_none() && ident.is_some_and(|ident| ident == "source") { + result.inferred_source = true; + } + + if result.backtrace.is_none() { + if is_option_type(ty) { + if option_inner_type(ty).is_some_and(is_backtrace_type) { + result.inferred_backtrace = true; + } + } else if is_backtrace_type(ty) { + result.inferred_backtrace = true; + } + } + result } + + pub fn has_source(&self) -> bool { + self.source.is_some() || self.inferred_source + } + + pub fn has_backtrace(&self) -> bool { + self.backtrace.is_some() || self.inferred_backtrace + } + + pub fn is_backtrace_inferred(&self) -> bool { + self.inferred_backtrace + } + + pub fn source_attribute(&self) -> Option<&Attribute> { + self.source.as_ref() + } + + pub fn backtrace_attribute(&self) -> Option<&Attribute> { + self.backtrace.as_ref() + } } #[derive(Debug)] @@ -403,16 +444,23 @@ fn validate_from_usage(fields: &Fields, display: &DisplaySpec, errors: &mut Vec< continue; } - if companion.attrs.backtrace.is_some() { + if companion.attrs.has_backtrace() { continue; } - if let Some(attr) = &companion.attrs.source { + if companion.attrs.has_source() { if companion.attrs.from.is_none() && !is_option_type(&companion.ty) { - errors.push(Error::new_spanned( - attr, - "additional #[source] fields used with #[from] must be Option<_>" - )); + if let Some(attr) = companion.attrs.source_attribute() { + errors.push(Error::new_spanned( + attr, + "additional #[source] fields used with #[from] must be Option<_>" + )); + } else { + errors.push(Error::new( + companion.span, + "additional #[source] fields used with #[from] must be Option<_>" + )); + } } continue; } @@ -442,7 +490,7 @@ fn validate_from_usage(fields: &Fields, display: &DisplaySpec, errors: &mut Vec< fn validate_backtrace_usage(fields: &Fields, errors: &mut Vec) { let backtrace_fields: Vec<_> = fields .iter() - .filter(|field| field.attrs.backtrace.is_some()) + .filter(|field| field.attrs.has_backtrace()) .collect(); for field in &backtrace_fields { @@ -454,17 +502,25 @@ fn validate_backtrace_usage(fields: &Fields, errors: &mut Vec) { } for field in backtrace_fields.iter().skip(1) { - if let Some(attr) = &field.attrs.backtrace { + if let Some(attr) = field.attrs.backtrace_attribute() { errors.push(Error::new_spanned( attr, "multiple #[backtrace] fields are not supported" )); + } else { + errors.push(Error::new( + field.span, + "multiple #[backtrace] fields are not supported" + )); } } } fn validate_backtrace_field_type(field: &Field, errors: &mut Vec) { - let Some(attr) = &field.attrs.backtrace else { + let Some(attr) = field.attrs.backtrace_attribute() else { + if field.attrs.is_backtrace_inferred() { + return; + } return; }; diff --git a/tests/error_derive.rs b/tests/error_derive.rs index af896c7..dbf488b 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -150,6 +150,41 @@ enum EnumWithBacktrace { Unit } +#[derive(Debug, Error)] +#[error("auto {source}")] +struct AutoSourceStruct { + source: LeafError +} + +#[derive(Debug, Error)] +enum AutoSourceEnum { + #[error("named {source}")] + Named { source: LeafError } +} + +#[derive(Debug, Error)] +#[error("captured")] +struct AutoBacktraceStruct { + trace: std::backtrace::Backtrace +} + +#[derive(Debug, Error)] +#[error("optional")] +struct AutoOptionalBacktraceStruct { + trace: Option +} + +#[derive(Debug, Error)] +enum AutoBacktraceEnum { + #[error("named {message}")] + Named { + message: &'static str, + trace: std::backtrace::Backtrace + }, + #[error("tuple {0:?}")] + Tuple(Option) +} + #[derive(Debug, Error)] #[error( "display={value} debug={value:?} #debug={value:#?} x={value:x} X={value:X} \ @@ -407,6 +442,72 @@ fn supports_display_and_debug_formatters() { assert!(StdError::source(&err).is_none()); } +#[test] +fn struct_named_source_is_inferred() { + let err = AutoSourceStruct { + source: LeafError + }; + assert_eq!(err.to_string(), "auto leaf failure"); + let source = StdError::source(&err).expect("source"); + assert_eq!(source.to_string(), "leaf failure"); +} + +#[test] +fn enum_named_source_is_inferred() { + let err = AutoSourceEnum::Named { + source: LeafError + }; + assert_eq!(err.to_string(), "named leaf failure"); + let source = StdError::source(&err).expect("source"); + assert_eq!(source.to_string(), "leaf failure"); +} + +#[test] +fn struct_backtrace_is_inferred_without_attribute() { + let err = AutoBacktraceStruct { + trace: std::backtrace::Backtrace::capture() + }; + assert_backtrace_interfaces(&err, &err.trace); + assert!(StdError::source(&err).is_none()); +} + +#[test] +fn struct_optional_backtrace_is_inferred_without_attribute() { + let err = AutoOptionalBacktraceStruct { + trace: Some(std::backtrace::Backtrace::capture()) + }; + let stored = err.trace.as_ref().expect("trace stored"); + assert_backtrace_interfaces(&err, stored); + assert!(StdError::source(&err).is_none()); +} + +#[test] +fn enum_backtrace_is_inferred_without_attribute() { + let named = AutoBacktraceEnum::Named { + message: "named", + trace: std::backtrace::Backtrace::capture() + }; + if let AutoBacktraceEnum::Named { + trace, .. + } = &named + { + assert_backtrace_interfaces(&named, trace); + } + assert!(StdError::source(&named).is_none()); + + let tuple = AutoBacktraceEnum::Tuple(Some(std::backtrace::Backtrace::capture())); + if let AutoBacktraceEnum::Tuple(Some(trace)) = &tuple { + assert_backtrace_interfaces(&tuple, trace); + } + assert!(StdError::source(&tuple).is_none()); + + #[cfg(error_generic_member_access)] + { + let none = AutoBacktraceEnum::Tuple(None); + assert!(std::error::Error::backtrace(&none).is_none()); + } +} + #[test] fn supports_extended_formatters() { let value = 0x5A5Au32;