Skip to main content

dumpit_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::quote;
4use syn::{
5    Data, DeriveInput, Expr, Fields, Index, Lit, Token,
6    parse::{Parse, ParseStream},
7    parse_macro_input,
8};
9
10// ---------------------------------------------------------------------------
11// Attribute model
12// ---------------------------------------------------------------------------
13
14#[derive(Default)]
15struct FieldAttr {
16    skip: bool,
17    skip_if: Option<String>,
18    format_str: Option<String>,
19    format_args: Vec<String>,
20    literal: Option<Lit>,
21    with: Option<String>,
22    take: Option<Lit>,
23    truncate: Option<usize>,
24}
25
26struct FormatArgs {
27    fmt: String,
28    args: Vec<String>,
29}
30
31impl Parse for FormatArgs {
32    fn parse(input: ParseStream) -> syn::Result<Self> {
33        let lit: syn::LitStr = input.parse()?;
34        let mut args = Vec::new();
35        while input.peek(Token![,]) {
36            let _: Token![,] = input.parse()?;
37            if input.is_empty() {
38                break;
39            }
40            let expr: Expr = input.parse()?;
41            args.push(quote!(#expr).to_string());
42        }
43        Ok(FormatArgs {
44            fmt: lit.value(),
45            args,
46        })
47    }
48}
49
50fn parse_field_attrs(attrs: &[syn::Attribute]) -> syn::Result<FieldAttr> {
51    let mut fa = FieldAttr::default();
52    for attr in attrs {
53        if !attr.path().is_ident("dump") {
54            continue;
55        }
56        attr.parse_nested_meta(|meta| {
57            if meta.path.is_ident("skip") {
58                fa.skip = true;
59                return Ok(());
60            }
61            if meta.path.is_ident("skip_if") {
62                if meta.input.peek(syn::token::Paren) {
63                    let content;
64                    syn::parenthesized!(content in meta.input);
65                    let lit: syn::LitStr = content.parse()?;
66                    fa.skip_if = Some(lit.value());
67                } else {
68                    let _: Token![=] = meta.input.parse()?;
69                    let lit: syn::LitStr = meta.input.parse()?;
70                    fa.skip_if = Some(lit.value());
71                }
72                return Ok(());
73            }
74            if meta.path.is_ident("format") {
75                let content;
76                syn::parenthesized!(content in meta.input);
77                let parsed: FormatArgs = content.parse()?;
78                fa.format_str = Some(parsed.fmt);
79                fa.format_args = parsed.args;
80                return Ok(());
81            }
82            if meta.path.is_ident("literal") {
83                if meta.input.peek(syn::token::Paren) {
84                    let content;
85                    syn::parenthesized!(content in meta.input);
86                    let lit: Lit = content.parse()?;
87                    fa.literal = Some(lit);
88                } else {
89                    let _: Token![=] = meta.input.parse()?;
90                    let lit: Lit = meta.input.parse()?;
91                    fa.literal = Some(lit);
92                }
93                return Ok(());
94            }
95            if meta.path.is_ident("with") {
96                if meta.input.peek(syn::token::Paren) {
97                    let content;
98                    syn::parenthesized!(content in meta.input);
99                    let lit: syn::LitStr = content.parse()?;
100                    fa.with = Some(lit.value());
101                } else {
102                    let _: Token![=] = meta.input.parse()?;
103                    let lit: syn::LitStr = meta.input.parse()?;
104                    fa.with = Some(lit.value());
105                }
106                return Ok(());
107            }
108            if meta.path.is_ident("take") {
109                if meta.input.peek(syn::token::Paren) {
110                    let content;
111                    syn::parenthesized!(content in meta.input);
112                    let lit: Lit = content.parse()?;
113                    fa.take = Some(lit);
114                } else {
115                    let _: Token![=] = meta.input.parse()?;
116                    let lit: Lit = meta.input.parse()?;
117                    fa.take = Some(lit);
118                }
119                return Ok(());
120            }
121            if meta.path.is_ident("truncate") {
122                if meta.input.peek(syn::token::Paren) {
123                    let content;
124                    syn::parenthesized!(content in meta.input);
125                    let lit: syn::LitInt = content.parse()?;
126                    fa.truncate = Some(lit.base10_parse()?);
127                } else {
128                    let _: Token![=] = meta.input.parse()?;
129                    let lit: syn::LitInt = meta.input.parse()?;
130                    fa.truncate = Some(lit.base10_parse()?);
131                }
132                return Ok(());
133            }
134            Err(meta.error("unrecognized dump attribute"))
135        })?;
136    }
137    Ok(fa)
138}
139
140// ---------------------------------------------------------------------------
141// Value expression generation
142// ---------------------------------------------------------------------------
143
144/// Generate the complete field call statement including any let-bindings
145/// needed for temporaries. `emit_field` receives a `&dyn Debug` expression.
146fn field_debug_token(
147    attr: &FieldAttr,
148    access: &TokenStream2,
149    field_name: Option<&str>,
150    emit_field: impl FnOnce(TokenStream2) -> TokenStream2,
151) -> TokenStream2 {
152    if attr.skip {
153        return quote! {};
154    }
155
156    // take needs special handling: the field name becomes "name(n/total)"
157    let body = if let Some(n) = &attr.take {
158        if let Some(name) = field_name {
159            quote! {
160                {
161                    let __take_val = ::dumpit::TakeIter(#access, #n);
162                    let __field_name = __take_val.field_name(#name);
163                    __ds.field(&__field_name, &__take_val as &dyn ::core::fmt::Debug);
164                }
165            }
166        } else {
167            // unnamed field (tuple struct/variant) — no name to attach count to
168            let bindings = quote! {
169                let __take_val = ::dumpit::TakeIter(#access, #n);
170            };
171            let val_ref = quote! { &__take_val as &dyn ::core::fmt::Debug };
172            let field_call = emit_field(val_ref);
173            quote! {
174                {
175                    #bindings
176                    #field_call
177                }
178            }
179        }
180    } else {
181        let (bindings, val_ref) = field_value_parts(attr, access);
182        let field_call = emit_field(val_ref);
183        quote! {
184            {
185                #bindings
186                #field_call
187            }
188        }
189    };
190
191    if let Some(cond) = &attr.skip_if {
192        let cond_expr: Expr = syn::parse_str(cond).expect("invalid skip_if expression");
193        quote! {
194            if !(#cond_expr) {
195                #body
196            }
197        }
198    } else {
199        body
200    }
201}
202
203/// Returns (binding_statements, value_ref_expr) where value_ref_expr is
204/// something like `&__val as &dyn Debug` that can be used in `.field()`.
205/// The binding_statements must appear before value_ref_expr is used.
206fn field_value_parts(attr: &FieldAttr, access: &TokenStream2) -> (TokenStream2, TokenStream2) {
207    if let Some(lit) = &attr.literal {
208        return (quote! {}, quote! { &#lit as &dyn ::core::fmt::Debug });
209    }
210    if let Some(func_path) = &attr.with {
211        let path: syn::ExprPath =
212            syn::parse_str(func_path).expect("invalid function path in #[dump(with)]");
213        let bindings = quote! {
214            let __with = ::dumpit::WithFn(#access, #path);
215        };
216        return (bindings, quote! { &__with as &dyn ::core::fmt::Debug });
217    }
218    if let Some(fmt_str) = &attr.format_str {
219        let extra: Vec<TokenStream2> = attr
220            .format_args
221            .iter()
222            .map(|a| {
223                let expr: Expr = syn::parse_str(a).expect("invalid format arg");
224                quote! { , #expr }
225            })
226            .collect();
227        let bindings = quote! {
228            let __fmt = ::dumpit::Formatted(::std::format!(#fmt_str #(#extra)*));
229        };
230        return (bindings, quote! { &__fmt as &dyn ::core::fmt::Debug });
231    }
232    if attr.take.is_some() {
233        // take is handled specially in field_debug_token
234        unreachable!("take should be handled before field_value_parts");
235    }
236    if let Some(limit) = &attr.truncate {
237        let bindings = quote! {
238            use ::dumpit::DebugFallbackBuild as _;
239            let __wrap = ::dumpit::TruncateWrap(#access, #limit);
240            let __val = __wrap.__dumpit_build();
241        };
242        return (bindings, quote! { &__val as &dyn ::core::fmt::Debug });
243    }
244    // default — autoref specialization
245    let bindings = quote! {
246        use ::dumpit::DebugFallbackBuild as _;
247        let __wrap = ::dumpit::DebugWrap(#access);
248        let __val = __wrap.__dumpit_build();
249    };
250    (bindings, quote! { &__val as &dyn ::core::fmt::Debug })
251}
252
253// ---------------------------------------------------------------------------
254// Derive entry point
255// ---------------------------------------------------------------------------
256
257/// The main entry point for the #[derive(Dump)] macro.
258///
259/// supported attributes:
260/// - `#[dump(skip)]` — skip this field entirely
261/// - `#[dump(skip_if = "expr")]` — skip if the expression evaluates to true (expr can refer to `self` and field names)
262/// - `#[dump(format = "fmt", arg1, arg2, ...)]` — use a custom format string with optional arguments.
263/// - `#[dump(literal = "value")]` — ignore the field value and print the literal instead
264/// - `#[dump(with = "path::to::func")]` — use a custom function to format the field. The function should have signature `fn(&T, &mut Formatter) -> fmt::Result` where T is the field type.
265/// - `#[dump(take = n)]` — for iterable fields, only include the first n items and show the count as "name(n/total)"
266/// - `#[dump(truncate = n)]` — debug-format the field value, then truncate the output to n characters (adding "..." if truncated).
267#[proc_macro_derive(Dump, attributes(dump))]
268pub fn derive_dump(input: TokenStream) -> TokenStream {
269    let input = parse_macro_input!(input as DeriveInput);
270    let name = &input.ident;
271    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
272
273    let body = match &input.data {
274        Data::Struct(data_struct) => generate_struct_body(name, &data_struct.fields),
275        Data::Enum(data_enum) => {
276            let arms: Vec<_> = data_enum
277                .variants
278                .iter()
279                .map(|v| generate_enum_arm(name, &v.ident, &v.fields))
280                .collect();
281            quote! {
282                match self {
283                    #(#arms),*
284                }
285            }
286        }
287        Data::Union(_) => {
288            return syn::Error::new_spanned(name, "Dump cannot be derived for unions")
289                .to_compile_error()
290                .into();
291        }
292    };
293
294    let expanded = quote! {
295        impl #impl_generics ::core::fmt::Debug for #name #ty_generics #where_clause {
296            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
297                #body
298            }
299        }
300    };
301    expanded.into()
302}
303
304// ---------------------------------------------------------------------------
305// Struct codegen
306// ---------------------------------------------------------------------------
307
308fn generate_struct_body(name: &syn::Ident, fields: &Fields) -> TokenStream2 {
309    let name_str = name.to_string();
310    match fields {
311        Fields::Named(named) => {
312            let field_calls: Vec<_> = named
313                .named
314                .iter()
315                .map(|f| {
316                    let attr = parse_field_attrs(&f.attrs).expect("invalid dump attr");
317                    let ident = f.ident.as_ref().unwrap();
318                    let ident_str = ident.to_string();
319                    let access = quote! { &self.#ident };
320                    field_debug_token(&attr, &access, Some(&ident_str), |val| {
321                        quote! { __ds.field(#ident_str, #val); }
322                    })
323                })
324                .collect();
325            quote! {
326                let mut __ds = f.debug_struct(#name_str);
327                #(#field_calls)*
328                __ds.finish()
329            }
330        }
331        Fields::Unnamed(unnamed) => {
332            let field_calls: Vec<_> = unnamed
333                .unnamed
334                .iter()
335                .enumerate()
336                .map(|(i, f)| {
337                    let attr = parse_field_attrs(&f.attrs).expect("invalid dump attr");
338                    let idx = Index::from(i);
339                    let access = quote! { &self.#idx };
340                    field_debug_token(&attr, &access, None, |val| {
341                        quote! { __dt.field(#val); }
342                    })
343                })
344                .collect();
345            quote! {
346                let mut __dt = f.debug_tuple(#name_str);
347                #(#field_calls)*
348                __dt.finish()
349            }
350        }
351        Fields::Unit => {
352            quote! { f.write_str(#name_str) }
353        }
354    }
355}
356
357// ---------------------------------------------------------------------------
358// Enum codegen
359// ---------------------------------------------------------------------------
360
361fn generate_enum_arm(
362    enum_name: &syn::Ident,
363    variant_name: &syn::Ident,
364    fields: &Fields,
365) -> TokenStream2 {
366    let variant_str = variant_name.to_string();
367    match fields {
368        Fields::Named(named) => {
369            let field_idents: Vec<_> = named
370                .named
371                .iter()
372                .map(|f| f.ident.as_ref().unwrap())
373                .collect();
374            let field_calls: Vec<_> = named
375                .named
376                .iter()
377                .map(|f| {
378                    let attr = parse_field_attrs(&f.attrs).expect("invalid dump attr");
379                    let ident = f.ident.as_ref().unwrap();
380                    let ident_str = ident.to_string();
381                    let access = quote! { #ident };
382                    field_debug_token(&attr, &access, Some(&ident_str), |val| {
383                        quote! { __ds.field(#ident_str, #val); }
384                    })
385                })
386                .collect();
387            quote! {
388                #enum_name::#variant_name { #(#field_idents),* } => {
389                    let mut __ds = f.debug_struct(#variant_str);
390                    #(#field_calls)*
391                    __ds.finish()
392                }
393            }
394        }
395        Fields::Unnamed(unnamed) => {
396            let bindings: Vec<syn::Ident> = (0..unnamed.unnamed.len())
397                .map(|i| syn::Ident::new(&format!("__field{}", i), proc_macro2::Span::call_site()))
398                .collect();
399            let field_calls: Vec<_> = unnamed
400                .unnamed
401                .iter()
402                .enumerate()
403                .map(|(i, f)| {
404                    let attr = parse_field_attrs(&f.attrs).expect("invalid dump attr");
405                    let binding = &bindings[i];
406                    let access = quote! { #binding };
407                    field_debug_token(&attr, &access, None, |val| {
408                        quote! { __dt.field(#val); }
409                    })
410                })
411                .collect();
412            quote! {
413                #enum_name::#variant_name(#(#bindings),*) => {
414                    let mut __dt = f.debug_tuple(#variant_str);
415                    #(#field_calls)*
416                    __dt.finish()
417                }
418            }
419        }
420        Fields::Unit => {
421            quote! {
422                #enum_name::#variant_name => {
423                    f.write_str(#variant_str)
424                }
425            }
426        }
427    }
428}