1#![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#![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(
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#![cfg_attr(test, allow(clippy::non_ascii_literal, clippy::unwrap_used))]
64
65#[allow(unused_extern_crates)]
66extern crate proc_macro;
67
68#[cfg(feature = "slog")]
69use proc_macro2::Span;
70use proc_macro2::{Ident, TokenStream};
71use proc_macro_crate::{crate_name, FoundCrate};
72use quote::{format_ident, quote};
73#[cfg(feature = "slog")]
74use syn::parse_quote;
75use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Result};
76
77mod container;
78mod derive_enum;
79mod derive_struct;
80mod generics;
81mod redacted_display;
82mod strategy;
83mod transform;
84mod types;
85use container::{parse_container_options, ContainerOptions};
86use derive_enum::derive_enum;
87use derive_struct::derive_struct;
88use generics::{
89 add_classified_value_bounds, add_clone_bounds, add_container_bounds, add_debug_bounds,
90 add_display_bounds, add_redacted_display_bounds,
91};
92use redacted_display::derive_redacted_display;
93
94#[proc_macro_derive(Sensitive, attributes(sensitive))]
133pub fn derive_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
134 let input = parse_macro_input!(input as DeriveInput);
135 match expand(input, SlogMode::RedactedJson) {
136 Ok(tokens) => tokens.into(),
137 Err(err) => err.into_compile_error().into(),
138 }
139}
140
141#[proc_macro_derive(SensitiveError, attributes(sensitive, error))]
159pub fn derive_sensitive_error(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
160 let input = parse_macro_input!(input as DeriveInput);
161 match expand(input, SlogMode::RedactedDisplayString) {
162 Ok(tokens) => tokens.into(),
163 Err(err) => err.into_compile_error().into(),
164 }
165}
166
167fn crate_root() -> proc_macro2::TokenStream {
172 match crate_name("redaction") {
173 Ok(FoundCrate::Itself) => quote! { crate },
174 Ok(FoundCrate::Name(name)) => {
175 let ident = format_ident!("{}", name);
176 quote! { ::#ident }
177 }
178 Err(_) => quote! { ::redaction },
179 }
180}
181
182#[cfg(feature = "slog")]
188fn slog_crate() -> Result<proc_macro2::TokenStream> {
189 match crate_name("slog") {
190 Ok(FoundCrate::Itself) => Ok(quote! { crate }),
191 Ok(FoundCrate::Name(name)) => {
192 let ident = format_ident!("{}", name);
193 Ok(quote! { ::#ident })
194 }
195 Err(_) => {
196 let env_value = std::env::var("REDACTION_SLOG_CRATE").map_err(|_| {
197 syn::Error::new(
198 Span::call_site(),
199 "slog support is enabled, but no top-level `slog` crate was found. \
200Set the REDACTION_SLOG_CRATE env var to a path (e.g., `my_log::slog`) or add \
201`slog` as a direct dependency.",
202 )
203 })?;
204 let path = syn::parse_str::<syn::Path>(&env_value).map_err(|_| {
205 syn::Error::new(
206 Span::call_site(),
207 format!("REDACTION_SLOG_CRATE must be a valid Rust path (got `{env_value}`)"),
208 )
209 })?;
210 Ok(quote! { #path })
211 }
212 }
213}
214
215fn crate_path(item: &str) -> proc_macro2::TokenStream {
216 let root = crate_root();
217 let item_ident = syn::parse_str::<syn::Path>(item).expect("redaction crate path should parse");
218 quote! { #root::#item_ident }
219}
220
221struct DeriveOutput {
222 redaction_body: TokenStream,
223 used_generics: Vec<Ident>,
224 classified_generics: Vec<Ident>,
225 debug_redacted_body: TokenStream,
226 debug_redacted_generics: Vec<Ident>,
227 debug_unredacted_body: TokenStream,
228 debug_unredacted_generics: Vec<Ident>,
229 redacted_display_body: Option<TokenStream>,
230 redacted_display_generics: Vec<Ident>,
231 redacted_display_debug_generics: Vec<Ident>,
232 redacted_display_clone_generics: Vec<Ident>,
233 redacted_display_nested_generics: Vec<Ident>,
234}
235
236enum SlogMode {
237 RedactedJson,
238 RedactedDisplayString,
239}
240
241#[allow(clippy::too_many_lines)]
242fn expand(input: DeriveInput, slog_mode: SlogMode) -> Result<TokenStream> {
243 let DeriveInput {
244 ident,
245 generics,
246 data,
247 attrs,
248 ..
249 } = input;
250
251 let ContainerOptions { skip_debug } = parse_container_options(&attrs)?;
252
253 let crate_root = crate_root();
254
255 let redacted_display_output = if matches!(slog_mode, SlogMode::RedactedDisplayString) {
256 Some(derive_redacted_display(&ident, &data, &attrs, &generics)?)
257 } else {
258 None
259 };
260
261 let derive_output = match &data {
262 Data::Struct(data) => {
263 let output = derive_struct(&ident, data.clone(), &generics)?;
264 DeriveOutput {
265 redaction_body: output.redaction_body,
266 used_generics: output.used_generics,
267 classified_generics: output.classified_generics,
268 debug_redacted_body: output.debug_redacted_body,
269 debug_redacted_generics: output.debug_redacted_generics,
270 debug_unredacted_body: output.debug_unredacted_body,
271 debug_unredacted_generics: output.debug_unredacted_generics,
272 redacted_display_body: redacted_display_output
273 .as_ref()
274 .map(|output| output.body.clone()),
275 redacted_display_generics: redacted_display_output
276 .as_ref()
277 .map(|output| output.display_generics.clone())
278 .unwrap_or_default(),
279 redacted_display_debug_generics: redacted_display_output
280 .as_ref()
281 .map(|output| output.debug_generics.clone())
282 .unwrap_or_default(),
283 redacted_display_clone_generics: redacted_display_output
284 .as_ref()
285 .map(|output| output.clone_generics.clone())
286 .unwrap_or_default(),
287 redacted_display_nested_generics: redacted_display_output
288 .as_ref()
289 .map(|output| output.nested_generics.clone())
290 .unwrap_or_default(),
291 }
292 }
293 Data::Enum(data) => {
294 let output = derive_enum(&ident, data.clone(), &generics)?;
295 DeriveOutput {
296 redaction_body: output.redaction_body,
297 used_generics: output.used_generics,
298 classified_generics: output.classified_generics,
299 debug_redacted_body: output.debug_redacted_body,
300 debug_redacted_generics: output.debug_redacted_generics,
301 debug_unredacted_body: output.debug_unredacted_body,
302 debug_unredacted_generics: output.debug_unredacted_generics,
303 redacted_display_body: redacted_display_output
304 .as_ref()
305 .map(|output| output.body.clone()),
306 redacted_display_generics: redacted_display_output
307 .as_ref()
308 .map(|output| output.display_generics.clone())
309 .unwrap_or_default(),
310 redacted_display_debug_generics: redacted_display_output
311 .as_ref()
312 .map(|output| output.debug_generics.clone())
313 .unwrap_or_default(),
314 redacted_display_clone_generics: redacted_display_output
315 .as_ref()
316 .map(|output| output.clone_generics.clone())
317 .unwrap_or_default(),
318 redacted_display_nested_generics: redacted_display_output
319 .as_ref()
320 .map(|output| output.nested_generics.clone())
321 .unwrap_or_default(),
322 }
323 }
324 Data::Union(u) => {
325 return Err(syn::Error::new(
326 u.union_token.span(),
327 "`Sensitive` cannot be derived for unions",
328 ));
329 }
330 };
331
332 let classify_generics = add_container_bounds(generics.clone(), &derive_output.used_generics);
333 let classify_generics =
334 add_classified_value_bounds(classify_generics, &derive_output.classified_generics);
335 let (impl_generics, ty_generics, where_clause) = classify_generics.split_for_impl();
336 let debug_redacted_generics =
337 add_debug_bounds(generics.clone(), &derive_output.debug_redacted_generics);
338 let (debug_redacted_impl_generics, debug_redacted_ty_generics, debug_redacted_where_clause) =
339 debug_redacted_generics.split_for_impl();
340 let debug_unredacted_generics =
341 add_debug_bounds(generics.clone(), &derive_output.debug_unredacted_generics);
342 let (
343 debug_unredacted_impl_generics,
344 debug_unredacted_ty_generics,
345 debug_unredacted_where_clause,
346 ) = debug_unredacted_generics.split_for_impl();
347 let redaction_body = &derive_output.redaction_body;
348 let debug_redacted_body = &derive_output.debug_redacted_body;
349 let debug_unredacted_body = &derive_output.debug_unredacted_body;
350 let debug_impl = if skip_debug {
351 quote! {}
352 } else {
353 quote! {
354 #[cfg(any(test, feature = "testing"))]
355 impl #debug_unredacted_impl_generics ::core::fmt::Debug for #ident #debug_unredacted_ty_generics #debug_unredacted_where_clause {
356 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
357 #debug_unredacted_body
358 }
359 }
360
361 #[cfg(not(any(test, feature = "testing")))]
362 #[allow(unused_variables, unused_assignments)]
363 impl #debug_redacted_impl_generics ::core::fmt::Debug for #ident #debug_redacted_ty_generics #debug_redacted_where_clause {
364 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
365 #debug_redacted_body
366 }
367 }
368 }
369 };
370
371 let redacted_display_body = derive_output.redacted_display_body.as_ref();
372 let redacted_display_impl = if matches!(slog_mode, SlogMode::RedactedDisplayString) {
373 let redacted_display_generics =
374 add_display_bounds(generics.clone(), &derive_output.redacted_display_generics);
375 let redacted_display_generics = add_debug_bounds(
376 redacted_display_generics,
377 &derive_output.redacted_display_debug_generics,
378 );
379 let redacted_display_generics = add_clone_bounds(
380 redacted_display_generics,
381 &derive_output.redacted_display_clone_generics,
382 );
383 let redacted_display_generics = add_redacted_display_bounds(
384 redacted_display_generics,
385 &derive_output.redacted_display_nested_generics,
386 );
387 let (display_impl_generics, display_ty_generics, display_where_clause) =
388 redacted_display_generics.split_for_impl();
389 let redacted_display_body = redacted_display_body
390 .cloned()
391 .unwrap_or_else(TokenStream::new);
392 quote! {
393 impl #display_impl_generics #crate_root::slog::RedactedDisplay for #ident #display_ty_generics #display_where_clause {
394 fn fmt_redacted(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
395 #redacted_display_body
396 }
397 }
398 }
399 } else {
400 quote! {}
401 };
402
403 #[cfg(feature = "slog")]
406 let slog_impl = {
407 let slog_crate = slog_crate()?;
408 let mut slog_generics = generics;
409 let slog_where_clause = slog_generics.make_where_clause();
410 let self_ty: syn::Type = parse_quote!(#ident #ty_generics);
411 match slog_mode {
412 SlogMode::RedactedJson => {
413 slog_where_clause
414 .predicates
415 .push(parse_quote!(#self_ty: ::core::clone::Clone));
416 slog_where_clause
419 .predicates
420 .push(parse_quote!(#self_ty: ::serde::Serialize));
421 slog_where_clause
422 .predicates
423 .push(parse_quote!(#self_ty: #crate_root::slog::IntoRedactedJson));
424 let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
425 slog_generics.split_for_impl();
426 quote! {
427 impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
428 fn serialize(
429 &self,
430 _record: &#slog_crate::Record<'_>,
431 key: #slog_crate::Key,
432 serializer: &mut dyn #slog_crate::Serializer,
433 ) -> #slog_crate::Result {
434 let redacted = #crate_root::slog::IntoRedactedJson::into_redacted_json(self.clone());
435 #slog_crate::Value::serialize(&redacted, _record, key, serializer)
436 }
437 }
438 }
439 }
440 SlogMode::RedactedDisplayString => {
441 slog_where_clause
442 .predicates
443 .push(parse_quote!(#self_ty: #crate_root::slog::RedactedDisplay));
444 let (slog_impl_generics, slog_ty_generics, slog_where_clause) =
445 slog_generics.split_for_impl();
446 quote! {
447 impl #slog_impl_generics #slog_crate::Value for #ident #slog_ty_generics #slog_where_clause {
448 fn serialize(
449 &self,
450 _record: &#slog_crate::Record<'_>,
451 key: #slog_crate::Key,
452 serializer: &mut dyn #slog_crate::Serializer,
453 ) -> #slog_crate::Result {
454 let redacted = #crate_root::slog::RedactedDisplay::redacted_display(self);
455 serializer.emit_arguments(key, &format_args!("{}", redacted))
456 }
457 }
458 }
459 }
460 }
461 };
462
463 #[cfg(not(feature = "slog"))]
464 let slog_impl = quote! {};
465
466 let trait_impl = quote! {
467 #[allow(unused_assignments)]
468 impl #impl_generics #crate_root::SensitiveType for #ident #ty_generics #where_clause {
469 fn redact_with<M: #crate_root::RedactionMapper>(self, mapper: &M) -> Self {
470 use #crate_root::SensitiveType as _;
471 #redaction_body
472 }
473 }
474
475 #debug_impl
476
477 #redacted_display_impl
478
479 #slog_impl
480
481 };
484 Ok(trait_impl)
485}