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