Repository: JelteF/derive_more
Pull request #438 (2025-01-16 12:49:29)

Fix transparency for top-level shared attribute in `Display`
## Synopsis

The transparency is not considered when the top-level shared `#[display(...)]` attribute on enum uses a field directly.

```rust
#[derive(Display)]
#[display("{_0}")]
enum Foo<A> {
    A(A),
}

assert_eq!(format!("{:03}", Foo::<i8>::A(7)), "007");
```
results in:
```
assertion `left == right` failed
  left: "7"
 right: "007"
```

## Solution

Consider field's transparency for the top-level shared `#[display(...)]` attribute.

## Checklist

- [x] ~~Documentation is updated~~ (not required)
- [x] Tests are added/updated
- [x] [CHANGELOG entry](/CHANGELOG.md) is added



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7370e5d..ebf7bfc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
   ([#431](https://github.com/JelteF/derive_more/pull/431))
 - Incorrect rendering of raw identifiers as field names in `Debug` expansions.
   ([#431](https://github.com/JelteF/derive_more/pull/431))
+- Top-level `#[display("...")]` attribute on an enum not working transparently
+  for directly specified fields.
+  ([#438](https://github.com/JelteF/derive_more/pull/438))
 
 
 ## 1.0.0 - 2024-08-07
diff --git a/impl/src/fmt/debug.rs b/impl/src/fmt/debug.rs
index f31270c..e2c8bc8 100644
--- a/impl/src/fmt/debug.rs
+++ b/impl/src/fmt/debug.rs
@@ -13,7 +13,7 @@ use crate::utils::{
 
 use super::{
     trait_name_to_attribute_name, ContainerAttributes, ContainsGenericsExt as _,
-    FieldsExt as _, FmtAttribute,
+    FmtAttribute,
 };
 
 /// Expands a [`fmt::Debug`] derive macro.
@@ -250,23 +250,17 @@ impl Expansion<'_> {
     /// [`Debug::fmt()`]: std::fmt::Debug::fmt()
     fn generate_body(&self) -> syn::Result<TokenStream> {
         if let Some(fmt) = &self.attr.fmt {
-            return Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() {
-                let expr = if let Some(field) = self
-                    .fields
-                    .fmt_args_idents()
-                    .find(|field| expr == *field || expr == field.unraw())
+            return Ok(
+                if let Some((expr, trait_ident)) =
+                    fmt.transparent_call_on_fields(self.fields)
                 {
-                    quote! { #field }
+                    quote! { derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) }
                 } else {
-                    quote! { &(#expr) }
-                };
-
-                quote! { derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) }
-            } else {
-                let deref_args = fmt.additional_deref_args(self.fields);
+                    let deref_args = fmt.additional_deref_args(self.fields);
 
-                quote! { derive_more::core::write!(__derive_more_f, #fmt, #(#deref_args),*) }
-            });
+                    quote! { derive_more::core::write!(__derive_more_f, #fmt, #(#deref_args),*) }
+                },
+            );
         };
 
         match self.fields {
diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs
index a7cca60..758d858 100644
--- a/impl/src/fmt/display.rs
+++ b/impl/src/fmt/display.rs
@@ -11,7 +11,7 @@ use crate::utils::{attr::ParseMultiple as _, Spanning};
 
 use super::{
     trait_name_to_attribute_name, ContainerAttributes, ContainsGenericsExt as _,
-    FieldsExt as _, FmtAttribute,
+    FmtAttribute,
 };
 
 /// Expands a [`fmt::Display`]-like derive macro.
@@ -291,17 +291,9 @@ impl Expansion<'_> {
                     let deref_args = fmt.additional_deref_args(self.fields);
 
                     quote! { &derive_more::core::format_args!(#fmt, #(#deref_args),*) }
-                } else if let Some((expr, trait_ident)) = fmt.transparent_call() {
-                    let expr = if let Some(field) = self
-                        .fields
-                        .fmt_args_idents()
-                        .find(|field| expr == *field || expr == field.unraw())
-                    {
-                        quote! { #field }
-                    } else {
-                        quote! { &(#expr) }
-                    };
-
+                } else if let Some((expr, trait_ident)) =
+                    fmt.transparent_call_on_fields(self.fields)
+                {
                     quote! { derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) }
                 } else {
                     let deref_args = fmt.additional_deref_args(self.fields);
@@ -358,8 +350,14 @@ impl Expansion<'_> {
             if let Some(shared_fmt) = &self.shared_attr {
                 let deref_args = shared_fmt.additional_deref_args(self.fields);
 
-                let shared_body = quote! {
-                    derive_more::core::write!(__derive_more_f, #shared_fmt, #(#deref_args),*)
+                let shared_body = if let Some((expr, trait_ident)) =
+                    shared_fmt.transparent_call_on_fields(self.fields)
+                {
+                    quote! { derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) }
+                } else {
+                    quote! {
+                        derive_more::core::write!(__derive_more_f, #shared_fmt, #(#deref_args),*)
+                    }
                 };
 
                 body = if body.is_empty() {
diff --git a/impl/src/fmt/mod.rs b/impl/src/fmt/mod.rs
index 8dc7e33..70ed407 100644
--- a/impl/src/fmt/mod.rs
+++ b/impl/src/fmt/mod.rs
@@ -13,6 +13,7 @@ use quote::{format_ident, quote, ToTokens};
 use syn::{
     ext::IdentExt as _,
     parse::{Parse, ParseStream},
+    parse_quote,
     punctuated::Punctuated,
     spanned::Spanned as _,
     token,
@@ -199,6 +200,30 @@ impl FmtAttribute {
         Some((expr, format_ident!("{trait_name}")))
     }
 
+    /// Same as [`transparent_call()`], but additionally checks the returned [`Expr`] whether it's
+    /// one of the [`fmt_args_idents`] of the provided [`syn::Fields`], and makes it suitable for
+    /// passing directly into the transparent call of the delegated formatting trait.
+    ///
+    /// [`fmt_args_idents`]: FieldsExt::fmt_args_idents
+    /// [`transparent_call()`]: FmtAttribute::transparent_call
+    fn transparent_call_on_fields(
+        &self,
+        fields: &syn::Fields,
+    ) -> Option<(Expr, syn::Ident)> {
+        self.transparent_call().map(|(expr, trait_ident)| {
+            let expr = if let Some(field) = fields
+                .fmt_args_idents()
+                .find(|field| expr == *field || expr == field.unraw())
+            {
+                field.into()
+            } else {
+                parse_quote! { &(#expr) }
+            };
+
+            (expr, trait_ident)
+        })
+    }
+
     /// Returns an [`Iterator`] over bounded [`syn::Type`]s (and correspondent trait names) by this
     /// [`FmtAttribute`].
     fn bounded_types<'a>(


Test patch:
diff --git a/tests/display.rs b/tests/display.rs
index a48ed79..4120e3a 100644
--- a/tests/display.rs
+++ b/tests/display.rs
@@ -2629,6 +2629,286 @@ mod generic {
                     format!("{:018p}", &a),
                 );
             }
+
+            mod shared_format {
+                use super::*;
+
+                #[derive(Display)]
+                #[display("{_0}")]
+                enum EnumDisplay<A, B> {
+                    A(A),
+                    B(B),
+                }
+
+                #[derive(Display)]
+                #[display("{:b}", _0)]
+                enum EnumBinary<A, B, C, D> {
+                    A(A, C),
+                    B(B, D),
+                }
+
+                #[derive(Display)]
+                #[display("{:o}", d)]
+                enum EnumOctal<A, B, C, D> {
+                    A { b: A, d: C },
+                    B { b: B, d: D },
+                }
+
+                #[derive(Display)]
+                #[display("{field:x}")]
+                enum EnumLowerHex<A, B> {
+                    A { field: A },
+                    B { field: B },
+                }
+
+                #[derive(Display)]
+                #[display("{:X}", field)]
+                enum EnumUpperHex<A, B> {
+                    A { field: A },
+                    B { field: B },
+                }
+
+                #[derive(Display)]
+                #[display("{_0:e}")]
+                enum EnumLowerExp<A, B> {
+                    A(A),
+                    B(B),
+                }
+
+                #[derive(Display)]
+                #[display("{:E}", _0)]
+                enum EnumUpperExp<A, B> {
+                    A(A),
+                    B(B),
+                }
+
+                #[derive(Display)]
+                #[display("{_0:p}")]
+                enum EnumPointer<A, B> {
+                    A(A),
+                    B(B),
+                }
+
+                #[test]
+                fn assert() {
+                    assert_eq!(format!("{:03}", EnumDisplay::<i8, u8>::A(7)), "007");
+                    assert_eq!(format!("{:03}", EnumDisplay::<i8, u8>::B(8)), "008");
+                    assert_eq!(format!("{:07}", TupleBinary(7, ())), "0000111");
+                    assert_eq!(
+                        format!("{:07}", EnumBinary::<i8, u8, (), ()>::A(7, ())),
+                        "0000111",
+                    );
+                    assert_eq!(
+                        format!("{:07}", EnumBinary::<i8, u8, (), ()>::B(8, ())),
+                        "0001000",
+                    );
+                    assert_eq!(
+                        format!(
+                            "{:03}",
+                            EnumOctal::<(), (), i8, u8>::A { b: (), d: 9 },
+                        ),
+                        "011",
+                    );
+                    assert_eq!(
+                        format!(
+                            "{:03}",
+                            EnumOctal::<(), (), i8, u8>::B { b: (), d: 10 },
+                        ),
+                        "012",
+                    );
+                    assert_eq!(
+                        format!("{:03}", EnumLowerHex::<i8, u8>::A { field: 42 }),
+                        "02a",
+                    );
+                    assert_eq!(
+                        format!("{:03}", EnumLowerHex::<i8, u8>::B { field: 43 }),
+                        "02b",
+                    );
+                    assert_eq!(
+                        format!("{:03}", EnumUpperHex::<i8, u8>::A { field: 42 }),
+                        "02A",
+                    );
+                    assert_eq!(
+                        format!("{:03}", EnumUpperHex::<i8, u8>::B { field: 43 }),
+                        "02B",
+                    );
+                    assert_eq!(
+                        format!("{:07}", EnumLowerExp::<f32, f64>::A(42.0)),
+                        "004.2e1"
+                    );
+                    assert_eq!(
+                        format!("{:07}", EnumLowerExp::<f32, f64>::B(43.0)),
+                        "004.3e1"
+                    );
+                    assert_eq!(
+                        format!("{:07}", EnumUpperExp::<f32, f64>::A(42.0)),
+                        "004.2E1"
+                    );
+                    assert_eq!(
+                        format!("{:07}", EnumUpperExp::<f32, f64>::B(43.0)),
+                        "004.3E1",
+                    );
+                    let (a, b) = (42, 7);
+                    assert_eq!(
+                        format!("{:018}", EnumPointer::<&i8, &u8>::A(&b)),
+                        format!("{:018p}", &b),
+                    );
+                    assert_eq!(
+                        format!("{:018}", EnumPointer::<&i8, &u8>::B(&a)),
+                        format!("{:018p}", &a),
+                    );
+                }
+
+                mod mixed {
+                    use super::*;
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumDisplay<A, B> {
+                        A(A),
+                        #[display("{_0}")]
+                        B(B),
+                    }
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumBinary<A, B, C, D> {
+                        #[display("{_0:b}")]
+                        A(A, C),
+                        #[display("{:b}", b)]
+                        B { b: B, d: D },
+                    }
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumOctal<A, B, C, D> {
+                        #[display("{_1:o}")]
+                        A(A, C),
+                        #[display("{:o}", d)]
+                        B { b: B, d: D },
+                    }
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumLowerHex<A, B> {
+                        #[display("{_0:x}")]
+                        A(A),
+                        #[display("{:x}", field)]
+                        B { field: B },
+                    }
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumUpperHex<A, B> {
+                        #[display("{_0:X}")]
+                        A(A),
+                        #[display("{:X}", field)]
+                        B { field: B },
+                    }
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumLowerExp<A, B> {
+                        #[display("{:e}", _0)]
+                        A(A),
+                        #[display("{field:e}")]
+                        B { field: B },
+                    }
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumUpperExp<A, B> {
+                        #[display("{:E}", _0)]
+                        A(A),
+                        #[display("{field:E}")]
+                        B { field: B },
+                    }
+
+                    #[derive(Display)]
+                    #[display("{_variant}")]
+                    enum EnumPointer<A, B> {
+                        #[display("{_0:p}")]
+                        A(A),
+                        #[display("{field:p}")]
+                        B { field: B },
+                    }
+
+                    #[test]
+                    fn assert() {
+                        assert_eq!(
+                            format!("{:03}", EnumDisplay::<i8, u8>::A(7)),
+                            "007",
+                        );
+                        assert_eq!(
+                            format!("{:03}", EnumDisplay::<i8, u8>::B(8)),
+                            "008",
+                        );
+                        assert_eq!(
+                            format!("{:07}", EnumBinary::<_, i8, _, ()>::A(7, ())),
+                            "0000111",
+                        );
+                        assert_eq!(
+                            format!(
+                                "{:07}",
+                                EnumBinary::<i8, _, (), _>::B { b: 8, d: () },
+                            ),
+                            "0001000",
+                        );
+                        assert_eq!(
+                            format!("{:03}", EnumOctal::<_, (), _, i8>::A((), 9)),
+                            "011",
+                        );
+                        assert_eq!(
+                            format!(
+                                "{:03}",
+                                EnumOctal::<(), _, i8, _>::B { b: (), d: 10 }
+                            ),
+                            "012",
+                        );
+                        assert_eq!(
+                            format!("{:03}", EnumLowerHex::<_, i8>::A(42)),
+                            "02a",
+                        );
+                        assert_eq!(
+                            format!("{:03}", EnumLowerHex::<i8, _>::B { field: 43 }),
+                            "02b",
+                        );
+                        assert_eq!(
+                            format!("{:03}", EnumUpperHex::<_, i8>::A(42)),
+                            "02A",
+                        );
+                        assert_eq!(
+                            format!("{:03}", EnumUpperHex::<i8, _>::B { field: 43 }),
+                            "02B",
+                        );
+                        assert_eq!(
+                            format!("{:07}", EnumLowerExp::<_, i8>::A(42.0)),
+                            "004.2e1",
+                        );
+                        assert_eq!(
+                            format!("{:07}", EnumLowerExp::<i8, _>::B { field: 43.0 }),
+                            "004.3e1",
+                        );
+                        assert_eq!(
+                            format!("{:07}", EnumUpperExp::<_, i8>::A(42.0)),
+                            "004.2E1",
+                        );
+                        assert_eq!(
+                            format!("{:07}", EnumUpperExp::<i8, _>::B { field: 43.0 }),
+                            "004.3E1",
+                        );
+                        let (a, b) = (42, 7);
+                        assert_eq!(
+                            format!("{:018}", EnumPointer::<_, &i8>::A(&b)),
+                            format!("{:018p}", &b),
+                        );
+                        assert_eq!(
+                            format!("{:018}", EnumPointer::<&i8, _>::B { field: &a }),
+                            format!("{:018p}", &a),
+                        );
+                    }
+                }
+            }
         }
 
         mod omitted {