Skip to content

Commit

Permalink
impl rename_all for #[derive(Display)] (JelteF#216)
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Feb 2, 2025
1 parent c5e5e82 commit 3b4683f
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 19 deletions.
4 changes: 2 additions & 2 deletions impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ add = []
add_assign = []
as_ref = ["syn/extra-traits", "syn/visit"]
constructor = []
debug = ["syn/extra-traits", "dep:unicode-xid"]
debug = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"]
deref = []
deref_mut = []
display = ["syn/extra-traits", "dep:unicode-xid"]
display = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"]
error = ["syn/extra-traits"]
from = ["syn/extra-traits"]
from_str = []
Expand Down
35 changes: 27 additions & 8 deletions impl/src/fmt/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::utils::{attr::ParseMultiple as _, Spanning};

use super::{
trait_name_to_attribute_name, ContainerAttributes, ContainsGenericsExt as _,
FmtAttribute,
FmtAttribute, RenameAllAttribute,
};

/// Expands a [`fmt::Display`]-like derive macro.
Expand All @@ -29,9 +29,13 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result<TokenSt
let trait_name = normalize_trait_name(trait_name);
let attr_name = format_ident!("{}", trait_name_to_attribute_name(trait_name));

let attrs = ContainerAttributes::parse_attrs(&input.attrs, &attr_name)?
.map(Spanning::into_inner)
.unwrap_or_default();
let attrs = ContainerAttributes::parse_attrs(&input.attrs, &attr_name)?;
if let Some(attrs) = &attrs {
if matches!(input.data, syn::Data::Struct(_)) {
attrs.validate_for_struct(&attr_name)?;
}
}
let attrs = attrs.map(Spanning::into_inner).unwrap_or_default();
let trait_ident = format_ident!("{trait_name}");
let ident = &input.ident;

Expand Down Expand Up @@ -97,6 +101,7 @@ fn expand_struct(
) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
let s = Expansion {
shared_attr: None,
rename_all: None,
attrs,
fields: &s.fields,
type_params,
Expand Down Expand Up @@ -148,14 +153,18 @@ fn expand_enum(
let (bounds, match_arms) = e.variants.iter().try_fold(
(Vec::new(), TokenStream::new()),
|(mut bounds, mut arms), variant| {
let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?
.map(Spanning::into_inner)
.unwrap_or_default();
let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?;
if let Some(attrs) = &attrs {
attrs.validate_for_struct(attr_name)?;
};
let attrs = attrs.map(Spanning::into_inner).unwrap_or_default();

let ident = &variant.ident;

if attrs.fmt.is_none()
&& variant.fields.is_empty()
&& attr_name != "display"
&& container_attrs.rename_all.is_none()
{
return Err(syn::Error::new(
e.variants.span(),
Expand All @@ -168,6 +177,7 @@ fn expand_enum(

let v = Expansion {
shared_attr: container_attrs.fmt.as_ref(),
rename_all: container_attrs.rename_all,
attrs: &attrs,
fields: &variant.fields,
type_params,
Expand Down Expand Up @@ -234,6 +244,11 @@ struct Expansion<'a> {
/// [`None`] for a struct.
shared_attr: Option<&'a FmtAttribute>,

/// [`RenameAllAttribute`] placed on enum.
///
/// [`None`] for a struct.
rename_all: Option<RenameAllAttribute>,

/// Derive macro [`ContainerAttributes`].
attrs: &'a ContainerAttributes,

Expand Down Expand Up @@ -305,7 +320,11 @@ impl Expansion<'_> {
None => {
if shared_attr_is_wrapping || !has_shared_attr {
body = if self.fields.is_empty() {
let ident_str = self.ident.unraw().to_string();
let ident_str = if let Some(rename_all) = &self.rename_all {
rename_all.convert_case(self.ident)
} else {
self.ident.unraw().to_string()
};

if shared_attr_is_wrapping {
quote! { #ident_str }
Expand Down
145 changes: 138 additions & 7 deletions impl/src/fmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pub(crate) mod debug;
pub(crate) mod display;
mod parsing;

use std::fmt::Display;

use convert_case::{Case, Casing as _};
use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
Expand All @@ -16,7 +19,7 @@ use syn::{
parse_quote,
punctuated::Punctuated,
spanned::Spanned as _,
token,
token, LitStr, Token,
};

use crate::{
Expand Down Expand Up @@ -88,6 +91,62 @@ impl BoundsAttribute {
}
}

/// Representation of a `rename_all` macro attribute.
///
/// ```rust,ignore
/// #[<attribute>(rename_all = "...")]
/// ```
///
/// Possible Cases:
/// - `lowercase`
/// - `UPPERCASE`
/// - `PascalCase`
/// - `camelCase`
/// - `snake_case`
/// - `SCREAMING_SNAKE_CASE`
/// - `kebab-case`
/// - `SCREAMING-KEBAB-CASE`
#[derive(Debug, Clone, Copy)]
struct RenameAllAttribute(Case);

impl RenameAllAttribute {
fn convert_case(&self, ident: &syn::Ident) -> String {
ident.unraw().to_string().to_case(self.0)
}
}

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

input.parse::<Token![=]>()?;

let value: LitStr = input.parse()?;

// TODO should we really do a case insensitive comparision here?
Ok(Self(match value.value().replace(['-', '_'], "").to_lowercase().as_str() {
"lowercase" => Case::Flat,
"uppercase" => Case::UpperFlat,
"pascalcase" => Case::Pascal,
"camelcase" => Case::Camel,
"snakecase" => Case::Snake,
"screamingsnakecase" => Case::UpperSnake,
"kebabcase" => Case::Kebab,
"screamingkebabcase" => Case::UpperKebab,
_ => return Err(syn::Error::new_spanned(value, "unexpected casing expected one of: \"lowercase\", \"UPPERCASE\", \"PascalCase\", \"camelCase\", \"snake_case\", \"SCREAMING_SNAKE_CASE\", \"kebab-case\", or \"SCREAMING-KEBAB-CASE\""))
}))
}
}

/// Representation of a [`fmt`]-like attribute.
///
/// ```rust,ignore
Expand Down Expand Up @@ -516,19 +575,81 @@ struct ContainerAttributes {

/// Addition trait bounds.
bounds: BoundsAttribute,

/// Rename unit enum variants following a similar behavior as [`serde`](https://serde.rs/container-attrs.html#rename_all).
rename_all: Option<RenameAllAttribute>,
}

impl Spanning<ContainerAttributes> {
fn validate_for_struct(&self, attr_name: impl Display) -> syn::Result<()> {
if self.rename_all.is_some() {
Err(syn::Error::new(
self.span,
format_args!("`#[{attr_name}(rename_all=\"...\")]` can not be specified on structs or variants"),
))
} else {
Ok(())
}
}
}

mod kw {
use syn::custom_keyword;

custom_keyword!(rename_all);
custom_keyword!(bounds);
custom_keyword!(bound);
}

impl Parse for ContainerAttributes {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
// We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow
// any error of the `Either::Left` if the `Either::Right` succeeds.
FmtAttribute::check_legacy_fmt(input)?;
<Either<FmtAttribute, BoundsAttribute>>::parse(input).map(|v| match v {
Either::Left(fmt) => Self {
let lookahead = input.lookahead1();
Ok(if lookahead.peek(LitStr) {
Self {
fmt: Some(input.parse()?),
bounds: BoundsAttribute::default(),
fmt: Some(fmt),
},
Either::Right(bounds) => Self { bounds, fmt: None },
rename_all: None,
}
} else if lookahead.peek(kw::rename_all)
|| lookahead.peek(kw::bounds)
|| lookahead.peek(kw::bound)
|| lookahead.peek(Token![where])
{
let mut bounds = BoundsAttribute::default();
let mut rename_all = None;

while !input.is_empty() {
let lookahead = input.lookahead1();
if lookahead.peek(kw::rename_all) {
if rename_all.is_some() {
return Err(
input.error("`rename_all` can only be specified once")
);
} else {
rename_all = Some(input.parse()?);
}
} else if lookahead.peek(kw::bounds)
|| lookahead.peek(kw::bound)
|| lookahead.peek(Token![where])
{
bounds.0.extend(input.parse::<BoundsAttribute>()?.0)
} else {
return Err(lookahead.error());
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Self {
fmt: None,
bounds,
rename_all,
}
} else {
return Err(lookahead.error());
})
}
}
Expand All @@ -554,6 +675,16 @@ impl attr::ParseMultiple for ContainerAttributes {
format!("multiple `#[{name}(\"...\", ...)]` 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"),
));
}
prev.bounds.0.extend(new.bounds.0);

Ok(Spanning::new(
Expand Down Expand Up @@ -582,7 +713,7 @@ where
}
}

/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to travers its type parameters.
/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to traverse its type parameters.
trait ContainsGenericsExt {
/// Checks whether this definition contains any of the provided `type_params`.
fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool;
Expand Down
2 changes: 1 addition & 1 deletion tests/compile_fail/debug/unknown_attribute.stderr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
error: unknown attribute argument, expected `bound(...)`
error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where`
--> tests/compile_fail/debug/unknown_attribute.rs:2:9
|
2 | #[debug(unknown = "unknown")]
Expand Down
7 changes: 7 additions & 0 deletions tests/compile_fail/display/invalid_casing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[derive(derive_more::Display)]
#[display(rename_all = "Whatever")]
enum Enum {
UnitVariant,
}

fn main() {}
5 changes: 5 additions & 0 deletions tests/compile_fail/display/invalid_casing.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: unexpected casing expected one of: "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", or "SCREAMING-KEBAB-CASE"
--> tests/compile_fail/display/invalid_casing.rs:2:24
|
2 | #[display(rename_all = "Whatever")]
| ^^^^^^^^^^
11 changes: 11 additions & 0 deletions tests/compile_fail/display/rename_all_on_struct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#[derive(derive_more::Display)]
enum Enum {
#[display(rename_all = "lowercase")]
RenameAllOnVariant,
}

#[derive(derive_more::Display)]
#[display(rename_all = "lowercase")]
struct Struct;

fn main() {}
11 changes: 11 additions & 0 deletions tests/compile_fail/display/rename_all_on_struct.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: `#[display(rename_all="...")]` can not be specified on structs or variants
--> tests/compile_fail/display/rename_all_on_struct.rs:3:5
|
3 | #[display(rename_all = "lowercase")]
| ^

error: `#[display(rename_all="...")]` can not be specified on structs or variants
--> tests/compile_fail/display/rename_all_on_struct.rs:8:1
|
8 | #[display(rename_all = "lowercase")]
| ^
2 changes: 1 addition & 1 deletion tests/compile_fail/display/unknown_attribute.stderr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
error: unknown attribute argument, expected `bound(...)`
error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where`
--> tests/compile_fail/display/unknown_attribute.rs:3:11
|
3 | #[display(unknown = "unknown")]
Expand Down
Loading

0 comments on commit 3b4683f

Please sign in to comment.