Skip to main content

redactable_derive/
lib.rs

1//! Derive macros for `redactable`.
2//!
3//! This crate generates traversal code behind `#[derive(Sensitive)]`,
4//! `#[derive(SensitiveDisplay)]`, `#[derive(NotSensitive)]`, and
5//! `#[derive(NotSensitiveDisplay)]`. It:
6//! - reads `#[sensitive(...)]` and `#[not_sensitive]` attributes
7//! - emits trait implementations for redaction and logging integration
8//!
9//! It does **not** define policy markers or text policies. Those live in the main
10//! `redactable` crate and are applied at runtime.
11
12// <https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html>
13#![warn(
14    anonymous_parameters,
15    bare_trait_objects,
16    elided_lifetimes_in_paths,
17    missing_copy_implementations,
18    rust_2018_idioms,
19    trivial_casts,
20    trivial_numeric_casts,
21    unreachable_pub,
22    unsafe_code,
23    unused_extern_crates,
24    unused_import_braces
25)]
26// <https://rust-lang.github.io/rust-clippy/stable>
27#![warn(
28    clippy::all,
29    clippy::cargo,
30    clippy::dbg_macro,
31    clippy::float_cmp_const,
32    clippy::get_unwrap,
33    clippy::mem_forget,
34    clippy::nursery,
35    clippy::pedantic,
36    clippy::todo,
37    clippy::unwrap_used,
38    clippy::uninlined_format_args
39)]
40// Allow some clippy lints
41#![allow(
42    clippy::default_trait_access,
43    clippy::doc_markdown,
44    clippy::if_not_else,
45    clippy::module_name_repetitions,
46    clippy::multiple_crate_versions,
47    clippy::must_use_candidate,
48    clippy::needless_pass_by_value,
49    clippy::needless_ifs,
50    clippy::use_self,
51    clippy::cargo_common_metadata,
52    clippy::missing_errors_doc,
53    clippy::enum_glob_use,
54    clippy::struct_excessive_bools,
55    clippy::missing_const_for_fn,
56    clippy::redundant_pub_crate,
57    clippy::result_large_err,
58    clippy::future_not_send,
59    clippy::option_if_let_else,
60    clippy::from_over_into,
61    clippy::manual_inspect
62)]
63// Allow some lints while testing
64#![cfg_attr(test, allow(clippy::non_ascii_literal, clippy::unwrap_used))]
65
66#[allow(unused_extern_crates)]
67extern crate proc_macro;
68
69use proc_macro_crate::{FoundCrate, crate_name};
70#[cfg(feature = "slog")]
71use proc_macro2::Span;
72use proc_macro2::{Ident, TokenStream};
73use quote::{format_ident, quote};
74#[cfg(feature = "slog")]
75use syn::parse_quote;
76use syn::{
77    Data, DataEnum, DataStruct, DeriveInput, Fields, Result, parse_macro_input, spanned::Spanned,
78};
79
80mod container;
81mod derive_enum;
82mod derive_struct;
83mod generics;
84mod redacted_display;
85mod strategy;
86mod transform;
87mod types;
88use container::{ContainerOptions, parse_container_options};
89use derive_enum::derive_enum;
90use derive_struct::derive_struct;
91use generics::{
92    add_container_bounds, add_debug_bounds, add_display_bounds, add_policy_applicable_bounds,
93    add_policy_applicable_ref_bounds, add_redacted_display_bounds, collect_generics_from_type,
94};
95use redacted_display::derive_redacted_display;
96
97/// Derives `redactable::RedactableWithMapper` (and related impls) for structs and enums.
98///
99/// # Container Attributes
100///
101/// These attributes are placed on the struct/enum itself:
102///
103/// - `#[sensitive(skip_debug)]` - Opt out of `Debug` impl generation. Use this when you need a
104///   custom `Debug` implementation or the type already derives `Debug` elsewhere.
105///
106/// # Field Attributes
107///
108/// - **No annotation**: The field is traversed by default. Scalars pass through unchanged; nested
109///   structs/enums are walked using `RedactableWithMapper` (so external types must implement it).
110///
111/// - `#[sensitive(Secret)]`: For scalar types (i32, bool, char, etc.), redacts to default values
112///   (0, false, '*'). For string-like types, applies full redaction to `"[REDACTED]"`.
113///
114/// - `#[sensitive(Policy)]`: Applies the policy's redaction rules to string-like
115///   values. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`. Scalars can only
116///   use `#[sensitive(Secret)]`.
117///
118/// - `#[not_sensitive]`: Explicit passthrough - the field is not transformed at all. Use this
119///   for foreign types that don't implement `RedactableWithMapper`. This is equivalent to wrapping
120///   the field type in `NotSensitiveValue<T>`, but without changing the type signature.
121///
122/// Unions are rejected at compile time.
123///
124/// # Additional Generated Impls
125///
126/// - `Debug`: when *not* building with `cfg(any(test, feature = "testing"))`, sensitive fields are
127///   formatted as the string `"[REDACTED]"` rather than their values. Use `#[sensitive(skip_debug)]`
128///   on the container to opt out.
129/// - `slog::Value` (behind `cfg(feature = "slog")`): implemented by cloning the value and routing
130///   it through `redactable::slog::SlogRedactedExt`. **Note:** this impl requires `Clone` and
131///   `serde::Serialize` because it emits structured JSON. The derive first looks for a top-level
132///   `slog` crate; if not found, it checks the `REDACTABLE_SLOG_CRATE` env var for an alternate path
133///   (e.g., `my_log::slog`). If neither is available, compilation fails with a clear error.
134#[proc_macro_derive(Sensitive, attributes(sensitive, not_sensitive))]
135pub fn derive_sensitive_container(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
136    let input = parse_macro_input!(input as DeriveInput);
137    match expand(input, SlogMode::RedactedJson) {
138        Ok(tokens) => tokens.into(),
139        Err(err) => err.into_compile_error().into(),
140    }
141}
142
143/// Derives a no-op `redactable::RedactableWithMapper` implementation, along with
144/// `slog::Value` / `SlogRedacted` and `TracingRedacted`.
145///
146/// This is useful for types that are known to be non-sensitive but still need to
147/// satisfy `RedactableWithMapper` / `Redactable` bounds. Because the type has no
148/// sensitive data, logging integration works without wrappers.
149///
150/// # Generated Impls
151///
152/// - `RedactableWithMapper`: no-op passthrough (the type has no sensitive data)
153/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): serializes the value
154///   directly as structured JSON without redaction (same format as `Sensitive`, but skips
155///   the redaction step). Requires `Serialize` on the type.
156/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
157///
158/// `NotSensitive` does **not** generate a `Debug` impl - there's nothing to redact.
159/// Use `#[derive(Debug)]` when needed.
160///
161/// # Rejected Attributes
162///
163/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
164/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
165/// latter is redundant (the entire type is already non-sensitive).
166///
167/// Unions are rejected at compile time.
168#[proc_macro_derive(NotSensitive, attributes(not_sensitive))]
169pub fn derive_not_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
170    let input = parse_macro_input!(input as DeriveInput);
171    match expand_not_sensitive(input) {
172        Ok(tokens) => tokens.into(),
173        Err(err) => err.into_compile_error().into(),
174    }
175}
176
177/// Rejects `#[sensitive]` and `#[not_sensitive]` attributes on a non-sensitive type.
178///
179/// Checks both container-level and field-level attributes. `#[sensitive]` is wrong
180/// because the type is explicitly non-sensitive; `#[not_sensitive]` is redundant
181/// because the entire type is already non-sensitive.
182fn reject_sensitivity_attrs(attrs: &[syn::Attribute], data: &Data, macro_name: &str) -> Result<()> {
183    let check_attr = |attr: &syn::Attribute| -> Result<()> {
184        if attr.path().is_ident("sensitive") {
185            return Err(syn::Error::new(
186                attr.span(),
187                format!("`#[sensitive]` attributes are not allowed on `{macro_name}` types"),
188            ));
189        }
190        if attr.path().is_ident("not_sensitive") {
191            return Err(syn::Error::new(
192                attr.span(),
193                format!(
194                    "`#[not_sensitive]` attributes are not needed on `{macro_name}` types (the entire type is already non-sensitive)"
195                ),
196            ));
197        }
198        Ok(())
199    };
200
201    for attr in attrs {
202        check_attr(attr)?;
203    }
204
205    match data {
206        Data::Struct(data) => {
207            for field in &data.fields {
208                for attr in &field.attrs {
209                    check_attr(attr)?;
210                }
211            }
212        }
213        Data::Enum(data) => {
214            for variant in &data.variants {
215                for field in &variant.fields {
216                    for attr in &field.attrs {
217                        check_attr(attr)?;
218                    }
219                }
220            }
221        }
222        Data::Union(_) => {}
223    }
224
225    Ok(())
226}
227
228fn expand_not_sensitive(input: DeriveInput) -> Result<TokenStream> {
229    let DeriveInput {
230        ident,
231        generics,
232        data,
233        attrs,
234        ..
235    } = input;
236
237    // Reject unions
238    if let Data::Union(u) = &data {
239        return Err(syn::Error::new(
240            u.union_token.span(),
241            "`NotSensitive` cannot be derived for unions",
242        ));
243    }
244
245    reject_sensitivity_attrs(&attrs, &data, "NotSensitive")?;
246
247    let crate_root = crate_root();
248
249    // RedactableWithMapper impl (no-op passthrough)
250    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
251    let container_impl = quote! {
252        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
253            fn redact_with<M: #crate_root::RedactableMapper>(self, _mapper: &M) -> Self {
254                self
255            }
256        }
257    };
258
259    // slog impl - serialize directly as structured JSON (no redaction needed)
260    #[cfg(feature = "slog")]
261    let slog_impl = {
262        let slog_crate = slog_crate()?;
263        let mut slog_generics = generics.clone();
264        let (_, ty_generics, _) = slog_generics.split_for_impl();
265        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
266        slog_generics
267            .make_where_clause()
268            .predicates
269            .push(parse_quote!(#self_ty: ::serde::Serialize));
270        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
271            slog_generics.split_for_impl();
272        quote! {
273            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
274                fn serialize(
275                    &self,
276                    _record: &#slog_crate::Record<'_>,
277                    key: #slog_crate::Key,
278                    serializer: &mut dyn #slog_crate::Serializer,
279                ) -> #slog_crate::Result {
280                    #crate_root::slog::__slog_serialize_not_sensitive(self, _record, key, serializer)
281                }
282            }
283
284            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
285        }
286    };
287
288    #[cfg(not(feature = "slog"))]
289    let slog_impl = quote! {};
290
291    // tracing impl
292    #[cfg(feature = "tracing")]
293    let tracing_impl = {
294        let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
295            generics.split_for_impl();
296        quote! {
297            impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
298        }
299    };
300
301    #[cfg(not(feature = "tracing"))]
302    let tracing_impl = quote! {};
303
304    Ok(quote! {
305        #container_impl
306        #slog_impl
307        #tracing_impl
308    })
309}
310
311/// Derives `redactable::RedactableWithFormatter` for types with no sensitive data.
312///
313/// This is the display counterpart to `NotSensitive`. Use it when you have a type
314/// with no sensitive data that needs logging integration (e.g., for use with slog).
315///
316/// Unlike `SensitiveDisplay`, this derive does **not** require a display template.
317/// Instead, it delegates directly to the type's existing `Display` implementation.
318///
319/// # Required Bounds
320///
321/// The type must implement `Display`. This is required because `RedactableWithFormatter` delegates
322/// to `Display::fmt`.
323///
324/// # Generated Impls
325///
326/// - `RedactableWithMapper`: no-op passthrough (allows use inside `Sensitive` containers)
327/// - `RedactableWithFormatter`: delegates to `Display::fmt`
328/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): uses `RedactableWithFormatter` output
329/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
330///
331/// # Debug
332///
333/// `NotSensitiveDisplay` does **not** generate a `Debug` impl - there's nothing to redact.
334/// Use `#[derive(Debug)]` alongside `NotSensitiveDisplay` when needed:
335///
336/// # Rejected Attributes
337///
338/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
339/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
340/// latter is redundant (the entire type is already non-sensitive).
341///
342/// # Example
343///
344/// ```ignore
345/// use redactable::NotSensitiveDisplay;
346///
347/// #[derive(Clone, NotSensitiveDisplay)]
348/// #[display(fmt = "RetryDecision")]  // Or use displaydoc/thiserror for Display impl
349/// enum RetryDecision {
350///     Retry,
351///     Abort,
352/// }
353/// ```
354#[proc_macro_derive(NotSensitiveDisplay)]
355pub fn derive_not_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
356    let input = parse_macro_input!(input as DeriveInput);
357    match expand_not_sensitive_display(input) {
358        Ok(tokens) => tokens.into(),
359        Err(err) => err.into_compile_error().into(),
360    }
361}
362
363fn expand_not_sensitive_display(input: DeriveInput) -> Result<TokenStream> {
364    let DeriveInput {
365        ident,
366        generics,
367        data,
368        attrs,
369        ..
370    } = input;
371
372    // Reject unions
373    if let Data::Union(u) = &data {
374        return Err(syn::Error::new(
375            u.union_token.span(),
376            "`NotSensitiveDisplay` cannot be derived for unions",
377        ));
378    }
379
380    reject_sensitivity_attrs(&attrs, &data, "NotSensitiveDisplay")?;
381
382    let crate_root = crate_root();
383
384    // Generate the RedactableWithMapper no-op passthrough impl
385    // This is always generated, allowing NotSensitiveDisplay to be used inside Sensitive containers
386    let (container_impl_generics, container_ty_generics, container_where_clause) =
387        generics.split_for_impl();
388    let container_impl = quote! {
389        impl #container_impl_generics #crate_root::RedactableWithMapper for #ident #container_ty_generics #container_where_clause {
390            fn redact_with<M: #crate_root::RedactableMapper>(self, _mapper: &M) -> Self {
391                self
392            }
393        }
394    };
395
396    // Always delegate to Display::fmt (no template parsing for NotSensitiveDisplay)
397    // Add Display bound to generics for RedactableWithFormatter impl
398    let mut display_generics = generics.clone();
399    let display_where_clause = display_generics.make_where_clause();
400    // Collect type parameters that need Display bound
401    for param in generics.type_params() {
402        let ident = &param.ident;
403        display_where_clause
404            .predicates
405            .push(syn::parse_quote!(#ident: ::core::fmt::Display));
406    }
407
408    let (display_impl_generics, display_ty_generics, display_where_clause) =
409        display_generics.split_for_impl();
410
411    // RedactableWithFormatter impl - delegates to Display
412    let redacted_display_impl = quote! {
413        impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
414            fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
415                ::core::fmt::Display::fmt(self, f)
416            }
417        }
418    };
419
420    // slog impl
421    #[cfg(feature = "slog")]
422    let slog_impl = {
423        let slog_crate = slog_crate()?;
424        let mut slog_generics = generics.clone();
425        let (_, ty_generics, _) = slog_generics.split_for_impl();
426        let self_ty: syn::Type = syn::parse_quote!(#ident #ty_generics);
427        slog_generics
428            .make_where_clause()
429            .predicates
430            .push(syn::parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
431        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
432            slog_generics.split_for_impl();
433        quote! {
434            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
435                fn serialize(
436                    &self,
437                    _record: &#slog_crate::Record<'_>,
438                    key: #slog_crate::Key,
439                    serializer: &mut dyn #slog_crate::Serializer,
440                ) -> #slog_crate::Result {
441                    let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
442                    serializer.emit_arguments(key, &format_args!("{}", redacted))
443                }
444            }
445
446            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
447        }
448    };
449
450    #[cfg(not(feature = "slog"))]
451    let slog_impl = quote! {};
452
453    // tracing impl - uses the original generics (no extra Display bounds needed for marker trait)
454    #[cfg(feature = "tracing")]
455    let tracing_impl = {
456        let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
457            generics.split_for_impl();
458        quote! {
459            impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
460        }
461    };
462
463    #[cfg(not(feature = "tracing"))]
464    let tracing_impl = quote! {};
465
466    Ok(quote! {
467        #container_impl
468        #redacted_display_impl
469        #slog_impl
470        #tracing_impl
471    })
472}
473
474/// Derives `redactable::RedactableWithFormatter` using a display template.
475///
476/// This generates a redacted string representation without requiring `Clone`.
477/// Unannotated fields use `RedactableWithFormatter` by default (passthrough for scalars,
478/// redacted display for nested `SensitiveDisplay` types).
479///
480/// # Container Attributes
481///
482/// - `#[sensitive(skip_debug)]` - Opt out of `Debug` impl generation. Use this when you need a
483///   custom `Debug` implementation or the type already derives `Debug` elsewhere.
484///
485/// # Field Annotations
486///
487/// - *(none)*: Uses `RedactableWithFormatter` (requires the field type to implement it)
488/// - `#[sensitive(Policy)]`: Apply the policy's redaction rules
489/// - `#[not_sensitive]`: Render raw via `Display` (use for types without `RedactableWithFormatter`)
490///
491/// The display template is taken from `#[error("...")]` (thiserror-style) or from
492/// doc comments (displaydoc-style). If neither is present, the derive fails.
493///
494/// Fields are redacted by reference, so field types do not need `Clone`.
495///
496/// # Additional Generated Impls
497///
498/// - `Debug`: when *not* building with `cfg(any(test, feature = "testing"))`, `Debug` formats via
499///   `RedactableWithFormatter::fmt_redacted`. In test/testing builds, it shows actual values for
500///   debugging.
501#[proc_macro_derive(SensitiveDisplay, attributes(sensitive, not_sensitive, error))]
502pub fn derive_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
503    let input = parse_macro_input!(input as DeriveInput);
504    match expand(input, SlogMode::RedactedDisplay) {
505        Ok(tokens) => tokens.into(),
506        Err(err) => err.into_compile_error().into(),
507    }
508}
509
510/// Returns the token stream to reference the redactable crate root.
511///
512/// Handles crate renaming (e.g., `my_redact = { package = "redactable", ... }`)
513/// and internal usage (when derive is used inside the redactable crate itself).
514fn crate_root() -> proc_macro2::TokenStream {
515    match crate_name("redactable") {
516        Ok(FoundCrate::Itself) => quote! { crate },
517        Ok(FoundCrate::Name(name)) => {
518            let ident = format_ident!("{}", name);
519            quote! { ::#ident }
520        }
521        Err(_) => quote! { ::redactable },
522    }
523}
524
525/// Returns the token stream to reference the slog crate root.
526///
527/// Handles crate renaming (e.g., `my_slog = { package = "slog", ... }`).
528/// If the top-level `slog` crate is not available, falls back to the
529/// `REDACTABLE_SLOG_CRATE` env var, which should be a path like `my_log::slog`.
530#[cfg(feature = "slog")]
531fn slog_crate() -> Result<proc_macro2::TokenStream> {
532    match crate_name("slog") {
533        Ok(FoundCrate::Itself) => Ok(quote! { crate }),
534        Ok(FoundCrate::Name(name)) => {
535            let ident = format_ident!("{}", name);
536            Ok(quote! { ::#ident })
537        }
538        Err(_) => {
539            let env_value = std::env::var("REDACTABLE_SLOG_CRATE").map_err(|_| {
540                syn::Error::new(
541                    Span::call_site(),
542                    "slog support is enabled, but no top-level `slog` crate was found. \
543Set the REDACTABLE_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
544`slog` as a direct dependency.",
545                )
546            })?;
547            let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
548                syn::Error::new(
549                    Span::call_site(),
550                    format!("REDACTABLE_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
551                )
552            })?;
553            Ok(quote! { #path })
554        }
555    }
556}
557
558fn crate_path(item: &str) -> proc_macro2::TokenStream {
559    let root = crate_root();
560    let item_ident = syn::parse_str::<syn::Path>(item).expect("redactable crate path should parse");
561    quote! { #root::#item_ident }
562}
563
564/// Output produced by struct/enum derive logic for `Sensitive`.
565///
566/// Shared by `derive_struct`, `derive_enum`, and the top-level `expand()`.
567pub(crate) struct DeriveOutput {
568    pub(crate) redaction_body: TokenStream,
569    pub(crate) used_generics: Vec<Ident>,
570    pub(crate) policy_applicable_generics: Vec<Ident>,
571    pub(crate) debug_redacted_body: TokenStream,
572    pub(crate) debug_redacted_generics: Vec<Ident>,
573    pub(crate) debug_unredacted_body: TokenStream,
574    pub(crate) debug_unredacted_generics: Vec<Ident>,
575}
576
577struct DebugOutput {
578    body: TokenStream,
579    generics: Vec<Ident>,
580}
581
582enum SlogMode {
583    RedactedJson,
584    RedactedDisplay,
585}
586
587#[allow(clippy::too_many_lines)]
588fn expand(input: DeriveInput, slog_mode: SlogMode) -> Result<TokenStream> {
589    let DeriveInput {
590        ident,
591        generics,
592        data,
593        attrs,
594        ..
595    } = input;
596
597    let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
598
599    let crate_root = crate_root();
600
601    if matches!(slog_mode, SlogMode::RedactedDisplay) {
602        let redacted_display_output = derive_redacted_display(&ident, &data, &attrs, &generics)?;
603        let redacted_display_generics =
604            add_display_bounds(generics.clone(), &redacted_display_output.display_generics);
605        let redacted_display_generics = add_debug_bounds(
606            redacted_display_generics,
607            &redacted_display_output.debug_generics,
608        );
609        let redacted_display_generics = add_policy_applicable_ref_bounds(
610            redacted_display_generics,
611            &redacted_display_output.policy_ref_generics,
612        );
613        let redacted_display_generics = add_redacted_display_bounds(
614            redacted_display_generics,
615            &redacted_display_output.nested_generics,
616        );
617        let (display_impl_generics, display_ty_generics, display_where_clause) =
618            redacted_display_generics.split_for_impl();
619        let redacted_display_body = redacted_display_output.body;
620        let redacted_display_impl = quote! {
621            impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
622                fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
623                    #redacted_display_body
624                }
625            }
626        };
627
628        let debug_impl = if skip_debug {
629            quote! {}
630        } else {
631            let debug_output = derive_unredacted_debug(&ident, &data, &generics)?;
632            let debug_unredacted_generics =
633                add_debug_bounds(generics.clone(), &debug_output.generics);
634            let (
635                debug_unredacted_impl_generics,
636                debug_unredacted_ty_generics,
637                debug_unredacted_where_clause,
638            ) = debug_unredacted_generics.split_for_impl();
639            let (
640                debug_redacted_impl_generics,
641                debug_redacted_ty_generics,
642                debug_redacted_where_clause,
643            ) = redacted_display_generics.split_for_impl();
644            let debug_unredacted_body = debug_output.body;
645            quote! {
646                #[cfg(any(test, feature = "testing"))]
647                impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
648                    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
649                        #debug_unredacted_body
650                    }
651                }
652
653                #[cfg(not(any(test, feature = "testing")))]
654                impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
655                    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
656                        #crate_root::RedactableWithFormatter::fmt_redacted(self, f)
657                    }
658                }
659            }
660        };
661
662        // Only generate slog impl when the slog feature is enabled on redactable-derive.
663        // If slog is not available, emit a clear error with instructions.
664        #[cfg(feature = "slog")]
665        let slog_impl = {
666            let slog_crate = slog_crate()?;
667            let mut slog_generics = generics;
668            // Get ty_generics first (immutable borrow) before make_where_clause (mutable borrow)
669            let (_, ty_generics, _) = slog_generics.split_for_impl();
670            let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
671            slog_generics
672                .make_where_clause()
673                .predicates
674                .push(parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
675            let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
676                slog_generics.split_for_impl();
677            quote! {
678                impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
679                    fn serialize(
680                        &self,
681                        _record: &#slog_crate::Record<'_>,
682                        key: #slog_crate::Key,
683                        serializer: &mut dyn #slog_crate::Serializer,
684                    ) -> #slog_crate::Result {
685                        let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
686                        serializer.emit_arguments(key, &format_args!("{}", redacted))
687                    }
688                }
689
690                impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
691            }
692        };
693
694        #[cfg(not(feature = "slog"))]
695        let slog_impl = quote! {};
696
697        #[cfg(feature = "tracing")]
698        let tracing_impl = {
699            let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
700                redacted_display_generics.split_for_impl();
701            quote! {
702                impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
703            }
704        };
705
706        #[cfg(not(feature = "tracing"))]
707        let tracing_impl = quote! {};
708
709        return Ok(quote! {
710            #redacted_display_impl
711            #debug_impl
712            #slog_impl
713            #tracing_impl
714        });
715    }
716
717    // Only SlogMode::RedactedJson reaches this point (RedactedDisplay returns early above).
718    // RedactableWithFormatter is not generated for the Sensitive derive.
719
720    let derive_output = match &data {
721        Data::Struct(data) => derive_struct(&ident, data.clone(), &generics)?,
722        Data::Enum(data) => derive_enum(&ident, data.clone(), &generics)?,
723        Data::Union(u) => {
724            return Err(syn::Error::new(
725                u.union_token.span(),
726                "`Sensitive` cannot be derived for unions",
727            ));
728        }
729    };
730
731    let policy_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
732    let policy_generics =
733        add_policy_applicable_bounds(policy_generics, &derive_output.policy_applicable_generics);
734    let (impl_generics, ty_generics, where_clause) = policy_generics.split_for_impl();
735    let debug_redacted_generics =
736        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
737    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
738        debug_redacted_generics.split_for_impl();
739    let debug_unredacted_generics =
740        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
741    let (
742        debug_unredacted_impl_generics,
743        debug_unredacted_ty_generics,
744        debug_unredacted_where_clause,
745    ) = debug_unredacted_generics.split_for_impl();
746    let redaction_body = &derive_output.redaction_body;
747    let debug_redacted_body = &derive_output.debug_redacted_body;
748    let debug_unredacted_body = &derive_output.debug_unredacted_body;
749    let debug_impl = if skip_debug {
750        quote! {}
751    } else {
752        quote! {
753            #[cfg(any(test, feature = "testing"))]
754            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
755                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
756                    #debug_unredacted_body
757                }
758            }
759
760            #[cfg(not(any(test, feature = "testing")))]
761            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
762                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
763                    #debug_redacted_body
764                }
765            }
766        }
767    };
768
769    // Only generate slog impl when the slog feature is enabled on redactable-derive.
770    // If slog is not available, emit a clear error with instructions.
771    #[cfg(feature = "slog")]
772    let slog_impl = {
773        let slog_crate = slog_crate()?;
774        let mut slog_generics = generics;
775        let slog_where_clause = slog_generics.make_where_clause();
776        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
777        slog_where_clause
778            .predicates
779            .push(parse_quote!(#self_ty: ::core::clone::Clone));
780        // SlogRedactedExt requires Self: Serialize, so we add this bound to enable
781        // generic types to work with slog when their type parameters implement Serialize.
782        slog_where_clause
783            .predicates
784            .push(parse_quote!(#self_ty: ::serde::Serialize));
785        slog_where_clause
786            .predicates
787            .push(parse_quote!(#self_ty: #crate_root::slog::SlogRedactedExt));
788        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
789            slog_generics.split_for_impl();
790        quote! {
791            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
792                fn serialize(
793                    &self,
794                    _record: &#slog_crate::Record<'_>,
795                    key: #slog_crate::Key,
796                    serializer: &mut dyn #slog_crate::Serializer,
797                ) -> #slog_crate::Result {
798                    let redacted = #crate_root::slog::SlogRedactedExt::slog_redacted_json(self.clone());
799                    #slog_crate::Value::serialize(&redacted, _record, key, serializer)
800                }
801            }
802
803            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
804        }
805    };
806
807    #[cfg(not(feature = "slog"))]
808    let slog_impl = quote! {};
809
810    #[cfg(feature = "tracing")]
811    let tracing_impl = quote! {
812        impl #impl_generics #crate_root::tracing::TracingRedacted for #ident #ty_generics #where_clause {}
813    };
814
815    #[cfg(not(feature = "tracing"))]
816    let tracing_impl = quote! {};
817
818    let trait_impl = quote! {
819        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
820            fn redact_with<M: #crate_root::RedactableMapper>(self, mapper: &M) -> Self {
821                use #crate_root::RedactableWithMapper as _;
822                #redaction_body
823            }
824        }
825
826        #debug_impl
827
828        #slog_impl
829
830        #tracing_impl
831
832        // `slog` already provides `impl<V: Value> Value for &V`, so a reference
833        // impl here would conflict with the blanket impl.
834    };
835    Ok(trait_impl)
836}
837
838fn derive_unredacted_debug(
839    name: &Ident,
840    data: &Data,
841    generics: &syn::Generics,
842) -> Result<DebugOutput> {
843    match data {
844        Data::Struct(data) => Ok(derive_unredacted_debug_struct(name, data, generics)),
845        Data::Enum(data) => Ok(derive_unredacted_debug_enum(name, data, generics)),
846        Data::Union(u) => Err(syn::Error::new(
847            u.union_token.span(),
848            "`SensitiveDisplay` cannot be derived for unions",
849        )),
850    }
851}
852
853fn derive_unredacted_debug_struct(
854    name: &Ident,
855    data: &DataStruct,
856    generics: &syn::Generics,
857) -> DebugOutput {
858    let mut debug_generics = Vec::new();
859    match &data.fields {
860        Fields::Named(fields) => {
861            let mut bindings = Vec::new();
862            let mut debug_fields = Vec::new();
863            for field in &fields.named {
864                let ident = field
865                    .ident
866                    .clone()
867                    .expect("named field should have identifier");
868                bindings.push(ident.clone());
869                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
870                debug_fields.push(quote! {
871                    debug.field(stringify!(#ident), #ident);
872                });
873            }
874            DebugOutput {
875                body: quote! {
876                    match self {
877                        Self { #(#bindings),* } => {
878                            let mut debug = f.debug_struct(stringify!(#name));
879                            #(#debug_fields)*
880                            debug.finish()
881                        }
882                    }
883                },
884                generics: debug_generics,
885            }
886        }
887        Fields::Unnamed(fields) => {
888            let mut bindings = Vec::new();
889            let mut debug_fields = Vec::new();
890            for (index, field) in fields.unnamed.iter().enumerate() {
891                let ident = format_ident!("field_{index}");
892                bindings.push(ident.clone());
893                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
894                debug_fields.push(quote! {
895                    debug.field(#ident);
896                });
897            }
898            DebugOutput {
899                body: quote! {
900                    match self {
901                        Self ( #(#bindings),* ) => {
902                            let mut debug = f.debug_tuple(stringify!(#name));
903                            #(#debug_fields)*
904                            debug.finish()
905                        }
906                    }
907                },
908                generics: debug_generics,
909            }
910        }
911        Fields::Unit => DebugOutput {
912            body: quote! {
913                f.write_str(stringify!(#name))
914            },
915            generics: debug_generics,
916        },
917    }
918}
919
920fn derive_unredacted_debug_enum(
921    name: &Ident,
922    data: &DataEnum,
923    generics: &syn::Generics,
924) -> DebugOutput {
925    let mut debug_generics = Vec::new();
926    let mut debug_arms = Vec::new();
927    for variant in &data.variants {
928        let variant_ident = &variant.ident;
929        match &variant.fields {
930            Fields::Unit => {
931                debug_arms.push(quote! {
932                    #name::#variant_ident => f.write_str(stringify!(#name::#variant_ident))
933                });
934            }
935            Fields::Named(fields) => {
936                let mut bindings = Vec::new();
937                let mut debug_fields = Vec::new();
938                for field in &fields.named {
939                    let ident = field
940                        .ident
941                        .clone()
942                        .expect("named field should have identifier");
943                    bindings.push(ident.clone());
944                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
945                    debug_fields.push(quote! {
946                        debug.field(stringify!(#ident), #ident);
947                    });
948                }
949                debug_arms.push(quote! {
950                    #name::#variant_ident { #(#bindings),* } => {
951                        let mut debug = f.debug_struct(stringify!(#name::#variant_ident));
952                        #(#debug_fields)*
953                        debug.finish()
954                    }
955                });
956            }
957            Fields::Unnamed(fields) => {
958                let mut bindings = Vec::new();
959                let mut debug_fields = Vec::new();
960                for (index, field) in fields.unnamed.iter().enumerate() {
961                    let ident = format_ident!("field_{index}");
962                    bindings.push(ident.clone());
963                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
964                    debug_fields.push(quote! {
965                        debug.field(#ident);
966                    });
967                }
968                debug_arms.push(quote! {
969                    #name::#variant_ident ( #(#bindings),* ) => {
970                        let mut debug = f.debug_tuple(stringify!(#name::#variant_ident));
971                        #(#debug_fields)*
972                        debug.finish()
973                    }
974                });
975            }
976        }
977    }
978    DebugOutput {
979        body: quote! {
980            match self {
981                #(#debug_arms),*
982            }
983        },
984        generics: debug_generics,
985    }
986}