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#[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
140fn 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 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 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
203fn 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 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 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#[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
304fn 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
357fn 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}