diff --git a/impl/src/attr.rs b/impl/src/attr.rs index 7ad83e0..5c94f52 100644 --- a/impl/src/attr.rs +++ b/impl/src/attr.rs @@ -15,6 +15,7 @@ pub struct Attrs<'a> { pub from: Option>, pub transparent: Option>, pub fmt: Option>, + pub boxing: Option>, } #[derive(Clone)] @@ -53,6 +54,12 @@ pub struct Fmt<'a> { pub path: ExprPath, } +#[derive(Copy, Clone)] +pub struct Boxing<'a> { + pub original: &'a Attribute, + pub span: Span, +} + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] pub enum Trait { Debug, @@ -74,6 +81,7 @@ pub fn get(input: &[Attribute]) -> Result { from: None, transparent: None, fmt: None, + boxing: None, }; for attr in input { @@ -100,7 +108,10 @@ pub fn get(input: &[Attribute]) -> Result { } else if attr.path().is_ident("from") { match attr.meta { Meta::Path(_) => {} - Meta::List(_) | Meta::NameValue(_) => { + Meta::List(_) => { + parse_from_list_attribute(&mut attrs, attr)?; + } + Meta::NameValue(_) => { // Assume this is meant for derive_more crate or something. continue; } @@ -299,6 +310,21 @@ fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result(attrs: &mut Attrs<'a>, attr: &'a Attribute) -> Result<()> { + mod kw { + syn::custom_keyword!(boxing); + } + + if let Ok(kw) = attr.parse_args::() { + attrs.boxing = Some(Boxing { + original: attr, + span: kw.span, + }); + } + + Ok(()) +} + impl ToTokens for Display<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { if self.infinite_recursive { diff --git a/impl/src/expand.rs b/impl/src/expand.rs index c693921..d4fc735 100644 --- a/impl/src/expand.rs +++ b/impl/src/expand.rs @@ -191,6 +191,32 @@ fn impl_struct(input: Struct) -> TokenStream { }) }); + let boxing_impl = input.boxing_field().map(|from_field| { + let span = from_field.attrs.from.unwrap().span; + let from = unoptional_type(from_field.ty); + let source_var = Ident::new("source", span); + let from_function = quote! { + fn from(#source_var: #from) -> Self { + ::std::boxed::Box::new(From::from(#source_var)) + } + }; + let boxing_impl = quote_spanned! {span=> + #[automatically_derived] + impl #impl_generics ::core::convert::From<#from> for ::std::boxed::Box < #ty #ty_generics > #where_clause { + #from_function + } + }; + Some(quote! { + #[allow( + deprecated, + unused_qualifications, + clippy::elidable_lifetime_names, + clippy::needless_lifetimes, + )] + #boxing_impl + }) + }); + if input.generics.type_params().next().is_some() { let self_token = ::default(); error_inferred_bounds.insert(self_token, Trait::Debug); @@ -207,6 +233,7 @@ fn impl_struct(input: Struct) -> TokenStream { } #display_impl #from_impl + #boxing_impl } } @@ -466,6 +493,33 @@ fn impl_enum(input: Enum) -> TokenStream { }) }); + let boxing_impls = input.variants.iter().filter_map(|variant| { + let from_field = variant.boxing_field()?; + let span = from_field.attrs.boxing.unwrap().span; + let from = unoptional_type(from_field.ty); + let source_var = Ident::new("source", span); + let from_function = quote! { + fn from(#source_var: #from) -> Self { + ::std::boxed::Box::new(From::from(#source_var)) + } + }; + let from_impl = quote_spanned! {span=> + #[automatically_derived] + impl #impl_generics ::core::convert::From<#from> for ::std::boxed::Box < #ty #ty_generics > #where_clause { + #from_function + } + }; + Some(quote! { + #[allow( + deprecated, + unused_qualifications, + clippy::elidable_lifetime_names, + clippy::needless_lifetimes, + )] + #from_impl + }) + }); + if input.generics.type_params().next().is_some() { let self_token = ::default(); error_inferred_bounds.insert(self_token, Trait::Debug); @@ -482,6 +536,7 @@ fn impl_enum(input: Enum) -> TokenStream { } #display_impl #(#from_impls)* + #(#boxing_impls)* } } diff --git a/impl/src/prop.rs b/impl/src/prop.rs index 0a101fc..35e83c7 100644 --- a/impl/src/prop.rs +++ b/impl/src/prop.rs @@ -20,6 +20,10 @@ impl Struct<'_> { let backtrace_field = self.backtrace_field()?; distinct_backtrace_field(backtrace_field, self.from_field()) } + + pub(crate) fn boxing_field(&self) -> Option<&Field> { + boxing_field(&self.fields) + } } impl Enum<'_> { @@ -67,6 +71,10 @@ impl Variant<'_> { let backtrace_field = self.backtrace_field()?; distinct_backtrace_field(backtrace_field, self.from_field()) } + + pub(crate) fn boxing_field(&self) -> Option<&Field> { + boxing_field(&self.fields) + } } impl Field<'_> { @@ -137,6 +145,12 @@ fn distinct_backtrace_field<'a, 'b>( } } +fn boxing_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + fields + .iter() + .find(|Field { attrs, .. }| attrs.from.is_some() && attrs.boxing.is_some()) +} + fn type_is_backtrace(ty: &Type) -> bool { let path = match ty { Type::Path(ty) => &ty.path, diff --git a/impl/src/valid.rs b/impl/src/valid.rs index 21bd885..39b3097 100644 --- a/impl/src/valid.rs +++ b/impl/src/valid.rs @@ -107,6 +107,13 @@ impl Field<'_> { } fn check_non_field_attrs(attrs: &Attrs) -> Result<()> { + if let Some(boxing) = &attrs.boxing { + return Err(Error::new_spanned( + boxing.original, + "not expected here; attribute #[from(boxing)] expected on a specific field", + )); + } + if let Some(from) = &attrs.from { return Err(Error::new_spanned( from.original, @@ -218,6 +225,7 @@ fn check_field_attrs(fields: &[Field]) -> Result<()> { )); } } + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index fa90229..7fc91e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -251,6 +251,41 @@ //! } //! ``` //! +//! - Additionally, the `#[from(boxing)]` also derives an implementation of `From` for the boxed error. +//! This is especially useful for ensuring large source errors do not needlessly enlarge every stack frame in which they may be used. +//! See also: . +//! +//! ``` +//! # use thiserror::Error; +//! # +//! #[derive(Error, Debug)] +//! #[error("...")] +//! pub struct ExternalError([u8; 2048]); +//! +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! #[error(transparent)] +//! ExternalCause(#[from(boxing)] ExternalError), +//! +//! #[error("other")] +//! Other, +//! } +//! +//! pub fn do_something() -> Result<(), ExternalError> { +//! # /* +//! ... +//! # */ +//! # Ok(()) +//! } +//! +//! // Automatically boxed +//! pub fn something_else() -> Result<(), Box> { +//! do_something()?; +//! +//! Ok(()) +//! } +//! ``` +//! //! - See also the [`anyhow`] library for a convenient single error type to use //! in application code. //! diff --git a/tests/test_boxing.rs b/tests/test_boxing.rs new file mode 100644 index 0000000..f065116 --- /dev/null +++ b/tests/test_boxing.rs @@ -0,0 +1,124 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("...")] +pub struct LargeError { + a: [u8; 2048], +} + +pub fn direct_return_large() -> Result<(), LargeError> { + Err(LargeError { a: [0; 2048] }) +} + +pub fn non_boxed() -> Result<(), Autoboxed> { + let _ = direct_return_large()?; + + Ok(()) +} + +#[derive(Error, Debug)] +#[error("...")] +pub enum Autoboxed { + Large(#[from(boxing)] LargeError), +} + +pub fn autobox() -> Result<(), Box> { + let _ = direct_return_large()?; + + Ok(()) +} + +#[derive(Error, Debug)] +#[error("...")] +pub struct Autoboxed2 { + #[from(boxing)] + err: LargeError, +} + +pub fn autobox2() -> Result<(), Box> { + let _ = direct_return_large()?; + + Ok(()) +} + +#[derive(Error, Debug)] +#[error("...")] +pub struct Autoboxed3 { + #[from(boxing)] + err: LargeError, +} + +pub fn autobox3() -> Result<(), Box> { + let _ = direct_return_large()?; + + Ok(()) +} + +#[derive(Error, Debug)] +#[error("...")] +pub enum Multiple { + #[error(transparent)] + A(#[from(boxing)] LargeError), + + #[error(transparent)] + B { + #[from(boxing)] + named_field: std::io::Error, + }, +} + +pub fn std_fallible() -> std::io::Result<()> { + unimplemented!() +} + +pub fn boxes_both() -> Result<(), Box> { + let _ = direct_return_large()?; + let _ = std_fallible()?; + + Ok(()) +} + +/// Deliberatly contrived typed to exercise the lifetimes and generics part of the proc macro. +pub trait Origin<'a>: std::error::Error { + fn origin(&self) -> &'a str; +} + +#[derive(Debug, Error)] +#[error("origin {0}")] +pub struct SomeOrigin<'a>(&'a str); + +impl<'a> Origin<'a> for SomeOrigin<'a> { + fn origin(&self) -> &'a str { + self.0 + } +} + +#[derive(Debug, Error)] +pub enum SomeErr<'a, T> +where + T: Origin<'a>, +{ + #[error("...")] + A(#[from(boxing)] T), + + #[error("...")] + B(&'a str), +} + +pub fn bad_thing<'a>(input: &'a str) -> Result<(), SomeOrigin<'a>> { + Err(SomeOrigin(input)) +} + +pub fn boxing_with_lifetimes_and_generics<'a>( + input: &'a str, +) -> Result<(), Box>>> { + let _ = bad_thing(input)?; + Ok(()) +} + +#[derive(Debug, Error)] +#[error("...")] +pub struct IgnoresUnknown { + #[from(other, thing)] + e: std::io::Error, +} diff --git a/tests/ui/from-boxing-duplicate.rs b/tests/ui/from-boxing-duplicate.rs new file mode 100644 index 0000000..2103707 --- /dev/null +++ b/tests/ui/from-boxing-duplicate.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub struct S { + #[from(boxing)] + a: std::io::Error, + + #[from(boxing)] + b: std::io::Error, +} + +fn main() {} diff --git a/tests/ui/from-boxing-duplicate.stderr b/tests/ui/from-boxing-duplicate.stderr new file mode 100644 index 0000000..d376445 --- /dev/null +++ b/tests/ui/from-boxing-duplicate.stderr @@ -0,0 +1,5 @@ +error: duplicate #[from] attribute + --> tests/ui/from-boxing-duplicate.rs:8:5 + | +8 | #[from(boxing)] + | ^^^^^^^^^^^^^^^ diff --git a/tests/ui/from-boxing-in-wrong-position.rs b/tests/ui/from-boxing-in-wrong-position.rs new file mode 100644 index 0000000..881129f --- /dev/null +++ b/tests/ui/from-boxing-in-wrong-position.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +#[from(boxing)] +enum E { + #[error("...")] + A, +} + +fn main() {} diff --git a/tests/ui/from-boxing-in-wrong-position.stderr b/tests/ui/from-boxing-in-wrong-position.stderr new file mode 100644 index 0000000..dcfae4f --- /dev/null +++ b/tests/ui/from-boxing-in-wrong-position.stderr @@ -0,0 +1,5 @@ +error: not expected here; attribute #[from(boxing)] expected on a specific field + --> tests/ui/from-boxing-in-wrong-position.rs:4:1 + | +4 | #[from(boxing)] + | ^^^^^^^^^^^^^^^