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