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]` 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 quote::quote;
70use syn::{DeriveInput, parse_macro_input};
71
72mod container;
73mod crate_paths;
74mod debug_impl;
75mod derive_enum;
76mod derive_struct;
77mod fresh_ident;
78mod generics;
79mod not_sensitive;
80mod redacted_display;
81mod sensitive;
82mod strategy;
83mod transform;
84
85pub(crate) use crate_paths::{crate_path, crate_root};
86use not_sensitive::{expand_not_sensitive, expand_not_sensitive_display};
87pub(crate) use sensitive::DeriveOutput;
88use sensitive::{DeriveKind, expand, expand_with_mode};
89
90/// Derives `redactable::RedactableWithMapper` (and related impls) for structs and enums.
91///
92/// # Container Attributes
93///
94/// These attributes are placed on the struct/enum itself:
95///
96/// `Sensitive` and `SensitiveDisplay` are standalone derives. Use `SensitiveDual` when a type
97/// needs both structural and display redaction.
98///
99/// Use `#[redactable(recursive)]` on a field whose crate-qualified, aliased, or
100/// mutually recursive type would otherwise create a self-referential inferred
101/// bound. Unannotated fields retain their exact complete-type bounds.
102/// `#[redactable(legacy_formatting)]` and `#[redactable(generated_formatting)]`
103/// are display-only formatting options; standalone `Sensitive` rejects **both**.
104/// Apply them on `SensitiveDisplay` or `SensitiveDual` (which is what to use when
105/// a type needs structural and display redaction together); those derives
106/// document what each option selects.
107///
108/// # Field Attributes
109///
110/// - **No annotation**: The field is traversed by default. Scalars pass through unchanged; nested
111///   structs/enums are walked using `RedactableWithMapper` (so external types must implement it).
112///
113/// - `#[sensitive(Secret)]`: For scalar types (i32, bool, char, etc.), redacts to default values
114///   (0, false, '*'). For string-like types, applies full redaction to `"[REDACTED]"`.
115///
116/// - `#[sensitive(Policy)]`: Applies the policy's redaction rules to string-like
117///   values. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`. Scalars can only
118///   use `#[sensitive(Secret)]`.
119///
120/// - `#[not_sensitive]`: Explicit passthrough - the field is not transformed at all. Use this
121///   for foreign types that don't implement `RedactableWithMapper`. This is equivalent to wrapping
122///   the field type in `NotSensitiveValue<T>`, but without changing the type signature.
123///
124/// Unions are rejected at compile time.
125///
126/// # Generated Impls
127///
128/// - `RedactableWithMapper`: always generated.
129/// - `Redactable`: always generated. Provides `.redact()` and certifies the type for the
130///   redacted-output extension traits (`RedactedOutputExt`, `RedactedJsonExt`, `SlogRedactedExt`).
131/// - `Debug`: redacted by default; actual values in the consumer's `cfg(test)` builds or when
132///   `redactable`'s `testing` feature is enabled.
133/// - `slog::Value` + `SlogRedacted` (requires `slog` feature): borrowed generated output is a
134///   fixed fail-closed placeholder and never clones or serializes the raw reference. Owned values
135///   can use `SlogRedactedExt::slog_redacted_json` for redact-then-serialize structured output.
136/// - `TracingRedacted` (requires `tracing` feature): marker trait.
137#[proc_macro_derive(Sensitive, attributes(sensitive, not_sensitive, redactable))]
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, DeriveKind::Sensitive) {
141        Ok(tokens) => tokens.into(),
142        Err(err) => err.into_compile_error().into(),
143    }
144}
145
146/// Derives structural and display redaction as one authenticated expansion.
147///
148/// Use this instead of combining `Sensitive` and `SensitiveDisplay` with the
149/// legacy `#[sensitive(dual)]` coordination attribute.
150#[proc_macro_derive(SensitiveDual, attributes(sensitive, not_sensitive, redactable, error))]
151pub fn derive_sensitive_dual(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
152    let input = parse_macro_input!(input as DeriveInput);
153    let sensitive = expand_with_mode(input.clone(), DeriveKind::Sensitive, true);
154    let display = expand_with_mode(input, DeriveKind::SensitiveDisplay, true);
155    match (sensitive, display) {
156        (Ok(sensitive), Ok(display)) => quote!(#sensitive #display).into(),
157        (Err(mut first), Err(second)) => {
158            first.combine(second);
159            first.into_compile_error().into()
160        }
161        (Err(err), _) | (_, Err(err)) => err.into_compile_error().into(),
162    }
163}
164
165/// Derives a no-op `redactable::RedactableWithMapper` implementation, along with
166/// `slog::Value` / `SlogRedacted` and `TracingRedacted`.
167///
168/// This is useful for types that are known to be non-sensitive but still need to
169/// satisfy `RedactableWithMapper` / `Redactable` bounds. Because the type has no
170/// sensitive data, logging integration works without wrappers.
171///
172/// # Generated Impls
173///
174/// - `RedactableWithMapper`: no-op passthrough (the type has no sensitive data)
175/// - `Redactable`: deriving `NotSensitive` is an explicit declaration, so the type is
176///   certified for consuming and borrowed adapters. Generated slog serialization borrows rather
177///   than clones; serde's `RefCell` implementation
178///   reports an active mutable borrow as an error, which is converted to `"[REDACTED]"`.
179/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): serializes the explicitly
180///   non-sensitive value directly as structured JSON. Requires `Serialize` on the type.
181/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
182///
183/// `NotSensitive` does **not** generate a `Debug` impl - there's nothing to redact.
184/// Use `#[derive(Debug)]` when needed.
185///
186/// # Rejected Attributes
187///
188/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
189/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
190/// latter is redundant (the entire type is already non-sensitive).
191///
192/// Unions are rejected at compile time.
193#[proc_macro_derive(NotSensitive, attributes(sensitive, not_sensitive))]
194pub fn derive_not_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
195    let input = parse_macro_input!(input as DeriveInput);
196    match expand_not_sensitive(input) {
197        Ok(tokens) => tokens.into(),
198        Err(err) => err.into_compile_error().into(),
199    }
200}
201
202/// Derives `redactable::RedactableWithFormatter` for types with no sensitive data.
203///
204/// This is the display counterpart to `NotSensitive`. Use it when you have a type
205/// with no sensitive data that needs logging integration (e.g., for use with slog).
206///
207/// Unlike `SensitiveDisplay`, this derive does **not** require a display template.
208/// Instead, it delegates directly to the type's existing `Display` implementation.
209///
210/// # Required Bounds
211///
212/// The type must implement `Display`. This is required because `RedactableWithFormatter` delegates
213/// to `Display::fmt`.
214///
215/// # Generated Impls
216///
217/// - `RedactableWithMapper`: no-op passthrough (allows use inside `Sensitive` containers)
218/// - `Redactable`: deriving `NotSensitiveDisplay` is an explicit declaration, so the type is
219///   certified for consuming and borrowed adapters.
220/// - `RedactableWithFormatter`: delegates to `Display::fmt`
221/// - `ToRedactedOutput`: emits the `Display` text; certifies the type for
222///   `slog_redacted_display()` and `tracing_redacted()`
223/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): uses `RedactableWithFormatter` output
224/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
225///
226/// # Debug
227///
228/// `NotSensitiveDisplay` does **not** generate a `Debug` impl - there's nothing to redact.
229/// Use `#[derive(Debug)]` alongside `NotSensitiveDisplay` when needed.
230///
231/// # Rejected Attributes
232///
233/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
234/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
235/// latter is redundant (the entire type is already non-sensitive).
236///
237/// # Example
238///
239/// ```ignore
240/// use redactable::NotSensitiveDisplay;
241/// use std::fmt;
242///
243/// #[derive(NotSensitiveDisplay)]
244/// enum RetryDecision {
245///     Retry,
246///     Abort,
247/// }
248///
249/// impl fmt::Display for RetryDecision {
250///     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251///         formatter.write_str(match self {
252///             Self::Retry => "Retry",
253///             Self::Abort => "Abort",
254///         })
255///     }
256/// }
257///
258/// assert_eq!(RetryDecision::Retry.to_string(), "Retry");
259/// ```
260#[proc_macro_derive(NotSensitiveDisplay, attributes(sensitive, not_sensitive))]
261pub fn derive_not_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
262    let input = parse_macro_input!(input as DeriveInput);
263    match expand_not_sensitive_display(input) {
264        Ok(tokens) => tokens.into(),
265        Err(err) => err.into_compile_error().into(),
266    }
267}
268
269/// Derives `redactable::RedactableWithFormatter` using a display template.
270///
271/// This generates a redacted string representation without requiring `Clone`.
272/// Unannotated fields use `RedactableWithFormatter` by default (passthrough for scalars,
273/// redacted display for nested `SensitiveDisplay` types).
274///
275/// # Field Annotations
276///
277/// - *(none)*: Uses `RedactableWithFormatter` (requires the field type to implement it)
278/// - `#[sensitive(Policy)]`: Apply the policy's redaction rules
279/// - `#[not_sensitive]`: Render raw via `Display` (use for types without `RedactableWithFormatter`)
280///
281/// The display template is taken from `#[error("...")]` (thiserror-style) or from
282/// doc comments (displaydoc-style). If neither is present, the derive fails.
283///
284/// Fields are redacted by reference, so field types do not need `Clone`.
285/// A custom `PolicyApplicableRef` leaf nested inside a container can explicitly
286/// select its ordinary borrowed projection with
287/// `#[redactable(legacy_formatting)]`. The explicit route does not require the
288/// direct-leaf formatting marker; it requires `PolicyApplicableRef` on the whole
289/// field and the selected format capability on its output. It inherits the
290/// projection's `Clone` requirements and borrow behavior; library-owned fields
291/// should stay on the default conflict-safe route. It composes with
292/// `#[redactable(recursive)]`, retaining the projection/output bounds while
293/// suppressing the cyclic inferred field bound.
294///
295/// `#[redactable(generated_formatting)]` instead selects the library-owned
296/// recursive formatter for an alias-hidden or otherwise ambiguous container
297/// field. `legacy_formatting` and `generated_formatting` are mutually exclusive,
298/// and standalone `Sensitive` rejects both (they only affect display output).
299///
300/// Use `SensitiveDual` instead when the same type also needs structural redaction.
301///
302/// # Generated Impls
303///
304/// - `RedactableWithFormatter`: always generated.
305/// - `ToRedactedOutput`: always generated; emits the redacted display text and certifies the
306///   type for `slog_redacted_display()` and `tracing_redacted()`.
307/// - `Debug`: redacted by default; actual values in the consumer's `cfg(test)` builds or when
308///   `redactable`'s `testing` feature is enabled.
309/// - `slog::Value` + `SlogRedacted`: emits the redacted display string (requires `slog` feature).
310/// - `TracingRedacted`: marker trait (requires `tracing` feature).
311#[proc_macro_derive(
312    SensitiveDisplay,
313    attributes(sensitive, not_sensitive, redactable, error)
314)]
315pub fn derive_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
316    let input = parse_macro_input!(input as DeriveInput);
317    match expand(input, DeriveKind::SensitiveDisplay) {
318        Ok(tokens) => tokens.into(),
319        Err(err) => err.into_compile_error().into(),
320    }
321}
322
323#[cfg(all(test, feature = "slog"))]
324mod generated_dependency_tests;
325
326#[cfg(all(test, feature = "slog"))]
327#[test]
328fn structural_generated_dependency_roots() {
329    generated_dependency_tests::run_structural_generated_dependency_roots();
330}