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 `RedactionWalker` 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};
70use syn::{parse_macro_input, parse_quote, spanned::Spanned, Data, DeriveInput, Result};
71
72mod derive_enum;
73mod derive_struct;
74mod generics;
75mod strategy;
76mod transform;
77mod types;
78use derive_enum::derive_enum;
79use derive_struct::derive_struct;
80use generics::{add_classified_value_bounds, add_debug_bounds, add_walker_bounds};
81
82/// Derives `redaction::RedactionWalker` (and related impls) for structs and enums.
83///
84/// Field attributes:
85/// - `#[sensitive(Classification)]` treats the field as a sensitive string-like value and applies
86///   the classification's policy via the active `RedactionMap`.
87/// - `#[sensitive]` treats the field as a sensitive scalar (numbers, booleans, chars) and redacts
88///   to their default values (0, false, etc.). `char` is a special case and redacts to `'X'`.
89///   For string-like values, use `#[sensitive(Classification)]`. Scalars reject classifications.
90///
91/// For fields without a `#[sensitive(...)]` attribute, the derive traverses into the field via
92/// `RedactionWalker`.
93///
94/// Unions are rejected at compile time.
95///
96/// Additional generated impls:
97/// - `Debug`: when *not* building with `cfg(any(test, feature = "testing"))`, classified fields are
98///   formatted as the string `"[REDACTED]"` rather than their values.
99/// - `slog::Value` (behind `cfg(feature = "slog")`): implemented by cloning the value and routing
100///   it through `redaction::slog::IntoRedactedJson`. **Note:** this impl requires the type to
101///   implement `Clone`.
102#[proc_macro_derive(Sensitive, attributes(sensitive))]
103pub fn derive_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
104    let input = parse_macro_input!(input as DeriveInput);
105    match expand(input) {
106        Ok(tokens) => tokens.into(),
107        Err(err) => err.into_compile_error().into(),
108    }
109}
110
111/// Returns the token stream to reference the redaction crate root.
112///
113/// Handles crate renaming (e.g., `my_redact = { package = "redaction", ... }`)
114/// and internal usage (when derive is used inside the redaction crate itself).
115fn crate_root() -> proc_macro2::TokenStream {
116    match crate_name("redaction") {
117        Ok(FoundCrate::Itself) => quote! { crate },
118        Ok(FoundCrate::Name(name)) => {
119            let ident = format_ident!("{}", name);
120            quote! { ::#ident }
121        }
122        Err(_) => quote! { ::redaction },
123    }
124}
125
126fn crate_path(item: &str) -> proc_macro2::TokenStream {
127    let root = crate_root();
128    let item_ident = syn::parse_str::<syn::Path>(item).expect("redaction crate path should parse");
129    quote! { #root::#item_ident }
130}
131
132struct DeriveOutput {
133    redaction_body: TokenStream,
134    used_generics: Vec<Ident>,
135    classified_generics: Vec<Ident>,
136    debug_redacted_body: TokenStream,
137    debug_redacted_generics: Vec<Ident>,
138    debug_unredacted_body: TokenStream,
139    debug_unredacted_generics: Vec<Ident>,
140}
141
142#[allow(clippy::too_many_lines)]
143fn expand(input: DeriveInput) -> Result<TokenStream> {
144    let DeriveInput {
145        ident,
146        generics,
147        data,
148        ..
149    } = input;
150
151    let crate_root = crate_root();
152
153    let derive_output = match &data {
154        Data::Struct(data) => {
155            let output = derive_struct(&ident, data.clone(), &generics)?;
156            DeriveOutput {
157                redaction_body: output.redaction_body,
158                used_generics: output.used_generics,
159                classified_generics: output.classified_generics,
160                debug_redacted_body: output.debug_redacted_body,
161                debug_redacted_generics: output.debug_redacted_generics,
162                debug_unredacted_body: output.debug_unredacted_body,
163                debug_unredacted_generics: output.debug_unredacted_generics,
164            }
165        }
166        Data::Enum(data) => {
167            let output = derive_enum(&ident, data.clone(), &generics)?;
168            DeriveOutput {
169                redaction_body: output.redaction_body,
170                used_generics: output.used_generics,
171                classified_generics: output.classified_generics,
172                debug_redacted_body: output.debug_redacted_body,
173                debug_redacted_generics: output.debug_redacted_generics,
174                debug_unredacted_body: output.debug_unredacted_body,
175                debug_unredacted_generics: output.debug_unredacted_generics,
176            }
177        }
178        Data::Union(u) => {
179            return Err(syn::Error::new(
180                u.union_token.span(),
181                "`Sensitive` cannot be derived for unions",
182            ));
183        }
184    };
185
186    let classify_generics = add_walker_bounds(generics.clone(), &derive_output.used_generics);
187    let classify_generics =
188        add_classified_value_bounds(classify_generics, &derive_output.classified_generics);
189    let (impl_generics, ty_generics, where_clause) = classify_generics.split_for_impl();
190    let debug_redacted_generics =
191        add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
192    let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
193        debug_redacted_generics.split_for_impl();
194    let debug_unredacted_generics =
195        add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
196    let (
197        debug_unredacted_impl_generics,
198        debug_unredacted_ty_generics,
199        debug_unredacted_where_clause,
200    ) = debug_unredacted_generics.split_for_impl();
201    let redaction_body = &derive_output.redaction_body;
202    let debug_redacted_body = &derive_output.debug_redacted_body;
203    let debug_unredacted_body = &derive_output.debug_unredacted_body;
204    let mut slog_generics = generics;
205    let slog_where_clause = slog_generics.make_where_clause();
206    let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
207    slog_where_clause
208        .predicates
209        .push(parse_quote!(#self_ty: ::core::clone::Clone));
210    slog_where_clause
211        .predicates
212        .push(parse_quote!(#self_ty: #crate_root::slog::IntoRedactedJson));
213    let (slog_impl_generics, slog_ty_generics, slog_where_clause) = slog_generics.split_for_impl();
214    let trait_impl = quote! {
215        impl #impl_generics #crate_root::RedactionWalker for #ident #ty_generics #where_clause {
216            type Output = Self;
217
218            fn redact_with<M: #crate_root::RedactionMap>(self, mapper: &M) -> Self::Output {
219                use #crate_root::RedactionWalker as _;
220                #redaction_body
221            }
222        }
223
224        #[cfg(any(test, feature = "testing"))]
225        impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
226            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
227                #debug_unredacted_body
228            }
229        }
230
231        #[cfg(not(any(test, feature = "testing")))]
232        #[allow(unused_variables)]
233        impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
234            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
235                #debug_redacted_body
236            }
237        }
238
239        #[cfg(feature = "slog")]
240        impl #slog_impl_generics ::slog::Value for #ident #slog_ty_generics #slog_where_clause {
241            fn serialize(
242                &self,
243                _record: &::slog::Record<'_>,
244                key: ::slog::Key,
245                serializer: &mut dyn ::slog::Serializer,
246            ) -> ::slog::Result {
247                let redacted = #crate_root::slog::IntoRedactedJson::into_redacted_json(self.clone());
248                ::slog::Value::serialize(&redacted, _record, key, serializer)
249            }
250        }
251
252        // `slog` already provides `impl<V: Value> Value for &V`, so a reference
253        // impl here would conflict with the blanket impl.
254    };
255    Ok(trait_impl)
256}