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