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