1use std::fmt::Write as _;
2
3use proc_macro2::TokenStream as TokenStream2;
4use quote::{format_ident, quote};
5use syn::{parse_quote, Field, Fields, Index, Type, WherePredicate};
6
7use crate::consts;
8
9pub(crate) fn codegen(
10 fields: &Fields,
11 format_string: &mut String,
12 patterns: &mut Vec<TokenStream2>,
13 defmt_path: &syn::Path,
14) -> syn::Result<(Vec<TokenStream2>, Vec<WherePredicate>)> {
15 let (fields, fields_are_named) = match fields {
16 Fields::Named(named) => (&named.named, true),
17 Fields::Unit => return Ok((vec![], vec![])),
18 Fields::Unnamed(unnamed) => (&unnamed.unnamed, false),
19 };
20
21 if fields.is_empty() {
22 return Ok((vec![], vec![]));
23 }
24
25 if fields_are_named {
26 format_string.push_str(" {{ ");
27 } else {
28 format_string.push('(');
29 }
30
31 let mut stmts = vec![];
32 let mut where_predicates = vec![];
33 let mut is_first = true;
34 for (index, field) in fields.iter().enumerate() {
35 if is_first {
36 is_first = false;
37 } else {
38 format_string.push_str(", ");
39 }
40
41 let format_opt = get_defmt_format_option(field)?;
42 // Find out if the field type is natively supported by defmt. `ty` will be None if not.
43 let ty = as_native_type(&field.ty);
44 // `field_ty` will be the field's type if it is not natively supported by defmt
45 let field_ty = if ty.is_none() { Some(&field.ty) } else { None };
46 // Get the field format specifier. Either the native specifier or '?'.
47 let ty = ty.unwrap_or_else(|| consts::TYPE_FORMAT.to_string());
48 let ident = field
49 .ident
50 .clone()
51 .unwrap_or_else(|| format_ident!("arg{}", index));
52 // Find the required trait bounds for the field and add the formatting statement depending on the field type and the formatting options
53 let bound: Option<syn::Path> = if let Some(FormatOption::Debug2Format) = format_opt {
54 stmts.push(quote!(#defmt_path::export::fmt(&#defmt_path::Debug2Format(&#ident))));
55 field_ty.map(|_| parse_quote!(::core::fmt::Debug))
56 } else if let Some(FormatOption::Display2Format) = format_opt {
57 stmts.push(quote!(#defmt_path::export::fmt(&#defmt_path::Display2Format(&#ident))));
58 field_ty.map(|_| parse_quote!(::core::fmt::Display))
59 } else if ty == consts::TYPE_FORMAT {
60 stmts.push(quote!(#defmt_path::export::fmt(#ident)));
61 field_ty.map(|_| parse_quote!(#defmt_path::Format))
62 } else {
63 let method = format_ident!("{}", ty);
64 stmts.push(quote!(#defmt_path::export::#method(#ident)));
65 field_ty.map(|_| parse_quote!(#defmt_path::Format))
66 };
67 if let Some(bound) = bound {
68 where_predicates.push(parse_quote!(#field_ty: #bound));
69 }
70
71 if field.ident.is_some() {
72 // Named field.
73 write!(format_string, "{ident}: {{={ty}:?}}").ok();
74
75 patterns.push(quote!( #ident ));
76 } else {
77 // Unnamed (tuple) field.
78 write!(format_string, "{{={ty}}}").ok();
79
80 let index = Index::from(index);
81 patterns.push(quote!( #index: #ident ));
82 }
83 }
84
85 if fields_are_named {
86 format_string.push_str(" }}");
87 } else {
88 format_string.push(')');
89 }
90
91 Ok((stmts, where_predicates))
92}
93
94#[derive(Copy, Clone, Eq, PartialEq, Debug)]
95enum FormatOption {
96 Debug2Format,
97 Display2Format,
98}
99
100/// If the field has a valid defmt attribute (e.g. `#[defmt(Debug2Format)]`), returns `Ok(Some(FormatOption))`.
101/// Returns `Err` if we can't parse a valid defmt attribute.
102/// Returns `Ok(None)` if there are no `defmt` attributes on the field.
103fn get_defmt_format_option(field: &Field) -> syn::Result<Option<FormatOption>> {
104 let mut format_option = None;
105
106 for attr in &field.attrs {
107 if attr.path().is_ident("defmt") {
108 if format_option.is_some() {
109 return Err(syn::Error::new_spanned(
110 field,
111 "multiple `defmt` attributes not supported",
112 ));
113 }
114
115 let mut parsed_format = None;
116
117 attr.parse_nested_meta(|meta| {
118 // #[defmt(Debug2Format)]
119 if meta.path.is_ident("Debug2Format") {
120 parsed_format = Some(FormatOption::Debug2Format);
121 return Ok(());
122 }
123
124 // #[defmt(Display2Format)]
125 if meta.path.is_ident("Display2Format") {
126 parsed_format = Some(FormatOption::Display2Format);
127 return Ok(());
128 }
129
130 Err(meta.error("expected `Debug2Format` or `Display2Format`"))
131 })?;
132
133 if parsed_format.is_none() {
134 return Err(syn::Error::new_spanned(
135 &attr.meta,
136 "expected 1 attribute argument",
137 ));
138 }
139
140 format_option = parsed_format;
141 }
142 }
143
144 Ok(format_option)
145}
146
147/// Returns `Some` if `ty` refers to a builtin Rust type that has native support from defmt and does
148/// not have to go through the `Format` trait.
149///
150/// This should return `Some` for all types that can be used as `{=TYPE}`.
151///
152/// Note: This is technically incorrect, since builtin types can be shadowed. However the efficiency
153/// gains are too big to pass up, so we expect user code to not do that.
154fn as_native_type(ty: &Type) -> Option<String> {
155 match ty {
156 Type::Path(path: &TypePath) => {
157 let ident: &Ident = path.path.get_ident()?;
158 let ty_name: String = ident.to_string();
159
160 match &*ty_name {
161 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
162 | "i128" | "isize" | "f32" | "f64" | "bool" | "str" => Some(ty_name),
163 _ => None,
164 }
165 }
166 Type::Reference(ty_ref: &TypeReference) => as_native_type(&ty_ref.elem),
167 _ => None,
168 }
169}
170