Repository: knurling-rs/defmt
Pull request #662 (2022-02-17 17:47:33)

Add attributes for Format derive to use Debug2Format on specific fields
I often have structs or enums where most fields impl `Format`, but a few third-party types do not. It is a pain to have to manually impl `Format` for the whole type when only one field doesn't work. I added an attribute that lets you override specific fields to use `Debug2Format` or `Display2Format` like so:

```rust
#[derive(defmt::Format)]
pub enum Error {
    Serial(serial::Error),
    Timeout(us_timer::TimeoutError),
    Serde(#[defmt(Debug2Format)] serde_json::Error),
    ResponseTooLarge,
}
```


Patch:
diff --git a/book/src/format.md b/book/src/format.md
index 903ac0a..8968adb 100644
--- a/book/src/format.md
+++ b/book/src/format.md
@@ -79,5 +79,32 @@ If you quickly want to get some code running and do not care about it being effi
 
 Note that this always uses `{:?}` to format the contained value, meaning that any provided defmt display hints will be ignored.
 
+When using `#[derive(Format)]` you may use the `#[defmt()]` attribute on specific fields to use these adapter types.
+Example below:
+
+``` rust
+# extern crate defmt;
+# use defmt::Format;
+# mod serde_json {
+#     #[derive(Debug)]
+#     pub enum Error {}
+# }
+#[derive(Format)]
+enum Error {
+    Serde(#[defmt(Debug2Format)] serde_json::Error),
+    ResponseTooLarge,
+}
+
+# struct Celsius();
+# impl std::fmt::Display for Celsius {
+#     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { Ok(()) }
+# }
+#[derive(Format)]
+struct Reading {
+    #[defmt(Display2Format)]
+    temperature: Celsius,
+}
+```
+
 [`Display2Format`]: https://docs.rs/defmt/*/defmt/struct.Display2Format.html
 [`Debug2Format`]: https://docs.rs/defmt/*/defmt/struct.Debug2Format.html
diff --git a/macros/src/derives/format.rs b/macros/src/derives/format.rs
index 877248a..bc6bc3c 100644
--- a/macros/src/derives/format.rs
+++ b/macros/src/derives/format.rs
@@ -9,12 +9,17 @@ pub(crate) fn expand(input: TokenStream) -> TokenStream {
     let mut input = parse_macro_input!(input as DeriveInput);
 
     let ident = &input.ident;
-    let codegen::EncodeData { format_tag, stmts } = match &input.data {
+    let encode_data = match &input.data {
         Data::Enum(data) => codegen::encode_enum_data(ident, data),
         Data::Struct(data) => codegen::encode_struct_data(ident, data),
         Data::Union(_) => abort_call_site!("`#[derive(Format)]` does not support unions"),
     };
 
+    let codegen::EncodeData { format_tag, stmts } = match encode_data {
+        Ok(data) => data,
+        Err(e) => return e.into_compile_error().into(),
+    };
+
     let codegen::Generics {
         impl_generics,
         type_generics,
diff --git a/macros/src/derives/format/codegen.rs b/macros/src/derives/format/codegen.rs
index bbbf7c6..578b7de 100644
--- a/macros/src/derives/format/codegen.rs
+++ b/macros/src/derives/format/codegen.rs
@@ -14,13 +14,13 @@ pub(crate) struct EncodeData {
     pub(crate) stmts: Vec<TokenStream2>,
 }
 
-pub(crate) fn encode_struct_data(ident: &Ident, data: &DataStruct) -> EncodeData {
+pub(crate) fn encode_struct_data(ident: &Ident, data: &DataStruct) -> syn::Result<EncodeData> {
     let mut format_string = ident.to_string();
     let mut stmts = vec![];
     let mut field_patterns = vec![];
 
     let encode_fields_stmts =
-        fields::codegen(&data.fields, &mut format_string, &mut field_patterns);
+        fields::codegen(&data.fields, &mut format_string, &mut field_patterns)?;
 
     stmts.push(quote!(match self {
         Self { #(#field_patterns),* } => {
@@ -29,7 +29,7 @@ pub(crate) fn encode_struct_data(ident: &Ident, data: &DataStruct) -> EncodeData
     }));
 
     let format_tag = construct::interned_string(&format_string, "derived", false);
-    EncodeData { format_tag, stmts }
+    Ok(EncodeData { format_tag, stmts })
 }
 
 pub(crate) struct Generics<'a> {
diff --git a/macros/src/derives/format/codegen/enum_data.rs b/macros/src/derives/format/codegen/enum_data.rs
index 4823245..b44e48e 100644
--- a/macros/src/derives/format/codegen/enum_data.rs
+++ b/macros/src/derives/format/codegen/enum_data.rs
@@ -7,12 +7,12 @@ use crate::construct;
 
 use super::EncodeData;
 
-pub(crate) fn encode(ident: &Ident, data: &DataEnum) -> EncodeData {
+pub(crate) fn encode(ident: &Ident, data: &DataEnum) -> syn::Result<EncodeData> {
     if data.variants.is_empty() {
-        return EncodeData {
+        return Ok(EncodeData {
             stmts: vec![quote!(match *self {})],
             format_tag: construct::interned_string("!", "derived", false),
-        };
+        });
     }
 
     let mut format_string = String::new();
@@ -33,7 +33,7 @@ pub(crate) fn encode(ident: &Ident, data: &DataEnum) -> EncodeData {
 
         let mut field_patterns = vec![];
         let encode_fields_stmts =
-            super::fields::codegen(&variant.fields, &mut format_string, &mut field_patterns);
+            super::fields::codegen(&variant.fields, &mut format_string, &mut field_patterns)?;
         let pattern = quote!( { #(#field_patterns),* } );
 
         let encode_discriminant_stmt = discriminant_encoder.encode(index);
@@ -51,7 +51,7 @@ pub(crate) fn encode(ident: &Ident, data: &DataEnum) -> EncodeData {
         #(#match_arms)*
     })];
 
-    EncodeData { format_tag, stmts }
+    Ok(EncodeData { format_tag, stmts })
 }
 
 enum DiscriminantEncoder {
diff --git a/macros/src/derives/format/codegen/fields.rs b/macros/src/derives/format/codegen/fields.rs
index c47d283..02a1a8e 100644
--- a/macros/src/derives/format/codegen/fields.rs
+++ b/macros/src/derives/format/codegen/fields.rs
@@ -2,7 +2,7 @@ use std::fmt::Write as _;
 
 use proc_macro2::TokenStream as TokenStream2;
 use quote::{format_ident, quote};
-use syn::{Fields, Index, Type};
+use syn::{Field, Fields, Index, Meta, NestedMeta, Type};
 
 use crate::consts;
 
@@ -10,15 +10,15 @@ pub(crate) fn codegen(
     fields: &Fields,
     format_string: &mut String,
     patterns: &mut Vec<TokenStream2>,
-) -> Vec<TokenStream2> {
+) -> syn::Result<Vec<TokenStream2>> {
     let (fields, fields_are_named) = match fields {
         Fields::Named(named) => (&named.named, true),
-        Fields::Unit => return vec![],
+        Fields::Unit => return Ok(vec![]),
         Fields::Unnamed(unnamed) => (&unnamed.unnamed, false),
     };
 
     if fields.is_empty() {
-        return vec![];
+        return Ok(vec![]);
     }
 
     if fields_are_named {
@@ -36,31 +36,33 @@ pub(crate) fn codegen(
             format_string.push_str(", ");
         }
 
+        let format_opt = get_defmt_format_option(field)?;
         let ty = as_native_type(&field.ty).unwrap_or_else(|| consts::TYPE_FORMAT.to_string());
-        if let Some(ident) = field.ident.as_ref() {
-            write!(format_string, "{}: {{={}:?}}", ident, ty).ok();
+        let ident = field
+            .ident
+            .clone()
+            .unwrap_or_else(|| format_ident!("arg{}", index));
+
+        if let Some(FormatOption::Debug2Format) = format_opt {
+            stmts.push(quote!(defmt::export::fmt(&defmt::Debug2Format(&#ident))));
+        } else if let Some(FormatOption::Display2Format) = format_opt {
+            stmts.push(quote!(defmt::export::fmt(&defmt::Display2Format(&#ident))));
+        } else if ty == consts::TYPE_FORMAT {
+            stmts.push(quote!(defmt::export::fmt(#ident)));
+        } else {
+            let method = format_ident!("{}", ty);
+            stmts.push(quote!(defmt::export::#method(#ident)));
+        }
 
-            if ty == consts::TYPE_FORMAT {
-                stmts.push(quote!(defmt::export::fmt(#ident)));
-            } else {
-                let method = format_ident!("{}", ty);
-                stmts.push(quote!(defmt::export::#method(#ident)));
-            }
+        if field.ident.is_some() {
+            // Named field.
+            write!(format_string, "{}: {{={}:?}}", ident, ty).ok();
 
             patterns.push(quote!( #ident ));
         } else {
             // Unnamed (tuple) field.
-
             write!(format_string, "{{={}}}", ty).ok();
 
-            let ident = format_ident!("arg{}", index);
-            if ty == consts::TYPE_FORMAT {
-                stmts.push(quote!(defmt::export::fmt(#ident)));
-            } else {
-                let method = format_ident!("{}", ty);
-                stmts.push(quote!(defmt::export::#method(#ident)));
-            }
-
             let index = Index::from(index);
             patterns.push(quote!( #index: #ident ));
         }
@@ -72,7 +74,64 @@ pub(crate) fn codegen(
         format_string.push(')');
     }
 
-    stmts
+    Ok(stmts)
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+enum FormatOption {
+    Debug2Format,
+    Display2Format,
+}
+
+/// If the field has a valid defmt attribute (e.g. `#[defmt(Debug2Format)]`), returns `Ok(Some(FormatOption))`.
+/// Returns `Err` if we can't parse a valid defmt attribute.
+/// Returns `Ok(None)` if there are no `defmt` attributes on the field.
+fn get_defmt_format_option(field: &Field) -> syn::Result<Option<FormatOption>> {
+    use syn::Error;
+    let attrs = field
+        .attrs
+        .iter()
+        .filter(|a| a.path.is_ident("defmt"))
+        .map(|a| a.parse_meta())
+        .collect::<syn::Result<Vec<_>>>()?;
+    if attrs.len() == 0 {
+        return Ok(None);
+    } else if attrs.len() > 1 {
+        return Err(Error::new_spanned(
+            field,
+            "multiple `defmt` attributes not supported",
+        ));
+    } // else attrs.len() == 1
+    let attr = &attrs[0];
+    let args = match attr {
+        Meta::List(list) => &list.nested,
+        bad => return Err(syn::Error::new_spanned(bad, "unrecognized attribute")),
+    };
+    if args.len() != 1 {
+        return Err(syn::Error::new_spanned(
+            attr,
+            "expected 1 attribute argument",
+        ));
+    }
+    let arg = match &args[0] {
+        NestedMeta::Meta(Meta::Path(arg)) => arg,
+        bad => {
+            return Err(syn::Error::new_spanned(
+                bad,
+                "expected `Debug2Format` or `Display2Format`",
+            ))
+        }
+    };
+    if arg.is_ident("Debug2Format") {
+        Ok(Some(FormatOption::Debug2Format))
+    } else if arg.is_ident("Display2Format") {
+        Ok(Some(FormatOption::Display2Format))
+    } else {
+        Err(syn::Error::new_spanned(
+            arg,
+            "expected `Debug2Format` or `Display2Format`",
+        ))
+    }
 }
 
 /// Returns `Some` if `ty` refers to a builtin Rust type that has native support from defmt and does
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 33f76f0..75d1f02 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -34,7 +34,7 @@ pub fn panic_handler(args: TokenStream, item: TokenStream) -> TokenStream {
 }
 
 /* # Derives */
-#[proc_macro_derive(Format)]
+#[proc_macro_derive(Format, attributes(defmt))]
 #[proc_macro_error]
 pub fn format(input: TokenStream) -> TokenStream {
     derives::format::expand(input)


Test patch:
diff --git a/defmt/tests/encode.rs b/defmt/tests/encode.rs
index 49c7cc6..c87d27c 100644
--- a/defmt/tests/encode.rs
+++ b/defmt/tests/encode.rs
@@ -151,6 +151,91 @@ fn bitfields_assert_range_exclusive() {
     ]);
 }
 
+#[test]
+fn debug_attr_struct() {
+    #[derive(Debug)]
+    struct DebugOnly {
+        _a: i32,
+    }
+
+    #[derive(Format)]
+    struct X {
+        y: bool,
+        #[defmt(Debug2Format)]
+        d: DebugOnly,
+    }
+
+    let index = fetch_string_index();
+    check_format!(
+        &X {
+            y: false,
+            d: DebugOnly { _a: 3 }
+        },
+        [
+            index,         // "X {{ y: {=bool}, d: {=?} }}"
+            0b0u8,         // y
+            inc(index, 1), // DebugOnly's format string
+            b'D',          // Text of the Debug output
+            b'e',
+            b'b',
+            b'u',
+            b'g',
+            b'O',
+            b'n',
+            b'l',
+            b'y',
+            b' ',
+            b'{',
+            b' ',
+            b'_',
+            b'a',
+            b':',
+            b' ',
+            b'3',
+            b' ',
+            b'}',
+            0xffu8
+        ],
+    )
+}
+
+#[test]
+fn display_attr_enum() {
+    use std::fmt;
+    struct DisplayOnly {}
+
+    impl fmt::Display for DisplayOnly {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            f.write_str("Display")
+        }
+    }
+
+    #[derive(Format)]
+    enum X {
+        #[allow(dead_code)]
+        Bool(bool),
+        Display(#[defmt(Display2Format)] DisplayOnly),
+    }
+
+    let index = fetch_string_index();
+    check_format!(
+        &X::Display(DisplayOnly {}),
+        [
+            index,         // "Bool({=bool})|Display({=?})"
+            0b1u8,         // Variant: Display
+            inc(index, 1), // DisplayOnly's format string
+            b'D',          // Text of the Display output
+            b'i',
+            b's',
+            b'p',
+            b'l',
+            b'a',
+            b'y',
+            0xffu8
+        ],
+    )
+}
+
 #[test]
 fn boolean_struct() {
     #[derive(Format)]
@@ -163,7 +248,7 @@ fn boolean_struct() {
     check_format!(
         &X { y: false, z: true },
         [
-            index, // "X {{ x: {=bool}, y: {=bool} }}"
+            index, // "X {{ y: {=bool}, z: {=bool} }}"
             0b0u8, // y
             0b1u8, // z
         ],
@@ -182,7 +267,7 @@ fn single_struct() {
     check_format!(
         &X { y: 1, z: 2 },
         [
-            index, // "X {{ x: {=u8}, y: {=u16} }}"
+            index, // "X {{ y: {=u8}, z: {=u16} }}"
             1u8,   // x
             2u8,   // y.low
             0u8,   // y.high
diff --git a/defmt/tests/ui/derive-empty-attr.rs b/defmt/tests/ui/derive-empty-attr.rs
new file mode 100644
index 0000000..996b0af
--- /dev/null
+++ b/defmt/tests/ui/derive-empty-attr.rs
@@ -0,0 +1,7 @@
+#[derive(defmt::Format)]
+struct S {
+    #[defmt()]
+    f: bool,
+}
+
+fn main() {}
diff --git a/defmt/tests/ui/derive-empty-attr.stderr b/defmt/tests/ui/derive-empty-attr.stderr
new file mode 100644
index 0000000..407dcfa
--- /dev/null
+++ b/defmt/tests/ui/derive-empty-attr.stderr
@@ -0,0 +1,5 @@
+error: expected 1 attribute argument
+ --> $DIR/derive-empty-attr.rs:3:7
+  |
+3 |     #[defmt()]
+  |       ^^^^^^^
diff --git a/defmt/tests/ui/derive-invalid-attr-arg.rs b/defmt/tests/ui/derive-invalid-attr-arg.rs
new file mode 100644
index 0000000..8c558bf
--- /dev/null
+++ b/defmt/tests/ui/derive-invalid-attr-arg.rs
@@ -0,0 +1,7 @@
+#[derive(defmt::Format)]
+struct S {
+    #[defmt(FooBar)]
+    f: bool,
+}
+
+fn main() {}
diff --git a/defmt/tests/ui/derive-invalid-attr-arg.stderr b/defmt/tests/ui/derive-invalid-attr-arg.stderr
new file mode 100644
index 0000000..a67a181
--- /dev/null
+++ b/defmt/tests/ui/derive-invalid-attr-arg.stderr
@@ -0,0 +1,5 @@
+error: expected `Debug2Format` or `Display2Format`
+ --> $DIR/derive-invalid-attr-arg.rs:3:13
+  |
+3 |     #[defmt(FooBar)]
+  |             ^^^^^^
diff --git a/defmt/tests/ui/derive-multi-attr.rs b/defmt/tests/ui/derive-multi-attr.rs
new file mode 100644
index 0000000..6397033
--- /dev/null
+++ b/defmt/tests/ui/derive-multi-attr.rs
@@ -0,0 +1,8 @@
+#[derive(defmt::Format)]
+struct S {
+    #[defmt(Debug2Format)]
+    #[defmt()]
+    f: bool,
+}
+
+fn main() {}
diff --git a/defmt/tests/ui/derive-multi-attr.stderr b/defmt/tests/ui/derive-multi-attr.stderr
new file mode 100644
index 0000000..c5038c8
--- /dev/null
+++ b/defmt/tests/ui/derive-multi-attr.stderr
@@ -0,0 +1,7 @@
+error: multiple `defmt` attributes not supported
+ --> $DIR/derive-multi-attr.rs:3:5
+  |
+3 | /     #[defmt(Debug2Format)]
+4 | |     #[defmt()]
+5 | |     f: bool,
+  | |___________^