#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#![allow(clippy::needless_doctest_main)]
use facet_macros_parse::*;
use quote::quote;
type BoxedResult<T> = std::result::Result<T, Box<facet_macros_parse::Error>>;
#[proc_macro_derive(SafeDebug)]
pub fn derive_safe_debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = TokenStream::from(input);
match derive_facet_debug_impl(&input) {
Ok(output) => output.into(),
Err(e) => {
let error = format!(
"SafeDebug derive error: {}\n\n\
Help: SafeDebug requires the Facet trait to be derived or implemented.\n\
Make sure you have `#[derive(Facet, SafeDebug)]` on your type.\n\
\n\
For types with generics, ensure all type parameters implement Debug.\n\
Example: `struct Foo<T: Debug>`",
e
);
quote! {
compile_error!(#error);
}
.into()
}
}
}
fn derive_facet_debug_impl(input: &TokenStream) -> BoxedResult<TokenStream> {
let mut iter = input.to_token_iter();
let adt: AdtDecl = iter.parse().map_err(Box::new)?;
match adt {
AdtDecl::Struct(s) => derive_for_struct(s),
AdtDecl::Enum(e) => derive_for_enum(e),
}
}
macro_rules! extract_type_param_bounds {
($generics:expr) => {{
if let Some(ref g) = $generics {
let params_str = g.params.to_token_stream().to_string();
params_str
.split(',')
.filter_map(|param| {
let param = param.trim();
if param.is_empty() || param.starts_with('\'') {
return None;
}
let ident_name = param
.split(|c: char| c.is_whitespace() || c == ':')
.find(|s| !s.is_empty())?;
let ident = quote::format_ident!("{}", ident_name);
Some(quote! { #ident: ::std::fmt::Debug + for<'__facet> ::facet::Facet<'__facet> })
})
.collect::<Vec<_>>()
} else {
vec![]
}
}};
}
fn derive_for_struct(parsed: Struct) -> BoxedResult<TokenStream> {
let struct_name = &parsed.name;
let generics = if let Some(ref g) = parsed.generics {
let params_ts = g.params.to_token_stream();
quote! { < #params_ts > }
} else {
quote! {}
};
let (has_existing_where, existing_where_clause) = match &parsed.kind {
StructKind::Struct { clauses, .. }
| StructKind::TupleStruct { clauses, .. }
| StructKind::UnitStruct { clauses, .. } => {
if let Some(w) = clauses {
let clauses_ts = w.to_token_stream();
(true, clauses_ts)
} else {
(false, quote! {})
}
}
};
let trait_bounds = extract_type_param_bounds!(parsed.generics);
let where_clause = if !trait_bounds.is_empty() {
if has_existing_where {
quote! { #existing_where_clause, #(#trait_bounds),* }
} else {
quote! { where #(#trait_bounds),* }
}
} else {
if has_existing_where {
quote! { #existing_where_clause }
} else {
quote! {}
}
};
let format_fields = match parsed.kind {
StructKind::Struct { ref fields, .. } => {
let field_checks: Vec<_> = fields
.content
.iter()
.enumerate()
.map(|(idx, field)| {
let field_name = &field.value.name;
let field_name_str = field_name.to_string();
quote! {
if struct_type.fields[#idx].is_sensitive() {
debug_struct.field(#field_name_str, &"[REDACTED]");
} else {
debug_struct.field(#field_name_str, &self.#field_name);
}
}
})
.collect();
let fallback_fields: Vec<_> = fields
.content
.iter()
.map(|field| {
let field_name = &field.value.name;
let field_name_str = field_name.to_string();
quote! {
debug_struct.field(#field_name_str, &"[REDACTED:NO_METADATA]");
}
})
.collect();
quote! {
let mut debug_struct = f.debug_struct(stringify!(#struct_name));
let shape = Self::SHAPE;
if let ::facet::Type::User(::facet::UserType::Struct(ref struct_type)) = shape.ty {
#(#field_checks)*
} else {
#(#fallback_fields)*
}
debug_struct.finish()
}
}
StructKind::TupleStruct { ref fields, .. } => {
let field_checks: Vec<_> = fields
.content
.iter()
.enumerate()
.map(|(idx, _field)| {
let field_idx = Literal::usize_unsuffixed(idx);
quote! {
if struct_type.fields[#idx].is_sensitive() {
debug_tuple.field(&"[REDACTED]");
} else {
debug_tuple.field(&self.#field_idx);
}
}
})
.collect();
let fallback_fields: Vec<_> = (0..fields.content.len())
.map(|_| {
quote! {
debug_tuple.field(&"[REDACTED:NO_METADATA]");
}
})
.collect();
quote! {
let mut debug_tuple = f.debug_tuple(stringify!(#struct_name));
let shape = Self::SHAPE;
if let ::facet::Type::User(::facet::UserType::Struct(ref struct_type)) = shape.ty {
#(#field_checks)*
} else {
#(#fallback_fields)*
}
debug_tuple.finish()
}
}
StructKind::UnitStruct { .. } => {
quote! {
f.debug_struct(stringify!(#struct_name)).finish()
}
}
};
let impl_block = quote! {
impl #generics ::std::fmt::Debug for #struct_name #generics #where_clause {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
#format_fields
}
}
};
Ok(impl_block)
}
fn derive_for_enum(parsed: Enum) -> BoxedResult<TokenStream> {
let enum_name = &parsed.name;
let generics = if let Some(ref g) = parsed.generics {
let params_ts = g.params.to_token_stream();
quote! { < #params_ts > }
} else {
quote! {}
};
let (has_existing_where, existing_where_clause) = if let Some(ref clauses) = parsed.clauses {
let clauses_ts = clauses.to_token_stream();
(true, clauses_ts)
} else {
(false, quote! {})
};
let trait_bounds = extract_type_param_bounds!(parsed.generics);
let where_clause = if !trait_bounds.is_empty() {
if has_existing_where {
quote! { #existing_where_clause, #(#trait_bounds),* }
} else {
quote! { where #(#trait_bounds),* }
}
} else if has_existing_where {
quote! { #existing_where_clause }
} else {
quote! {}
};
let match_arms: Vec<_> = parsed
.body
.content
.iter()
.enumerate()
.map(|(variant_idx, variant_like)| {
let variant = &variant_like.value.variant;
match variant {
EnumVariantData::Unit(unit_variant) => {
let variant_name = &unit_variant.name;
quote! {
#enum_name::#variant_name => {
write!(f, concat!(stringify!(#enum_name), "::", stringify!(#variant_name)))
}
}
}
EnumVariantData::Tuple(tuple_variant) => {
let variant_name = &tuple_variant.name;
let field_count = tuple_variant.fields.content.len();
let bindings: Vec<_> = (0..field_count).map(|i| quote::format_ident!("_field_{}", i)).collect();
let field_checks: Vec<_> = bindings
.iter()
.enumerate()
.map(|(field_idx, binding)| {
quote! {
if enum_type.variants[#variant_idx].data.fields[#field_idx].is_sensitive() {
debug_tuple.field(&"[REDACTED]");
} else {
debug_tuple.field(#binding);
}
}
})
.collect();
quote! {
#enum_name::#variant_name(#(#bindings),*) => {
let mut debug_tuple = f.debug_tuple(
concat!(stringify!(#enum_name), "::", stringify!(#variant_name))
);
#(#field_checks)*
debug_tuple.finish()
}
}
}
EnumVariantData::Struct(struct_variant) => {
let variant_name = &struct_variant.name;
let fields = &struct_variant.fields.content;
let field_names: Vec<_> = fields.iter().map(|field| &field.value.name).collect();
let field_checks: Vec<_> = field_names
.iter()
.enumerate()
.map(|(field_idx, name)| {
let name_str = name.to_string();
quote! {
if enum_type.variants[#variant_idx].data.fields[#field_idx].is_sensitive() {
debug_struct.field(#name_str, &"[REDACTED]");
} else {
debug_struct.field(#name_str, #name);
}
}
})
.collect();
quote! {
#enum_name::#variant_name { #(#field_names),* } => {
let mut debug_struct = f.debug_struct(
concat!(stringify!(#enum_name), "::", stringify!(#variant_name))
);
#(#field_checks)*
debug_struct.finish()
}
}
}
}
})
.collect();
let impl_block = quote! {
impl #generics ::std::fmt::Debug for #enum_name #generics #where_clause {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let shape = Self::SHAPE;
if let ::facet::Type::User(::facet::UserType::Enum(ref enum_type)) = shape.ty {
match self {
#(#match_arms),*
}
} else {
write!(f, "{}", stringify!(#enum_name))
}
}
}
};
Ok(impl_block)
}