Repository: JelteF/derive_more
Pull request #243 (2023-03-02 07:05:04)

Error on mutlifield struct or enum variant without formatting attribute in `Display`-like macros
Resolves https://github.com/JelteF/derive_more/pull/232#discussion_r1085791282




## Synopsis

After redesign in #225, some test cases were left unchecked. 




## Solution

Error on mutlifield struct or enum variant without formatting attribute in `Display`-like macros




## Checklist

- [x] Documentation is updated (if required)
- [x] Tests are added/updated (if required)
- [x] [CHANGELOG entry][l:1] is added (if required)




[l:1]: /CHANGELOG.md



Patch:
diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs
index 48d3a6a..d2a8d54 100644
--- a/impl/src/fmt/display.rs
+++ b/impl/src/fmt/display.rs
@@ -81,7 +81,7 @@ fn expand_struct(
         ident,
     };
     let bounds = s.generate_bounds();
-    let body = s.generate_body();
+    let body = s.generate_body()?;
 
     let vars = s.fields.iter().enumerate().map(|(i, f)| {
         let var = f.ident.clone().unwrap_or_else(|| format_ident!("_{i}"));
@@ -138,7 +138,7 @@ fn expand_enum(
                 trait_ident,
                 ident,
             };
-            let arm_body = v.generate_body();
+            let arm_body = v.generate_body()?;
             bounds.extend(v.generate_bounds());
 
             let fields_idents =
@@ -290,29 +290,46 @@ struct Expansion<'a> {
 impl<'a> Expansion<'a> {
     /// Generates [`Display::fmt()`] implementation for a struct or an enum variant.
     ///
+    /// # Errors
+    ///
+    /// In case [`FmtAttribute`] is [`None`] and [`syn::Fields`] length is
+    /// greater than 1.
+    ///
     /// [`Display::fmt()`]: fmt::Display::fmt()
-    fn generate_body(&self) -> TokenStream {
-        if let Some(fmt) = &self.attrs.fmt {
-            let (lit, args) = (&fmt.lit, &fmt.args);
-            quote! {
-                ::core::write!(__derive_more_f, #lit, #( #args ),*)
+    fn generate_body(&self) -> Result<TokenStream> {
+        match &self.attrs.fmt {
+            Some(fmt) => {
+                let (lit, args) = (&fmt.lit, &fmt.args);
+                Ok(quote! {
+                    ::core::write!(__derive_more_f, #lit, #( #args ),*)
+                })
             }
-        } else if self.fields.iter().count() == 1 {
-            let field = self
-                .fields
-                .iter()
-                .next()
-                .unwrap_or_else(|| unreachable!("count() == 1"));
-            let ident = field.ident.clone().unwrap_or_else(|| format_ident!("_0"));
-            let trait_ident = self.trait_ident;
-            quote! {
-                ::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
+            None if self.fields.is_empty() => {
+                let ident_str = self.ident.to_string();
+                Ok(quote! {
+                    ::core::write!(__derive_more_f, #ident_str)
+                })
             }
-        } else {
-            let ident_str = self.ident.to_string();
-            quote! {
-                ::core::write!(__derive_more_f, #ident_str)
+            None if self.fields.len() == 1 => {
+                let field = self
+                    .fields
+                    .iter()
+                    .next()
+                    .unwrap_or_else(|| unreachable!("count() == 1"));
+                let ident = field.ident.clone().unwrap_or_else(|| format_ident!("_0"));
+                let trait_ident = self.trait_ident;
+                Ok(quote! {
+                    ::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
+                })
             }
+            _ => Err(Error::new(
+                self.fields.span(),
+                format!(
+                    "struct or enum variant with more than 1 field must have \
+                     `#[{}(\"...\", ...)]` attribute",
+                    trait_name_to_attribute_name(self.trait_ident),
+                ),
+            )),
         }
     }
 
@@ -338,16 +355,19 @@ impl<'a> Expansion<'a> {
 }
 
 /// Matches the provided `trait_name` to appropriate [`Attribute::Fmt`] argument name.
-fn trait_name_to_attribute_name(trait_name: &str) -> &'static str {
-    match trait_name {
-        "Binary" => "binary",
-        "Display" => "display",
-        "LowerExp" => "lower_exp",
-        "LowerHex" => "lower_hex",
-        "Octal" => "octal",
-        "Pointer" => "pointer",
-        "UpperExp" => "upper_exp",
-        "UpperHex" => "upper_hex",
+fn trait_name_to_attribute_name<T>(trait_name: T) -> &'static str
+where
+    T: for<'a> PartialEq<&'a str>,
+{
+    match () {
+        _ if trait_name == "Binary" => "binary",
+        _ if trait_name == "Display" => "display",
+        _ if trait_name == "LowerExp" => "lower_exp",
+        _ if trait_name == "LowerHex" => "lower_hex",
+        _ if trait_name == "Octal" => "octal",
+        _ if trait_name == "Pointer" => "pointer",
+        _ if trait_name == "UpperExp" => "upper_exp",
+        _ if trait_name == "UpperHex" => "upper_hex",
         _ => unimplemented!(),
     }
 }


Test patch:
diff --git a/tests/compile_fail/display/mutlifield_enum_variant_without_attribute.rs b/tests/compile_fail/display/mutlifield_enum_variant_without_attribute.rs
new file mode 100644
index 0000000..4deec36
--- /dev/null
+++ b/tests/compile_fail/display/mutlifield_enum_variant_without_attribute.rs
@@ -0,0 +1,6 @@
+#[derive(derive_more::Display)]
+enum Enum {
+    Variant { foo: String, bar: String },
+}
+
+fn main() {}
diff --git a/tests/compile_fail/display/mutlifield_enum_variant_without_attribute.stderr b/tests/compile_fail/display/mutlifield_enum_variant_without_attribute.stderr
new file mode 100644
index 0000000..e6c29e2
--- /dev/null
+++ b/tests/compile_fail/display/mutlifield_enum_variant_without_attribute.stderr
@@ -0,0 +1,5 @@
+error: struct or enum variant with more than 1 field must have `#[display("...", ...)]` attribute
+ --> tests/compile_fail/display/mutlifield_enum_variant_without_attribute.rs:3:13
+  |
+3 |     Variant { foo: String, bar: String },
+  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/tests/compile_fail/display/mutlifield_struct_without_attribute.rs b/tests/compile_fail/display/mutlifield_struct_without_attribute.rs
new file mode 100644
index 0000000..a9df0d4
--- /dev/null
+++ b/tests/compile_fail/display/mutlifield_struct_without_attribute.rs
@@ -0,0 +1,7 @@
+#[derive(derive_more::Display)]
+pub struct Foo {
+    foo: String,
+    bar: String,
+}
+
+fn main() {}
diff --git a/tests/compile_fail/display/mutlifield_struct_without_attribute.stderr b/tests/compile_fail/display/mutlifield_struct_without_attribute.stderr
new file mode 100644
index 0000000..f3cbc67
--- /dev/null
+++ b/tests/compile_fail/display/mutlifield_struct_without_attribute.stderr
@@ -0,0 +1,9 @@
+error: struct or enum variant with more than 1 field must have `#[display("...", ...)]` attribute
+ --> tests/compile_fail/display/mutlifield_struct_without_attribute.rs:2:16
+  |
+2 |   pub struct Foo {
+  |  ________________^
+3 | |     foo: String,
+4 | |     bar: String,
+5 | | }
+  | |_^