Skip to main content

redaction_derive/
lib.rs

1//! Derive macros for `redaction`.
2//!
3//! This crate generates the traversal code behind `#[derive(Sensitive)]`. It:
4//! - reads `#[sensitive(...)]` field attributes
5//! - emits a `SensitiveType` implementation that calls into a mapper
6//!
7//! It does **not** define classifications or policies. Those live in the main
8//! `redaction` crate and are applied at runtime.
9
10// <https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html>
11#![warn(
12    anonymous_parameters,
13    bare_trait_objects,
14    elided_lifetimes_in_paths,
15    missing_copy_implementations,
16    rust_2018_idioms,
17    trivial_casts,
18    trivial_numeric_casts,
19    unreachable_pub,
20    unsafe_code,
21    unused_extern_crates,
22    unused_import_braces
23)]
24// <https://rust-lang.github.io/rust-clippy/stable>
25#![warn(
26    clippy::all,
27    clippy::cargo,
28    clippy::dbg_macro,
29    clippy::float_cmp_const,
30    clippy::get_unwrap,
31    clippy::mem_forget,
32    clippy::nursery,
33    clippy::pedantic,
34    clippy::todo,
35    clippy::unwrap_used,
36    clippy::uninlined_format_args
37)]
38// Allow some clippy lints
39#![allow(
40    clippy::default_trait_access,
41    clippy::doc_markdown,
42    clippy::if_not_else,
43    clippy::module_name_repetitions,
44    clippy::multiple_crate_versions,
45    clippy::must_use_candidate,
46    clippy::needless_pass_by_value,
47    clippy::needless_ifs,
48    clippy::use_self,
49    clippy::cargo_common_metadata,
50    clippy::missing_errors_doc,
51    clippy::enum_glob_use,
52    clippy::struct_excessive_bools,
53    clippy::missing_const_for_fn,
54    clippy::redundant_pub_crate,
55    clippy::result_large_err,
56    clippy::future_not_send,
57    clippy::option_if_let_else,
58    clippy::from_over_into,
59    clippy::manual_inspect
60)]
61// Allow some lints while testing
62#![cfg_attr(test, allow(clippy::non_ascii_literal, clippy::unwrap_used))]
63
64#[allow(unused_extern_crates)]
65extern crate proc_macro;
66
67use proc_macro2::{Ident, TokenStream};
68use proc_macro_crate::{crate_name, FoundCrate};
69use quote::{format_ident, quote};
70#[cfg(feature = "slog")]
71use syn::parse_quote;
72use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Result};
73
74mod container;
75mod derive_enum;
76mod derive_struct;
77mod generics;
78mod strategy;
79mod transform;
80mod types;
81use container::{parse_container_options, ContainerOptions};
82use derive_enum::derive_enum;
83use derive_struct::derive_struct;
84use generics::{add_classified_value_bounds, add_container_bounds, add_debug_bounds};
85
86/// Derives `redaction::SensitiveType` (and related impls) for structs and enums.
87///
88/// # Container Attributes
89///
90/// These attributes are placed on the struct/enum itself:
91///
92/// - `#[sensitive(skip_debug)]` - Opt out of `Debug` impl generation. Use this when you need a
93///   custom `Debug` implementation or the type already derives `Debug` elsewhere.
94///
95/// # Field Attributes
96///
97/// - **No annotation**: The field passes through unchanged. Use this for fields that don't contain
98///   sensitive data, including external types like `chrono::DateTime` or `rust_decimal::Decimal`.
99///
100/// - `#[sensitive]`: For scalar types (i32, bool, char, etc.), redacts to default values (0, false,
101///   'X'). For struct/enum types that derive `Sensitive`, walks into them using `SensitiveType`.
102///
103/// - `#[sensitive(Classification)]`: Treats the field as a sensitive string-like value and applies
104///   the classification's policy. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`.
105///   The type must implement `SensitiveValue`.
106///
107/// Unions are rejected at compile time.
108///
109/// # Additional Generated Impls
110///
111/// - `Debug`: when *not* building with `cfg(any(test, feature = "testing"))`, sensitive fields are
112///   formatted as the string `"[REDACTED]"` rather than their values. Use `#[sensitive(skip_debug)]`
113///   on the container to opt out.
114/// - `slog::Value` (behind `cfg(feature = "slog")`): implemented by cloning the value and routing
115///   it through `redaction::slog::IntoRedactedJson`. **Note:** this impl requires the type to
116///   implement `Clone`.
117#[proc_macro_derive(Sensitive, attributes(sensitive))]
118pub fn derive_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
119    let input = parse_macro_input!(input as DeriveInput);
120    match expand(input) {
121        Ok(tokens) => tokens.into(),
122        Err(err) => err.into_compile_error().into(),
123    }
124}
125
126/// Returns the token stream to reference the redaction crate root.
127///
128/// Handles crate renaming (e.g., `my_redact = { package = "redaction", ... }`)
129/// and internal usage (when derive is used inside the redaction crate itself).
130fn crate_root() -> proc_macro2::TokenStream {
131    match crate_name("redaction") {
132        Ok(FoundCrate::Itself) => quote! { crate },
133        Ok(FoundCrate::Name(name)) => {
134            let ident = format_ident!("{}", name);
135            quote! { ::#ident }
136        }
137        Err(_) => quote! { ::redaction },
138    }
139}
140
141fn crate_path(item: &str) -> proc_macro2::TokenStream {
142    let root = crate_root();
143    let item_ident = syn::parse_str::<syn::Path>(item).expect("redaction crate path should parse");
144    quote! { #root::#item_ident }
145}
146
147struct DeriveOutput {
148    redaction_body: TokenStream,
149    used_generics: Vec<Ident>,
150    classified_generics: Vec<Ident>,
151    debug_redacted_body: TokenStream,
152    debug_redacted_generics: Vec<Ident>,
153    debug_unredacted_body: TokenStream,
154    debug_unredacted_generics: Vec<Ident>,
155}
156
157#[allow(clippy::too_many_lines)]
158fn expand(input: DeriveInput) -> Result<TokenStream> {
159    let DeriveInput {
160        ident,
161        generics,
162        data,
163        attrs,
164        ..
165    } = input;
166
167    let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
168
169    let crate_root = crate_root();
170
171    let derive_output = match &data {
172        Data::Struct(data) => {
173            let output = derive_struct(&ident, data.clone(), &generics)?;
174            DeriveOutput {
175                redaction_body: output.redaction_body,
176                used_generics: output.used_generics,
177                classified_generics: output.classified_generics,
178                debug_redacted_body: output.debug_redacted_body,
179                debug_redacted_generics: output.debug_redacted_generics,
180                debug_unredacted_body: output.debug_unredacted_body,
181                debug_unredacted_generics: output.debug_unredacted_generics,
182            }
183        }
184        Data::Enum(data) => {
185            let output = derive_enum(&ident, data.clone(), &generics)?;
186            DeriveOutput {
187                redaction_body: output.redaction_body,
188                used_generics: output.used_generics,
189                classified_generics: output.classified_generics,
190                debug_redacted_body: output.debug_redacted_body,
191                debug_redacted_generics: output.debug_redacted_generics,
192                debug_unredacted_body: output.debug_unredacted_body,
193                debug_unredacted_generics: output.debug_unredacted_generics,
194            }
195        }
196        Data::Union(u) => {
197            return Err(syn::Error::new(
198                u.union_token.span(),
199                "`Sensitive` cannot be derived for unions",
200            ));
201        }
202    };
203
204    let classify_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
205    let classify_generics =
206        add_classified_value_bounds(classify_generics, &derive_output.classified_generics);
207    let (impl_generics, ty_generics, where_clause) = classify_generics.split_for_impl();
208    let debug_redacted_generics =
209        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
210    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
211        debug_redacted_generics.split_for_impl();
212    let debug_unredacted_generics =
213        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
214    let (
215        debug_unredacted_impl_generics,
216        debug_unredacted_ty_generics,
217        debug_unredacted_where_clause,
218    ) = debug_unredacted_generics.split_for_impl();
219    let redaction_body = &derive_output.redaction_body;
220    let debug_redacted_body = &derive_output.debug_redacted_body;
221    let debug_unredacted_body = &derive_output.debug_unredacted_body;
222    let debug_impl = if skip_debug {
223        quote! {}
224    } else {
225        quote! {
226            #[cfg(any(test, feature = "testing"))]
227            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
228                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
229                    #debug_unredacted_body
230                }
231            }
232
233            #[cfg(not(any(test, feature = "testing")))]
234            #[allow(unused_variables)]
235            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
236                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
237                    #debug_redacted_body
238                }
239            }
240        }
241    };
242
243    // Only generate slog impl when the slog feature is enabled on redaction-derive.
244    // This is checked at macro compile time, not in generated code, so downstream
245    // crates don't need to define a slog feature themselves.
246    #[cfg(feature = "slog")]
247    let slog_impl = {
248        let mut slog_generics = generics;
249        let slog_where_clause = slog_generics.make_where_clause();
250        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
251        slog_where_clause
252            .predicates
253            .push(parse_quote!(#self_ty: ::core::clone::Clone));
254        // IntoRedactedJson requires Self: Serialize, so we add this bound to enable
255        // generic types to work with slog when their type parameters implement Serialize.
256        slog_where_clause
257            .predicates
258            .push(parse_quote!(#self_ty: ::serde::Serialize));
259        slog_where_clause
260            .predicates
261            .push(parse_quote!(#self_ty: #crate_root::slog::IntoRedactedJson));
262        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
263            slog_generics.split_for_impl();
264        quote! {
265            impl #slog_impl_generics ::slog::Value for #ident #slog_ty_generics #slog_where_clause {
266                fn serialize(
267                    &self,
268                    _record: &::slog::Record<'_>,
269                    key: ::slog::Key,
270                    serializer: &mut dyn ::slog::Serializer,
271                ) -> ::slog::Result {
272                    let redacted = #crate_root::slog::IntoRedactedJson::into_redacted_json(self.clone());
273                    ::slog::Value::serialize(&redacted, _record, key, serializer)
274                }
275            }
276        }
277    };
278
279    #[cfg(not(feature = "slog"))]
280    let slog_impl = quote! {};
281
282    let trait_impl = quote! {
283        impl #impl_generics #crate_root::SensitiveType for #ident #ty_generics #where_clause {
284            fn redact_with<M: #crate_root::RedactionMapper>(self, mapper: &M) -> Self {
285                use #crate_root::SensitiveType as _;
286                #redaction_body
287            }
288        }
289
290        #debug_impl
291
292        #slog_impl
293
294        // `slog` already provides `impl<V: Value> Value for &V`, so a reference
295        // impl here would conflict with the blanket impl.
296    };
297    Ok(trait_impl)
298}