custom_debug_derive/
lib.rs

1use darling::FromMeta;
2use proc_macro2::TokenStream;
3use quote::quote;
4use syn::{Fields, Result};
5use synstructure::{decl_derive, AddBounds, BindingInfo, Structure, VariantInfo};
6
7use crate::field_attributes::{DebugFormat, FieldAttributes, SkipMode};
8use crate::result_into_stream_ext::ResultIntoStreamExt;
9use crate::retain_ext::RetainExt;
10
11mod field_attributes;
12mod result_into_stream_ext;
13mod retain_ext;
14#[cfg(test)]
15mod tests;
16
17decl_derive!([Debug, attributes(debug)] => custom_debug_derive);
18
19fn custom_debug_derive(mut structure: Structure) -> Result<TokenStream> {
20    filter_out_skipped_fields(&mut structure)?;
21
22    structure.add_bounds(AddBounds::Fields);
23
24    let match_arms =
25        structure.each_variant(|variant| generate_match_arm_body(variant).into_stream());
26
27    Ok(structure.gen_impl(quote! {
28        gen impl ::core::fmt::Debug for @Self {
29            fn fmt(&self, fmt: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
30                match self {
31                    #match_arms
32                }
33            }
34        }
35    }))
36}
37
38fn filter_out_skipped_fields(structure: &mut Structure) -> Result<()> {
39    structure.try_retain(|binding| {
40        let field_attributes = parse_field_attributes(binding)?;
41
42        Ok(field_attributes.skip_mode != SkipMode::Always)
43    })?;
44
45    Ok(())
46}
47
48fn generate_match_arm_body(variant: &VariantInfo) -> Result<TokenStream> {
49    let name = variant.ast().ident.to_string();
50    let debug_builder = match variant.ast().fields {
51        Fields::Named(_) | Fields::Unit => quote! { debug_struct },
52        Fields::Unnamed(_) => quote! { debug_tuple },
53    };
54    let mut debug_builder_calls = Vec::new();
55
56    for binding in variant.bindings() {
57        let field_attributes = parse_field_attributes(binding)?;
58
59        let debug_builder_call = match &field_attributes.skip_mode {
60            SkipMode::Default => generate_debug_builder_call(binding, &field_attributes)?,
61            SkipMode::Condition(condition) => {
62                let debug_builder_call = generate_debug_builder_call(binding, &field_attributes)?;
63
64                quote! {
65                    if (!#condition(#binding)) {
66                        #debug_builder_call
67                    }
68                }
69            }
70            SkipMode::Always => quote! {},
71        };
72
73        debug_builder_calls.push(debug_builder_call);
74    }
75
76    Ok(quote! {
77        let mut debug_builder = fmt.#debug_builder(#name);
78
79        #(#debug_builder_calls)*
80
81        debug_builder.finish()
82    })
83}
84
85fn generate_debug_builder_call(
86    binding: &BindingInfo,
87    field_attributes: &FieldAttributes,
88) -> Result<TokenStream> {
89    let format = generate_debug_impl(binding, &field_attributes.debug_format);
90
91    let debug_builder_call =
92        if let Some(ref name) = binding.ast().ident.as_ref().map(<_>::to_string) {
93            quote! {
94                debug_builder.field(#name, #format);
95            }
96        } else {
97            quote! {
98                debug_builder.field(#format);
99            }
100        };
101
102    Ok(debug_builder_call)
103}
104
105fn generate_debug_impl(binding: &BindingInfo, debug_format: &DebugFormat) -> TokenStream {
106    match debug_format {
107        DebugFormat::Default => quote! { #binding },
108        DebugFormat::Format(format) => quote! { &format_args!(#format, #binding) },
109        DebugFormat::With(with) => quote! {
110            {
111                struct DebugWith<'a, T: 'a> {
112                    data: &'a T,
113                    fmt: fn(&T, &mut ::core::fmt::Formatter) -> ::core::fmt::Result,
114                }
115
116                impl<'a, T: 'a> ::core::fmt::Debug for DebugWith<'a, T> {
117                    fn fmt(&self, fmt: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
118                        (self.fmt)(self.data, fmt)
119                    }
120                }
121
122                &DebugWith {
123                    data: #binding,
124                    fmt: #with,
125                }
126            }
127        },
128    }
129}
130
131fn parse_field_attributes(binding: &BindingInfo<'_>) -> Result<FieldAttributes> {
132    let mut combined_field_attributes = FieldAttributes::default();
133
134    for attr in &binding.ast().attrs {
135        if !attr.path().is_ident("debug") {
136            continue;
137        }
138
139        let field_attributes = FieldAttributes::from_meta(&attr.meta)?;
140
141        combined_field_attributes = combined_field_attributes.try_combine(field_attributes)?;
142    }
143
144    Ok(combined_field_attributes)
145}