1use proc_macro::TokenStream;
8use proc_macro2::TokenStream as TokenStream2;
9use quote::quote;
10use syn::{Data, DeriveInput, Fields, parse_macro_input};
11
12#[derive(Clone, Copy)]
13enum FieldRedaction {
14 Url,
15 Key,
16 Raw,
17}
18
19#[proc_macro_derive(RedactingDebug, attributes(redact, raw))]
29pub fn derive_redacting_debug(input: TokenStream) -> TokenStream {
30 let input = parse_macro_input!(input as DeriveInput);
31 expand(&input)
32 .unwrap_or_else(syn::Error::into_compile_error)
33 .into()
34}
35
36fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
37 let Data::Struct(data) = &input.data else {
38 return Err(syn::Error::new_spanned(
39 input,
40 "RedactingDebug only supports structs with named fields (enums are not supported \
41 — see issue #1238's Out of Scope; they need per-variant redaction logic)",
42 ));
43 };
44 let Fields::Named(fields) = &data.fields else {
45 return Err(syn::Error::new_spanned(
46 &data.fields,
47 "RedactingDebug requires named fields (tuple structs and unit structs are not \
48 supported in this version — see issue #1238's Out of Scope)",
49 ));
50 };
51 if fields.named.is_empty() {
52 return Err(syn::Error::new_spanned(
53 fields,
54 "RedactingDebug requires at least one named field — a zero-field struct has \
55 nothing to redact; use a hand-written `impl Debug` instead",
56 ));
57 }
58
59 let mut errors: Vec<syn::Error> = Vec::new();
60 let mut field_tokens = Vec::new();
61
62 for field in &fields.named {
63 let Some(field_ident) = &field.ident else {
65 continue;
66 };
67 match field_redaction_kind(field) {
68 Ok(kind) => {
69 let name = field_ident.to_string();
70 field_tokens.push(match kind {
71 FieldRedaction::Url => quote! {
72 .field(#name, &::deps_core::redact_debug::__redact_url_field(&self.#field_ident))
73 },
74 FieldRedaction::Key => quote! {
75 .field(#name, &::deps_core::redact_debug::__redact_key_field(&self.#field_ident))
76 },
77 FieldRedaction::Raw => quote! {
78 .field(#name, &self.#field_ident)
79 },
80 });
81 }
82 Err(e) => errors.push(e),
83 }
84 }
85
86 if let Some(combined) = errors.into_iter().reduce(|mut acc, e| {
87 acc.combine(e);
88 acc
89 }) {
90 return Err(combined);
91 }
92
93 let ident = &input.ident;
94 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
95 let name_str = ident.to_string();
96
97 Ok(quote! {
98 #[automatically_derived]
99 impl #impl_generics ::std::fmt::Debug for #ident #ty_generics #where_clause {
100 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
101 f.debug_struct(#name_str)
102 #(#field_tokens)*
103 .finish()
104 }
105 }
106 })
107}
108
109fn field_redaction_kind(field: &syn::Field) -> syn::Result<FieldRedaction> {
113 let mut found: Option<FieldRedaction> = None;
114
115 for attr in &field.attrs {
116 if attr.path().is_ident("raw") {
117 record_redaction(&mut found, FieldRedaction::Raw, attr)?;
118 } else if attr.path().is_ident("redact") {
119 let mut kind = None;
120 attr.parse_nested_meta(|meta| {
121 let this_kind = if meta.path.is_ident("url") {
122 FieldRedaction::Url
123 } else if meta.path.is_ident("key") {
124 FieldRedaction::Key
125 } else {
126 return Err(meta.error("expected `url` or `key` inside `#[redact(...)]`"));
127 };
128 if kind.is_some() {
132 return Err(
133 meta.error("`#[redact(...)]` accepts exactly one of `url`/`key`, not both")
134 );
135 }
136 kind = Some(this_kind);
137 Ok(())
138 })?;
139 let Some(kind) = kind else {
140 return Err(syn::Error::new_spanned(
141 attr,
142 "`#[redact(...)]` requires `url` or `key`, e.g. `#[redact(url)]`",
143 ));
144 };
145 record_redaction(&mut found, kind, attr)?;
146 }
147 }
148
149 found.ok_or_else(|| {
150 let field_name = field
151 .ident
152 .as_ref()
153 .map_or_else(|| "<field>".to_string(), ToString::to_string);
154 syn::Error::new_spanned(
155 field,
156 format!(
157 "field `{field_name}` must carry exactly one of `#[redact(url)]`, \
158 `#[redact(key)]`, or `#[raw]` (RedactingDebug, #1238)"
159 ),
160 )
161 })
162}
163
164fn record_redaction(
167 found: &mut Option<FieldRedaction>,
168 kind: FieldRedaction,
169 attr: &syn::Attribute,
170) -> syn::Result<()> {
171 if found.is_some() {
172 return Err(syn::Error::new_spanned(
173 attr,
174 "field carries more than one of `#[redact(url)]`, `#[redact(key)]`, `#[raw]` — \
175 exactly one is required",
176 ));
177 }
178 *found = Some(kind);
179 Ok(())
180}