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