diff --git a/impl/src/attr.rs b/impl/src/attr.rs index 7ad83e0..dba3151 100644 --- a/impl/src/attr.rs +++ b/impl/src/attr.rs @@ -12,6 +12,7 @@ pub struct Attrs<'a> { pub display: Option>, pub source: Option>, pub backtrace: Option<&'a Attribute>, + pub location: Option<&'a Attribute>, pub from: Option>, pub transparent: Option>, pub fmt: Option>, @@ -71,6 +72,7 @@ pub fn get(input: &[Attribute]) -> Result { display: None, source: None, backtrace: None, + location: None, from: None, transparent: None, fmt: None, @@ -97,6 +99,12 @@ pub fn get(input: &[Attribute]) -> Result { return Err(Error::new_spanned(attr, "duplicate #[backtrace] attribute")); } attrs.backtrace = Some(attr); + } else if attr.path().is_ident("location") { + attr.meta.require_path_only()?; + if attrs.location.is_some() { + return Err(Error::new_spanned(attr, "duplicate #[location] attribute")); + } + attrs.location = Some(attr); } else if attr.path().is_ident("from") { match attr.meta { Meta::Path(_) => {} diff --git a/impl/src/expand.rs b/impl/src/expand.rs index a304632..d76a6e5 100644 --- a/impl/src/expand.rs +++ b/impl/src/expand.rs @@ -99,10 +99,28 @@ fn impl_struct(input: Struct) -> TokenStream { #request.provide_ref::<::thiserror::__private::Backtrace>(&self.#backtrace); }) }; + let location_provide = if let Some(location_field) = input.location_field() { + let location = &location_field.member; + + if type_is_option(location_field.ty) { + Some(quote! { + if let ::core::option::Option::Some(location) = &self.#location { + #request.provide_ref::<::core::panic::Location>(location); + } + }) + } else { + Some(quote! { + #request.provide_ref::<::core::panic::Location>(&self.#location); + }) + } + } else { + None + }; quote! { use ::thiserror::__private::ThiserrorProvide as _; #source_provide #self_provide + #location_provide } } else if type_is_option(backtrace_field.ty) { quote! { @@ -167,9 +185,16 @@ fn impl_struct(input: Struct) -> TokenStream { let span = from_field.attrs.from.unwrap().span; let backtrace_field = input.distinct_backtrace_field(); let from = unoptional_type(from_field.ty); + let track_caller = input.location_field().map(|_| quote!(#[track_caller])); let source_var = Ident::new("source", span); - let body = from_initializer(from_field, backtrace_field, &source_var); + let body = from_initializer( + from_field, + backtrace_field, + &source_var, + input.location_field(), + ); let from_function = quote! { + #track_caller fn from(#source_var: #from) -> Self { #ty #body } @@ -266,25 +291,43 @@ fn impl_enum(input: Enum) -> TokenStream { let request = quote!(request); let arms = input.variants.iter().map(|variant| { let ident = &variant.ident; - match (variant.backtrace_field(), variant.source_field()) { - (Some(backtrace_field), Some(source_field)) - if backtrace_field.attrs.backtrace.is_none() => - { - let backtrace = &backtrace_field.member; - let source = &source_field.member; - let varsource = quote!(source); - let source_provide = if type_is_option(source_field.ty) { - quote_spanned! {source.span()=> - if let ::core::option::Option::Some(source) = #varsource { - source.thiserror_provide(#request); - } - } + + let mut arm = vec![]; + let mut head = vec![]; + + if let Some((source_field, backtrace_field)) = variant + .backtrace_field() + .and_then(|b| variant.source_field().take().map(|s| (s, b))) + .and_then(|(s, b)| { + // TODO: replace with take_if upon stabilization + + if s.member == b.member { + Some((s, b)) } else { - quote_spanned! {source.span()=> - #varsource.thiserror_provide(#request); + None + } + }) + { + let backtrace = &backtrace_field.member; + let varsource = quote!(source); + let source_provide = if type_is_option(source_field.ty) { + quote_spanned! {backtrace.span()=> + if let ::core::option::Option::Some(source) = #varsource { + source.thiserror_provide(#request); } - }; - let self_provide = if type_is_option(backtrace_field.ty) { + } + } else { + quote_spanned! {backtrace.span()=> + #varsource.thiserror_provide(#request); + } + }; + + head.push(quote! { #backtrace: #varsource }); + arm.push(quote! { #source_provide }); + } else { + if let Some(backtrace_field) = variant.backtrace_field() { + let backtrace = &backtrace_field.member; + let body = if type_is_option(backtrace_field.ty) { quote! { if let ::core::option::Option::Some(backtrace) = backtrace { #request.provide_ref::<::thiserror::__private::Backtrace>(backtrace); @@ -295,63 +338,59 @@ fn impl_enum(input: Enum) -> TokenStream { #request.provide_ref::<::thiserror::__private::Backtrace>(backtrace); } }; - quote! { - #ty::#ident { - #backtrace: backtrace, - #source: #varsource, - .. - } => { - use ::thiserror::__private::ThiserrorProvide as _; - #source_provide - #self_provide - } - } + + head.push(quote! { #backtrace: backtrace }); + arm.push(quote! { #body }); } - (Some(backtrace_field), Some(source_field)) - if backtrace_field.member == source_field.member => - { - let backtrace = &backtrace_field.member; + + if let Some(source_field) = variant.source_field() { + let source = &source_field.member; let varsource = quote!(source); + let source_provide = if type_is_option(source_field.ty) { - quote_spanned! {backtrace.span()=> + quote_spanned! {source.span()=> if let ::core::option::Option::Some(source) = #varsource { source.thiserror_provide(#request); } } } else { - quote_spanned! {backtrace.span()=> + quote_spanned! {source.span()=> #varsource.thiserror_provide(#request); } }; + + head.push(quote! { #source: #varsource }); + arm.push(quote! { #source_provide }); + } + } + + if let Some(location_field) = variant.location_field() { + let location = &location_field.member; + + let location_provide = if type_is_option(location_field.ty) { quote! { - #ty::#ident {#backtrace: #varsource, ..} => { - use ::thiserror::__private::ThiserrorProvide as _; - #source_provide + if let ::core::option::Option::Some(location) = location { + #request.provide_ref::<::core::panic::Location>(location); } } - } - (Some(backtrace_field), _) => { - let backtrace = &backtrace_field.member; - let body = if type_is_option(backtrace_field.ty) { - quote! { - if let ::core::option::Option::Some(backtrace) = backtrace { - #request.provide_ref::<::thiserror::__private::Backtrace>(backtrace); - } - } - } else { - quote! { - #request.provide_ref::<::thiserror::__private::Backtrace>(backtrace); - } - }; + } else { quote! { - #ty::#ident {#backtrace: backtrace, ..} => { - #body - } + #request.provide_ref::<::core::panic::Location>(location); } + }; + + head.push(quote! { #location: location }); + arm.push(quote! { #location_provide }); + } + + quote! { + #ty::#ident { + #(#head,)* + .. + } => { + use thiserror::__private::ThiserrorProvide as _; + #(#arm)* } - (None, _) => quote! { - #ty::#ident {..} => {} - }, } }); Some(quote! { @@ -435,11 +474,14 @@ fn impl_enum(input: Enum) -> TokenStream { let from_field = variant.from_field()?; let span = from_field.attrs.from.unwrap().span; let backtrace_field = variant.distinct_backtrace_field(); + let location_field = variant.location_field(); let variant = &variant.ident; let from = unoptional_type(from_field.ty); let source_var = Ident::new("source", span); - let body = from_initializer(from_field, backtrace_field, &source_var); + let body = from_initializer(from_field, backtrace_field, &source_var, location_field); + let track_caller = location_field.map(|_| quote!(#[track_caller])); let from_function = quote! { + #track_caller fn from(#source_var: #from) -> Self { #ty::#variant #body } @@ -512,6 +554,7 @@ fn from_initializer( from_field: &Field, backtrace_field: Option<&Field>, source_var: &Ident, + location_field: Option<&Field>, ) -> TokenStream { let from_member = &from_field.member; let some_source = if type_is_option(from_field.ty) { @@ -531,9 +574,23 @@ fn from_initializer( } } }); + let location = location_field.map(|location_field| { + let location_member = &location_field.member; + + if type_is_option(location_field.ty) { + quote! { + #location_member: ::core::option::Option::Some(::core::panic::Location::caller()), + } + } else { + quote! { + #location_member: ::core::convert::From::from(::core::panic::Location::caller()), + } + } + }); quote!({ #from_member: #some_source, #backtrace + #location }) } diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 7f3767e..5394b78 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -32,7 +32,7 @@ mod valid; use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; -#[proc_macro_derive(Error, attributes(backtrace, error, from, source))] +#[proc_macro_derive(Error, attributes(backtrace, error, from, source, location))] pub fn derive_error(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); expand::derive(&input).into() diff --git a/impl/src/prop.rs b/impl/src/prop.rs index 0a101fc..4153a90 100644 --- a/impl/src/prop.rs +++ b/impl/src/prop.rs @@ -2,6 +2,9 @@ use crate::ast::{Enum, Field, Struct, Variant}; use crate::unraw::MemberUnraw; use proc_macro2::Span; use syn::Type; +use syn::{ + AngleBracketedGenericArguments, GenericArgument, Lifetime, PathArguments, TypeReference, +}; impl Struct<'_> { pub(crate) fn from_field(&self) -> Option<&Field> { @@ -16,6 +19,10 @@ impl Struct<'_> { backtrace_field(&self.fields) } + pub(crate) fn location_field(&self) -> Option<&Field> { + location_field(&self.fields) + } + pub(crate) fn distinct_backtrace_field(&self) -> Option<&Field> { let backtrace_field = self.backtrace_field()?; distinct_backtrace_field(backtrace_field, self.from_field()) @@ -63,6 +70,10 @@ impl Variant<'_> { backtrace_field(&self.fields) } + pub(crate) fn location_field(&self) -> Option<&Field> { + location_field(&self.fields) + } + pub(crate) fn distinct_backtrace_field(&self) -> Option<&Field> { let backtrace_field = self.backtrace_field()?; distinct_backtrace_field(backtrace_field, self.from_field()) @@ -74,6 +85,10 @@ impl Field<'_> { type_is_backtrace(self.ty) } + pub(crate) fn is_location(&self) -> bool { + type_is_location(self.ty) + } + pub(crate) fn source_span(&self) -> Span { if let Some(source_attr) = &self.attrs.source { source_attr.span @@ -123,6 +138,20 @@ fn backtrace_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { None } +fn location_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + for field in fields { + if field.attrs.location.is_some() { + return Some(field); + } + } + for field in fields { + if field.is_location() { + return Some(field); + } + } + None +} + // The #[backtrace] field, if it is not the same as the #[from] field. fn distinct_backtrace_field<'a, 'b>( backtrace_field: &'a Field<'b>, @@ -146,3 +175,30 @@ fn type_is_backtrace(ty: &Type) -> bool { let last = path.segments.last().unwrap(); last.ident == "Backtrace" && last.arguments.is_empty() } + +fn type_is_location(ty: &Type) -> bool { + let path = match ty { + Type::Reference(TypeReference { + lifetime: Some(Lifetime { ident: ltident, .. }), + elem, // TODO: replace with `elem: box Type::Path(path)` once box_patterns stabalizes + .. + }) if ltident == "static" => match &**elem { + Type::Path(ty) => &ty.path, + _ => return false, + }, + _ => return false, + }; + + let last = path.segments.last().unwrap(); + + last.ident == "Location" + && match &last.arguments { + PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) => { + match args.first() { + Some(GenericArgument::Lifetime(Lifetime { ident, .. })) => ident == "static", + _ => false, + } + } + _ => false, + } +} diff --git a/impl/src/valid.rs b/impl/src/valid.rs index 21bd885..b5f7d28 100644 --- a/impl/src/valid.rs +++ b/impl/src/valid.rs @@ -151,8 +151,10 @@ fn check_non_field_attrs(attrs: &Attrs) -> Result<()> { fn check_field_attrs(fields: &[Field]) -> Result<()> { let mut from_field = None; let mut source_field = None; + let mut location_field = None; let mut backtrace_field = None; let mut has_backtrace = false; + let mut has_location = false; for field in fields { if let Some(from) = field.attrs.from { if from_field.is_some() { @@ -182,6 +184,17 @@ fn check_field_attrs(fields: &[Field]) -> Result<()> { backtrace_field = Some(field); has_backtrace = true; } + if let Some(location) = field.attrs.location { + if location_field.is_some() { + return Err(Error::new_spanned( + location, + "duplicate #[location] attribute", + )); + } + + location_field = Some(field); + has_location = true; + } if let Some(transparent) = field.attrs.transparent { return Err(Error::new_spanned( transparent.original, @@ -189,6 +202,7 @@ fn check_field_attrs(fields: &[Field]) -> Result<()> { )); } has_backtrace |= field.is_backtrace(); + has_location |= field.is_location(); } if let (Some(from_field), Some(source_field)) = (from_field, source_field) { if from_field.member != source_field.member { @@ -199,14 +213,21 @@ fn check_field_attrs(fields: &[Field]) -> Result<()> { } } if let Some(from_field) = from_field { - let max_expected_fields = match backtrace_field { - Some(backtrace_field) => 1 + (from_field.member != backtrace_field.member) as usize, - None => 1 + has_backtrace as usize, + let extra_fields = has_backtrace as usize + has_location as usize; + let max_expected_fields = match (backtrace_field, location_field) { + (Some(backtrace_field), Some(_)) => { + 2 + (from_field.member != backtrace_field.member) as usize + } + (Some(backtrace_field), None) => { + 1 + (from_field.member != backtrace_field.member) as usize + } + (None, Some(_)) => 1 + extra_fields, + (None, None) => 1 + extra_fields, }; if fields.len() > max_expected_fields { return Err(Error::new_spanned( from_field.attrs.from.unwrap().original, - "deriving From requires no fields other than source and backtrace", + "deriving From requires no fields other than source, backtrace, and location", )); } } diff --git a/tests/test_location.rs b/tests/test_location.rs new file mode 100644 index 0000000..a8c7069 --- /dev/null +++ b/tests/test_location.rs @@ -0,0 +1,42 @@ +use std::{fmt::Debug, io, panic::Location}; +use thiserror::Error; + +#[derive(Error, Debug)] +enum MError { + #[error("At {location}: location test error, sourced from {other}")] + Test { + #[from] + other: io::Error, + location: &'static Location<'static>, + }, +} + +#[derive(Error, Debug)] +#[error("At {location} test error, sourced from {other}")] +pub struct TestError { + #[from] + other: io::Error, + location: &'static Location<'static>, +} + +#[test] +#[should_panic] +fn test_enum() { + fn inner() -> Result<(), MError> { + Err(io::Error::new(io::ErrorKind::AddrInUse, String::new()))?; + Ok(()) + } + + inner().unwrap(); +} + +#[test] +#[should_panic] +fn test_struct() { + fn inner() -> Result<(), TestError> { + Err(io::Error::new(io::ErrorKind::AddrInUse, String::new()))?; + Ok(()) + } + + inner().unwrap(); +} diff --git a/tests/ui/from-backtrace-backtrace.stderr b/tests/ui/from-backtrace-backtrace.stderr index 5c0b9a3..5ead7a5 100644 --- a/tests/ui/from-backtrace-backtrace.stderr +++ b/tests/ui/from-backtrace-backtrace.stderr @@ -1,4 +1,4 @@ -error: deriving From requires no fields other than source and backtrace +error: deriving From requires no fields other than source, backtrace, and location --> tests/ui/from-backtrace-backtrace.rs:9:5 | 9 | #[from]