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 by default; actual values in the consumer's `cfg(test)` builds or when
129///   `redactable`'s `testing` feature is enabled. Skipped when `#[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 container-level, variant-level, and field-level attributes. `#[sensitive]`
182/// is wrong because the type is explicitly non-sensitive; `#[not_sensitive]` is
183/// redundant 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 attr in &variant.attrs {
218                    check_attr(attr)?;
219                }
220                for field in &variant.fields {
221                    for attr in &field.attrs {
222                        check_attr(attr)?;
223                    }
224                }
225            }
226        }
227        Data::Union(_) => {}
228    }
229
230    Ok(())
231}
232
233fn expand_not_sensitive(input: DeriveInput) -> Result<TokenStream> {
234    let DeriveInput {
235        ident,
236        generics,
237        data,
238        attrs,
239        ..
240    } = input;
241
242    // Reject unions
243    if let Data::Union(u) = &data {
244        return Err(syn::Error::new(
245            u.union_token.span(),
246            "`NotSensitive` cannot be derived for unions",
247        ));
248    }
249
250    reject_sensitivity_attrs(&attrs, &data, "NotSensitive")?;
251
252    let crate_root = crate_root();
253
254    // RedactableWithMapper impl (no-op passthrough). Deriving NotSensitive is
255    // an explicit declaration, so the type also gets DeclaredRedactable.
256    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
257    let container_impl = quote! {
258        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
259            fn redact_with<M: #crate_root::RedactableMapper>(self, _mapper: &M) -> Self {
260                self
261            }
262        }
263
264        impl #impl_generics #crate_root::DeclaredRedactable for #ident #ty_generics #where_clause {}
265    };
266
267    // slog impl - serialize directly as structured JSON (no redaction needed)
268    #[cfg(feature = "slog")]
269    let slog_impl = {
270        let slog_crate = slog_crate()?;
271        let mut slog_generics = generics.clone();
272        let (_, ty_generics, _) = slog_generics.split_for_impl();
273        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
274        slog_generics
275            .make_where_clause()
276            .predicates
277            .push(parse_quote!(#self_ty: ::serde::Serialize));
278        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
279            slog_generics.split_for_impl();
280        quote! {
281            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
282                fn serialize(
283                    &self,
284                    _record: &#slog_crate::Record<'_>,
285                    key: #slog_crate::Key,
286                    serializer: &mut dyn #slog_crate::Serializer,
287                ) -> #slog_crate::Result {
288                    #crate_root::slog::__slog_serialize_not_sensitive(self, _record, key, serializer)
289                }
290            }
291
292            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
293        }
294    };
295
296    #[cfg(not(feature = "slog"))]
297    let slog_impl = quote! {};
298
299    // tracing impl
300    #[cfg(feature = "tracing")]
301    let tracing_impl = {
302        let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
303            generics.split_for_impl();
304        quote! {
305            impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
306        }
307    };
308
309    #[cfg(not(feature = "tracing"))]
310    let tracing_impl = quote! {};
311
312    Ok(quote! {
313        #container_impl
314        #slog_impl
315        #tracing_impl
316    })
317}
318
319/// Derives `redactable::RedactableWithFormatter` for types with no sensitive data.
320///
321/// This is the display counterpart to `NotSensitive`. Use it when you have a type
322/// with no sensitive data that needs logging integration (e.g., for use with slog).
323///
324/// Unlike `SensitiveDisplay`, this derive does **not** require a display template.
325/// Instead, it delegates directly to the type's existing `Display` implementation.
326///
327/// # Required Bounds
328///
329/// The type must implement `Display`. This is required because `RedactableWithFormatter` delegates
330/// to `Display::fmt`.
331///
332/// # Generated Impls
333///
334/// - `RedactableWithMapper`: no-op passthrough (allows use inside `Sensitive` containers)
335/// - `RedactableWithFormatter`: delegates to `Display::fmt`
336/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): uses `RedactableWithFormatter` output
337/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
338///
339/// # Debug
340///
341/// `NotSensitiveDisplay` does **not** generate a `Debug` impl - there's nothing to redact.
342/// Use `#[derive(Debug)]` alongside `NotSensitiveDisplay` when needed.
343///
344/// # Rejected Attributes
345///
346/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
347/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
348/// latter is redundant (the entire type is already non-sensitive).
349///
350/// # Example
351///
352/// ```ignore
353/// use redactable::NotSensitiveDisplay;
354///
355/// #[derive(Clone, NotSensitiveDisplay)]
356/// #[display(fmt = "RetryDecision")]  // Or use displaydoc/thiserror for Display impl
357/// enum RetryDecision {
358///     Retry,
359///     Abort,
360/// }
361/// ```
362#[proc_macro_derive(NotSensitiveDisplay)]
363pub fn derive_not_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
364    let input = parse_macro_input!(input as DeriveInput);
365    match expand_not_sensitive_display(input) {
366        Ok(tokens) => tokens.into(),
367        Err(err) => err.into_compile_error().into(),
368    }
369}
370
371fn expand_not_sensitive_display(input: DeriveInput) -> Result<TokenStream> {
372    let DeriveInput {
373        ident,
374        generics,
375        data,
376        attrs,
377        ..
378    } = input;
379
380    // Reject unions
381    if let Data::Union(u) = &data {
382        return Err(syn::Error::new(
383            u.union_token.span(),
384            "`NotSensitiveDisplay` cannot be derived for unions",
385        ));
386    }
387
388    reject_sensitivity_attrs(&attrs, &data, "NotSensitiveDisplay")?;
389
390    let crate_root = crate_root();
391
392    // Generate the RedactableWithMapper no-op passthrough impl
393    // This is always generated, allowing NotSensitiveDisplay to be used inside Sensitive containers.
394    // Deriving NotSensitiveDisplay is an explicit declaration, so the type also
395    // gets DeclaredRedactable.
396    let (container_impl_generics, container_ty_generics, container_where_clause) =
397        generics.split_for_impl();
398    let container_impl = quote! {
399        impl #container_impl_generics #crate_root::RedactableWithMapper for #ident #container_ty_generics #container_where_clause {
400            fn redact_with<M: #crate_root::RedactableMapper>(self, _mapper: &M) -> Self {
401                self
402            }
403        }
404
405        impl #container_impl_generics #crate_root::DeclaredRedactable for #ident #container_ty_generics #container_where_clause {}
406    };
407
408    // Always delegate to Display::fmt (no template parsing for NotSensitiveDisplay)
409    // Add Display bound to generics for RedactableWithFormatter impl
410    let mut display_generics = generics.clone();
411    let display_where_clause = display_generics.make_where_clause();
412    // Collect type parameters that need Display bound
413    for param in generics.type_params() {
414        let ident = &param.ident;
415        display_where_clause
416            .predicates
417            .push(syn::parse_quote!(#ident: ::core::fmt::Display));
418    }
419
420    let (display_impl_generics, display_ty_generics, display_where_clause) =
421        display_generics.split_for_impl();
422
423    // RedactableWithFormatter impl - delegates to Display
424    let redacted_display_impl = quote! {
425        impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
426            fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
427                ::core::fmt::Display::fmt(self, f)
428            }
429        }
430    };
431
432    // slog impl
433    #[cfg(feature = "slog")]
434    let slog_impl = {
435        let slog_crate = slog_crate()?;
436        let mut slog_generics = generics.clone();
437        let (_, ty_generics, _) = slog_generics.split_for_impl();
438        let self_ty: syn::Type = syn::parse_quote!(#ident #ty_generics);
439        slog_generics
440            .make_where_clause()
441            .predicates
442            .push(syn::parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
443        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
444            slog_generics.split_for_impl();
445        quote! {
446            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
447                fn serialize(
448                    &self,
449                    _record: &#slog_crate::Record<'_>,
450                    key: #slog_crate::Key,
451                    serializer: &mut dyn #slog_crate::Serializer,
452                ) -> #slog_crate::Result {
453                    let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
454                    serializer.emit_arguments(key, &format_args!("{}", redacted))
455                }
456            }
457
458            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
459        }
460    };
461
462    #[cfg(not(feature = "slog"))]
463    let slog_impl = quote! {};
464
465    // tracing impl - uses the original generics (no extra Display bounds needed for marker trait)
466    #[cfg(feature = "tracing")]
467    let tracing_impl = {
468        let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
469            generics.split_for_impl();
470        quote! {
471            impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
472        }
473    };
474
475    #[cfg(not(feature = "tracing"))]
476    let tracing_impl = quote! {};
477
478    Ok(quote! {
479        #container_impl
480        #redacted_display_impl
481        #slog_impl
482        #tracing_impl
483    })
484}
485
486/// Derives `redactable::RedactableWithFormatter` using a display template.
487///
488/// This generates a redacted string representation without requiring `Clone`.
489/// Unannotated fields use `RedactableWithFormatter` by default (passthrough for scalars,
490/// redacted display for nested `SensitiveDisplay` types).
491///
492/// # Field Annotations
493///
494/// - *(none)*: Uses `RedactableWithFormatter` (requires the field type to implement it)
495/// - `#[sensitive(Policy)]`: Apply the policy's redaction rules
496/// - `#[not_sensitive]`: Render raw via `Display` (use for types without `RedactableWithFormatter`)
497///
498/// The display template is taken from `#[error("...")]` (thiserror-style) or from
499/// doc comments (displaydoc-style). If neither is present, the derive fails.
500///
501/// Fields are redacted by reference, so field types do not need `Clone`.
502///
503/// # Generated Impls
504///
505/// - `RedactableWithFormatter`: always generated.
506/// - `Debug`: redacted by default; actual values in the consumer's `cfg(test)` builds or when
507///   `redactable`'s `testing` feature is enabled.
508/// - `slog::Value` + `SlogRedacted`: emits the redacted display string (requires `slog` feature).
509///   Skipped when `#[sensitive(dual)]` is set (Sensitive provides them instead).
510/// - `TracingRedacted`: marker trait (requires `tracing` feature).
511///   Skipped when `#[sensitive(dual)]` is set.
512#[proc_macro_derive(SensitiveDisplay, attributes(sensitive, not_sensitive, error))]
513pub fn derive_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
514    let input = parse_macro_input!(input as DeriveInput);
515    match expand(input, DeriveKind::SensitiveDisplay) {
516        Ok(tokens) => tokens.into(),
517        Err(err) => err.into_compile_error().into(),
518    }
519}
520
521/// Returns the token stream to reference the redactable crate root.
522///
523/// Handles crate renaming (e.g., `my_redact = { package = "redactable", ... }`)
524/// and internal usage (when derive is used inside the redactable crate itself).
525fn crate_root() -> proc_macro2::TokenStream {
526    match crate_name("redactable") {
527        Ok(FoundCrate::Itself) => quote! { crate },
528        Ok(FoundCrate::Name(name)) => {
529            let ident = format_ident!("{}", name);
530            quote! { ::#ident }
531        }
532        Err(_) => quote! { ::redactable },
533    }
534}
535
536/// Returns the token stream to reference the slog crate root.
537///
538/// Handles crate renaming (e.g., `my_slog = { package = "slog", ... }`).
539/// If the top-level `slog` crate is not available, falls back to the
540/// `REDACTABLE_SLOG_CRATE` env var, which should be a path like `my_log::slog`.
541#[cfg(feature = "slog")]
542fn slog_crate() -> Result<proc_macro2::TokenStream> {
543    match crate_name("slog") {
544        Ok(FoundCrate::Itself) => Ok(quote! { crate }),
545        Ok(FoundCrate::Name(name)) => {
546            let ident = format_ident!("{}", name);
547            Ok(quote! { ::#ident })
548        }
549        Err(_) => {
550            let env_value = std::env::var("REDACTABLE_SLOG_CRATE").map_err(|_| {
551                syn::Error::new(
552                    Span::call_site(),
553                    "slog support is enabled, but no top-level `slog` crate was found. \
554Set the REDACTABLE_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
555`slog` as a direct dependency.",
556                )
557            })?;
558            let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
559                syn::Error::new(
560                    Span::call_site(),
561                    format!("REDACTABLE_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
562                )
563            })?;
564            Ok(quote! { #path })
565        }
566    }
567}
568
569fn crate_path(item: &str) -> proc_macro2::TokenStream {
570    let root = crate_root();
571    let item_ident = syn::parse_str::<syn::Path>(item).expect("redactable crate path should parse");
572    quote! { #root::#item_ident }
573}
574
575/// Output produced by struct/enum derive logic for `Sensitive`.
576///
577/// Shared by `derive_struct`, `derive_enum`, and the top-level `expand()`.
578pub(crate) struct DeriveOutput {
579    pub(crate) redaction_body: TokenStream,
580    pub(crate) used_generics: Vec<Ident>,
581    pub(crate) policy_applicable_generics: Vec<Ident>,
582    pub(crate) debug_redacted_body: TokenStream,
583    pub(crate) debug_unredacted_body: TokenStream,
584    pub(crate) debug_unredacted_generics: Vec<Ident>,
585}
586
587struct DebugOutput {
588    body: TokenStream,
589    generics: Vec<Ident>,
590}
591
592/// Which derive macro invoked `expand()`.
593///
594/// Controls what impls are generated: `Sensitive` emits `RedactableWithMapper` (structural
595/// traversal), while `SensitiveDisplay` emits `RedactableWithFormatter` (display formatting).
596enum DeriveKind {
597    /// `#[derive(Sensitive)]` — structural redaction via `RedactableWithMapper`.
598    Sensitive,
599    /// `#[derive(SensitiveDisplay)]` — display formatting via `RedactableWithFormatter`.
600    SensitiveDisplay,
601}
602
603#[allow(clippy::too_many_lines)]
604fn expand(input: DeriveInput, kind: DeriveKind) -> Result<TokenStream> {
605    let DeriveInput {
606        ident,
607        generics,
608        data,
609        attrs,
610        ..
611    } = input;
612
613    let ContainerOptions { dual } = parse_container_options(&attrs)?;
614
615    let crate_root = crate_root();
616
617    // `#[sensitive(dual)]` is honor-system between the two macros: each one
618    // skips impls it expects the other to provide, and a macro cannot see its
619    // sibling derives. Assert at compile time that the counterpart's trait
620    // actually exists so dual with only one derive fails loudly instead of
621    // silently dropping the redacted Debug (or slog/tracing) impls. The
622    // assertion needs a fully concrete type name, so generic types are not
623    // checked.
624    let dual_pairing_assert = if dual && generics.params.is_empty() {
625        match kind {
626            DeriveKind::Sensitive => quote! {
627                const _: fn() = || {
628                    // dual skips Debug here because SensitiveDisplay provides it;
629                    // this fails to compile when SensitiveDisplay is not derived.
630                    fn dual_requires_sensitive_display<T: #crate_root::RedactableWithFormatter>() {}
631                    dual_requires_sensitive_display::<#ident>();
632                };
633            },
634            DeriveKind::SensitiveDisplay => quote! {
635                const _: fn() = || {
636                    // dual skips slog/tracing here because Sensitive provides them;
637                    // this fails to compile when Sensitive is not derived.
638                    fn dual_requires_sensitive<T: #crate_root::RedactableWithMapper>() {}
639                    dual_requires_sensitive::<#ident>();
640                };
641            },
642        }
643    } else {
644        quote! {}
645    };
646
647    if matches!(kind, DeriveKind::SensitiveDisplay) {
648        let redacted_display_output = derive_redacted_display(&ident, &data, &attrs, &generics)?;
649        let redacted_display_generics =
650            add_display_bounds(generics.clone(), &redacted_display_output.display_generics);
651        let redacted_display_generics = add_debug_bounds(
652            redacted_display_generics,
653            &redacted_display_output.debug_generics,
654        );
655        let redacted_display_generics = add_policy_applicable_ref_bounds(
656            redacted_display_generics,
657            &redacted_display_output.policy_ref_generics,
658        );
659        let redacted_display_generics = add_redacted_display_bounds(
660            redacted_display_generics,
661            &redacted_display_output.nested_generics,
662        );
663        let (display_impl_generics, display_ty_generics, display_where_clause) =
664            redacted_display_generics.split_for_impl();
665        let redacted_display_body = redacted_display_output.body;
666        // Deriving SensitiveDisplay is an explicit declaration, so the type
667        // also gets DeclaredRedactable (certifying it for the formatter-side
668        // logging extensions). In dual mode Sensitive emits the marker instead.
669        let declared_impl = if dual {
670            quote! {}
671        } else {
672            let (marker_impl_generics, marker_ty_generics, marker_where_clause) =
673                generics.split_for_impl();
674            quote! {
675                impl #marker_impl_generics #crate_root::DeclaredRedactable for #ident #marker_ty_generics #marker_where_clause {}
676            }
677        };
678        let redacted_display_impl = quote! {
679            impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
680                fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
681                    #redacted_display_body
682                }
683            }
684
685            #declared_impl
686        };
687        let to_redacted_output_impl = quote! {
688            impl #display_impl_generics #crate_root::ToRedactedOutput for #ident #display_ty_generics #display_where_clause {
689                fn to_redacted_output(&self) -> #crate_root::RedactedOutput {
690                    #crate_root::RedactedOutput::Text(
691                        #crate_root::RedactableWithFormatter::redacted_display(self).to_string(),
692                    )
693                }
694            }
695        };
696
697        let debug_output = derive_unredacted_debug(&ident, &data, &generics)?;
698        // A single impl branches at runtime on `cfg!(test) || redactable::__TESTING`
699        // rather than emitting two `#[cfg]`-gated impls. The `feature = "testing"`
700        // check must resolve against `redactable`'s own feature, not the consumer's,
701        // so it is routed through the `__TESTING` constant. The where-clause is the
702        // union of the formatter bounds (redacted body) and the Debug bounds
703        // (unredacted body) because both bodies live in the same impl.
704        let debug_generics =
705            add_debug_bounds(redacted_display_generics.clone(), &debug_output.generics);
706        let (debug_impl_generics, debug_ty_generics, debug_where_clause) =
707            debug_generics.split_for_impl();
708        let debug_unredacted_body = debug_output.body;
709        let debug_impl = quote! {
710            impl #debug_impl_generics ::core::fmt::Debug for #ident #debug_ty_generics #debug_where_clause {
711                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
712                    if ::core::cfg!(test) || #crate_root::__TESTING {
713                        #debug_unredacted_body
714                    } else {
715                        #crate_root::RedactableWithFormatter::fmt_redacted(self, f)
716                    }
717                }
718            }
719        };
720
721        // In dual mode, Sensitive provides slog and tracing impls — skip them here.
722        let slog_impl = if dual {
723            quote! {}
724        } else {
725            #[cfg(feature = "slog")]
726            {
727                let slog_crate = slog_crate()?;
728                let mut slog_generics = generics;
729                let (_, ty_generics, _) = slog_generics.split_for_impl();
730                let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
731                slog_generics
732                    .make_where_clause()
733                    .predicates
734                    .push(parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
735                let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
736                    slog_generics.split_for_impl();
737                quote! {
738                    impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
739                        fn serialize(
740                            &self,
741                            _record: &#slog_crate::Record<'_>,
742                            key: #slog_crate::Key,
743                            serializer: &mut dyn #slog_crate::Serializer,
744                        ) -> #slog_crate::Result {
745                            let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
746                            serializer.emit_arguments(key, &format_args!("{}", redacted))
747                        }
748                    }
749
750                    impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
751                }
752            }
753
754            #[cfg(not(feature = "slog"))]
755            {
756                quote! {}
757            }
758        };
759
760        let tracing_impl = if dual {
761            quote! {}
762        } else {
763            #[cfg(feature = "tracing")]
764            {
765                let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
766                    redacted_display_generics.split_for_impl();
767                quote! {
768                    impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
769                }
770            }
771
772            #[cfg(not(feature = "tracing"))]
773            {
774                quote! {}
775            }
776        };
777
778        return Ok(quote! {
779            #redacted_display_impl
780            #to_redacted_output_impl
781            #debug_impl
782            #slog_impl
783            #tracing_impl
784            #dual_pairing_assert
785        });
786    }
787
788    // Only DeriveKind::Sensitive reaches this point (SensitiveDisplay returns early above).
789
790    let derive_output = match data {
791        Data::Struct(data) => derive_struct(&ident, data, &generics)?,
792        Data::Enum(data) => derive_enum(&ident, data, &generics)?,
793        Data::Union(u) => {
794            return Err(syn::Error::new(
795                u.union_token.span(),
796                "`Sensitive` cannot be derived for unions",
797            ));
798        }
799    };
800
801    let policy_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
802    let policy_generics =
803        add_policy_applicable_bounds(policy_generics, &derive_output.policy_applicable_generics);
804    let (impl_generics, ty_generics, where_clause) = policy_generics.split_for_impl();
805    // The merged Debug impl uses the unredacted bounds (a superset of the
806    // redacted bounds) because both bodies share one impl.
807    let debug_unredacted_generics =
808        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
809    let (
810        debug_unredacted_impl_generics,
811        debug_unredacted_ty_generics,
812        debug_unredacted_where_clause,
813    ) = debug_unredacted_generics.split_for_impl();
814    let redaction_body = &derive_output.redaction_body;
815    let debug_redacted_body = &derive_output.debug_redacted_body;
816    let debug_unredacted_body = &derive_output.debug_unredacted_body;
817    // In dual mode, SensitiveDisplay provides Debug — skip it here.
818    //
819    // A single impl branches at runtime on `cfg!(test) || redactable::__TESTING`
820    // rather than emitting two `#[cfg]`-gated impls. The `feature = "testing"`
821    // check must resolve against `redactable`'s own feature, not the consumer's,
822    // so it is routed through the `__TESTING` constant. The where-clause uses the
823    // unredacted bounds (a superset of the redacted bounds) because both bodies
824    // live in the same impl.
825    let debug_impl = if dual {
826        quote! {}
827    } else {
828        quote! {
829            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
830                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
831                    if ::core::cfg!(test) || #crate_root::__TESTING {
832                        #debug_unredacted_body
833                    } else {
834                        #debug_redacted_body
835                    }
836                }
837            }
838        }
839    };
840
841    #[cfg(feature = "slog")]
842    let slog_impl = {
843        let slog_crate = slog_crate()?;
844        let mut slog_generics = generics;
845        let slog_where_clause = slog_generics.make_where_clause();
846        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
847        slog_where_clause
848            .predicates
849            .push(parse_quote!(#self_ty: ::core::clone::Clone));
850        // SlogRedactedExt requires Self: Serialize, so we add this bound to enable
851        // generic types to work with slog when their type parameters implement Serialize.
852        slog_where_clause
853            .predicates
854            .push(parse_quote!(#self_ty: ::serde::Serialize));
855        slog_where_clause
856            .predicates
857            .push(parse_quote!(#self_ty: #crate_root::slog::SlogRedactedExt));
858        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
859            slog_generics.split_for_impl();
860        quote! {
861            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
862                fn serialize(
863                    &self,
864                    _record: &#slog_crate::Record<'_>,
865                    key: #slog_crate::Key,
866                    serializer: &mut dyn #slog_crate::Serializer,
867                ) -> #slog_crate::Result {
868                    let redacted = #crate_root::slog::SlogRedactedExt::slog_redacted_json(self.clone());
869                    #slog_crate::Value::serialize(&redacted, _record, key, serializer)
870                }
871            }
872
873            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
874        }
875    };
876
877    #[cfg(not(feature = "slog"))]
878    let slog_impl = quote! {};
879
880    #[cfg(feature = "tracing")]
881    let tracing_impl = quote! {
882        impl #impl_generics #crate_root::tracing::TracingRedacted for #ident #ty_generics #where_clause {}
883    };
884
885    #[cfg(not(feature = "tracing"))]
886    let tracing_impl = quote! {};
887
888    let trait_impl = quote! {
889        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
890            fn redact_with<M: #crate_root::RedactableMapper>(self, mapper: &M) -> Self {
891                use #crate_root::RedactableWithMapper as _;
892                #redaction_body
893            }
894        }
895
896        impl #impl_generics #crate_root::DeclaredRedactable for #ident #ty_generics #where_clause {}
897
898        #debug_impl
899
900        #slog_impl
901
902        #tracing_impl
903
904        #dual_pairing_assert
905    };
906    Ok(trait_impl)
907}
908
909fn derive_unredacted_debug(
910    name: &Ident,
911    data: &Data,
912    generics: &syn::Generics,
913) -> Result<DebugOutput> {
914    match data {
915        Data::Struct(data) => Ok(derive_unredacted_debug_struct(name, data, generics)),
916        Data::Enum(data) => Ok(derive_unredacted_debug_enum(name, data, generics)),
917        Data::Union(u) => Err(syn::Error::new(
918            u.union_token.span(),
919            "`SensitiveDisplay` cannot be derived for unions",
920        )),
921    }
922}
923
924fn derive_unredacted_debug_struct(
925    name: &Ident,
926    data: &DataStruct,
927    generics: &syn::Generics,
928) -> DebugOutput {
929    let mut debug_generics = Vec::new();
930    match &data.fields {
931        Fields::Named(fields) => {
932            let mut bindings = Vec::new();
933            let mut debug_fields = Vec::new();
934            for field in &fields.named {
935                let ident = field
936                    .ident
937                    .clone()
938                    .expect("named field should have identifier");
939                bindings.push(ident.clone());
940                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
941                debug_fields.push(quote! {
942                    debug.field(stringify!(#ident), #ident);
943                });
944            }
945            DebugOutput {
946                body: quote! {
947                    match self {
948                        Self { #(#bindings),* } => {
949                            let mut debug = f.debug_struct(stringify!(#name));
950                            #(#debug_fields)*
951                            debug.finish()
952                        }
953                    }
954                },
955                generics: debug_generics,
956            }
957        }
958        Fields::Unnamed(fields) => {
959            let mut bindings = Vec::new();
960            let mut debug_fields = Vec::new();
961            for (index, field) in fields.unnamed.iter().enumerate() {
962                let ident = format_ident!("field_{index}");
963                bindings.push(ident.clone());
964                collect_generics_from_type(&field.ty, generics, &mut debug_generics);
965                debug_fields.push(quote! {
966                    debug.field(#ident);
967                });
968            }
969            DebugOutput {
970                body: quote! {
971                    match self {
972                        Self ( #(#bindings),* ) => {
973                            let mut debug = f.debug_tuple(stringify!(#name));
974                            #(#debug_fields)*
975                            debug.finish()
976                        }
977                    }
978                },
979                generics: debug_generics,
980            }
981        }
982        Fields::Unit => DebugOutput {
983            body: quote! {
984                f.write_str(stringify!(#name))
985            },
986            generics: debug_generics,
987        },
988    }
989}
990
991fn derive_unredacted_debug_enum(
992    name: &Ident,
993    data: &DataEnum,
994    generics: &syn::Generics,
995) -> DebugOutput {
996    let mut debug_generics = Vec::new();
997    let mut debug_arms = Vec::new();
998    for variant in &data.variants {
999        let variant_ident = &variant.ident;
1000        match &variant.fields {
1001            Fields::Unit => {
1002                debug_arms.push(quote! {
1003                    #name::#variant_ident => f.write_str(stringify!(#name::#variant_ident))
1004                });
1005            }
1006            Fields::Named(fields) => {
1007                let mut bindings = Vec::new();
1008                let mut debug_fields = Vec::new();
1009                for field in &fields.named {
1010                    let ident = field
1011                        .ident
1012                        .clone()
1013                        .expect("named field should have identifier");
1014                    bindings.push(ident.clone());
1015                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
1016                    debug_fields.push(quote! {
1017                        debug.field(stringify!(#ident), #ident);
1018                    });
1019                }
1020                debug_arms.push(quote! {
1021                    #name::#variant_ident { #(#bindings),* } => {
1022                        let mut debug = f.debug_struct(stringify!(#name::#variant_ident));
1023                        #(#debug_fields)*
1024                        debug.finish()
1025                    }
1026                });
1027            }
1028            Fields::Unnamed(fields) => {
1029                let mut bindings = Vec::new();
1030                let mut debug_fields = Vec::new();
1031                for (index, field) in fields.unnamed.iter().enumerate() {
1032                    let ident = format_ident!("field_{index}");
1033                    bindings.push(ident.clone());
1034                    collect_generics_from_type(&field.ty, generics, &mut debug_generics);
1035                    debug_fields.push(quote! {
1036                        debug.field(#ident);
1037                    });
1038                }
1039                debug_arms.push(quote! {
1040                    #name::#variant_ident ( #(#bindings),* ) => {
1041                        let mut debug = f.debug_tuple(stringify!(#name::#variant_ident));
1042                        #(#debug_fields)*
1043                        debug.finish()
1044                    }
1045                });
1046            }
1047        }
1048    }
1049    DebugOutput {
1050        body: quote! {
1051            match self {
1052                #(#debug_arms),*
1053            }
1054        },
1055        generics: debug_generics,
1056    }
1057}