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        let to_redacted_output_impl = quote! {
634            impl #display_impl_generics #crate_root::ToRedactedOutput for #ident #display_ty_generics #display_where_clause {
635                fn to_redacted_output(&self) -> #crate_root::RedactedOutput {
636                    #crate_root::RedactedOutput::Text(
637                        #crate_root::RedactableWithFormatter::redacted_display(self).to_string(),
638                    )
639                }
640            }
641        };
642
643        let debug_output = derive_unredacted_debug(&ident, &data, &generics)?;
644        let debug_unredacted_generics = add_debug_bounds(generics.clone(), &debug_output.generics);
645        let (
646            debug_unredacted_impl_generics,
647            debug_unredacted_ty_generics,
648            debug_unredacted_where_clause,
649        ) = debug_unredacted_generics.split_for_impl();
650        let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
651            redacted_display_generics.split_for_impl();
652        let debug_unredacted_body = debug_output.body;
653        let debug_impl = quote! {
654            #[cfg(any(test, feature = "testing"))]
655            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
656                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
657                    #debug_unredacted_body
658                }
659            }
660
661            #[cfg(not(any(test, feature = "testing")))]
662            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
663                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
664                    #crate_root::RedactableWithFormatter::fmt_redacted(self, f)
665                }
666            }
667        };
668
669        // In dual mode, Sensitive provides slog and tracing impls — skip them here.
670        let slog_impl = if dual {
671            quote! {}
672        } else {
673            #[cfg(feature = "slog")]
674            {
675                let slog_crate = slog_crate()?;
676                let mut slog_generics = generics;
677                let (_, ty_generics, _) = slog_generics.split_for_impl();
678                let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
679                slog_generics
680                    .make_where_clause()
681                    .predicates
682                    .push(parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
683                let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
684                    slog_generics.split_for_impl();
685                quote! {
686                    impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
687                        fn serialize(
688                            &self,
689                            _record: &#slog_crate::Record<'_>,
690                            key: #slog_crate::Key,
691                            serializer: &mut dyn #slog_crate::Serializer,
692                        ) -> #slog_crate::Result {
693                            let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
694                            serializer.emit_arguments(key, &format_args!("{}", redacted))
695                        }
696                    }
697
698                    impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
699                }
700            }
701
702            #[cfg(not(feature = "slog"))]
703            {
704                quote! {}
705            }
706        };
707
708        let tracing_impl = if dual {
709            quote! {}
710        } else {
711            #[cfg(feature = "tracing")]
712            {
713                let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
714                    redacted_display_generics.split_for_impl();
715                quote! {
716                    impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
717                }
718            }
719
720            #[cfg(not(feature = "tracing"))]
721            {
722                quote! {}
723            }
724        };
725
726        return Ok(quote! {
727            #redacted_display_impl
728            #to_redacted_output_impl
729            #debug_impl
730            #slog_impl
731            #tracing_impl
732        });
733    }
734
735    // Only DeriveKind::Sensitive reaches this point (SensitiveDisplay returns early above).
736
737    let derive_output = match data {
738        Data::Struct(data) => derive_struct(&ident, data, &generics)?,
739        Data::Enum(data) => derive_enum(&ident, data, &generics)?,
740        Data::Union(u) => {
741            return Err(syn::Error::new(
742                u.union_token.span(),
743                "`Sensitive` cannot be derived for unions",
744            ));
745        }
746    };
747
748    let policy_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
749    let policy_generics =
750        add_policy_applicable_bounds(policy_generics, &derive_output.policy_applicable_generics);
751    let (impl_generics, ty_generics, where_clause) = policy_generics.split_for_impl();
752    let debug_redacted_generics =
753        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
754    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
755        debug_redacted_generics.split_for_impl();
756    let debug_unredacted_generics =
757        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
758    let (
759        debug_unredacted_impl_generics,
760        debug_unredacted_ty_generics,
761        debug_unredacted_where_clause,
762    ) = debug_unredacted_generics.split_for_impl();
763    let redaction_body = &derive_output.redaction_body;
764    let debug_redacted_body = &derive_output.debug_redacted_body;
765    let debug_unredacted_body = &derive_output.debug_unredacted_body;
766    // In dual mode, SensitiveDisplay provides Debug — skip it here.
767    let debug_impl = if dual {
768        quote! {}
769    } else {
770        quote! {
771            #[cfg(any(test, feature = "testing"))]
772            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
773                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
774                    #debug_unredacted_body
775                }
776            }
777
778            #[cfg(not(any(test, feature = "testing")))]
779            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
780                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
781                    #debug_redacted_body
782                }
783            }
784        }
785    };
786
787    #[cfg(feature = "slog")]
788    let slog_impl = {
789        let slog_crate = slog_crate()?;
790        let mut slog_generics = generics;
791        let slog_where_clause = slog_generics.make_where_clause();
792        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
793        slog_where_clause
794            .predicates
795            .push(parse_quote!(#self_ty: ::core::clone::Clone));
796        // SlogRedactedExt requires Self: Serialize, so we add this bound to enable
797        // generic types to work with slog when their type parameters implement Serialize.
798        slog_where_clause
799            .predicates
800            .push(parse_quote!(#self_ty: ::serde::Serialize));
801        slog_where_clause
802            .predicates
803            .push(parse_quote!(#self_ty: #crate_root::slog::SlogRedactedExt));
804        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
805            slog_generics.split_for_impl();
806        quote! {
807            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
808                fn serialize(
809                    &self,
810                    _record: &#slog_crate::Record<'_>,
811                    key: #slog_crate::Key,
812                    serializer: &mut dyn #slog_crate::Serializer,
813                ) -> #slog_crate::Result {
814                    let redacted = #crate_root::slog::SlogRedactedExt::slog_redacted_json(self.clone());
815                    #slog_crate::Value::serialize(&redacted, _record, key, serializer)
816                }
817            }
818
819            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
820        }
821    };
822
823    #[cfg(not(feature = "slog"))]
824    let slog_impl = quote! {};
825
826    #[cfg(feature = "tracing")]
827    let tracing_impl = quote! {
828        impl #impl_generics #crate_root::tracing::TracingRedacted for #ident #ty_generics #where_clause {}
829    };
830
831    #[cfg(not(feature = "tracing"))]
832    let tracing_impl = quote! {};
833
834    let trait_impl = quote! {
835        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
836            fn redact_with<M: #crate_root::RedactableMapper>(self, mapper: &M) -> Self {
837                use #crate_root::RedactableWithMapper as _;
838                #redaction_body
839            }
840        }
841
842        #debug_impl
843
844        #slog_impl
845
846        #tracing_impl
847    };
848    Ok(trait_impl)
849}
850
851fn derive_unredacted_debug(
852    name: &Ident,
853    data: &Data,
854    generics: &syn::Generics,
855) -> Result<DebugOutput> {
856    match data {
857        Data::Struct(data) => Ok(derive_unredacted_debug_struct(name, data, generics)),
858        Data::Enum(data) => Ok(derive_unredacted_debug_enum(name, data, generics)),
859        Data::Union(u) => Err(syn::Error::new(
860            u.union_token.span(),
861            "`SensitiveDisplay` cannot be derived for unions",
862        )),
863    }
864}
865
866fn derive_unredacted_debug_struct(
867    name: &Ident,
868    data: &DataStruct,
869    generics: &syn::Generics,
870) -> DebugOutput {
871    let mut debug_generics = Vec::new();
872    match &data.fields {
873        Fields::Named(fields) => {
874            let mut bindings = Vec::new();
875            let mut debug_fields = Vec::new();
876            for field in &fields.named {
877                let ident = field
878                    .ident
879                    .clone()
880                    .expect("named field should have identifier");
881                bindings.push(ident.clone());
882                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
883                debug_fields.push(quote! {
884                    debug.field(stringify!(#ident), #ident);
885                });
886            }
887            DebugOutput {
888                body: quote! {
889                    match self {
890                        Self { #(#bindings),* } => {
891                            let mut debug = f.debug_struct(stringify!(#name));
892                            #(#debug_fields)*
893                            debug.finish()
894                        }
895                    }
896                },
897                generics: debug_generics,
898            }
899        }
900        Fields::Unnamed(fields) => {
901            let mut bindings = Vec::new();
902            let mut debug_fields = Vec::new();
903            for (index, field) in fields.unnamed.iter().enumerate() {
904                let ident = format_ident!("field_{index}");
905                bindings.push(ident.clone());
906                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
907                debug_fields.push(quote! {
908                    debug.field(#ident);
909                });
910            }
911            DebugOutput {
912                body: quote! {
913                    match self {
914                        Self ( #(#bindings),* ) => {
915                            let mut debug = f.debug_tuple(stringify!(#name));
916                            #(#debug_fields)*
917                            debug.finish()
918                        }
919                    }
920                },
921                generics: debug_generics,
922            }
923        }
924        Fields::Unit => DebugOutput {
925            body: quote! {
926                f.write_str(stringify!(#name))
927            },
928            generics: debug_generics,
929        },
930    }
931}
932
933fn derive_unredacted_debug_enum(
934    name: &Ident,
935    data: &DataEnum,
936    generics: &syn::Generics,
937) -> DebugOutput {
938    let mut debug_generics = Vec::new();
939    let mut debug_arms = Vec::new();
940    for variant in &data.variants {
941        let variant_ident = &variant.ident;
942        match &variant.fields {
943            Fields::Unit => {
944                debug_arms.push(quote! {
945                    #name::#variant_ident => f.write_str(stringify!(#name::#variant_ident))
946                });
947            }
948            Fields::Named(fields) => {
949                let mut bindings = Vec::new();
950                let mut debug_fields = Vec::new();
951                for field in &fields.named {
952                    let ident = field
953                        .ident
954                        .clone()
955                        .expect("named field should have identifier");
956                    bindings.push(ident.clone());
957                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
958                    debug_fields.push(quote! {
959                        debug.field(stringify!(#ident), #ident);
960                    });
961                }
962                debug_arms.push(quote! {
963                    #name::#variant_ident { #(#bindings),* } => {
964                        let mut debug = f.debug_struct(stringify!(#name::#variant_ident));
965                        #(#debug_fields)*
966                        debug.finish()
967                    }
968                });
969            }
970            Fields::Unnamed(fields) => {
971                let mut bindings = Vec::new();
972                let mut debug_fields = Vec::new();
973                for (index, field) in fields.unnamed.iter().enumerate() {
974                    let ident = format_ident!("field_{index}");
975                    bindings.push(ident.clone());
976                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
977                    debug_fields.push(quote! {
978                        debug.field(#ident);
979                    });
980                }
981                debug_arms.push(quote! {
982                    #name::#variant_ident ( #(#bindings),* ) => {
983                        let mut debug = f.debug_tuple(stringify!(#name::#variant_ident));
984                        #(#debug_fields)*
985                        debug.finish()
986                    }
987                });
988            }
989        }
990    }
991    DebugOutput {
992        body: quote! {
993            match self {
994                #(#debug_arms),*
995            }
996        },
997        generics: debug_generics,
998    }
999}