From ec4826b74bee617e02d5ba01d28c9ef6dc6a89f8 Mon Sep 17 00:00:00 2001 From: Gavin Crawford <94875769+gavincrawford@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:57:45 -0600 Subject: [PATCH 1/3] feat: `#[constructor(into)]` syntax for automatic `Into` generics --- impl/src/constructor.rs | 133 +++++++++++++++++++++++++++++++--------- impl/src/lib.rs | 8 ++- 2 files changed, 112 insertions(+), 29 deletions(-) diff --git a/impl/src/constructor.rs b/impl/src/constructor.rs index 2ea2612c..b64b008d 100644 --- a/impl/src/constructor.rs +++ b/impl/src/constructor.rs @@ -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 { 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::>>()?; + + // 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 = if named { + field_idents(&fields) + .iter() + .map(|ident| (*ident).clone()) + .collect() + } else { + numbered_vars(fields.len(), "") + }; + + // This is where `impl Into` 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 = 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| { @@ -30,8 +92,7 @@ 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 @@ -39,22 +100,38 @@ pub fn expand(input: &DeriveInput, _: &str) -> TokenStream { #[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) { - let vars = &numbered_vars(fields.len(), ""); - (quote! { #return_type(#(#vars),*) }, vars.clone()) -} - -fn struct_body(return_type: &Ident, fields: &[&Field]) -> (TokenStream, Vec) { - let field_names: &Vec = - &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 { + 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) } diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 0133ed05..d0cf9d63 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -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); From 53b88c0b334203ec2629cad6fb130266fe9b2849 Mon Sep 17 00:00:00 2001 From: Gavin Crawford <94875769+gavincrawford@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:57:45 -0600 Subject: [PATCH 2/3] test: Add tests for `Into` constructors --- tests/compile_fail/constructor/enum.rs | 6 +++ tests/compile_fail/constructor/enum.stderr | 7 ++++ .../constructor/unknown_attribute_argument.rs | 4 ++ .../unknown_attribute_argument.stderr | 5 +++ tests/constructor.rs | 40 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 tests/compile_fail/constructor/enum.rs create mode 100644 tests/compile_fail/constructor/enum.stderr create mode 100644 tests/compile_fail/constructor/unknown_attribute_argument.rs create mode 100644 tests/compile_fail/constructor/unknown_attribute_argument.stderr diff --git a/tests/compile_fail/constructor/enum.rs b/tests/compile_fail/constructor/enum.rs new file mode 100644 index 00000000..83117ecb --- /dev/null +++ b/tests/compile_fail/constructor/enum.rs @@ -0,0 +1,6 @@ +#[derive(derive_more::Constructor)] +enum Foo { + Bar(i32), +} + +fn main() {} diff --git a/tests/compile_fail/constructor/enum.stderr b/tests/compile_fail/constructor/enum.stderr new file mode 100644 index 00000000..849f57ce --- /dev/null +++ b/tests/compile_fail/constructor/enum.stderr @@ -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 | | } + | |_^ diff --git a/tests/compile_fail/constructor/unknown_attribute_argument.rs b/tests/compile_fail/constructor/unknown_attribute_argument.rs new file mode 100644 index 00000000..12e87559 --- /dev/null +++ b/tests/compile_fail/constructor/unknown_attribute_argument.rs @@ -0,0 +1,4 @@ +#[derive(derive_more::Constructor)] +struct Foo(#[constructor(skip)] i32); + +fn main() {} diff --git a/tests/compile_fail/constructor/unknown_attribute_argument.stderr b/tests/compile_fail/constructor/unknown_attribute_argument.stderr new file mode 100644 index 00000000..bdb12510 --- /dev/null +++ b/tests/compile_fail/constructor/unknown_attribute_argument.stderr @@ -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); + | ^^^^ diff --git a/tests/constructor.rs b/tests/constructor.rs index b1af19a5..312029fd 100644 --- a/tests/constructor.rs +++ b/tests/constructor.rs @@ -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)] @@ -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` + 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, From eb32766e0a48f041cf727a2c873b38d02477abea Mon Sep 17 00:00:00 2001 From: gavincrawford <94875769+gavincrawford@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:56:56 -0600 Subject: [PATCH 3/3] doc: Constructor `Into` --- CHANGELOG.md | 3 +++ impl/doc/constructor.md | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833502c2..1349b8aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/impl/doc/constructor.md b/impl/doc/constructor.md index 796a6337..cd8e9cac 100644 --- a/impl/doc/constructor.md +++ b/impl/doc/constructor.md @@ -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) -> 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`