diff --git a/build.rs b/build.rs index 40e16de..92a32ca 100644 --- a/build.rs +++ b/build.rs @@ -17,6 +17,7 @@ fn main() { } fn run() -> Result<(), Box> { + println!("cargo:rustc-check-cfg=cfg(error_generic_member_access)"); println!("cargo:rerun-if-changed=Cargo.toml"); println!("cargo:rerun-if-changed=README.template.md"); println!("cargo:rerun-if-changed=build/readme.rs"); diff --git a/masterror-derive/src/error_trait.rs b/masterror-derive/src/error_trait.rs index f0efe44..4782ac0 100644 --- a/masterror-derive/src/error_trait.rs +++ b/masterror-derive/src/error_trait.rs @@ -15,6 +15,14 @@ pub fn expand(input: &ErrorInput) -> Result { fn expand_struct(input: &ErrorInput, data: &StructData) -> Result { let body = struct_source_body(&data.fields, &data.display); + let backtrace_method = struct_backtrace_method(&data.fields); + let has_backtrace = backtrace_method.is_some(); + let backtrace_method = backtrace_method.unwrap_or_default(); + let provide_method = if has_backtrace { + provide_method_tokens() + } else { + TokenStream::new() + }; let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); @@ -24,6 +32,8 @@ fn expand_struct(input: &ErrorInput, data: &StructData) -> Result Option<&(dyn std::error::Error + 'static)> { #body } + #backtrace_method + #provide_method } }) } @@ -34,6 +44,15 @@ fn expand_enum(input: &ErrorInput, variants: &[VariantData]) -> Result Result Option { + let field = fields.backtrace_field()?; + let member = &field.member; + let body = field_backtrace_expr(quote!(self.#member), quote!(&self.#member), &field.ty); + Some(quote! { + #[cfg(error_generic_member_access)] + fn backtrace(&self) -> Option<&std::backtrace::Backtrace> { + #body + } + }) +} + +fn enum_backtrace_method(variants: &[VariantData]) -> Option { + let mut has_backtrace = false; + let mut arms = Vec::new(); + for variant in variants { + if variant.fields.backtrace_field().is_some() { + has_backtrace = true; + } + arms.push(variant_backtrace_arm(variant)); + } + + if has_backtrace { + Some(quote! { + #[cfg(error_generic_member_access)] + fn backtrace(&self) -> Option<&std::backtrace::Backtrace> { + match self { + #(#arms),* + } + } + }) + } else { + None + } +} + +fn variant_backtrace_arm(variant: &VariantData) -> TokenStream { + let variant_ident = &variant.ident; + let backtrace_field = variant.fields.backtrace_field(); + + match (&variant.fields, backtrace_field) { + (Fields::Unit, _) => quote! { Self::#variant_ident => None }, + (Fields::Named(fields), Some(field)) => { + let field_ident = field.ident.clone().expect("named field"); + let binding = binding_ident(field); + let pattern = if fields.len() == 1 { + quote!(Self::#variant_ident { #field_ident: #binding }) + } else { + quote!(Self::#variant_ident { #field_ident: #binding, .. }) + }; + let body = field_backtrace_expr(quote!(#binding), quote!(#binding), &field.ty); + quote! { + #pattern => { #body } + } + } + (Fields::Unnamed(fields), Some(field)) => { + let index = field.index; + let binding = binding_ident(field); + let pattern_elements: Vec<_> = fields + .iter() + .enumerate() + .map(|(idx, _)| { + if idx == index { + quote!(#binding) + } else { + quote!(_) + } + }) + .collect(); + let body = field_backtrace_expr(quote!(#binding), quote!(#binding), &field.ty); + quote! { + Self::#variant_ident(#(#pattern_elements),*) => { #body } + } + } + (Fields::Named(_), None) => quote! { Self::#variant_ident { .. } => None }, + (Fields::Unnamed(fields), None) => { + if fields.is_empty() { + quote! { Self::#variant_ident() => None } + } else { + let placeholders = vec![quote!(_); fields.len()]; + quote! { Self::#variant_ident(#(#placeholders),*) => None } + } + } + } +} + +fn field_backtrace_expr( + owned_expr: TokenStream, + referenced_expr: TokenStream, + ty: &syn::Type +) -> TokenStream { + if is_option_type(ty) { + quote! { #owned_expr.as_ref() } + } else { + quote! { Some(#referenced_expr) } + } +} + +fn provide_method_tokens() -> TokenStream { + quote! { + #[cfg(error_generic_member_access)] + fn provide<'a>(&'a self, request: &mut core::error::Request<'a>) { + if let Some(backtrace) = std::error::Error::backtrace(self) { + request.provide_ref::(backtrace); + } + } + } +} + fn binding_ident(field: &Field) -> Ident { field .ident diff --git a/masterror-derive/src/input.rs b/masterror-derive/src/input.rs index 649f462..7cce9aa 100644 --- a/masterror-derive/src/input.rs +++ b/masterror-derive/src/input.rs @@ -1,7 +1,7 @@ use proc_macro2::Span; use syn::{ Attribute, Data, DataEnum, DataStruct, DeriveInput, Error, Field as SynField, - Fields as SynFields, Ident, LitStr, spanned::Spanned + Fields as SynFields, GenericArgument, Ident, LitStr, spanned::Spanned }; use crate::template_support::{DisplayTemplate, TemplateIdentifierSpec, parse_display_template}; @@ -94,6 +94,10 @@ impl Fields { pub fn first_from_field(&self) -> Option<&Field> { self.iter().find(|field| field.attrs.from.is_some()) } + + pub fn backtrace_field(&self) -> Option<&Field> { + self.iter().find(|field| field.attrs.backtrace.is_some()) + } } pub enum FieldIter<'a> { @@ -250,6 +254,7 @@ fn parse_struct( let fields = Fields::from_syn(&data.fields, errors); validate_from_usage(&fields, &display, errors); + validate_backtrace_usage(&fields, errors); validate_transparent(&fields, &display, errors, None); Ok(ErrorData::Struct(Box::new(StructData { @@ -295,6 +300,7 @@ fn parse_variant(variant: syn::Variant, errors: &mut Vec) -> Result) { + let backtrace_fields: Vec<_> = fields + .iter() + .filter(|field| field.attrs.backtrace.is_some()) + .collect(); + + for field in &backtrace_fields { + validate_backtrace_field_type(field, errors); + } + + if backtrace_fields.len() <= 1 { + return; + } + + for field in backtrace_fields.iter().skip(1) { + if let Some(attr) = &field.attrs.backtrace { + errors.push(Error::new_spanned( + attr, + "multiple #[backtrace] fields are not supported" + )); + } + } +} + +fn validate_backtrace_field_type(field: &Field, errors: &mut Vec) { + let Some(attr) = &field.attrs.backtrace else { + return; + }; + + let ty = &field.ty; + if is_option_type(ty) { + if option_inner_type(ty).is_some_and(is_backtrace_type) { + return; + } + } else if is_backtrace_type(ty) { + return; + } + + errors.push(Error::new_spanned( + attr, + "fields with #[backtrace] must be std::backtrace::Backtrace or Option" + )); +} + fn validate_transparent( fields: &Fields, display: &DisplaySpec, @@ -493,6 +543,39 @@ pub fn is_option_type(ty: &syn::Type) -> bool { false } +fn option_inner_type(ty: &syn::Type) -> Option<&syn::Type> { + let syn::Type::Path(path) = ty else { + return None; + }; + if path.qself.is_some() { + return None; + } + let last = path.path.segments.last()?; + if last.ident != "Option" { + return None; + } + let syn::PathArguments::AngleBracketed(arguments) = &last.arguments else { + return None; + }; + arguments.args.iter().find_map(|argument| match argument { + GenericArgument::Type(inner) => Some(inner), + _ => None + }) +} + +fn is_backtrace_type(ty: &syn::Type) -> bool { + let syn::Type::Path(path) = ty else { + return false; + }; + if path.qself.is_some() { + return false; + } + let Some(last) = path.path.segments.last() else { + return false; + }; + last.ident == "Backtrace" && matches!(last.arguments, syn::PathArguments::None) +} + pub fn placeholder_error(span: Span, identifier: &TemplateIdentifierSpec) -> Error { match identifier { TemplateIdentifierSpec::Named(name) => { diff --git a/tests/error_derive.rs b/tests/error_derive.rs index 9ffb51e..3ce1a20 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -1,6 +1,8 @@ #![allow(unused_variables, non_shorthand_field_patterns)] use std::error::Error as StdError; +#[cfg(error_generic_member_access)] +use std::ptr; use masterror::Error; @@ -127,6 +129,46 @@ enum VariantFromWithBacktrace { } } +#[derive(Debug, Error)] +#[error("captured")] +struct StructWithBacktrace { + #[backtrace] + trace: std::backtrace::Backtrace +} + +#[derive(Debug, Error)] +enum EnumWithBacktrace { + #[error("tuple {0}")] + Tuple(&'static str, #[backtrace] std::backtrace::Backtrace), + #[error("named {message}")] + Named { + message: &'static str, + #[backtrace] + trace: std::backtrace::Backtrace + }, + #[error("unit")] + Unit +} + +#[cfg(error_generic_member_access)] +fn assert_backtrace_interfaces(error: &E, expected: &std::backtrace::Backtrace) +where + E: StdError + ?Sized +{ + let reported = std::error::Error::backtrace(error).expect("backtrace"); + assert!(ptr::eq(expected, reported)); + let provided = + std::error::request_ref::(error).expect("provided backtrace"); + assert!(ptr::eq(reported, provided)); +} + +#[cfg(not(error_generic_member_access))] +fn assert_backtrace_interfaces(_error: &E, _expected: &std::backtrace::Backtrace) +where + E: StdError + ?Sized +{ +} + #[test] fn named_struct_display_and_source() { let err = NamedError { @@ -247,6 +289,8 @@ fn transparent_enum_variant_from_impl() { fn struct_from_with_backtrace_field_captures_trace() { let err = StructFromWithBacktrace::from(LeafError); assert!(err.trace.is_some()); + let stored = err.trace.as_ref().expect("trace stored"); + assert_backtrace_interfaces(&err, stored); assert_eq!( StdError::source(&err).map(|err| err.to_string()), Some(String::from("leaf failure")) @@ -256,15 +300,51 @@ fn struct_from_with_backtrace_field_captures_trace() { #[test] fn enum_from_with_backtrace_field_captures_trace() { let err = VariantFromWithBacktrace::from(LeafError); - match &err { + let trace = match &err { VariantFromWithBacktrace::WithTrace { trace, .. } => { assert!(trace.is_some()); + trace.as_ref().unwrap() } - } + }; + assert_backtrace_interfaces(&err, trace); assert_eq!( StdError::source(&err).map(|err| err.to_string()), Some(String::from("leaf failure")) ); } + +#[test] +fn struct_backtrace_field_is_returned() { + let err = StructWithBacktrace { + trace: std::backtrace::Backtrace::capture() + }; + assert_backtrace_interfaces(&err, &err.trace); + assert!(StdError::source(&err).is_none()); +} + +#[test] +fn enum_backtrace_field_is_returned() { + let tuple = EnumWithBacktrace::Tuple("tuple", std::backtrace::Backtrace::capture()); + if let EnumWithBacktrace::Tuple(_, trace) = &tuple { + assert_backtrace_interfaces(&tuple, trace); + } + + let named = EnumWithBacktrace::Named { + message: "named", + trace: std::backtrace::Backtrace::capture() + }; + if let EnumWithBacktrace::Named { + trace, .. + } = &named + { + assert_backtrace_interfaces(&named, trace); + } + + let unit = EnumWithBacktrace::Unit; + #[cfg(error_generic_member_access)] + { + assert!(std::error::Error::backtrace(&unit).is_none()); + } +} diff --git a/tests/error_derive_from_trybuild.rs b/tests/error_derive_from_trybuild.rs index 377c5c2..2853697 100644 --- a/tests/error_derive_from_trybuild.rs +++ b/tests/error_derive_from_trybuild.rs @@ -11,3 +11,9 @@ fn transparent_attribute_compile_failures() { let t = TestCases::new(); t.compile_fail("tests/ui/transparent/*.rs"); } + +#[test] +fn backtrace_attribute_compile_failures() { + let t = TestCases::new(); + t.compile_fail("tests/ui/backtrace/*.rs"); +} diff --git a/tests/ui/backtrace/duplicate.rs b/tests/ui/backtrace/duplicate.rs new file mode 100644 index 0000000..2a35171 --- /dev/null +++ b/tests/ui/backtrace/duplicate.rs @@ -0,0 +1,12 @@ +use masterror::Error; + +#[derive(Debug, Error)] +#[error("duplicate backtrace fields")] +struct DuplicateBacktrace { + #[backtrace] + first: std::backtrace::Backtrace, + #[backtrace] + second: std::backtrace::Backtrace +} + +fn main() {} diff --git a/tests/ui/backtrace/duplicate.stderr b/tests/ui/backtrace/duplicate.stderr new file mode 100644 index 0000000..522d31c --- /dev/null +++ b/tests/ui/backtrace/duplicate.stderr @@ -0,0 +1,5 @@ +error: multiple #[backtrace] fields are not supported + --> tests/ui/backtrace/duplicate.rs:8:5 + | +8 | #[backtrace] + | ^^^^^^^^^^^^ diff --git a/tests/ui/backtrace/invalid_type.rs b/tests/ui/backtrace/invalid_type.rs new file mode 100644 index 0000000..3bc51cb --- /dev/null +++ b/tests/ui/backtrace/invalid_type.rs @@ -0,0 +1,10 @@ +use masterror::Error; + +#[derive(Debug, Error)] +#[error("invalid backtrace field")] +struct InvalidBacktrace { + #[backtrace] + trace: String +} + +fn main() {} diff --git a/tests/ui/backtrace/invalid_type.stderr b/tests/ui/backtrace/invalid_type.stderr new file mode 100644 index 0000000..d59b070 --- /dev/null +++ b/tests/ui/backtrace/invalid_type.stderr @@ -0,0 +1,5 @@ +error: fields with #[backtrace] must be std::backtrace::Backtrace or Option + --> tests/ui/backtrace/invalid_type.rs:6:5 + | +6 | #[backtrace] + | ^^^^^^^^^^^^