Skip to content
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Add `Hash` derive similar to `std`'s one, but considering generics correctly,
and supporting custom hash functions per field or skipping fields.
([#532](https://github.com/JelteF/derive_more/pull/532))
- Support `#[constructor(into)]` attribute on fields in `Constructor` derive, making
the generated `new()` method accept any `impl Into<_>` for that field.
([#551](https://github.com/JelteF/derive_more/pull/551))

### Fixed

Expand Down
55 changes: 55 additions & 0 deletions impl/doc/constructor.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,61 @@ The generated code is similar for more or less fields.



## `Into` arguments

By default the generated `new` method takes each field by its exact type. Adding
`#[constructor(into)]` to a field makes `new` accept any type that implements
[`Into`] that field's type, and calls [`Into::into`] to convert it.

```rust
# use derive_more::Constructor;
#
#[derive(Constructor)]
struct Foo {
id: i32,
#[constructor(into)]
name: String,
}
```

Code like this will be generated:

```rust
# struct Foo {
# id: i32,
# name: String,
# }
impl Foo {
pub fn new(id: i32, name: impl ::core::convert::Into<String>) -> Foo {
Foo { id: id, name: ::core::convert::Into::into(name) }
}
}
```

So a `&str` can be passed where a `String` is expected:

```rust
# use derive_more::Constructor;
#
# #[derive(Constructor)]
# struct Foo {
# id: i32,
# #[constructor(into)]
# name: String,
# }
let foo = Foo::new(1, "hello");
```

Note that, unlike the default, a `new` method with any `#[constructor(into)]` field
cannot be `const`, because trait methods like [`Into::into`] are not yet callable in
`const` contexts.

[`Into`]: https://doc.rust-lang.org/core/convert/trait.Into.html
[`Into::into`]: https://doc.rust-lang.org/core/convert/trait.Into.html#tymethod.into




## Enums

Currently `Constructor` cannot be derived for enums. This is because the `new`
Expand Down
133 changes: 105 additions & 28 deletions impl/src/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,87 @@ use crate::utils::{
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Field, Fields, Ident};
use syn::{
parse::ParseStream, spanned::Spanned as _, Data, DeriveInput, Field, Fields, Ident,
};

/// Provides the hook to expand `#[derive(Constructor)]` into an implementation of `Constructor`
pub fn expand(input: &DeriveInput, _: &str) -> TokenStream {
pub fn expand(input: &DeriveInput, _: &str) -> syn::Result<TokenStream> {
let input_type = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let ((body, vars), fields) = match input.data {
Data::Struct(ref data_struct) => match data_struct.fields {
Fields::Unnamed(ref fields) => {
let field_vec = unnamed_to_vec(fields);
(tuple_body(input_type, &field_vec), field_vec)

let (fields, named) = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Unnamed(fields) => (unnamed_to_vec(fields), false),
Fields::Named(fields) => (named_to_vec(fields), true),
Fields::Unit => (vec![], true),
},
_ => {
return Err(syn::Error::new_spanned(
input,
"only structs can derive a constructor",
))
}
};

// Determine whether each field is annotated with `#[constructor(into)]`.
let into_flags = fields
.iter()
.map(|field| field_is_into(field))
.collect::<syn::Result<Vec<_>>>()?;

// Any `Into` fields will prevent `new()` from being constant
let has_into = into_flags.iter().any(|&into| into);

let constness = if has_into {
quote! {}
} else {
quote! { const }
};

// Parameter names: the field names for named structs, `__0`, `__1`, ... otherwise.
let types = get_field_types(&fields);

let vars: Vec<Ident> = if named {
field_idents(&fields)
.iter()
.map(|ident| (*ident).clone())
.collect()
} else {
numbered_vars(fields.len(), "")
};

// This is where `impl Into<T>` is derived for all fields that have `#[constructor(into)]`
// attached. Otherwise, use raw type
let args = vars
.iter()
.zip(&types)
.zip(&into_flags)
.map(|((var, ty), &into)| {
if into {
quote! { #var: impl ::core::convert::Into<#ty> }
} else {
quote! { #var: #ty }
}
Fields::Named(ref fields) => {
let field_vec = named_to_vec(fields);
(struct_body(input_type, &field_vec), field_vec)
});

// Field initializer expressions, calling `.into()` for `#[constructor(into)]` fields.
let exprs: Vec<TokenStream> = vars
.iter()
.zip(&into_flags)
.map(|(var, &into)| {
if into {
quote! { ::core::convert::Into::into(#var) }
} else {
quote! { #var }
}
Fields::Unit => (struct_body(input_type, &[]), vec![]),
},
_ => panic!("Only structs can derive a constructor"),
})
.collect();

let body = if named {
quote! { #input_type { #(#vars: #exprs),* } }
} else {
quote! { #input_type(#(#exprs),*) }
};

let inherited_lint_attrs = input.attrs.iter().filter(|attr| {
Expand All @@ -30,31 +92,46 @@ pub fn expand(input: &DeriveInput, _: &str) -> TokenStream {
.is_some_and(|name| name == "allow" || name == "expect")
});

let original_types = &get_field_types(&fields);
quote! {
Ok(quote! {
#[allow(deprecated)] // omit warnings on deprecated fields/variants
#[allow(missing_docs)]
#[allow(unreachable_code)] // omit warnings for `!` and other unreachable types
#(#inherited_lint_attrs)* // proxy-pass any `#[allow]`/`#[expect]` attributes
#[automatically_derived]
impl #impl_generics #input_type #ty_generics #where_clause {
#[inline]
pub const fn new(#(#vars: #original_types),*) -> #input_type #ty_generics {
pub #constness fn new(#(#args),*) -> #input_type #ty_generics {
#body
}
}
}
})
}

fn tuple_body(return_type: &Ident, fields: &[&Field]) -> (TokenStream, Vec<Ident>) {
let vars = &numbered_vars(fields.len(), "");
(quote! { #return_type(#(#vars),*) }, vars.clone())
}

fn struct_body(return_type: &Ident, fields: &[&Field]) -> (TokenStream, Vec<Ident>) {
let field_names: &Vec<Ident> =
&field_idents(fields).iter().map(|f| (**f).clone()).collect();
let vars = field_names;
let ret_vars = field_names.clone();
(quote! { #return_type{#(#field_names: #vars),*} }, ret_vars)
/// Parses a field's attributes, returning whether it is annotated with
/// `#[constructor(into)]`.
fn field_is_into(field: &Field) -> syn::Result<bool> {
let mut is_into = false;
for attr in &field.attrs {
if !attr.path().is_ident("constructor") {
continue;
}
attr.parse_args_with(|input: ParseStream<'_>| {
let path: syn::Path = input.parse()?;
if !path.is_ident("into") {
return Err(syn::Error::new(
path.span(),
"only `into` is supported by `#[constructor(...)]`",
));
}
if is_into {
return Err(syn::Error::new(
path.span(),
"only single `#[constructor(into)]` attribute is allowed here",
));
}
is_into = true;
Ok(())
})?;
}
Ok(is_into)
}
8 changes: 7 additions & 1 deletion impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,13 @@ create_derive!(
create_derive!("as_ref", r#as::r#mut, AsMut, as_mut_derive, as_mut);
create_derive!("as_ref", r#as::r#ref, AsRef, as_ref_derive, as_ref);

create_derive!("constructor", constructor, Constructor, constructor_derive);
create_derive!(
"constructor",
constructor,
Constructor,
constructor_derive,
constructor,
);

create_derive!("debug", fmt::debug, Debug, debug_derive, debug);

Expand Down
6 changes: 6 additions & 0 deletions tests/compile_fail/constructor/enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[derive(derive_more::Constructor)]
enum Foo {
Bar(i32),
}

fn main() {}
7 changes: 7 additions & 0 deletions tests/compile_fail/constructor/enum.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
error: only structs can derive a constructor
--> tests/compile_fail/constructor/enum.rs:2:1
|
2 | / enum Foo {
3 | | Bar(i32),
4 | | }
| |_^
4 changes: 4 additions & 0 deletions tests/compile_fail/constructor/unknown_attribute_argument.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#[derive(derive_more::Constructor)]
struct Foo(#[constructor(skip)] i32);

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: only `into` is supported by `#[constructor(...)]`
--> tests/compile_fail/constructor/unknown_attribute_argument.rs:2:26
|
2 | struct Foo(#[constructor(skip)] i32);
| ^^^^
40 changes: 40 additions & 0 deletions tests/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
#![cfg_attr(nightly, feature(never_type))]
#![allow(dead_code)] // some code is tested for type checking only

#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::string::String;

use derive_more::Constructor;

#[derive(Constructor)]
Expand All @@ -24,6 +29,41 @@ struct MyInts(i32, i32);

const MY_INTS: MyInts = MyInts::new(1, 2);

#[derive(Constructor)]
struct IntoTuple(#[constructor(into)] String);

#[derive(Constructor)]
struct IntoStruct {
#[constructor(into)]
value: String,
}

#[derive(Constructor)]
struct IntoMixed {
id: i32,
#[constructor(into)]
name: String,
}

#[test]
fn into_arguments_are_converted() {
// Accepts any `Into<String>`
let tuple = IntoTuple::new("tuple");
assert_eq!(tuple.0, "tuple");

let named = IntoStruct::new("named");
assert_eq!(named.value, "named");

// Non-`Into<...>` fields keep requiring their exact type.
let mixed = IntoMixed::new(1, "mixed");
assert_eq!(mixed.id, 1);
assert_eq!(mixed.name, "mixed");

// An owned `String` should still work
let owned = IntoTuple::new(String::from("owned"));
assert_eq!(owned.0, "owned");
}

#[derive(Constructor)]
struct Point2D {
x: i32,
Expand Down
Loading