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