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(dual)]` - Use when deriving both `Sensitive` and `SensitiveDisplay` on the same
104///   type. `Sensitive` skips its `Debug` impl (letting `SensitiveDisplay` provide it), and
105///   `SensitiveDisplay` skips its `slog`/`tracing` impls (letting `Sensitive` provide them).
106///
107/// # Field Attributes
108///
109/// - **No annotation**: The field is traversed by default. Scalars pass through unchanged; nested
110///   structs/enums are walked using `RedactableWithMapper` (so external types must implement it).
111///
112/// - `#[sensitive(Secret)]`: For scalar types (i32, bool, char, etc.), redacts to default values
113///   (0, false, '*'). For string-like types, applies full redaction to `"[REDACTED]"`.
114///
115/// - `#[sensitive(Policy)]`: Applies the policy's redaction rules to string-like
116///   values. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`. Scalars can only
117///   use `#[sensitive(Secret)]`.
118///
119/// - `#[not_sensitive]`: Explicit passthrough - the field is not transformed at all. Use this
120///   for foreign types that don't implement `RedactableWithMapper`. This is equivalent to wrapping
121///   the field type in `NotSensitiveValue<T>`, but without changing the type signature.
122///
123/// Unions are rejected at compile time.
124///
125/// # Generated Impls
126///
127/// - `RedactableWithMapper`: always generated.
128/// - `Debug`: redacted in production, actual values in test/testing builds. Skipped when
129///   `#[sensitive(dual)]` is set.
130/// - `slog::Value` + `SlogRedacted` (requires `slog` feature): implemented by cloning the value
131///   and routing it through `redactable::slog::SlogRedactedExt`. Requires `Clone` and
132///   `serde::Serialize` because it emits structured JSON. The derive first looks for a top-level
133///   `slog` crate; if not found, it checks the `REDACTABLE_SLOG_CRATE` env var for an alternate
134///   path (e.g., `my_log::slog`). If neither is available, compilation fails with a clear error.
135/// - `TracingRedacted` (requires `tracing` feature): marker trait.
136#[proc_macro_derive(Sensitive, attributes(sensitive, not_sensitive))]
137pub fn derive_sensitive_container(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
138    let input = parse_macro_input!(input as DeriveInput);
139    match expand(input, DeriveKind::Sensitive) {
140        Ok(tokens) => tokens.into(),
141        Err(err) => err.into_compile_error().into(),
142    }
143}
144
145/// Derives a no-op `redactable::RedactableWithMapper` implementation, along with
146/// `slog::Value` / `SlogRedacted` and `TracingRedacted`.
147///
148/// This is useful for types that are known to be non-sensitive but still need to
149/// satisfy `RedactableWithMapper` / `Redactable` bounds. Because the type has no
150/// sensitive data, logging integration works without wrappers.
151///
152/// # Generated Impls
153///
154/// - `RedactableWithMapper`: no-op passthrough (the type has no sensitive data)
155/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): serializes the value
156///   directly as structured JSON without redaction (same format as `Sensitive`, but skips
157///   the redaction step). Requires `Serialize` on the type.
158/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
159///
160/// `NotSensitive` does **not** generate a `Debug` impl - there's nothing to redact.
161/// Use `#[derive(Debug)]` when needed.
162///
163/// # Rejected Attributes
164///
165/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
166/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
167/// latter is redundant (the entire type is already non-sensitive).
168///
169/// Unions are rejected at compile time.
170#[proc_macro_derive(NotSensitive, attributes(not_sensitive))]
171pub fn derive_not_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
172    let input = parse_macro_input!(input as DeriveInput);
173    match expand_not_sensitive(input) {
174        Ok(tokens) => tokens.into(),
175        Err(err) => err.into_compile_error().into(),
176    }
177}
178
179/// Rejects `#[sensitive]` and `#[not_sensitive]` attributes on a non-sensitive type.
180///
181/// Checks both container-level and field-level attributes. `#[sensitive]` is wrong
182/// because the type is explicitly non-sensitive; `#[not_sensitive]` is redundant
183/// because the entire type is already non-sensitive.
184fn reject_sensitivity_attrs(attrs: &[syn::Attribute], data: &Data, macro_name: &str) -> Result<()> {
185    let check_attr = |attr: &syn::Attribute| -> Result<()> {
186        if attr.path().is_ident("sensitive") {
187            return Err(syn::Error::new(
188                attr.span(),
189                format!("`#[sensitive]` attributes are not allowed on `{macro_name}` types"),
190            ));
191        }
192        if attr.path().is_ident("not_sensitive") {
193            return Err(syn::Error::new(
194                attr.span(),
195                format!(
196                    "`#[not_sensitive]` attributes are not needed on `{macro_name}` types (the entire type is already non-sensitive)"
197                ),
198            ));
199        }
200        Ok(())
201    };
202
203    for attr in attrs {
204        check_attr(attr)?;
205    }
206
207    match data {
208        Data::Struct(data) => {
209            for field in &data.fields {
210                for attr in &field.attrs {
211                    check_attr(attr)?;
212                }
213            }
214        }
215        Data::Enum(data) => {
216            for variant in &data.variants {
217                for field in &variant.fields {
218                    for attr in &field.attrs {
219                        check_attr(attr)?;
220                    }
221                }
222            }
223        }
224        Data::Union(_) => {}
225    }
226
227    Ok(())
228}
229
230fn expand_not_sensitive(input: DeriveInput) -> Result<TokenStream> {
231    let DeriveInput {
232        ident,
233        generics,
234        data,
235        attrs,
236        ..
237    } = input;
238
239    // Reject unions
240    if let Data::Union(u) = &data {
241        return Err(syn::Error::new(
242            u.union_token.span(),
243            "`NotSensitive` cannot be derived for unions",
244        ));
245    }
246
247    reject_sensitivity_attrs(&attrs, &data, "NotSensitive")?;
248
249    let crate_root = crate_root();
250
251    // RedactableWithMapper impl (no-op passthrough)
252    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
253    let container_impl = quote! {
254        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
255            fn redact_with<M: #crate_root::RedactableMapper>(self, _mapper: &M) -> Self {
256                self
257            }
258        }
259    };
260
261    // slog impl - serialize directly as structured JSON (no redaction needed)
262    #[cfg(feature = "slog")]
263    let slog_impl = {
264        let slog_crate = slog_crate()?;
265        let mut slog_generics = generics.clone();
266        let (_, ty_generics, _) = slog_generics.split_for_impl();
267        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
268        slog_generics
269            .make_where_clause()
270            .predicates
271            .push(parse_quote!(#self_ty: ::serde::Serialize));
272        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
273            slog_generics.split_for_impl();
274        quote! {
275            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
276                fn serialize(
277                    &self,
278                    _record: &#slog_crate::Record<'_>,
279                    key: #slog_crate::Key,
280                    serializer: &mut dyn #slog_crate::Serializer,
281                ) -> #slog_crate::Result {
282                    #crate_root::slog::__slog_serialize_not_sensitive(self, _record, key, serializer)
283                }
284            }
285
286            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
287        }
288    };
289
290    #[cfg(not(feature = "slog"))]
291    let slog_impl = quote! {};
292
293    // tracing impl
294    #[cfg(feature = "tracing")]
295    let tracing_impl = {
296        let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
297            generics.split_for_impl();
298        quote! {
299            impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
300        }
301    };
302
303    #[cfg(not(feature = "tracing"))]
304    let tracing_impl = quote! {};
305
306    Ok(quote! {
307        #container_impl
308        #slog_impl
309        #tracing_impl
310    })
311}
312
313/// Derives `redactable::RedactableWithFormatter` for types with no sensitive data.
314///
315/// This is the display counterpart to `NotSensitive`. Use it when you have a type
316/// with no sensitive data that needs logging integration (e.g., for use with slog).
317///
318/// Unlike `SensitiveDisplay`, this derive does **not** require a display template.
319/// Instead, it delegates directly to the type's existing `Display` implementation.
320///
321/// # Required Bounds
322///
323/// The type must implement `Display`. This is required because `RedactableWithFormatter` delegates
324/// to `Display::fmt`.
325///
326/// # Generated Impls
327///
328/// - `RedactableWithMapper`: no-op passthrough (allows use inside `Sensitive` containers)
329/// - `RedactableWithFormatter`: delegates to `Display::fmt`
330/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): uses `RedactableWithFormatter` output
331/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
332///
333/// # Debug
334///
335/// `NotSensitiveDisplay` does **not** generate a `Debug` impl - there's nothing to redact.
336/// Use `#[derive(Debug)]` alongside `NotSensitiveDisplay` when needed.
337///
338/// # Rejected Attributes
339///
340/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
341/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
342/// latter is redundant (the entire type is already non-sensitive).
343///
344/// # Example
345///
346/// ```ignore
347/// use redactable::NotSensitiveDisplay;
348///
349/// #[derive(Clone, NotSensitiveDisplay)]
350/// #[display(fmt = "RetryDecision")]  // Or use displaydoc/thiserror for Display impl
351/// enum RetryDecision {
352///     Retry,
353///     Abort,
354/// }
355/// ```
356#[proc_macro_derive(NotSensitiveDisplay)]
357pub fn derive_not_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
358    let input = parse_macro_input!(input as DeriveInput);
359    match expand_not_sensitive_display(input) {
360        Ok(tokens) => tokens.into(),
361        Err(err) => err.into_compile_error().into(),
362    }
363}
364
365fn expand_not_sensitive_display(input: DeriveInput) -> Result<TokenStream> {
366    let DeriveInput {
367        ident,
368        generics,
369        data,
370        attrs,
371        ..
372    } = input;
373
374    // Reject unions
375    if let Data::Union(u) = &data {
376        return Err(syn::Error::new(
377            u.union_token.span(),
378            "`NotSensitiveDisplay` cannot be derived for unions",
379        ));
380    }
381
382    reject_sensitivity_attrs(&attrs, &data, "NotSensitiveDisplay")?;
383
384    let crate_root = crate_root();
385
386    // Generate the RedactableWithMapper no-op passthrough impl
387    // This is always generated, allowing NotSensitiveDisplay to be used inside Sensitive containers
388    let (container_impl_generics, container_ty_generics, container_where_clause) =
389        generics.split_for_impl();
390    let container_impl = quote! {
391        impl #container_impl_generics #crate_root::RedactableWithMapper for #ident #container_ty_generics #container_where_clause {
392            fn redact_with<M: #crate_root::RedactableMapper>(self, _mapper: &M) -> Self {
393                self
394            }
395        }
396    };
397
398    // Always delegate to Display::fmt (no template parsing for NotSensitiveDisplay)
399    // Add Display bound to generics for RedactableWithFormatter impl
400    let mut display_generics = generics.clone();
401    let display_where_clause = display_generics.make_where_clause();
402    // Collect type parameters that need Display bound
403    for param in generics.type_params() {
404        let ident = &param.ident;
405        display_where_clause
406            .predicates
407            .push(syn::parse_quote!(#ident: ::core::fmt::Display));
408    }
409
410    let (display_impl_generics, display_ty_generics, display_where_clause) =
411        display_generics.split_for_impl();
412
413    // RedactableWithFormatter impl - delegates to Display
414    let redacted_display_impl = quote! {
415        impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
416            fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
417                ::core::fmt::Display::fmt(self, f)
418            }
419        }
420    };
421
422    // slog impl
423    #[cfg(feature = "slog")]
424    let slog_impl = {
425        let slog_crate = slog_crate()?;
426        let mut slog_generics = generics.clone();
427        let (_, ty_generics, _) = slog_generics.split_for_impl();
428        let self_ty: syn::Type = syn::parse_quote!(#ident #ty_generics);
429        slog_generics
430            .make_where_clause()
431            .predicates
432            .push(syn::parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
433        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
434            slog_generics.split_for_impl();
435        quote! {
436            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
437                fn serialize(
438                    &self,
439                    _record: &#slog_crate::Record<'_>,
440                    key: #slog_crate::Key,
441                    serializer: &mut dyn #slog_crate::Serializer,
442                ) -> #slog_crate::Result {
443                    let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
444                    serializer.emit_arguments(key, &format_args!("{}", redacted))
445                }
446            }
447
448            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
449        }
450    };
451
452    #[cfg(not(feature = "slog"))]
453    let slog_impl = quote! {};
454
455    // tracing impl - uses the original generics (no extra Display bounds needed for marker trait)
456    #[cfg(feature = "tracing")]
457    let tracing_impl = {
458        let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
459            generics.split_for_impl();
460        quote! {
461            impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
462        }
463    };
464
465    #[cfg(not(feature = "tracing"))]
466    let tracing_impl = quote! {};
467
468    Ok(quote! {
469        #container_impl
470        #redacted_display_impl
471        #slog_impl
472        #tracing_impl
473    })
474}
475
476/// Derives `redactable::RedactableWithFormatter` using a display template.
477///
478/// This generates a redacted string representation without requiring `Clone`.
479/// Unannotated fields use `RedactableWithFormatter` by default (passthrough for scalars,
480/// redacted display for nested `SensitiveDisplay` types).
481///
482/// # Field Annotations
483///
484/// - *(none)*: Uses `RedactableWithFormatter` (requires the field type to implement it)
485/// - `#[sensitive(Policy)]`: Apply the policy's redaction rules
486/// - `#[not_sensitive]`: Render raw via `Display` (use for types without `RedactableWithFormatter`)
487///
488/// The display template is taken from `#[error("...")]` (thiserror-style) or from
489/// doc comments (displaydoc-style). If neither is present, the derive fails.
490///
491/// Fields are redacted by reference, so field types do not need `Clone`.
492///
493/// # Generated Impls
494///
495/// - `RedactableWithFormatter`: always generated.
496/// - `Debug`: redacted in production, actual values in test/testing builds.
497/// - `slog::Value` + `SlogRedacted`: emits the redacted display string (requires `slog` feature).
498///   Skipped when `#[sensitive(dual)]` is set (Sensitive provides them instead).
499/// - `TracingRedacted`: marker trait (requires `tracing` feature).
500///   Skipped when `#[sensitive(dual)]` is set.
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, DeriveKind::SensitiveDisplay) {
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
582/// Which derive macro invoked `expand()`.
583///
584/// Controls what impls are generated: `Sensitive` emits `RedactableWithMapper` (structural
585/// traversal), while `SensitiveDisplay` emits `RedactableWithFormatter` (display formatting).
586enum DeriveKind {
587    /// `#[derive(Sensitive)]` — structural redaction via `RedactableWithMapper`.
588    Sensitive,
589    /// `#[derive(SensitiveDisplay)]` — display formatting via `RedactableWithFormatter`.
590    SensitiveDisplay,
591}
592
593#[allow(clippy::too_many_lines)]
594fn expand(input: DeriveInput, kind: DeriveKind) -> Result<TokenStream> {
595    let DeriveInput {
596        ident,
597        generics,
598        data,
599        attrs,
600        ..
601    } = input;
602
603    let ContainerOptions { dual } = parse_container_options(&attrs)?;
604
605    let crate_root = crate_root();
606
607    if matches!(kind, DeriveKind::SensitiveDisplay) {
608        let redacted_display_output = derive_redacted_display(&ident, &data, &attrs, &generics)?;
609        let redacted_display_generics =
610            add_display_bounds(generics.clone(), &redacted_display_output.display_generics);
611        let redacted_display_generics = add_debug_bounds(
612            redacted_display_generics,
613            &redacted_display_output.debug_generics,
614        );
615        let redacted_display_generics = add_policy_applicable_ref_bounds(
616            redacted_display_generics,
617            &redacted_display_output.policy_ref_generics,
618        );
619        let redacted_display_generics = add_redacted_display_bounds(
620            redacted_display_generics,
621            &redacted_display_output.nested_generics,
622        );
623        let (display_impl_generics, display_ty_generics, display_where_clause) =
624            redacted_display_generics.split_for_impl();
625        let redacted_display_body = redacted_display_output.body;
626        let redacted_display_impl = quote! {
627            impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
628                fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
629                    #redacted_display_body
630                }
631            }
632        };
633
634        let debug_output = derive_unredacted_debug(&ident, &data, &generics)?;
635        let debug_unredacted_generics = add_debug_bounds(generics.clone(), &debug_output.generics);
636        let (
637            debug_unredacted_impl_generics,
638            debug_unredacted_ty_generics,
639            debug_unredacted_where_clause,
640        ) = debug_unredacted_generics.split_for_impl();
641        let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
642            redacted_display_generics.split_for_impl();
643        let debug_unredacted_body = debug_output.body;
644        let debug_impl = quote! {
645            #[cfg(any(test, feature = "testing"))]
646            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
647                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
648                    #debug_unredacted_body
649                }
650            }
651
652            #[cfg(not(any(test, feature = "testing")))]
653            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
654                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
655                    #crate_root::RedactableWithFormatter::fmt_redacted(self, f)
656                }
657            }
658        };
659
660        // In dual mode, Sensitive provides slog and tracing impls — skip them here.
661        let slog_impl = if dual {
662            quote! {}
663        } else {
664            #[cfg(feature = "slog")]
665            {
666                let slog_crate = slog_crate()?;
667                let mut slog_generics = generics;
668                let (_, ty_generics, _) = slog_generics.split_for_impl();
669                let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
670                slog_generics
671                    .make_where_clause()
672                    .predicates
673                    .push(parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
674                let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
675                    slog_generics.split_for_impl();
676                quote! {
677                    impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
678                        fn serialize(
679                            &self,
680                            _record: &#slog_crate::Record<'_>,
681                            key: #slog_crate::Key,
682                            serializer: &mut dyn #slog_crate::Serializer,
683                        ) -> #slog_crate::Result {
684                            let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
685                            serializer.emit_arguments(key, &format_args!("{}", redacted))
686                        }
687                    }
688
689                    impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
690                }
691            }
692
693            #[cfg(not(feature = "slog"))]
694            {
695                quote! {}
696            }
697        };
698
699        let tracing_impl = if dual {
700            quote! {}
701        } else {
702            #[cfg(feature = "tracing")]
703            {
704                let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
705                    redacted_display_generics.split_for_impl();
706                quote! {
707                    impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
708                }
709            }
710
711            #[cfg(not(feature = "tracing"))]
712            {
713                quote! {}
714            }
715        };
716
717        return Ok(quote! {
718            #redacted_display_impl
719            #debug_impl
720            #slog_impl
721            #tracing_impl
722        });
723    }
724
725    // Only DeriveKind::Sensitive reaches this point (SensitiveDisplay returns early above).
726
727    let derive_output = match data {
728        Data::Struct(data) => derive_struct(&ident, data, &generics)?,
729        Data::Enum(data) => derive_enum(&ident, data, &generics)?,
730        Data::Union(u) => {
731            return Err(syn::Error::new(
732                u.union_token.span(),
733                "`Sensitive` cannot be derived for unions",
734            ));
735        }
736    };
737
738    let policy_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
739    let policy_generics =
740        add_policy_applicable_bounds(policy_generics, &derive_output.policy_applicable_generics);
741    let (impl_generics, ty_generics, where_clause) = policy_generics.split_for_impl();
742    let debug_redacted_generics =
743        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
744    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
745        debug_redacted_generics.split_for_impl();
746    let debug_unredacted_generics =
747        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
748    let (
749        debug_unredacted_impl_generics,
750        debug_unredacted_ty_generics,
751        debug_unredacted_where_clause,
752    ) = debug_unredacted_generics.split_for_impl();
753    let redaction_body = &derive_output.redaction_body;
754    let debug_redacted_body = &derive_output.debug_redacted_body;
755    let debug_unredacted_body = &derive_output.debug_unredacted_body;
756    // In dual mode, SensitiveDisplay provides Debug — skip it here.
757    let debug_impl = if dual {
758        quote! {}
759    } else {
760        quote! {
761            #[cfg(any(test, feature = "testing"))]
762            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
763                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
764                    #debug_unredacted_body
765                }
766            }
767
768            #[cfg(not(any(test, feature = "testing")))]
769            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
770                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
771                    #debug_redacted_body
772                }
773            }
774        }
775    };
776
777    #[cfg(feature = "slog")]
778    let slog_impl = {
779        let slog_crate = slog_crate()?;
780        let mut slog_generics = generics;
781        let slog_where_clause = slog_generics.make_where_clause();
782        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
783        slog_where_clause
784            .predicates
785            .push(parse_quote!(#self_ty: ::core::clone::Clone));
786        // SlogRedactedExt requires Self: Serialize, so we add this bound to enable
787        // generic types to work with slog when their type parameters implement Serialize.
788        slog_where_clause
789            .predicates
790            .push(parse_quote!(#self_ty: ::serde::Serialize));
791        slog_where_clause
792            .predicates
793            .push(parse_quote!(#self_ty: #crate_root::slog::SlogRedactedExt));
794        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
795            slog_generics.split_for_impl();
796        quote! {
797            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
798                fn serialize(
799                    &self,
800                    _record: &#slog_crate::Record<'_>,
801                    key: #slog_crate::Key,
802                    serializer: &mut dyn #slog_crate::Serializer,
803                ) -> #slog_crate::Result {
804                    let redacted = #crate_root::slog::SlogRedactedExt::slog_redacted_json(self.clone());
805                    #slog_crate::Value::serialize(&redacted, _record, key, serializer)
806                }
807            }
808
809            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
810        }
811    };
812
813    #[cfg(not(feature = "slog"))]
814    let slog_impl = quote! {};
815
816    #[cfg(feature = "tracing")]
817    let tracing_impl = quote! {
818        impl #impl_generics #crate_root::tracing::TracingRedacted for #ident #ty_generics #where_clause {}
819    };
820
821    #[cfg(not(feature = "tracing"))]
822    let tracing_impl = quote! {};
823
824    let trait_impl = quote! {
825        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
826            fn redact_with<M: #crate_root::RedactableMapper>(self, mapper: &M) -> Self {
827                use #crate_root::RedactableWithMapper as _;
828                #redaction_body
829            }
830        }
831
832        #debug_impl
833
834        #slog_impl
835
836        #tracing_impl
837    };
838    Ok(trait_impl)
839}
840
841fn derive_unredacted_debug(
842    name: &Ident,
843    data: &Data,
844    generics: &syn::Generics,
845) -> Result<DebugOutput> {
846    match data {
847        Data::Struct(data) => Ok(derive_unredacted_debug_struct(name, data, generics)),
848        Data::Enum(data) => Ok(derive_unredacted_debug_enum(name, data, generics)),
849        Data::Union(u) => Err(syn::Error::new(
850            u.union_token.span(),
851            "`SensitiveDisplay` cannot be derived for unions",
852        )),
853    }
854}
855
856fn derive_unredacted_debug_struct(
857    name: &Ident,
858    data: &DataStruct,
859    generics: &syn::Generics,
860) -> DebugOutput {
861    let mut debug_generics = Vec::new();
862    match &data.fields {
863        Fields::Named(fields) => {
864            let mut bindings = Vec::new();
865            let mut debug_fields = Vec::new();
866            for field in &fields.named {
867                let ident = field
868                    .ident
869                    .clone()
870                    .expect("named field should have identifier");
871                bindings.push(ident.clone());
872                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
873                debug_fields.push(quote! {
874                    debug.field(stringify!(#ident), #ident);
875                });
876            }
877            DebugOutput {
878                body: quote! {
879                    match self {
880                        Self { #(#bindings),* } => {
881                            let mut debug = f.debug_struct(stringify!(#name));
882                            #(#debug_fields)*
883                            debug.finish()
884                        }
885                    }
886                },
887                generics: debug_generics,
888            }
889        }
890        Fields::Unnamed(fields) => {
891            let mut bindings = Vec::new();
892            let mut debug_fields = Vec::new();
893            for (index, field) in fields.unnamed.iter().enumerate() {
894                let ident = format_ident!("field_{index}");
895                bindings.push(ident.clone());
896                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
897                debug_fields.push(quote! {
898                    debug.field(#ident);
899                });
900            }
901            DebugOutput {
902                body: quote! {
903                    match self {
904                        Self ( #(#bindings),* ) => {
905                            let mut debug = f.debug_tuple(stringify!(#name));
906                            #(#debug_fields)*
907                            debug.finish()
908                        }
909                    }
910                },
911                generics: debug_generics,
912            }
913        }
914        Fields::Unit => DebugOutput {
915            body: quote! {
916                f.write_str(stringify!(#name))
917            },
918            generics: debug_generics,
919        },
920    }
921}
922
923fn derive_unredacted_debug_enum(
924    name: &Ident,
925    data: &DataEnum,
926    generics: &syn::Generics,
927) -> DebugOutput {
928    let mut debug_generics = Vec::new();
929    let mut debug_arms = Vec::new();
930    for variant in &data.variants {
931        let variant_ident = &variant.ident;
932        match &variant.fields {
933            Fields::Unit => {
934                debug_arms.push(quote! {
935                    #name::#variant_ident => f.write_str(stringify!(#name::#variant_ident))
936                });
937            }
938            Fields::Named(fields) => {
939                let mut bindings = Vec::new();
940                let mut debug_fields = Vec::new();
941                for field in &fields.named {
942                    let ident = field
943                        .ident
944                        .clone()
945                        .expect("named field should have identifier");
946                    bindings.push(ident.clone());
947                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
948                    debug_fields.push(quote! {
949                        debug.field(stringify!(#ident), #ident);
950                    });
951                }
952                debug_arms.push(quote! {
953                    #name::#variant_ident { #(#bindings),* } => {
954                        let mut debug = f.debug_struct(stringify!(#name::#variant_ident));
955                        #(#debug_fields)*
956                        debug.finish()
957                    }
958                });
959            }
960            Fields::Unnamed(fields) => {
961                let mut bindings = Vec::new();
962                let mut debug_fields = Vec::new();
963                for (index, field) in fields.unnamed.iter().enumerate() {
964                    let ident = format_ident!("field_{index}");
965                    bindings.push(ident.clone());
966                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
967                    debug_fields.push(quote! {
968                        debug.field(#ident);
969                    });
970                }
971                debug_arms.push(quote! {
972                    #name::#variant_ident ( #(#bindings),* ) => {
973                        let mut debug = f.debug_tuple(stringify!(#name::#variant_ident));
974                        #(#debug_fields)*
975                        debug.finish()
976                    }
977                });
978            }
979        }
980    }
981    DebugOutput {
982        body: quote! {
983            match self {
984                #(#debug_arms),*
985            }
986        },
987        generics: debug_generics,
988    }
989}