use std::collections::BTreeSet;
use proc_macro2::{
TokenStream,
TokenTree,
};
use quote::{
ToTokens,
format_ident,
quote,
};
use syn::punctuated::Punctuated;
use syn::{
Field,
GenericParam,
Generics,
Ident,
Lifetime,
Path,
Type,
WhereClause,
WherePredicate,
parse_quote,
};
use crate::{
field_mode::FieldMode,
internal::{
ContainerData,
FieldsData,
},
};
pub(crate) fn add_immutable_bounds(
generics: &mut Generics,
model: &ContainerData<'_>,
runtime: &Path,
) {
for_each_field(model, &mut |field, mode, _serialize_with| match mode {
FieldMode::Plain => {
add_trait_bound(generics, field, quote!(::core::fmt::Debug));
}
FieldMode::Level(_) => {
add_trait_bound(generics, field, quote!(#runtime::RedactValue));
}
FieldMode::Nested => {
add_trait_bound(generics, field, quote!(#runtime::Redact));
}
FieldMode::Skip | FieldMode::Map | FieldMode::Json => {}
});
}
pub(crate) fn add_mutable_bounds(
generics: &mut Generics,
model: &ContainerData<'_>,
runtime: &Path,
) {
for_each_field(model, &mut |field, mode, _serialize_with| match mode {
FieldMode::Level(_) => {
add_trait_bound(generics, field, quote!(#runtime::RedactValueMut));
}
FieldMode::Nested => {
add_trait_bound(generics, field, quote!(#runtime::RedactMut));
}
FieldMode::Plain
| FieldMode::Skip
| FieldMode::Map
| FieldMode::Json => {}
});
}
pub(crate) fn add_serialization_bounds(
generics: &mut Generics,
model: &ContainerData<'_>,
runtime: &Path,
serde: &Path,
) {
for_each_field(model, &mut |field, mode, serialize_with| match mode {
FieldMode::Plain if serialize_with.is_none() => {
add_trait_bound(generics, field, quote!(#serde::Serialize));
}
FieldMode::Level(_) => {
add_trait_bound(generics, field, quote!(#runtime::RedactValue));
}
FieldMode::Nested => {
add_trait_bound(
generics,
field,
quote!(#runtime::__private::RedactSerialize),
);
}
FieldMode::Plain
| FieldMode::Skip
| FieldMode::Map
| FieldMode::Json => {}
});
}
fn for_each_field(
model: &ContainerData<'_>,
callback: &mut impl FnMut(&Field, &FieldMode, Option<&Path>),
) {
match model {
ContainerData::Struct(fields) => for_each_fields(fields, callback),
ContainerData::Enum(variants) => {
for variant in variants {
for_each_fields(variant.fields(), callback);
}
}
}
}
fn for_each_fields(
fields: &FieldsData<'_>,
callback: &mut impl FnMut(&Field, &FieldMode, Option<&Path>),
) {
match fields {
FieldsData::Named(fields) => {
for field in fields {
callback(
field.field(),
field.attributes().mode(),
field.serde_attributes().serialize_with(),
);
}
}
FieldsData::Unnamed(fields) => {
for field in fields {
callback(
field.field(),
field.attributes().mode(),
field.serde_attributes().serialize_with(),
);
}
}
FieldsData::Unit => {}
}
}
fn add_trait_bound(
generics: &mut Generics,
field: &Field,
trait_path: TokenStream,
) {
if !uses_type_parameter(generics, &field.ty) {
return;
}
let field_type = &field.ty;
let predicate: WherePredicate = parse_quote!(#field_type: #trait_path);
let candidate = predicate.to_token_stream().to_string();
let where_clause = generics.make_where_clause();
if where_clause
.predicates
.iter()
.any(|item| item.to_token_stream().to_string() == candidate)
{
return;
}
where_clause.predicates.push(predicate);
}
fn uses_type_parameter(
generics: &Generics,
field_type: &impl ToTokens,
) -> bool {
let parameters: Vec<String> = generics
.params
.iter()
.filter_map(|parameter| {
let GenericParam::Type(parameter) = parameter else {
return None;
};
Some(parameter.ident.to_string())
})
.collect();
token_stream_uses_parameter(field_type.to_token_stream(), ¶meters)
}
pub(crate) fn generics_for_field(
generics: &Generics,
field_type: &Type,
) -> Generics {
let parameter_names = generic_parameter_names(generics);
let mut used = BTreeSet::new();
collect_parameter_names(
field_type.to_token_stream(),
¶meter_names,
&mut used,
);
loop {
let mut changed = false;
if let Some(where_clause) = &generics.where_clause {
for predicate in &where_clause.predicates {
let names = parameter_names_in(predicate, ¶meter_names);
if names.iter().any(|name| used.contains(name)) {
changed |=
names.iter().any(|name| used.insert(name.clone()));
}
}
}
if !changed {
break;
}
}
let mut filtered = generics.clone();
filtered.params = generics
.params
.iter()
.filter(|parameter| used.contains(&generic_parameter_name(parameter)))
.cloned()
.collect();
filtered.where_clause = filtered.where_clause.and_then(|where_clause| {
let predicates: Punctuated<WherePredicate, syn::token::Comma> =
where_clause
.predicates
.into_iter()
.filter(|predicate| {
let names = parameter_names_in(predicate, ¶meter_names);
names.iter().any(|name| used.contains(name))
})
.collect();
if predicates.is_empty() {
None
} else {
Some(WhereClause {
where_token: where_clause.where_token,
predicates,
})
}
});
filtered
}
#[must_use]
pub(crate) fn fresh_identifier(generics: &Generics, base: &str) -> Ident {
let used = generic_parameter_names(generics);
if !used.contains(base) {
return format_ident!("{base}");
}
(0..)
.map(|index| format_ident!("{base}_{index}"))
.find(|candidate| !used.contains(&candidate.to_string()))
.expect("an unused generated identifier should always exist")
}
#[must_use]
pub(crate) fn fresh_lifetime(generics: &Generics) -> Lifetime {
let used = generic_parameter_names(generics);
let base = "__qubit_redact_lifetime";
let name = if !used.contains(base) {
base.to_owned()
} else {
(0..)
.map(|index| format!("{base}_{index}"))
.find(|candidate| !used.contains(candidate))
.expect("an unused generated lifetime should always exist")
};
Lifetime::new(&format!("'{name}"), proc_macro2::Span::call_site())
}
fn generic_parameter_names(generics: &Generics) -> BTreeSet<String> {
generics.params.iter().map(generic_parameter_name).collect()
}
fn generic_parameter_name(parameter: &GenericParam) -> String {
match parameter {
GenericParam::Type(parameter) => parameter.ident.to_string(),
GenericParam::Const(parameter) => parameter.ident.to_string(),
GenericParam::Lifetime(parameter) => {
parameter.lifetime.ident.to_string()
}
}
}
fn parameter_names_in(
tokens: &impl ToTokens,
candidates: &BTreeSet<String>,
) -> BTreeSet<String> {
let mut names = BTreeSet::new();
collect_parameter_names(tokens.to_token_stream(), candidates, &mut names);
names
}
fn collect_parameter_names(
tokens: TokenStream,
candidates: &BTreeSet<String>,
names: &mut BTreeSet<String>,
) {
for token in tokens {
match token {
TokenTree::Ident(identifier) => {
let name = identifier.to_string();
if candidates.contains(&name) {
names.insert(name);
}
}
TokenTree::Group(group) => {
collect_parameter_names(group.stream(), candidates, names);
}
TokenTree::Punct(_) | TokenTree::Literal(_) => {}
}
}
}
fn token_stream_uses_parameter(
tokens: TokenStream,
parameters: &[String],
) -> bool {
tokens.into_iter().any(|token| match token {
TokenTree::Ident(identifier) => {
let name = identifier.to_string();
parameters.iter().any(|parameter| parameter == &name)
}
TokenTree::Group(group) => {
token_stream_uses_parameter(group.stream(), parameters)
}
TokenTree::Punct(_) | TokenTree::Literal(_) => false,
})
}