Skip to main content

redactable_derive/
lib.rs

1//! Derive macros for `redactable`.
2//!
3//! This crate generates traversal code behind `#[derive(Sensitive)]` and
4//! `#[derive(SensitiveDisplay)]`. It:
5//! - reads `#[sensitive(...)]` field attributes
6//! - emits a `RedactableContainer` implementation that calls into a mapper
7//!
8//! It does **not** define policy markers or text policies. Those live in the main
9//! `redactable` crate and are applied at runtime.
10
11// <https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html>
12#![warn(
13    anonymous_parameters,
14    bare_trait_objects,
15    elided_lifetimes_in_paths,
16    missing_copy_implementations,
17    rust_2018_idioms,
18    trivial_casts,
19    trivial_numeric_casts,
20    unreachable_pub,
21    unsafe_code,
22    unused_extern_crates,
23    unused_import_braces
24)]
25// <https://rust-lang.github.io/rust-clippy/stable>
26#![warn(
27    clippy::all,
28    clippy::cargo,
29    clippy::dbg_macro,
30    clippy::float_cmp_const,
31    clippy::get_unwrap,
32    clippy::mem_forget,
33    clippy::nursery,
34    clippy::pedantic,
35    clippy::todo,
36    clippy::unwrap_used,
37    clippy::uninlined_format_args
38)]
39// Allow some clippy lints
40#![allow(
41    clippy::default_trait_access,
42    clippy::doc_markdown,
43    clippy::if_not_else,
44    clippy::module_name_repetitions,
45    clippy::multiple_crate_versions,
46    clippy::must_use_candidate,
47    clippy::needless_pass_by_value,
48    clippy::needless_ifs,
49    clippy::use_self,
50    clippy::cargo_common_metadata,
51    clippy::missing_errors_doc,
52    clippy::enum_glob_use,
53    clippy::struct_excessive_bools,
54    clippy::missing_const_for_fn,
55    clippy::redundant_pub_crate,
56    clippy::result_large_err,
57    clippy::future_not_send,
58    clippy::option_if_let_else,
59    clippy::from_over_into,
60    clippy::manual_inspect
61)]
62// Allow some lints while testing
63#![cfg_attr(test, allow(clippy::non_ascii_literal, clippy::unwrap_used))]
64
65#[allow(unused_extern_crates)]
66extern crate proc_macro;
67
68use proc_macro_crate::{FoundCrate, crate_name};
69#[cfg(feature = "slog")]
70use proc_macro2::Span;
71use proc_macro2::{Ident, TokenStream};
72use quote::{format_ident, quote};
73#[cfg(feature = "slog")]
74use syn::parse_quote;
75use syn::{Data, DeriveInput, Result, parse_macro_input, spanned::Spanned};
76
77mod container;
78mod derive_enum;
79mod derive_struct;
80mod generics;
81mod redacted_display;
82mod strategy;
83mod transform;
84mod types;
85use container::{ContainerOptions, parse_container_options};
86use derive_enum::derive_enum;
87use derive_struct::derive_struct;
88use generics::{
89    add_container_bounds, add_debug_bounds, add_display_bounds, add_policy_applicable_bounds,
90    add_policy_applicable_ref_bounds, add_redacted_display_bounds,
91};
92use redacted_display::derive_redacted_display;
93
94/// Derives `redactable::RedactableContainer` (and related impls) for structs and enums.
95///
96/// # Container Attributes
97///
98/// These attributes are placed on the struct/enum itself:
99///
100/// - `#[sensitive(skip_debug)]` - Opt out of `Debug` impl generation. Use this when you need a
101///   custom `Debug` implementation or the type already derives `Debug` elsewhere.
102///
103/// # Field Attributes
104///
105/// - **No annotation**: The field is traversed by default. Scalars pass through unchanged; nested
106///   structs/enums are walked using `RedactableContainer` (so external types must implement it).
107///
108/// - `#[sensitive(Default)]`: For scalar types (i32, bool, char, etc.), redacts to default values
109///   (0, false, '*'). For string-like types, applies full redaction to `"[REDACTED]"`.
110///
111/// - `#[sensitive(Policy)]`: Applies the policy's redaction rules to string-like
112///   values. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`. Scalars can only
113///   use `#[sensitive(Default)]`.
114///
115/// Unions are rejected at compile time.
116///
117/// # Additional Generated Impls
118///
119/// - `Debug`: when *not* building with `cfg(any(test, feature = "testing"))`, sensitive fields are
120///   formatted as the string `"[REDACTED]"` rather than their values. Use `#[sensitive(skip_debug)]`
121///   on the container to opt out.
122/// - `slog::Value` (behind `cfg(feature = "slog")`): implemented by cloning the value and routing
123///   it through `redactable::slog::SlogRedactedExt`. **Note:** this impl requires `Clone` and
124///   `serde::Serialize` because it emits structured JSON. The derive first looks for a top-level
125///   `slog` crate; if not found, it checks the `REDACTABLE_SLOG_CRATE` env var for an alternate path
126///   (e.g., `my_log::slog`). If neither is available, compilation fails with a clear error.
127#[proc_macro_derive(Sensitive, attributes(sensitive))]
128pub fn derive_sensitive_container(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
129    let input = parse_macro_input!(input as DeriveInput);
130    match expand(input, SlogMode::RedactedJson) {
131        Ok(tokens) => tokens.into(),
132        Err(err) => err.into_compile_error().into(),
133    }
134}
135
136#[proc_macro_derive(SensitiveData, attributes(sensitive))]
137pub fn derive_sensitive_data(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
138    derive_sensitive_container(input)
139}
140
141/// Derives a no-op `redactable::RedactableContainer` implementation.
142///
143/// This is useful for types that are known to be non-sensitive but still need to
144/// satisfy `RedactableContainer` / `Redactable` bounds.
145#[proc_macro_derive(NotSensitive)]
146pub fn derive_not_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
147    let input = parse_macro_input!(input as DeriveInput);
148    let ident = input.ident;
149    let generics = input.generics;
150    let attrs = input.attrs;
151    let data = input.data;
152
153    let mut sensitive_attr_spans = Vec::new();
154    if let Some(attr) = attrs.iter().find(|attr| attr.path().is_ident("sensitive")) {
155        sensitive_attr_spans.push(attr.span());
156    }
157
158    match &data {
159        Data::Struct(data) => {
160            for field in &data.fields {
161                if field
162                    .attrs
163                    .iter()
164                    .any(|attr| attr.path().is_ident("sensitive"))
165                {
166                    sensitive_attr_spans.push(field.span());
167                }
168            }
169        }
170        Data::Enum(data) => {
171            for variant in &data.variants {
172                for field in &variant.fields {
173                    if field
174                        .attrs
175                        .iter()
176                        .any(|attr| attr.path().is_ident("sensitive"))
177                    {
178                        sensitive_attr_spans.push(field.span());
179                    }
180                }
181            }
182        }
183        Data::Union(data) => {
184            return syn::Error::new(
185                data.union_token.span(),
186                "`NotSensitive` cannot be derived for unions",
187            )
188            .into_compile_error()
189            .into();
190        }
191    }
192
193    if let Some(span) = sensitive_attr_spans.first() {
194        return syn::Error::new(
195            *span,
196            "`#[sensitive]` attributes are not allowed on `NotSensitive` types",
197        )
198        .into_compile_error()
199        .into();
200    }
201    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
202    let crate_root = crate_root();
203
204    let tokens = quote! {
205        impl #impl_generics #crate_root::RedactableContainer for #ident #ty_generics #where_clause {
206            fn redact_with<M: #crate_root::RedactableMapper>(self, _mapper: &M) -> Self {
207                self
208            }
209        }
210    };
211    tokens.into()
212}
213
214/// Derives `redactable::RedactableDisplay` using a display template.
215///
216/// This generates a redacted string representation without requiring `Clone`.
217/// Every field referenced in the template **must** be explicitly annotated:
218///
219/// - `#[sensitive(Policy)]`: Apply the policy's redaction rules
220/// - `#[not_sensitive]`: Render raw (explicit opt-out)
221///
222/// The display template is taken from `#[error("...")]` (thiserror-style) or from
223/// doc comments (displaydoc-style). If neither is present, the derive fails.
224///
225/// Fields are redacted by reference, so field types do not need `Clone`.
226#[proc_macro_derive(SensitiveDisplay, attributes(sensitive, not_sensitive, error))]
227pub fn derive_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
228    let input = parse_macro_input!(input as DeriveInput);
229    match expand(input, SlogMode::RedactedDisplay) {
230        Ok(tokens) => tokens.into(),
231        Err(err) => err.into_compile_error().into(),
232    }
233}
234
235/// Returns the token stream to reference the redactable crate root.
236///
237/// Handles crate renaming (e.g., `my_redact = { package = "redactable", ... }`)
238/// and internal usage (when derive is used inside the redactable crate itself).
239fn crate_root() -> proc_macro2::TokenStream {
240    match crate_name("redactable") {
241        Ok(FoundCrate::Itself) => quote! { crate },
242        Ok(FoundCrate::Name(name)) => {
243            let ident = format_ident!("{}", name);
244            quote! { ::#ident }
245        }
246        Err(_) => quote! { ::redactable },
247    }
248}
249
250/// Returns the token stream to reference the slog crate root.
251///
252/// Handles crate renaming (e.g., `my_slog = { package = "slog", ... }`).
253/// If the top-level `slog` crate is not available, falls back to the
254/// `REDACTABLE_SLOG_CRATE` env var, which should be a path like `my_log::slog`.
255#[cfg(feature = "slog")]
256fn slog_crate() -> Result<proc_macro2::TokenStream> {
257    match crate_name("slog") {
258        Ok(FoundCrate::Itself) => Ok(quote! { crate }),
259        Ok(FoundCrate::Name(name)) => {
260            let ident = format_ident!("{}", name);
261            Ok(quote! { ::#ident })
262        }
263        Err(_) => {
264            let env_value = std::env::var("REDACTABLE_SLOG_CRATE").map_err(|_| {
265                syn::Error::new(
266                    Span::call_site(),
267                    "slog support is enabled, but no top-level `slog` crate was found. \
268Set the REDACTABLE_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
269`slog` as a direct dependency.",
270                )
271            })?;
272            let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
273                syn::Error::new(
274                    Span::call_site(),
275                    format!("REDACTABLE_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
276                )
277            })?;
278            Ok(quote! { #path })
279        }
280    }
281}
282
283fn crate_path(item: &str) -> proc_macro2::TokenStream {
284    let root = crate_root();
285    let item_ident = syn::parse_str::<syn::Path>(item).expect("redactable crate path should parse");
286    quote! { #root::#item_ident }
287}
288
289struct DeriveOutput {
290    redaction_body: TokenStream,
291    used_generics: Vec<Ident>,
292    policy_applicable_generics: Vec<Ident>,
293    debug_redacted_body: TokenStream,
294    debug_redacted_generics: Vec<Ident>,
295    debug_unredacted_body: TokenStream,
296    debug_unredacted_generics: Vec<Ident>,
297    redacted_display_body: Option<TokenStream>,
298    redacted_display_generics: Vec<Ident>,
299    redacted_display_debug_generics: Vec<Ident>,
300    redacted_display_policy_ref_generics: Vec<Ident>,
301    redacted_display_nested_generics: Vec<Ident>,
302}
303
304enum SlogMode {
305    RedactedJson,
306    RedactedDisplay,
307}
308
309#[allow(clippy::too_many_lines)]
310fn expand(input: DeriveInput, slog_mode: SlogMode) -> Result<TokenStream> {
311    let DeriveInput {
312        ident,
313        generics,
314        data,
315        attrs,
316        ..
317    } = input;
318
319    let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
320
321    let crate_root = crate_root();
322
323    if matches!(slog_mode, SlogMode::RedactedDisplay) {
324        let redacted_display_output = derive_redacted_display(&ident, &data, &attrs, &generics)?;
325        let redacted_display_generics =
326            add_display_bounds(generics.clone(), &redacted_display_output.display_generics);
327        let redacted_display_generics = add_debug_bounds(
328            redacted_display_generics,
329            &redacted_display_output.debug_generics,
330        );
331        let redacted_display_generics = add_policy_applicable_ref_bounds(
332            redacted_display_generics,
333            &redacted_display_output.policy_ref_generics,
334        );
335        let redacted_display_generics = add_redacted_display_bounds(
336            redacted_display_generics,
337            &redacted_display_output.nested_generics,
338        );
339        let (display_impl_generics, display_ty_generics, display_where_clause) =
340            redacted_display_generics.split_for_impl();
341        let redacted_display_body = redacted_display_output.body;
342        let redacted_display_impl = quote! {
343            impl #display_impl_generics #crate_root::RedactableDisplay for #ident #display_ty_generics #display_where_clause {
344                fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
345                    #redacted_display_body
346                }
347            }
348        };
349
350        // Only generate slog impl when the slog feature is enabled on redactable-derive.
351        // If slog is not available, emit a clear error with instructions.
352        #[cfg(feature = "slog")]
353        let slog_impl = {
354            let slog_crate = slog_crate()?;
355            let mut slog_generics = generics;
356            // Get ty_generics first (immutable borrow) before make_where_clause (mutable borrow)
357            let (_, ty_generics, _) = slog_generics.split_for_impl();
358            let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
359            slog_generics
360                .make_where_clause()
361                .predicates
362                .push(parse_quote!(#self_ty: #crate_root::RedactableDisplay));
363            let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
364                slog_generics.split_for_impl();
365            quote! {
366                impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
367                    fn serialize(
368                        &self,
369                        _record: &#slog_crate::Record<'_>,
370                        key: #slog_crate::Key,
371                        serializer: &mut dyn #slog_crate::Serializer,
372                    ) -> #slog_crate::Result {
373                        let redacted = #crate_root::RedactableDisplay::redacted_display(self);
374                        serializer.emit_arguments(key, &format_args!("{}", redacted))
375                    }
376                }
377            }
378        };
379
380        #[cfg(not(feature = "slog"))]
381        let slog_impl = quote! {};
382
383        return Ok(quote! {
384            #redacted_display_impl
385            #slog_impl
386        });
387    }
388
389    let redacted_display_output = if matches!(slog_mode, SlogMode::RedactedDisplay) {
390        Some(derive_redacted_display(&ident, &data, &attrs, &generics)?)
391    } else {
392        None
393    };
394
395    let derive_output = match &data {
396        Data::Struct(data) => {
397            let output = derive_struct(&ident, data.clone(), &generics)?;
398            DeriveOutput {
399                redaction_body: output.redaction_body,
400                used_generics: output.used_generics,
401                policy_applicable_generics: output.policy_applicable_generics,
402                debug_redacted_body: output.debug_redacted_body,
403                debug_redacted_generics: output.debug_redacted_generics,
404                debug_unredacted_body: output.debug_unredacted_body,
405                debug_unredacted_generics: output.debug_unredacted_generics,
406                redacted_display_body: redacted_display_output
407                    .as_ref()
408                    .map(|output| output.body.clone()),
409                redacted_display_generics: redacted_display_output
410                    .as_ref()
411                    .map(|output| output.display_generics.clone())
412                    .unwrap_or_default(),
413                redacted_display_debug_generics: redacted_display_output
414                    .as_ref()
415                    .map(|output| output.debug_generics.clone())
416                    .unwrap_or_default(),
417                redacted_display_policy_ref_generics: redacted_display_output
418                    .as_ref()
419                    .map(|output| output.policy_ref_generics.clone())
420                    .unwrap_or_default(),
421                redacted_display_nested_generics: redacted_display_output
422                    .as_ref()
423                    .map(|output| output.nested_generics.clone())
424                    .unwrap_or_default(),
425            }
426        }
427        Data::Enum(data) => {
428            let output = derive_enum(&ident, data.clone(), &generics)?;
429            DeriveOutput {
430                redaction_body: output.redaction_body,
431                used_generics: output.used_generics,
432                policy_applicable_generics: output.policy_applicable_generics,
433                debug_redacted_body: output.debug_redacted_body,
434                debug_redacted_generics: output.debug_redacted_generics,
435                debug_unredacted_body: output.debug_unredacted_body,
436                debug_unredacted_generics: output.debug_unredacted_generics,
437                redacted_display_body: redacted_display_output
438                    .as_ref()
439                    .map(|output| output.body.clone()),
440                redacted_display_generics: redacted_display_output
441                    .as_ref()
442                    .map(|output| output.display_generics.clone())
443                    .unwrap_or_default(),
444                redacted_display_debug_generics: redacted_display_output
445                    .as_ref()
446                    .map(|output| output.debug_generics.clone())
447                    .unwrap_or_default(),
448                redacted_display_policy_ref_generics: redacted_display_output
449                    .as_ref()
450                    .map(|output| output.policy_ref_generics.clone())
451                    .unwrap_or_default(),
452                redacted_display_nested_generics: redacted_display_output
453                    .as_ref()
454                    .map(|output| output.nested_generics.clone())
455                    .unwrap_or_default(),
456            }
457        }
458        Data::Union(u) => {
459            return Err(syn::Error::new(
460                u.union_token.span(),
461                "`Sensitive` cannot be derived for unions",
462            ));
463        }
464    };
465
466    let policy_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
467    let policy_generics =
468        add_policy_applicable_bounds(policy_generics, &derive_output.policy_applicable_generics);
469    let (impl_generics, ty_generics, where_clause) = policy_generics.split_for_impl();
470    let debug_redacted_generics =
471        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
472    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
473        debug_redacted_generics.split_for_impl();
474    let debug_unredacted_generics =
475        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
476    let (
477        debug_unredacted_impl_generics,
478        debug_unredacted_ty_generics,
479        debug_unredacted_where_clause,
480    ) = debug_unredacted_generics.split_for_impl();
481    let redaction_body = &derive_output.redaction_body;
482    let debug_redacted_body = &derive_output.debug_redacted_body;
483    let debug_unredacted_body = &derive_output.debug_unredacted_body;
484    let debug_impl = if skip_debug {
485        quote! {}
486    } else {
487        quote! {
488            #[cfg(any(test, feature = "testing"))]
489            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
490                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
491                    #debug_unredacted_body
492                }
493            }
494
495            #[cfg(not(any(test, feature = "testing")))]
496            #[allow(unused_variables)]
497            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
498                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
499                    #debug_redacted_body
500                }
501            }
502        }
503    };
504
505    let redacted_display_body = derive_output.redacted_display_body.as_ref();
506    let redacted_display_impl = if matches!(slog_mode, SlogMode::RedactedDisplay) {
507        let redacted_display_generics =
508            add_display_bounds(generics.clone(), &derive_output.redacted_display_generics);
509        let redacted_display_generics = add_debug_bounds(
510            redacted_display_generics,
511            &derive_output.redacted_display_debug_generics,
512        );
513        let redacted_display_generics = add_policy_applicable_ref_bounds(
514            redacted_display_generics,
515            &derive_output.redacted_display_policy_ref_generics,
516        );
517        let redacted_display_generics = add_redacted_display_bounds(
518            redacted_display_generics,
519            &derive_output.redacted_display_nested_generics,
520        );
521        let (display_impl_generics, display_ty_generics, display_where_clause) =
522            redacted_display_generics.split_for_impl();
523        let redacted_display_body = redacted_display_body
524            .cloned()
525            .unwrap_or_else(TokenStream::new);
526        quote! {
527            impl #display_impl_generics #crate_root::RedactableDisplay for #ident #display_ty_generics #display_where_clause {
528                fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
529                    #redacted_display_body
530                }
531            }
532        }
533    } else {
534        quote! {}
535    };
536
537    // Only generate slog impl when the slog feature is enabled on redactable-derive.
538    // If slog is not available, emit a clear error with instructions.
539    #[cfg(feature = "slog")]
540    let slog_impl = {
541        let slog_crate = slog_crate()?;
542        let mut slog_generics = generics;
543        let slog_where_clause = slog_generics.make_where_clause();
544        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
545        match slog_mode {
546            SlogMode::RedactedJson => {
547                slog_where_clause
548                    .predicates
549                    .push(parse_quote!(#self_ty: ::core::clone::Clone));
550                // SlogRedactedExt requires Self: Serialize, so we add this bound to enable
551                // generic types to work with slog when their type parameters implement Serialize.
552                slog_where_clause
553                    .predicates
554                    .push(parse_quote!(#self_ty: ::serde::Serialize));
555                slog_where_clause
556                    .predicates
557                    .push(parse_quote!(#self_ty: #crate_root::slog::SlogRedactedExt));
558                let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
559                    slog_generics.split_for_impl();
560                quote! {
561                    impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
562                        fn serialize(
563                            &self,
564                            _record: &#slog_crate::Record<'_>,
565                            key: #slog_crate::Key,
566                            serializer: &mut dyn #slog_crate::Serializer,
567                        ) -> #slog_crate::Result {
568                            let redacted = #crate_root::slog::SlogRedactedExt::slog_redacted_json(self.clone());
569                            #slog_crate::Value::serialize(&redacted, _record, key, serializer)
570                        }
571                    }
572                }
573            }
574            SlogMode::RedactedDisplay => {
575                slog_where_clause
576                    .predicates
577                    .push(parse_quote!(#self_ty: #crate_root::RedactableDisplay));
578                let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
579                    slog_generics.split_for_impl();
580                quote! {
581                    impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
582                        fn serialize(
583                            &self,
584                            _record: &#slog_crate::Record<'_>,
585                            key: #slog_crate::Key,
586                            serializer: &mut dyn #slog_crate::Serializer,
587                        ) -> #slog_crate::Result {
588                            let redacted = #crate_root::RedactableDisplay::redacted_display(self);
589                            serializer.emit_arguments(key, &format_args!("{}", redacted))
590                        }
591                    }
592                }
593            }
594        }
595    };
596
597    #[cfg(not(feature = "slog"))]
598    let slog_impl = quote! {};
599
600    let trait_impl = quote! {
601        impl #impl_generics #crate_root::RedactableContainer for #ident #ty_generics #where_clause {
602            fn redact_with<M: #crate_root::RedactableMapper>(self, mapper: &M) -> Self {
603                use #crate_root::RedactableContainer as _;
604                #redaction_body
605            }
606        }
607
608        #debug_impl
609
610        #redacted_display_impl
611
612        #slog_impl
613
614        // `slog` already provides `impl<V: Value> Value for &V`, so a reference
615        // impl here would conflict with the blanket impl.
616    };
617    Ok(trait_impl)
618}