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};
70use proc_macro2::Span;
71use proc_macro2::{Ident, TokenStream};
72use quote::{format_ident, quote};
73use syn::{
74    Data, DataEnum, DataStruct, DeriveInput, Fields, Result, parse_macro_input, parse_quote,
75    spanned::Spanned,
76};
77
78mod container;
79mod derive_enum;
80mod derive_struct;
81mod generics;
82mod policy_guard;
83mod redacted_display;
84mod strategy;
85mod transform;
86mod types;
87use container::{ContainerOptions, parse_container_options};
88use derive_enum::derive_enum;
89use derive_struct::derive_struct;
90use generics::{add_predicates, push_debug_predicate};
91use redacted_display::derive_redacted_display;
92
93/// Derives `redactable::RedactableWithMapper` (and related impls) for structs and enums.
94///
95/// # Container Attributes
96///
97/// These attributes are placed on the struct/enum itself:
98///
99/// - `#[sensitive(dual)]` - Use when deriving both `Sensitive` and `SensitiveDisplay` on the same
100///   type. `Sensitive` skips its `Debug` impl (letting `SensitiveDisplay` provide it), and
101///   `SensitiveDisplay` skips its `slog`/`tracing` impls (letting `Sensitive` provide them).
102///   For non-generic types, generated code checks that the counterpart derive is present.
103///   Generic `dual` types cannot be checked at derive time; missing counterparts surface later as
104///   trait-bound errors when the type is used.
105///
106/// # Field Attributes
107///
108/// - **No annotation**: The field is traversed by default. Scalars pass through unchanged; nested
109///   structs/enums are walked using `RedactableWithMapper` (so external types must implement it).
110///
111/// - `#[sensitive(Secret)]`: For scalar types (i32, bool, char, etc.), redacts to default values
112///   (0, false, '*'). For string-like types, applies full redaction to `"[REDACTED]"`.
113///
114/// - `#[sensitive(Policy)]`: Applies the policy's redaction rules to string-like
115///   values. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`. Scalars can only
116///   use `#[sensitive(Secret)]`.
117///
118/// - `#[not_sensitive]`: Explicit passthrough - the field is not transformed at all. Use this
119///   for foreign types that don't implement `RedactableWithMapper`. This is equivalent to wrapping
120///   the field type in `NotSensitiveValue<T>`, but without changing the type signature.
121///
122/// Unions are rejected at compile time.
123///
124/// # Generated Impls
125///
126/// - `RedactableWithMapper`: always generated.
127/// - `Redactable`: always generated. Provides `.redact()` and certifies the type for the
128///   redacted-output extension traits (`RedactedOutputExt`, `RedactedJsonExt`, `SlogRedactedExt`).
129/// - `Debug`: redacted by default; actual values in the consumer's `cfg(test)` builds or when
130///   `redactable`'s `testing` feature is enabled. Skipped when `#[sensitive(dual)]` is set.
131/// - `slog::Value` + `SlogRedacted` (requires `slog` feature): implemented by cloning the value
132///   and routing it through `redactable::slog::SlogRedactedExt`. Requires `Clone` and
133///   `serde::Serialize` because it emits structured JSON. Generated dependency paths resolve
134///   through `redactable::__private`, including when the dependency is renamed.
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/// - `Redactable`: deriving `NotSensitive` is an explicit declaration, so the type is
156///   certified for the redacted-output extension traits
157/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): serializes the value
158///   directly as structured JSON without redaction (same format as `Sensitive`, but skips
159///   the redaction step). Requires `Serialize` on the type.
160/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
161///
162/// `NotSensitive` does **not** generate a `Debug` impl - there's nothing to redact.
163/// Use `#[derive(Debug)]` when needed.
164///
165/// # Rejected Attributes
166///
167/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
168/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
169/// latter is redundant (the entire type is already non-sensitive).
170///
171/// Unions are rejected at compile time.
172#[proc_macro_derive(NotSensitive, attributes(not_sensitive))]
173pub fn derive_not_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
174    let input = parse_macro_input!(input as DeriveInput);
175    match expand_not_sensitive(input) {
176        Ok(tokens) => tokens.into(),
177        Err(err) => err.into_compile_error().into(),
178    }
179}
180
181/// Rejects `#[sensitive]` and `#[not_sensitive]` attributes on a non-sensitive type.
182///
183/// Checks container-level, variant-level, and field-level attributes. `#[sensitive]`
184/// is wrong because the type is explicitly non-sensitive; `#[not_sensitive]` is
185/// redundant because the entire type is already non-sensitive.
186fn reject_sensitivity_attrs(attrs: &[syn::Attribute], data: &Data, macro_name: &str) -> Result<()> {
187    let check_attr = |attr: &syn::Attribute| -> Result<()> {
188        if attr.path().is_ident("sensitive") {
189            return Err(syn::Error::new(
190                attr.span(),
191                format!("`#[sensitive]` attributes are not allowed on `{macro_name}` types"),
192            ));
193        }
194        if attr.path().is_ident("not_sensitive") {
195            return Err(syn::Error::new(
196                attr.span(),
197                format!(
198                    "`#[not_sensitive]` attributes are not needed on `{macro_name}` types (the entire type is already non-sensitive)"
199                ),
200            ));
201        }
202        Ok(())
203    };
204
205    for attr in attrs {
206        check_attr(attr)?;
207    }
208
209    match data {
210        Data::Struct(data) => {
211            for field in &data.fields {
212                for attr in &field.attrs {
213                    check_attr(attr)?;
214                }
215            }
216        }
217        Data::Enum(data) => {
218            for variant in &data.variants {
219                for attr in &variant.attrs {
220                    check_attr(attr)?;
221                }
222                for field in &variant.fields {
223                    for attr in &field.attrs {
224                        check_attr(attr)?;
225                    }
226                }
227            }
228        }
229        Data::Union(_) => {}
230    }
231
232    Ok(())
233}
234
235fn expand_not_sensitive(input: DeriveInput) -> Result<TokenStream> {
236    let DeriveInput {
237        ident,
238        generics,
239        data,
240        attrs,
241        ..
242    } = input;
243
244    // Reject unions
245    if let Data::Union(u) = &data {
246        return Err(syn::Error::new(
247            u.union_token.span(),
248            "`NotSensitive` cannot be derived for unions",
249        ));
250    }
251
252    reject_sensitivity_attrs(&attrs, &data, "NotSensitive")?;
253
254    let crate_root = crate_root();
255    let mapper_type = internal_ident("__RedactableMapper");
256    let mapper = internal_ident("__redactable_mapper");
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<#mapper_type: #crate_root::RedactableMapper>(self, #mapper: &#mapper_type) -> 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 = quote! { #crate_root::__private::slog };
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: #crate_root::__private::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    let mapper_type = internal_ident("__RedactableMapper");
400    let mapper = internal_ident("__redactable_mapper");
401
402    // Generate the RedactableWithMapper no-op passthrough impl
403    // This is always generated, allowing NotSensitiveDisplay to be used inside Sensitive containers.
404    // Deriving NotSensitiveDisplay is an explicit declaration, so the type also
405    // gets Redactable.
406    let (container_impl_generics, container_ty_generics, container_where_clause) =
407        generics.split_for_impl();
408    let container_impl = quote! {
409        impl #container_impl_generics #crate_root::RedactableWithMapper for #ident #container_ty_generics #container_where_clause {
410            fn redact_with<#mapper_type: #crate_root::RedactableMapper>(self, #mapper: &#mapper_type) -> Self {
411                self
412            }
413        }
414
415        impl #container_impl_generics #crate_root::Redactable for #ident #container_ty_generics #container_where_clause {}
416    };
417
418    // Always delegate to Display::fmt (no template parsing for NotSensitiveDisplay)
419    // Constrain the complete derived type rather than every type parameter.
420    let mut display_generics = generics.clone();
421    let (_, display_ty, _) = generics.split_for_impl();
422    let display_self_ty: syn::Type = parse_quote!(#ident #display_ty);
423    let display_where_clause = display_generics.make_where_clause();
424    display_where_clause
425        .predicates
426        .push(parse_quote!(#display_self_ty: ::core::fmt::Display));
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, __redactable_f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
437                ::core::fmt::Display::fmt(self, __redactable_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 = quote! { #crate_root::__private::slog };
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/// When `#[sensitive(dual)]` is used with a non-generic type, generated code checks that the
522/// matching `Sensitive` derive is also present. Generic `dual` types cannot be checked at derive
523/// time; missing counterparts surface later as trait-bound errors when the type is used.
524///
525/// # Generated Impls
526///
527/// - `RedactableWithFormatter`: always generated.
528/// - `ToRedactedOutput`: always generated; emits the redacted display text and certifies the
529///   type for `slog_redacted_display()` and `tracing_redacted()`.
530/// - `Debug`: redacted by default; actual values in the consumer's `cfg(test)` builds or when
531///   `redactable`'s `testing` feature is enabled.
532/// - `slog::Value` + `SlogRedacted`: emits the redacted display string (requires `slog` feature).
533///   Skipped when `#[sensitive(dual)]` is set (Sensitive provides them instead).
534/// - `TracingRedacted`: marker trait (requires `tracing` feature).
535///   Skipped when `#[sensitive(dual)]` is set.
536#[proc_macro_derive(SensitiveDisplay, attributes(sensitive, not_sensitive, error))]
537pub fn derive_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
538    let input = parse_macro_input!(input as DeriveInput);
539    match expand(input, DeriveKind::SensitiveDisplay) {
540        Ok(tokens) => tokens.into(),
541        Err(err) => err.into_compile_error().into(),
542    }
543}
544
545/// Returns the token stream to reference the redactable crate root.
546///
547/// Handles crate renaming (e.g., `my_redact = { package = "redactable", ... }`)
548/// and internal usage (when derive is used inside the redactable crate itself).
549fn crate_root() -> proc_macro2::TokenStream {
550    match crate_name("redactable") {
551        Ok(FoundCrate::Itself) => quote! { crate },
552        Ok(FoundCrate::Name(name)) => {
553            let ident = format_ident!("{}", name);
554            quote! { ::#ident }
555        }
556        Err(_) => quote! { ::redactable },
557    }
558}
559
560fn crate_path(item: &str) -> proc_macro2::TokenStream {
561    let root = crate_root();
562    let item_ident = syn::parse_str::<syn::Path>(item).expect("redactable crate path should parse");
563    quote! { #root::#item_ident }
564}
565
566/// Creates an expansion-internal value identifier that cannot collide with caller bindings.
567pub(crate) fn internal_ident(name: &str) -> Ident {
568    Ident::new(name, Span::mixed_site())
569}
570
571/// Output produced by struct/enum derive logic for `Sensitive`.
572///
573/// Shared by `derive_struct`, `derive_enum`, and the top-level `expand()`.
574pub(crate) struct DeriveOutput {
575    pub(crate) redaction_body: TokenStream,
576    pub(crate) used_generics: Vec<syn::WherePredicate>,
577    pub(crate) policy_applicable_generics: Vec<syn::WherePredicate>,
578    pub(crate) debug_redacted_body: TokenStream,
579    pub(crate) debug_unredacted_body: TokenStream,
580    pub(crate) debug_unredacted_generics: Vec<syn::WherePredicate>,
581}
582
583struct DebugOutput {
584    body: TokenStream,
585    generics: Vec<syn::WherePredicate>,
586}
587
588/// Which derive macro invoked `expand()`.
589///
590/// Controls what impls are generated: `Sensitive` emits `RedactableWithMapper` (structural
591/// traversal), while `SensitiveDisplay` emits `RedactableWithFormatter` (display formatting).
592enum DeriveKind {
593    /// `#[derive(Sensitive)]` — structural redaction via `RedactableWithMapper`.
594    Sensitive,
595    /// `#[derive(SensitiveDisplay)]` — display formatting via `RedactableWithFormatter`.
596    SensitiveDisplay,
597}
598
599#[allow(clippy::too_many_lines)]
600fn expand(input: DeriveInput, kind: DeriveKind) -> Result<TokenStream> {
601    let DeriveInput {
602        ident,
603        generics,
604        data,
605        attrs,
606        ..
607    } = input;
608
609    let ContainerOptions { dual } = parse_container_options(&attrs)?;
610
611    let crate_root = crate_root();
612    let policy_guards = policy_guard::generate_policy_guards(&ident, &data, &generics)?;
613    let formatter = internal_ident("__redactable_f");
614    let mapper = internal_ident("__redactable_mapper");
615    let mapper_type = internal_ident("__RedactableMapper");
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_predicates(generics.clone(), &redacted_display_output.display_generics);
651        let redacted_display_generics = add_predicates(
652            redacted_display_generics,
653            &redacted_display_output.debug_generics,
654        );
655        let redacted_display_generics = add_predicates(
656            redacted_display_generics,
657            &redacted_display_output.policy_ref_generics,
658        );
659        let redacted_display_generics = add_predicates(
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        let redacted_display_impl = quote! {
667            impl #display_impl_generics #crate_root::RedactableWithFormatter for #ident #display_ty_generics #display_where_clause {
668                fn fmt_redacted(&self, #formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
669                    #redacted_display_body
670                }
671            }
672        };
673        let to_redacted_output_impl = quote! {
674            impl #display_impl_generics #crate_root::ToRedactedOutput for #ident #display_ty_generics #display_where_clause {
675                fn to_redacted_output(&self) -> #crate_root::RedactedOutput {
676                    #crate_root::RedactedOutput::Text(
677                        #crate_root::RedactableWithFormatter::redacted_display(self).to_string(),
678                    )
679                }
680            }
681        };
682
683        let debug_output = derive_unredacted_debug(&ident, &data, &generics)?;
684        // A single impl branches at runtime on `cfg!(test) || redactable::__TESTING`
685        // rather than emitting two `#[cfg]`-gated impls. The `feature = "testing"`
686        // check must resolve against `redactable`'s own feature, not the consumer's,
687        // so it is routed through the `__TESTING` constant. The where-clause is the
688        // union of the formatter bounds (redacted body) and the Debug bounds
689        // (unredacted body) because both bodies live in the same impl.
690        let debug_generics =
691            add_predicates(redacted_display_generics.clone(), &debug_output.generics);
692        let (debug_impl_generics, debug_ty_generics, debug_where_clause) =
693            debug_generics.split_for_impl();
694        let debug_unredacted_body = debug_output.body;
695        let debug_impl = quote! {
696            impl #debug_impl_generics ::core::fmt::Debug for #ident #debug_ty_generics #debug_where_clause {
697                fn fmt(&self, #formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
698                    if ::core::cfg!(test) || #crate_root::__TESTING {
699                        #debug_unredacted_body
700                    } else {
701                        #crate_root::RedactableWithFormatter::fmt_redacted(self, #formatter)
702                    }
703                }
704            }
705        };
706
707        // In dual mode, Sensitive provides slog and tracing impls — skip them here.
708        let slog_impl = if dual {
709            quote! {}
710        } else {
711            #[cfg(feature = "slog")]
712            {
713                let slog_crate = quote! { #crate_root::__private::slog };
714                let mut slog_generics = generics;
715                let (_, ty_generics, _) = slog_generics.split_for_impl();
716                let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
717                slog_generics
718                    .make_where_clause()
719                    .predicates
720                    .push(parse_quote!(#self_ty: #crate_root::RedactableWithFormatter));
721                let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
722                    slog_generics.split_for_impl();
723                quote! {
724                    impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
725                        fn serialize(
726                            &self,
727                            _record: &#slog_crate::Record<'_>,
728                            key: #slog_crate::Key,
729                            serializer: &mut dyn #slog_crate::Serializer,
730                        ) -> #slog_crate::Result {
731                            let redacted = #crate_root::RedactableWithFormatter::redacted_display(self);
732                            serializer.emit_arguments(key, &format_args!("{}", redacted))
733                        }
734                    }
735
736                    impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
737                }
738            }
739
740            #[cfg(not(feature = "slog"))]
741            {
742                quote! {}
743            }
744        };
745
746        let tracing_impl = if dual {
747            quote! {}
748        } else {
749            #[cfg(feature = "tracing")]
750            {
751                let (tracing_impl_generics, tracing_ty_generics, tracing_where_clause) =
752                    redacted_display_generics.split_for_impl();
753                quote! {
754                    impl #tracing_impl_generics #crate_root::tracing::TracingRedacted for #ident #tracing_ty_generics #tracing_where_clause {}
755                }
756            }
757
758            #[cfg(not(feature = "tracing"))]
759            {
760                quote! {}
761            }
762        };
763
764        return Ok(quote! {
765            #policy_guards
766            #redacted_display_impl
767            #to_redacted_output_impl
768            #debug_impl
769            #slog_impl
770            #tracing_impl
771            #dual_pairing_assert
772        });
773    }
774
775    // Only DeriveKind::Sensitive reaches this point (SensitiveDisplay returns early above).
776
777    let derive_output = match data {
778        Data::Struct(data) => derive_struct(&ident, data, &generics)?,
779        Data::Enum(data) => derive_enum(&ident, data, &generics)?,
780        Data::Union(u) => {
781            return Err(syn::Error::new(
782                u.union_token.span(),
783                "`Sensitive` cannot be derived for unions",
784            ));
785        }
786    };
787
788    let policy_generics = add_predicates(generics.clone(), &derive_output.used_generics);
789    let policy_generics =
790        add_predicates(policy_generics, &derive_output.policy_applicable_generics);
791    let (impl_generics, ty_generics, where_clause) = policy_generics.split_for_impl();
792    #[cfg(feature = "slog")]
793    let slog_base_generics = generics.clone();
794    // The merged Debug impl uses the unredacted bounds (a superset of the
795    // redacted bounds) because both bodies share one impl.
796    let debug_unredacted_generics =
797        add_predicates(generics, &derive_output.debug_unredacted_generics);
798    let (
799        debug_unredacted_impl_generics,
800        debug_unredacted_ty_generics,
801        debug_unredacted_where_clause,
802    ) = debug_unredacted_generics.split_for_impl();
803    let redaction_body = &derive_output.redaction_body;
804    let debug_redacted_body = &derive_output.debug_redacted_body;
805    let debug_unredacted_body = &derive_output.debug_unredacted_body;
806    // In dual mode, SensitiveDisplay provides Debug — skip it here.
807    //
808    // A single impl branches at runtime on `cfg!(test) || redactable::__TESTING`
809    // rather than emitting two `#[cfg]`-gated impls. The `feature = "testing"`
810    // check must resolve against `redactable`'s own feature, not the consumer's,
811    // so it is routed through the `__TESTING` constant. The where-clause uses the
812    // unredacted bounds (a superset of the redacted bounds) because both bodies
813    // live in the same impl.
814    let debug_impl = if dual {
815        quote! {}
816    } else {
817        quote! {
818            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
819                fn fmt(&self, #formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
820                    if ::core::cfg!(test) || #crate_root::__TESTING {
821                        #debug_unredacted_body
822                    } else {
823                        #debug_redacted_body
824                    }
825                }
826            }
827        }
828    };
829
830    #[cfg(feature = "slog")]
831    let slog_impl = {
832        let slog_crate = quote! { #crate_root::__private::slog };
833        let mut slog_generics = slog_base_generics;
834        let slog_where_clause = slog_generics.make_where_clause();
835        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
836        slog_where_clause
837            .predicates
838            .push(parse_quote!(#self_ty: ::core::clone::Clone));
839        // SlogRedactedExt requires Self: Serialize, so we add this bound to enable
840        // generic types to work with slog when their type parameters implement Serialize.
841        slog_where_clause
842            .predicates
843            .push(parse_quote!(#self_ty: #crate_root::__private::serde::Serialize));
844        slog_where_clause
845            .predicates
846            .push(parse_quote!(#self_ty: #crate_root::slog::SlogRedactedExt));
847        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
848            slog_generics.split_for_impl();
849        quote! {
850            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
851                fn serialize(
852                    &self,
853                    _record: &#slog_crate::Record<'_>,
854                    key: #slog_crate::Key,
855                    serializer: &mut dyn #slog_crate::Serializer,
856                ) -> #slog_crate::Result {
857                    let redacted = #crate_root::slog::SlogRedactedExt::slog_redacted_json(self.clone());
858                    #slog_crate::Value::serialize(&redacted, _record, key, serializer)
859                }
860            }
861
862            impl #slog_impl_generics #crate_root::slog::SlogRedacted for #ident #slog_ty_generics #slog_where_clause {}
863        }
864    };
865
866    #[cfg(not(feature = "slog"))]
867    let slog_impl = quote! {};
868
869    #[cfg(feature = "tracing")]
870    let tracing_impl = quote! {
871        impl #impl_generics #crate_root::tracing::TracingRedacted for #ident #ty_generics #where_clause {}
872    };
873
874    #[cfg(not(feature = "tracing"))]
875    let tracing_impl = quote! {};
876
877    let trait_impl = quote! {
878        #policy_guards
879        impl #impl_generics #crate_root::RedactableWithMapper for #ident #ty_generics #where_clause {
880            fn redact_with<#mapper_type: #crate_root::RedactableMapper>(self, #mapper: &#mapper_type) -> Self {
881                use #crate_root::RedactableWithMapper as _;
882                #redaction_body
883            }
884        }
885
886        impl #impl_generics #crate_root::Redactable for #ident #ty_generics #where_clause {}
887
888        #debug_impl
889
890        #slog_impl
891
892        #tracing_impl
893
894        #dual_pairing_assert
895    };
896    Ok(trait_impl)
897}
898
899fn derive_unredacted_debug(
900    name: &Ident,
901    data: &Data,
902    _generics: &syn::Generics,
903) -> Result<DebugOutput> {
904    match data {
905        Data::Struct(data) => Ok(derive_unredacted_debug_struct(name, data)),
906        Data::Enum(data) => Ok(derive_unredacted_debug_enum(name, data)),
907        Data::Union(u) => Err(syn::Error::new(
908            u.union_token.span(),
909            "`SensitiveDisplay` cannot be derived for unions",
910        )),
911    }
912}
913
914fn derive_unredacted_debug_struct(name: &Ident, data: &DataStruct) -> DebugOutput {
915    let formatter = internal_ident("__redactable_f");
916    let debug = internal_ident("__redactable_debug");
917    let mut debug_generics = Vec::new();
918    match &data.fields {
919        Fields::Named(fields) => {
920            let mut bindings = Vec::new();
921            let mut debug_fields = Vec::new();
922            for field in &fields.named {
923                let ident = field
924                    .ident
925                    .clone()
926                    .expect("named field should have identifier");
927                bindings.push(ident.clone());
928                push_debug_predicate(&mut debug_generics, &field.ty);
929                debug_fields.push(quote! {
930                    #debug.field(stringify!(#ident), #ident);
931                });
932            }
933            DebugOutput {
934                body: quote! {
935                    match self {
936                        Self { #(#bindings),* } => {
937                            let mut #debug = #formatter.debug_struct(stringify!(#name));
938                            #(#debug_fields)*
939                            #debug.finish()
940                        }
941                    }
942                },
943                generics: debug_generics,
944            }
945        }
946        Fields::Unnamed(fields) => {
947            let mut bindings = Vec::new();
948            let mut debug_fields = Vec::new();
949            for (index, field) in fields.unnamed.iter().enumerate() {
950                let ident = format_ident!("field_{index}", span = Span::mixed_site());
951                bindings.push(ident.clone());
952                push_debug_predicate(&mut debug_generics, &field.ty);
953                debug_fields.push(quote! {
954                    #debug.field(#ident);
955                });
956            }
957            DebugOutput {
958                body: quote! {
959                    match self {
960                        Self ( #(#bindings),* ) => {
961                            let mut #debug = #formatter.debug_tuple(stringify!(#name));
962                            #(#debug_fields)*
963                            #debug.finish()
964                        }
965                    }
966                },
967                generics: debug_generics,
968            }
969        }
970        Fields::Unit => DebugOutput {
971            body: quote! {
972                #formatter.write_str(stringify!(#name))
973            },
974            generics: debug_generics,
975        },
976    }
977}
978
979fn derive_unredacted_debug_enum(name: &Ident, data: &DataEnum) -> DebugOutput {
980    let formatter = internal_ident("__redactable_f");
981    let debug = internal_ident("__redactable_debug");
982    let mut debug_generics = Vec::new();
983    let mut debug_arms = Vec::new();
984    for variant in &data.variants {
985        let variant_ident = &variant.ident;
986        let debug_name = quote! { concat!(stringify!(#name), "::", stringify!(#variant_ident)) };
987        match &variant.fields {
988            Fields::Unit => {
989                debug_arms.push(quote! {
990                    #name::#variant_ident => #formatter.write_str(#debug_name)
991                });
992            }
993            Fields::Named(fields) => {
994                let mut bindings = Vec::new();
995                let mut debug_fields = Vec::new();
996                for field in &fields.named {
997                    let ident = field
998                        .ident
999                        .clone()
1000                        .expect("named field should have identifier");
1001                    bindings.push(ident.clone());
1002                    push_debug_predicate(&mut debug_generics, &field.ty);
1003                    debug_fields.push(quote! {
1004                        #debug.field(stringify!(#ident), #ident);
1005                    });
1006                }
1007                debug_arms.push(quote! {
1008                    #name::#variant_ident { #(#bindings),* } => {
1009                        let mut #debug = #formatter.debug_struct(#debug_name);
1010                        #(#debug_fields)*
1011                        #debug.finish()
1012                    }
1013                });
1014            }
1015            Fields::Unnamed(fields) => {
1016                let mut bindings = Vec::new();
1017                let mut debug_fields = Vec::new();
1018                for (index, field) in fields.unnamed.iter().enumerate() {
1019                    let ident = format_ident!("field_{index}", span = Span::mixed_site());
1020                    bindings.push(ident.clone());
1021                    push_debug_predicate(&mut debug_generics, &field.ty);
1022                    debug_fields.push(quote! {
1023                        #debug.field(#ident);
1024                    });
1025                }
1026                debug_arms.push(quote! {
1027                    #name::#variant_ident ( #(#bindings),* ) => {
1028                        let mut #debug = #formatter.debug_tuple(#debug_name);
1029                        #(#debug_fields)*
1030                        #debug.finish()
1031                    }
1032                });
1033            }
1034        }
1035    }
1036    let body = if debug_arms.is_empty() {
1037        quote! { match *self {} }
1038    } else {
1039        quote! {
1040            match self {
1041                #(#debug_arms),*
1042            }
1043        }
1044    };
1045    DebugOutput {
1046        body,
1047        generics: debug_generics,
1048    }
1049}
1050
1051#[cfg(all(test, feature = "slog"))]
1052mod generated_dependency_tests;
1053
1054#[cfg(all(test, feature = "slog"))]
1055#[test]
1056fn structural_generated_dependency_roots() {
1057    generated_dependency_tests::run_structural_generated_dependency_roots();
1058}