Skip to content
Open
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
172 changes: 150 additions & 22 deletions impl/src/from_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,11 @@ struct FlatExpansion<'i> {
/// a value-specific [`attr::RenameAll`] overriding [`FlatExpansion::rename_all`], if any.
///
/// [`syn::Ident`]: struct@syn::Ident
#[allow(clippy::type_complexity)]
matches: Vec<(
&'i syn::Ident,
Either<&'i syn::DataStruct, &'i syn::Variant>,
Option<attr::Rename>,
Option<attr::RenameAll>,
)>,

Expand All @@ -157,6 +159,14 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
fn try_from(input: &'i syn::DeriveInput) -> syn::Result<Self> {
let attr_ident = &format_ident!("from_str");

let FlatContainerAttributes {
rename,
rename_all,
error: custom_error,
} = FlatContainerAttributes::parse_attrs(&input.attrs, attr_ident)?
.map(Spanning::into_inner)
.unwrap_or_default();

let matches = match &input.data {
syn::Data::Struct(data) => {
if !data.fields.is_empty() {
Expand All @@ -165,7 +175,7 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
"only structs with no fields can derive `FromStr`",
));
}
vec![(&input.ident, Either::Left(data), None)]
vec![(&input.ident, Either::Left(data), rename, rename_all)]
}
syn::Data::Enum(data) => data
.variants
Expand All @@ -177,10 +187,16 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
"only enums with no fields can derive `FromStr`",
));
}
let attr =
attr::RenameAll::parse_attrs(&variant.attrs, attr_ident)?
.map(Spanning::into_inner);
Ok((&variant.ident, Either::Right(variant), attr))
let attrs =
FlatVariantAttributes::parse_attrs(&variant.attrs, attr_ident)?
.map(Spanning::into_inner)
.unwrap_or_default();
Ok((
&variant.ident,
Either::Right(variant),
attrs.rename,
attrs.rename_all,
))
})
.collect::<syn::Result<_>>()?,
syn::Data::Union(_) => {
Expand All @@ -191,19 +207,20 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
}
};

let FlatContainerAttributes {
rename_all,
error: custom_error,
} = FlatContainerAttributes::parse_attrs(&input.attrs, attr_ident)?
.map(Spanning::into_inner)
.unwrap_or_default();

let mut similar_matches = <HashMap<_, Vec<_>>>::new();
if rename_all.is_none() {
for (ident, _, renaming) in &matches {
for (ident, _, rename, renaming) in &matches {
let name = ident.to_string();
let lowercased = name.to_lowercase();
if let Some(rename) = renaming {
if let Some(rename) = rename {
let renamed_lowercased = rename.as_str().to_lowercase();
if renamed_lowercased != lowercased {
similar_matches
.entry(renamed_lowercased)
.or_default()
.push(*ident);
}
} else if let Some(rename) = renaming {
let renamed_lowercased = rename.convert_case(&name);
if renamed_lowercased != lowercased {
similar_matches
Expand All @@ -217,9 +234,11 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
}

let mut exact_matches = <HashMap<String, Vec<String>>>::new();
for (ident, _, renaming) in &matches {
for (ident, _, rename, renaming) in &matches {
let name = ident.to_string();
let exact = if let Some(default_renaming) = &rename_all {
let exact = if let Some(rename) = rename {
rename.as_str().to_owned()
} else if let Some(default_renaming) = &rename_all {
renaming
.as_ref()
.unwrap_or(default_renaming)
Expand Down Expand Up @@ -273,10 +292,14 @@ impl ToTokens for FlatExpansion<'_> {
let match_arms = if let Some(default_renaming) = self.rename_all {
self.matches
.iter()
.map(|(ident, value, renaming)| {
let converted = renaming
.unwrap_or(default_renaming)
.convert_case(&ident.to_string());
.map(|(ident, value, rename, renaming)| {
let converted = if let Some(rename) = rename {
rename.as_str().to_owned()
} else {
renaming
.unwrap_or(default_renaming)
.convert_case(&ident.to_string())
};
let constructor = value.self_constructor_empty();

quote! { #converted => #constructor, }
Expand All @@ -285,10 +308,14 @@ impl ToTokens for FlatExpansion<'_> {
} else {
self.matches
.iter()
.map(|(ident, value, renaming)| {
.map(|(ident, value, rename, renaming)| {
let name = ident.to_string();
let constructor = value.self_constructor_empty();
if let Some(rename) = renaming {
if let Some(rename) = rename {
let exact_name = rename.as_str();

quote! { _ if s == #exact_name => #constructor, }
} else if let Some(rename) = renaming {
let exact_name = rename.convert_case(&name);

quote! { _ if s == #exact_name => #constructor, }
Expand Down Expand Up @@ -465,6 +492,89 @@ impl<L: FieldsExt, R: FieldsExt> FieldsExt for Either<&L, &R> {
}
}

/// Representation of possible [`FromStr`] derive macro attributes placed on a variant.
///
/// ```rust,ignore
/// #[<attribute>(rename = "...")]
/// #[<attribute>(rename_all = "<casing>")]
/// ```
#[derive(Default)]
struct FlatVariantAttributes {
/// [`attr::Rename`] for custom renaming.
rename: Option<attr::Rename>,

/// [`attr::RenameAll`] for case conversion.
rename_all: Option<attr::RenameAll>,
}

impl Parse for FlatVariantAttributes {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
mod ident {
use syn::custom_keyword;

custom_keyword!(rename);
custom_keyword!(rename_all);
}

let ahead = input.lookahead1();
if ahead.peek(ident::rename) {
Ok(Self {
rename: Some(input.parse()?),
..Default::default()
})
} else if ahead.peek(ident::rename_all) {
Ok(Self {
rename_all: Some(input.parse()?),
..Default::default()
})
} else {
Err(ahead.error())
}
}
}

impl attr::ParseMultiple for FlatVariantAttributes {
fn merge_attrs(
prev: Spanning<Self>,
new: Spanning<Self>,
name: &syn::Ident,
) -> syn::Result<Spanning<Self>> {
let Spanning {
span: prev_span,
item: mut prev,
} = prev;
let Spanning {
span: new_span,
item: new,
} = new;

if new.rename.and_then(|n| prev.rename.replace(n)).is_some() {
return Err(syn::Error::new(
new_span,
format!(
"multiple `#[{name}(rename=\"...\")]` attributes aren't allowed"
),
));
}

if new
.rename_all
.and_then(|n| prev.rename_all.replace(n))
.is_some()
{
return Err(syn::Error::new(
new_span,
format!("multiple `#[{name}(rename_all=\"...\")]` attributes aren't allowed"),
));
}

Ok(Spanning::new(
prev,
prev_span.join(new_span).unwrap_or(prev_span),
))
}
}

/// Representation of possible [`FromStr`] derive macro attributes placed on an enum or a struct for
/// a [`FlatExpansion`].
///
Expand All @@ -478,6 +588,9 @@ impl<L: FieldsExt, R: FieldsExt> FieldsExt for Either<&L, &R> {
/// be specified only once.
#[derive(Default)]
struct FlatContainerAttributes {
/// [`attr::Rename`] for custom renaming.
rename: Option<attr::Rename>,

/// [`attr::RenameAll`] for case conversion.
rename_all: Option<attr::RenameAll>,

Expand All @@ -491,6 +604,7 @@ impl Parse for FlatContainerAttributes {
use syn::custom_keyword;

custom_keyword!(error);
custom_keyword!(rename);
custom_keyword!(rename_all);
}

Expand All @@ -500,6 +614,11 @@ impl Parse for FlatContainerAttributes {
error: Some(input.parse()?),
..Default::default()
})
} else if ahead.peek(ident::rename) {
Ok(Self {
rename: Some(input.parse()?),
..Default::default()
})
} else if ahead.peek(ident::rename_all) {
Ok(Self {
rename_all: Some(input.parse()?),
Expand All @@ -526,6 +645,15 @@ impl attr::ParseMultiple for FlatContainerAttributes {
item: new,
} = new;

if new.rename.and_then(|n| prev.rename.replace(n)).is_some() {
return Err(syn::Error::new(
new_span,
format!(
"multiple `#[{name}(rename=\"...\")]` attributes aren't allowed"
),
));
}

if new
.rename_all
.and_then(|n| prev.rename_all.replace(n))
Expand Down
49 changes: 49 additions & 0 deletions impl/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,8 @@ pub(crate) mod attr {
feature = "mul_assign",
))]
pub(crate) use self::forward::Forward;
#[cfg(feature = "from_str")]
pub(crate) use self::rename::Rename;
#[cfg(any(feature = "display", feature = "from_str"))]
pub(crate) use self::rename_all::RenameAll;
#[cfg(any(
Expand Down Expand Up @@ -2389,6 +2391,53 @@ pub(crate) mod attr {

impl ParseMultiple for RenameAll {}
}

#[cfg(any(feature = "display", feature = "from_str"))]
mod rename {
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned as _,
token,
};

use super::ParseMultiple;

/// Representation of a `rename` macro attribute.
///
/// ```rust,ignore
/// #[<attribute>(rename = "...")]
/// ```
#[derive(Clone, Debug)]
pub(crate) struct Rename(String);

impl Rename {
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}

impl Parse for Rename {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let _ = input.parse::<syn::Path>().and_then(|p| {
if p.is_ident("rename") {
Ok(p)
} else {
Err(syn::Error::new(
p.span(),
"unknown attribute argument, expected `rename = \"...\"`",
))
}
})?;

input.parse::<token::Eq>()?;

let lit: syn::LitStr = input.parse()?;
Ok(Self(lit.value()))
}
}

impl ParseMultiple for Rename {}
}
}

#[cfg(any(feature = "from", feature = "into"))]
Expand Down
3 changes: 3 additions & 0 deletions tests/compile_fail/as_mut/renamed_generic.stderr
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compiler error output diverges between stable and nightly. Not sure what we can do.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird. I run cargo +stable test --workspace --features full,testing-helpers locally and it doesn't fail:

rustc --version --verbose
rustc 1.92.0 (ded5c06cf 2025-12-08)
binary: rustc
commit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234
commit-date: 2025-12-08
host: aarch64-apple-darwin
release: 1.92.0
LLVM version: 21.1.3

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ error[E0599]: the method `as_mut` exists for struct `Baz<i32>`, but its trait bo
= note: trait bound `Foo<i32>: AsMut<Foo<i32>>` was not satisfied
note: the trait `AsMut` must be implemented
--> $RUST/core/src/convert/mod.rs
|
| pub const trait AsMut<T: PointeeSized>: PointeeSized {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following trait defines an item `as_mut`, perhaps you need to implement it:
candidate #1: `AsMut`
3 changes: 3 additions & 0 deletions tests/compile_fail/as_ref/renamed_generic.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ error[E0599]: the method `as_ref` exists for struct `Baz<i32>`, but its trait bo
= note: trait bound `Foo<i32>: AsRef<Foo<i32>>` was not satisfied
note: the trait `AsRef` must be implemented
--> $RUST/core/src/convert/mod.rs
|
| pub const trait AsRef<T: PointeeSized>: PointeeSized {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following trait defines an item `as_ref`, perhaps you need to implement it:
candidate #1: `AsRef`
2 changes: 1 addition & 1 deletion tests/compile_fail/from_str/enum_unknown_attribute.stderr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
error: expected `error` or `rename_all`
error: expected one of: `error`, `rename`, `rename_all`
--> tests/compile_fail/from_str/enum_unknown_attribute.rs:2:12
|
2 | #[from_str(unknown = "unknown")]
Expand Down
11 changes: 11 additions & 0 deletions tests/compile_fail/from_str/rename_collision.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use derive_more::FromStr;

#[derive(FromStr)]
#[from_str(rename_all = "lowercase")]
enum E {
#[from_str(rename = "b")]
A,
B,
}

fn main() {}
5 changes: 5 additions & 0 deletions tests/compile_fail/from_str/rename_collision.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: `A`, `B` variants cannot have the same "b" string representation
--> tests/compile_fail/from_str/rename_collision.rs:5:6
|
5 | enum E {
| ^
11 changes: 11 additions & 0 deletions tests/compile_fail/from_str/rename_duplicate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use derive_more::FromStr;

#[derive(FromStr)]
enum E {
#[from_str(rename = "a")]
A,
#[from_str(rename = "a")]
B,
}

fn main() {}
5 changes: 5 additions & 0 deletions tests/compile_fail/from_str/rename_duplicate.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: `A`, `B` variants cannot have the same "a" string representation
--> tests/compile_fail/from_str/rename_duplicate.rs:4:6
|
4 | enum E {
| ^
Loading
Loading