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
67#[cfg(feature = "slog")]
68use proc_macro2::Span;
69use proc_macro2::{Ident, TokenStream};
70use proc_macro_crate::{crate_name, FoundCrate};
71use quote::{format_ident, quote};
72#[cfg(feature = "slog")]
73use syn::parse_quote;
74use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Result};
75
76mod container;
77mod derive_enum;
78mod derive_struct;
79mod generics;
80mod strategy;
81mod transform;
82mod types;
83use container::{parse_container_options, ContainerOptions};
84use derive_enum::derive_enum;
85use derive_struct::derive_struct;
86use generics::{add_classified_value_bounds, add_container_bounds, add_debug_bounds};
87
88/// Derives `redaction::SensitiveType` (and related impls) for structs and enums.
89///
90/// # Container Attributes
91///
92/// These attributes are placed on the struct/enum itself:
93///
94/// - `#[sensitive(skip_debug)]` - Opt out of `Debug` impl generation. Use this when you need a
95///   custom `Debug` implementation or the type already derives `Debug` elsewhere.
96///
97/// # Field Attributes
98///
99/// - **No annotation**: The field passes through unchanged. Use this for fields that don't contain
100///   sensitive data, including external types like `chrono::DateTime` or `rust_decimal::Decimal`.
101///
102/// - `#[sensitive]`: For scalar types (i32, bool, char, etc.), redacts to default values (0, false,
103///   'X'). For struct/enum types that derive `Sensitive`, walks into them using `SensitiveType`.
104///
105/// - `#[sensitive(Classification)]`: Treats the field as a sensitive string-like value and applies
106///   the classification's policy. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`.
107///   The type must implement `SensitiveValue`.
108///
109/// Unions are rejected at compile time.
110///
111/// # Additional Generated Impls
112///
113/// - `Debug`: when *not* building with `cfg(any(test, feature = "testing"))`, sensitive fields are
114///   formatted as the string `"[REDACTED]"` rather than their values. Use `#[sensitive(skip_debug)]`
115///   on the container to opt out.
116/// - `slog::Value` (behind `cfg(feature = "slog")`): implemented by cloning the value and routing
117///   it through `redaction::slog::IntoRedactedJson`. **Note:** this impl requires the type to
118///   implement `Clone`. The derive first looks for a top-level `slog` crate; if not found, it
119///   checks the `REDACTION_SLOG_CRATE` env var for an alternate path (e.g., `my_log::slog`). If
120///   neither is available, compilation fails with a clear error.
121#[proc_macro_derive(Sensitive, attributes(sensitive))]
122pub fn derive_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
123    let input = parse_macro_input!(input as DeriveInput);
124    match expand(input) {
125        Ok(tokens) => tokens.into(),
126        Err(err) => err.into_compile_error().into(),
127    }
128}
129
130/// Returns the token stream to reference the redaction crate root.
131///
132/// Handles crate renaming (e.g., `my_redact = { package = "redaction", ... }`)
133/// and internal usage (when derive is used inside the redaction crate itself).
134fn crate_root() -> proc_macro2::TokenStream {
135    match crate_name("redaction") {
136        Ok(FoundCrate::Itself) => quote! { crate },
137        Ok(FoundCrate::Name(name)) => {
138            let ident = format_ident!("{}", name);
139            quote! { ::#ident }
140        }
141        Err(_) => quote! { ::redaction },
142    }
143}
144
145/// Returns the token stream to reference the slog crate root.
146///
147/// Handles crate renaming (e.g., `my_slog = { package = "slog", ... }`).
148/// If the top-level `slog` crate is not available, falls back to the
149/// `REDACTION_SLOG_CRATE` env var, which should be a path like `my_log::slog`.
150#[cfg(feature = "slog")]
151fn slog_crate() -> Result<proc_macro2::TokenStream> {
152    match crate_name("slog") {
153        Ok(FoundCrate::Itself) => Ok(quote! { crate }),
154        Ok(FoundCrate::Name(name)) => {
155            let ident = format_ident!("{}", name);
156            Ok(quote! { ::#ident })
157        }
158        Err(_) => {
159            let env_value = std::env::var("REDACTION_SLOG_CRATE").map_err(|_| {
160                syn::Error::new(
161                    Span::call_site(),
162                    "slog support is enabled, but no top-level `slog` crate was found. \
163Set the REDACTION_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
164`slog` as a direct dependency.",
165                )
166            })?;
167            let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
168                syn::Error::new(
169                    Span::call_site(),
170                    format!("REDACTION_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
171                )
172            })?;
173            Ok(quote! { #path })
174        }
175    }
176}
177
178fn crate_path(item: &str) -> proc_macro2::TokenStream {
179    let root = crate_root();
180    let item_ident = syn::parse_str::<syn::Path>(item).expect("redaction crate path should parse");
181    quote! { #root::#item_ident }
182}
183
184struct DeriveOutput {
185    redaction_body: TokenStream,
186    used_generics: Vec<Ident>,
187    classified_generics: Vec<Ident>,
188    debug_redacted_body: TokenStream,
189    debug_redacted_generics: Vec<Ident>,
190    debug_unredacted_body: TokenStream,
191    debug_unredacted_generics: Vec<Ident>,
192}
193
194#[allow(clippy::too_many_lines)]
195fn expand(input: DeriveInput) -> Result<TokenStream> {
196    let DeriveInput {
197        ident,
198        generics,
199        data,
200        attrs,
201        ..
202    } = input;
203
204    let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
205
206    let crate_root = crate_root();
207
208    let derive_output = match &data {
209        Data::Struct(data) => {
210            let output = derive_struct(&ident, data.clone(), &generics)?;
211            DeriveOutput {
212                redaction_body: output.redaction_body,
213                used_generics: output.used_generics,
214                classified_generics: output.classified_generics,
215                debug_redacted_body: output.debug_redacted_body,
216                debug_redacted_generics: output.debug_redacted_generics,
217                debug_unredacted_body: output.debug_unredacted_body,
218                debug_unredacted_generics: output.debug_unredacted_generics,
219            }
220        }
221        Data::Enum(data) => {
222            let output = derive_enum(&ident, data.clone(), &generics)?;
223            DeriveOutput {
224                redaction_body: output.redaction_body,
225                used_generics: output.used_generics,
226                classified_generics: output.classified_generics,
227                debug_redacted_body: output.debug_redacted_body,
228                debug_redacted_generics: output.debug_redacted_generics,
229                debug_unredacted_body: output.debug_unredacted_body,
230                debug_unredacted_generics: output.debug_unredacted_generics,
231            }
232        }
233        Data::Union(u) => {
234            return Err(syn::Error::new(
235                u.union_token.span(),
236                "`Sensitive` cannot be derived for unions",
237            ));
238        }
239    };
240
241    let classify_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
242    let classify_generics =
243        add_classified_value_bounds(classify_generics, &derive_output.classified_generics);
244    let (impl_generics, ty_generics, where_clause) = classify_generics.split_for_impl();
245    let debug_redacted_generics =
246        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
247    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
248        debug_redacted_generics.split_for_impl();
249    let debug_unredacted_generics =
250        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
251    let (
252        debug_unredacted_impl_generics,
253        debug_unredacted_ty_generics,
254        debug_unredacted_where_clause,
255    ) = debug_unredacted_generics.split_for_impl();
256    let redaction_body = &derive_output.redaction_body;
257    let debug_redacted_body = &derive_output.debug_redacted_body;
258    let debug_unredacted_body = &derive_output.debug_unredacted_body;
259    let debug_impl = if skip_debug {
260        quote! {}
261    } else {
262        quote! {
263            #[cfg(any(test, feature = "testing"))]
264            impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
265                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
266                    #debug_unredacted_body
267                }
268            }
269
270            #[cfg(not(any(test, feature = "testing")))]
271            #[allow(unused_variables)]
272            impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
273                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
274                    #debug_redacted_body
275                }
276            }
277        }
278    };
279
280    // Only generate slog impl when the slog feature is enabled on redaction-derive.
281    // If slog is not available, emit a clear error with instructions.
282    #[cfg(feature = "slog")]
283    let slog_impl = {
284        let slog_crate = slog_crate()?;
285        let mut slog_generics = generics;
286        let slog_where_clause = slog_generics.make_where_clause();
287        let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
288        slog_where_clause
289            .predicates
290            .push(parse_quote!(#self_ty: ::core::clone::Clone));
291        // IntoRedactedJson requires Self: Serialize, so we add this bound to enable
292        // generic types to work with slog when their type parameters implement Serialize.
293        slog_where_clause
294            .predicates
295            .push(parse_quote!(#self_ty: ::serde::Serialize));
296        slog_where_clause
297            .predicates
298            .push(parse_quote!(#self_ty: #crate_root::slog::IntoRedactedJson));
299        let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
300            slog_generics.split_for_impl();
301        quote! {
302            impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
303                fn serialize(
304                    &self,
305                    _record: &#slog_crate::Record<'_>,
306                    key: #slog_crate::Key,
307                    serializer: &mut dyn #slog_crate::Serializer,
308                ) -> #slog_crate::Result {
309                    let redacted = #crate_root::slog::IntoRedactedJson::into_redacted_json(self.clone());
310                    #slog_crate::Value::serialize(&redacted, _record, key, serializer)
311                }
312            }
313        }
314    };
315
316    #[cfg(not(feature = "slog"))]
317    let slog_impl = quote! {};
318
319    let trait_impl = quote! {
320        impl #impl_generics #crate_root::SensitiveType for #ident #ty_generics #where_clause {
321            fn redact_with<M: #crate_root::RedactionMapper>(self, mapper: &M) -> Self {
322                use #crate_root::SensitiveType as _;
323                #redaction_body
324            }
325        }
326
327        #debug_impl
328
329        #slog_impl
330
331        // `slog` already provides `impl<V: Value> Value for &V`, so a reference
332        // impl here would conflict with the blanket impl.
333    };
334    Ok(trait_impl)
335}