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/// - `RedactableWithMapper`: allows `SensitiveDisplay` types to be used as fields inside
499///   `Sensitive` containers. The impl walks inner fields and applies redaction — the same
500///   traversal logic used by `Sensitive`.
501/// - `Debug`: when *not* building with `cfg(any(test, feature = "testing"))`, `Debug` formats via
502///   `RedactableWithFormatter::fmt_redacted`. In test/testing builds, it shows actual values for
503///   debugging.
504#[proc_macro_derive(SensitiveDisplay, attributes(sensitive, not_sensitive, error))]
505pub fn derive_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
506    let input = parse_macro_input!(input as DeriveInput);
507    match expand(input, SlogMode::RedactedDisplay) {
508        Ok(tokens) => tokens.into(),
509        Err(err) => err.into_compile_error().into(),
510    }
511}
512
513/// Returns the token stream to reference the redactable crate root.
514///
515/// Handles crate renaming (e.g., `my_redact = { package = "redactable", ... }`)
516/// and internal usage (when derive is used inside the redactable crate itself).
517fn crate_root() -> proc_macro2::TokenStream {
518    match crate_name("redactable") {
519        Ok(FoundCrate::Itself) => quote! { crate },
520        Ok(FoundCrate::Name(name)) => {
521            let ident = format_ident!("{}", name);
522            quote! { ::#ident }
523        }
524        Err(_) => quote! { ::redactable },
525    }
526}
527
528/// Returns the token stream to reference the slog crate root.
529///
530/// Handles crate renaming (e.g., `my_slog = { package = "slog", ... }`).
531/// If the top-level `slog` crate is not available, falls back to the
532/// `REDACTABLE_SLOG_CRATE` env var, which should be a path like `my_log::slog`.
533#[cfg(feature = "slog")]
534fn slog_crate() -> Result<proc_macro2::TokenStream> {
535    match crate_name("slog") {
536        Ok(FoundCrate::Itself) => Ok(quote! { crate }),
537        Ok(FoundCrate::Name(name)) => {
538            let ident = format_ident!("{}", name);
539            Ok(quote! { ::#ident })
540        }
541        Err(_) => {
542            let env_value = std::env::var("REDACTABLE_SLOG_CRATE").map_err(|_| {
543                syn::Error::new(
544                    Span::call_site(),
545                    "slog support is enabled, but no top-level `slog` crate was found. \
546Set the REDACTABLE_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
547`slog` as a direct dependency.",
548                )
549            })?;
550            let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
551                syn::Error::new(
552                    Span::call_site(),
553                    format!("REDACTABLE_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
554                )
555            })?;
556            Ok(quote! { #path })
557        }
558    }
559}
560
561fn crate_path(item: &str) -> proc_macro2::TokenStream {
562    let root = crate_root();
563    let item_ident = syn::parse_str::<syn::Path>(item).expect("redactable crate path should parse");
564    quote! { #root::#item_ident }
565}
566
567/// Output produced by struct/enum derive logic for `Sensitive`.
568///
569/// Shared by `derive_struct`, `derive_enum`, and the top-level `expand()`.
570pub(crate) struct DeriveOutput {
571    pub(crate) redaction_body: TokenStream,
572    pub(crate) used_generics: Vec<Ident>,
573    pub(crate) policy_applicable_generics: Vec<Ident>,
574    pub(crate) debug_redacted_body: TokenStream,
575    pub(crate) debug_redacted_generics: Vec<Ident>,
576    pub(crate) debug_unredacted_body: TokenStream,
577    pub(crate) debug_unredacted_generics: Vec<Ident>,
578}
579
580struct DebugOutput {
581    body: TokenStream,
582    generics: Vec<Ident>,
583}
584
585enum SlogMode {
586    RedactedJson,
587    RedactedDisplay,
588}
589
590#[allow(clippy::too_many_lines)]
591fn expand(input: DeriveInput, slog_mode: SlogMode) -> Result<TokenStream> {
592    let DeriveInput {
593        ident,
594        generics,
595        data,
596        attrs,
597        ..
598    } = input;
599
600    let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
601
602    let crate_root = crate_root();
603
604    if matches!(slog_mode, SlogMode::RedactedDisplay) {
605        let redacted_display_output = derive_redacted_display(&ident, &data, &attrs, &generics)?;
606        let redacted_display_generics =
607            add_display_bounds(generics.clone(), &redacted_display_output.display_generics);
608        let redacted_display_generics = add_debug_bounds(
609            redacted_display_generics,
610            &redacted_display_output.debug_generics,
611        );
612        let redacted_display_generics = add_policy_applicable_ref_bounds(
613            redacted_display_generics,
614            &redacted_display_output.policy_ref_generics,
615        );
616        let redacted_display_generics = add_redacted_display_bounds(
617            redacted_display_generics,
618            &redacted_display_output.nested_generics,
619        );
620        let (display_impl_generics, display_ty_generics, display_where_clause) =
621            redacted_display_generics.split_for_impl();
622        let redacted_display_body = redacted_display_output.body;
623        let redacted_display_impl = quote! {
624            impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
625                fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
626                    #redacted_display_body
627                }
628            }
629        };
630
631        // Generate a real RedactableWithMapper impl so that SensitiveDisplay types are
632        // properly redacted when used as fields inside Sensitive containers. This reuses
633        // the same derive_struct/derive_enum logic that the Sensitive derive uses.
634        let container_derive_output = match &data {
635            Data::Struct(data) => derive_struct(&ident, data.clone(), &generics)?,
636            Data::Enum(data) => derive_enum(&ident, data.clone(), &generics)?,
637            Data::Union(u) => {
638                return Err(syn::Error::new(
639                    u.union_token.span(),
640                    "`SensitiveDisplay` cannot be derived for unions",
641                ));
642            }
643        };
644        let container_generics =
645            add_container_bounds(generics.clone(), &container_derive_output.used_generics);
646        let container_generics = add_policy_applicable_bounds(
647            container_generics,
648            &container_derive_output.policy_applicable_generics,
649        );
650        let (container_impl_generics, container_ty_generics, container_where_clause) =
651            container_generics.split_for_impl();
652        let container_redaction_body = &container_derive_output.redaction_body;
653        let container_impl = quote! {
654            impl #container_impl_generics #crate_root::RedactableWithMapper for #ident #container_ty_generics #container_where_clause {
655                fn redact_with<M: #crate_root::RedactableMapper>(self, mapper: &M) -> Self {
656                    use #crate_root::RedactableWithMapper as _;
657                    #container_redaction_body
658                }
659            }
660        };
661
662        let debug_impl = if skip_debug {
663            quote! {}
664        } else {
665            let debug_output = derive_unredacted_debug(&ident, &data, &generics)?;
666            let debug_unredacted_generics =
667                add_debug_bounds(generics.clone(), &debug_output.generics);
668            let (
669                debug_unredacted_impl_generics,
670                debug_unredacted_ty_generics,
671                debug_unredacted_where_clause,
672            ) = debug_unredacted_generics.split_for_impl();
673            let (
674                debug_redacted_impl_generics,
675                debug_redacted_ty_generics,
676                debug_redacted_where_clause,
677            ) = redacted_display_generics.split_for_impl();
678            let debug_unredacted_body = debug_output.body;
679            quote! {
680                #[cfg(any(test, feature = "testing"))]
681                impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
682                    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
683                        #debug_unredacted_body
684                    }
685                }
686
687                #[cfg(not(any(test, feature = "testing")))]
688                impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
689                    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
690                        #crate_root::RedactableWithFormatter::fmt_redacted(self, f)
691                    }
692                }
693            }
694        };
695
696        // Only generate slog impl when the slog feature is enabled on redactable-derive.
697        // If slog is not available, emit a clear error with instructions.
698        #[cfg(feature = "slog")]
699        let slog_impl = {
700            let slog_crate = slog_crate()?;
701            let mut slog_generics = generics;
702            // Get ty_generics first (immutable borrow) before make_where_clause (mutable borrow)
703            let (_, ty_generics, _) = slog_generics.split_for_impl();
704            let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
705            slog_generics
706                .make_where_clause()
707                .predicates
708                .push(parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
709            let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
710                slog_generics.split_for_impl();
711            quote! {
712                impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
713                    fn serialize(
714                        &self,
715                        _record: &#slog_crate::Record<'_>,
716                        key: #slog_crate::Key,
717                        serializer: &mut dyn #slog_crate::Serializer,
718                    ) -> #slog_crate::Result {
719                        let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
720                        serializer.emit_arguments(key, &format_args!("{}", redacted))
721                    }
722                }
723
724                impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
725            }
726        };
727
728        #[cfg(not(feature = "slog"))]
729        let slog_impl = quote! {};
730
731        #[cfg(feature = "tracing")]
732        let tracing_impl = {
733            let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
734                redacted_display_generics.split_for_impl();
735            quote! {
736                impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
737            }
738        };
739
740        #[cfg(not(feature = "tracing"))]
741        let tracing_impl = quote! {};
742
743        return Ok(quote! {
744            #redacted_display_impl
745            #container_impl
746            #debug_impl
747            #slog_impl
748            #tracing_impl
749        });
750    }
751
752    // Only SlogMode::RedactedJson reaches this point (RedactedDisplay returns early above).
753    // RedactableWithFormatter is not generated for the Sensitive derive.
754
755    let derive_output = match &data {
756        Data::Struct(data) => derive_struct(&ident, data.clone(), &generics)?,
757        Data::Enum(data) => derive_enum(&ident, data.clone(), &generics)?,
758        Data::Union(u) => {
759            return Err(syn::Error::new(
760                u.union_token.span(),
761                "`Sensitive` cannot be derived for unions",
762            ));
763        }
764    };
765
766    let policy_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
767    let policy_generics =
768        add_policy_applicable_bounds(policy_generics, &derive_output.policy_applicable_generics);
769    let (impl_generics, ty_generics, where_clause) = policy_generics.split_for_impl();
770    let debug_redacted_generics =
771        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
772    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
773        debug_redacted_generics.split_for_impl();
774    let debug_unredacted_generics =
775        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
776    let (
777        debug_unredacted_impl_generics,
778        debug_unredacted_ty_generics,
779        debug_unredacted_where_clause,
780    ) = debug_unredacted_generics.split_for_impl();
781    let redaction_body = &derive_output.redaction_body;
782    let debug_redacted_body = &derive_output.debug_redacted_body;
783    let debug_unredacted_body = &derive_output.debug_unredacted_body;
784    let debug_impl = if skip_debug {
785        quote! {}
786    } else {
787        quote! {
788            #[cfg(any(test, feature = "testing"))]
789            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
790                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
791                    #debug_unredacted_body
792                }
793            }
794
795            #[cfg(not(any(test, feature = "testing")))]
796            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
797                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
798                    #debug_redacted_body
799                }
800            }
801        }
802    };
803
804    // Only generate slog impl when the slog feature is enabled on redactable-derive.
805    // If slog is not available, emit a clear error with instructions.
806    #[cfg(feature = "slog")]
807    let slog_impl = {
808        let slog_crate = slog_crate()?;
809        let mut slog_generics = generics;
810        let slog_where_clause = slog_generics.make_where_clause();
811        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
812        slog_where_clause
813            .predicates
814            .push(parse_quote!(#self_ty: ::core::clone::Clone));
815        // SlogRedactedExt requires Self: Serialize, so we add this bound to enable
816        // generic types to work with slog when their type parameters implement Serialize.
817        slog_where_clause
818            .predicates
819            .push(parse_quote!(#self_ty: ::serde::Serialize));
820        slog_where_clause
821            .predicates
822            .push(parse_quote!(#self_ty: #crate_root::slog::SlogRedactedExt));
823        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
824            slog_generics.split_for_impl();
825        quote! {
826            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
827                fn serialize(
828                    &self,
829                    _record: &#slog_crate::Record<'_>,
830                    key: #slog_crate::Key,
831                    serializer: &mut dyn #slog_crate::Serializer,
832                ) -> #slog_crate::Result {
833                    let redacted = #crate_root::slog::SlogRedactedExt::slog_redacted_json(self.clone());
834                    #slog_crate::Value::serialize(&redacted, _record, key, serializer)
835                }
836            }
837
838            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
839        }
840    };
841
842    #[cfg(not(feature = "slog"))]
843    let slog_impl = quote! {};
844
845    #[cfg(feature = "tracing")]
846    let tracing_impl = quote! {
847        impl #impl_generics #crate_root::tracing::TracingRedacted for #ident #ty_generics #where_clause {}
848    };
849
850    #[cfg(not(feature = "tracing"))]
851    let tracing_impl = quote! {};
852
853    let trait_impl = quote! {
854        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
855            fn redact_with<M: #crate_root::RedactableMapper>(self, mapper: &M) -> Self {
856                use #crate_root::RedactableWithMapper as _;
857                #redaction_body
858            }
859        }
860
861        #debug_impl
862
863        #slog_impl
864
865        #tracing_impl
866
867        // `slog` already provides `impl<V: Value> Value for &V`, so a reference
868        // impl here would conflict with the blanket impl.
869    };
870    Ok(trait_impl)
871}
872
873fn derive_unredacted_debug(
874    name: &Ident,
875    data: &Data,
876    generics: &syn::Generics,
877) -> Result<DebugOutput> {
878    match data {
879        Data::Struct(data) => Ok(derive_unredacted_debug_struct(name, data, generics)),
880        Data::Enum(data) => Ok(derive_unredacted_debug_enum(name, data, generics)),
881        Data::Union(u) => Err(syn::Error::new(
882            u.union_token.span(),
883            "`SensitiveDisplay` cannot be derived for unions",
884        )),
885    }
886}
887
888fn derive_unredacted_debug_struct(
889    name: &Ident,
890    data: &DataStruct,
891    generics: &syn::Generics,
892) -> DebugOutput {
893    let mut debug_generics = Vec::new();
894    match &data.fields {
895        Fields::Named(fields) => {
896            let mut bindings = Vec::new();
897            let mut debug_fields = Vec::new();
898            for field in &fields.named {
899                let ident = field
900                    .ident
901                    .clone()
902                    .expect("named field should have identifier");
903                bindings.push(ident.clone());
904                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
905                debug_fields.push(quote! {
906                    debug.field(stringify!(#ident), #ident);
907                });
908            }
909            DebugOutput {
910                body: quote! {
911                    match self {
912                        Self { #(#bindings),* } => {
913                            let mut debug = f.debug_struct(stringify!(#name));
914                            #(#debug_fields)*
915                            debug.finish()
916                        }
917                    }
918                },
919                generics: debug_generics,
920            }
921        }
922        Fields::Unnamed(fields) => {
923            let mut bindings = Vec::new();
924            let mut debug_fields = Vec::new();
925            for (index, field) in fields.unnamed.iter().enumerate() {
926                let ident = format_ident!("field_{index}");
927                bindings.push(ident.clone());
928                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
929                debug_fields.push(quote! {
930                    debug.field(#ident);
931                });
932            }
933            DebugOutput {
934                body: quote! {
935                    match self {
936                        Self ( #(#bindings),* ) => {
937                            let mut debug = f.debug_tuple(stringify!(#name));
938                            #(#debug_fields)*
939                            debug.finish()
940                        }
941                    }
942                },
943                generics: debug_generics,
944            }
945        }
946        Fields::Unit => DebugOutput {
947            body: quote! {
948                f.write_str(stringify!(#name))
949            },
950            generics: debug_generics,
951        },
952    }
953}
954
955fn derive_unredacted_debug_enum(
956    name: &Ident,
957    data: &DataEnum,
958    generics: &syn::Generics,
959) -> DebugOutput {
960    let mut debug_generics = Vec::new();
961    let mut debug_arms = Vec::new();
962    for variant in &data.variants {
963        let variant_ident = &variant.ident;
964        match &variant.fields {
965            Fields::Unit => {
966                debug_arms.push(quote! {
967                    #name::#variant_ident => f.write_str(stringify!(#name::#variant_ident))
968                });
969            }
970            Fields::Named(fields) => {
971                let mut bindings = Vec::new();
972                let mut debug_fields = Vec::new();
973                for field in &fields.named {
974                    let ident = field
975                        .ident
976                        .clone()
977                        .expect("named field should have identifier");
978                    bindings.push(ident.clone());
979                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
980                    debug_fields.push(quote! {
981                        debug.field(stringify!(#ident), #ident);
982                    });
983                }
984                debug_arms.push(quote! {
985                    #name::#variant_ident { #(#bindings),* } => {
986                        let mut debug = f.debug_struct(stringify!(#name::#variant_ident));
987                        #(#debug_fields)*
988                        debug.finish()
989                    }
990                });
991            }
992            Fields::Unnamed(fields) => {
993                let mut bindings = Vec::new();
994                let mut debug_fields = Vec::new();
995                for (index, field) in fields.unnamed.iter().enumerate() {
996                    let ident = format_ident!("field_{index}");
997                    bindings.push(ident.clone());
998                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
999                    debug_fields.push(quote! {
1000                        debug.field(#ident);
1001                    });
1002                }
1003                debug_arms.push(quote! {
1004                    #name::#variant_ident ( #(#bindings),* ) => {
1005                        let mut debug = f.debug_tuple(stringify!(#name::#variant_ident));
1006                        #(#debug_fields)*
1007                        debug.finish()
1008                    }
1009                });
1010            }
1011        }
1012    }
1013    DebugOutput {
1014        body: quote! {
1015            match self {
1016                #(#debug_arms),*
1017            }
1018        },
1019        generics: debug_generics,
1020    }
1021}