Skip to content

Add argument #[from(boxing)] to allow automatic boxing #421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion impl/src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct Attrs<'a> {
pub from: Option<From<'a>>,
pub transparent: Option<Transparent<'a>>,
pub fmt: Option<Fmt<'a>>,
pub boxing: Option<Boxing<'a>>,
}

#[derive(Clone)]
Expand Down Expand Up @@ -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,
Expand All @@ -74,6 +81,7 @@ pub fn get(input: &[Attribute]) -> Result<Attrs> {
from: None,
transparent: None,
fmt: None,
boxing: None,
};

for attr in input {
Expand All @@ -100,7 +108,10 @@ pub fn get(input: &[Attribute]) -> Result<Attrs> {
} 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;
}
Expand Down Expand Up @@ -299,6 +310,21 @@ fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result<TokenStr
Ok(TokenStream::from_iter(tokens))
}

fn parse_from_list_attribute<'a>(attrs: &mut Attrs<'a>, attr: &'a Attribute) -> Result<()> {
mod kw {
syn::custom_keyword!(boxing);
}

if let Ok(kw) = attr.parse_args::<kw::boxing>() {
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 {
Expand Down
55 changes: 55 additions & 0 deletions impl/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Token![Self]>::default();
error_inferred_bounds.insert(self_token, Trait::Debug);
Expand All @@ -207,6 +233,7 @@ fn impl_struct(input: Struct) -> TokenStream {
}
#display_impl
#from_impl
#boxing_impl
}
}

Expand Down Expand Up @@ -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 = <Token![Self]>::default();
error_inferred_bounds.insert(self_token, Trait::Debug);
Expand All @@ -482,6 +536,7 @@ fn impl_enum(input: Enum) -> TokenStream {
}
#display_impl
#(#from_impls)*
#(#boxing_impls)*
}
}

Expand Down
14 changes: 14 additions & 0 deletions impl/src/prop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<'_> {
Expand Down Expand Up @@ -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<'_> {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions impl/src/valid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -218,6 +225,7 @@ fn check_field_attrs(fields: &[Field]) -> Result<()> {
));
}
}

Ok(())
}

Expand Down
35 changes: 35 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://rust-lang.github.io/rust-clippy/master/index.html#result_large_err>.
//!
//! ```
//! # 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<MyError>> {
//! do_something()?;
//!
//! Ok(())
//! }
//! ```
//!
//! - See also the [`anyhow`] library for a convenient single error type to use
//! in application code.
//!
Expand Down
124 changes: 124 additions & 0 deletions tests/test_boxing.rs
Original file line number Diff line number Diff line change
@@ -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<Autoboxed>> {
let _ = direct_return_large()?;

Ok(())
}

#[derive(Error, Debug)]
#[error("...")]
pub struct Autoboxed2 {
#[from(boxing)]
err: LargeError,
}

pub fn autobox2() -> Result<(), Box<Autoboxed2>> {
let _ = direct_return_large()?;

Ok(())
}

#[derive(Error, Debug)]
#[error("...")]
pub struct Autoboxed3 {
#[from(boxing)]
err: LargeError,
}

pub fn autobox3() -> Result<(), Box<Autoboxed3>> {
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<Multiple>> {
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<SomeErr<'a, SomeOrigin<'a>>>> {
let _ = bad_thing(input)?;
Ok(())
}

#[derive(Debug, Error)]
#[error("...")]
pub struct IgnoresUnknown {
#[from(other, thing)]
e: std::io::Error,
}
12 changes: 12 additions & 0 deletions tests/ui/from-boxing-duplicate.rs
Original file line number Diff line number Diff line change
@@ -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() {}
5 changes: 5 additions & 0 deletions tests/ui/from-boxing-duplicate.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: duplicate #[from] attribute
--> tests/ui/from-boxing-duplicate.rs:8:5
|
8 | #[from(boxing)]
| ^^^^^^^^^^^^^^^
10 changes: 10 additions & 0 deletions tests/ui/from-boxing-in-wrong-position.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use thiserror::Error;

#[derive(Debug, Error)]
#[from(boxing)]
enum E {
#[error("...")]
A,
}

fn main() {}
Loading