use proc_macro::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Expr, Fields, Type};
pub(crate) fn derive(input: TokenStream) -> TokenStream {
derive_impl(input.into()).into()
}
fn derive_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let input = match syn::parse2::<DeriveInput>(input) {
Ok(parsed) => parsed,
Err(err) => return err.to_compile_error(),
};
let struct_name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let named_fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
struct_name,
"`#[derive(Inspect)]` requires a struct with named fields",
)
.to_compile_error();
}
},
_ => {
return syn::Error::new_spanned(
struct_name,
"`#[derive(Inspect)]` only supports structs",
)
.to_compile_error();
}
};
let mut accessors = Vec::new();
for field in named_fields {
let args = match parse_inspect_attr(field) {
Ok(Some(a)) => a,
Ok(None) => continue,
Err(err) => return err.to_compile_error(),
};
let field_name = field.ident.as_ref().expect("named field has an ident");
let field_ty = &field.ty;
let method_suffix = args.name.unwrap_or_else(|| field_name.to_string());
let method_ident = format_ident!("inspect_{}", method_suffix);
let accessor = match args.mode {
InspectMode::Clone => quote_spanned! { field_ty.span() =>
pub fn #method_ident(&self) -> #field_ty {
::core::clone::Clone::clone(&self.#field_name)
}
},
InspectMode::Project(p) => {
let Projection { ret, with } = &*p;
quote! {
pub fn #method_ident(&self) -> #ret {
let __project: &dyn ::core::ops::Fn(&#field_ty) -> #ret = &(#with);
__project(&self.#field_name)
}
}
}
};
accessors.push(accessor);
}
let expanded = quote! {
#[allow(clippy::type_complexity)]
impl #impl_generics #struct_name #ty_generics #where_clause {
#(#accessors)*
}
};
expanded
}
struct InspectArgs {
name: Option<String>,
mode: InspectMode,
}
enum InspectMode {
Clone,
Project(Box<Projection>),
}
struct Projection {
ret: Type,
with: Expr,
}
fn parse_inspect_attr(field: &syn::Field) -> syn::Result<Option<InspectArgs>> {
let mut found: Option<&syn::Attribute> = None;
for attr in &field.attrs {
if attr.path().is_ident("inspect") {
if found.is_some() {
return Err(syn::Error::new_spanned(
attr,
"multiple `#[inspect(...)]` attributes on one field; combine them",
));
}
found = Some(attr);
}
}
let Some(attr) = found else { return Ok(None) };
if matches!(attr.meta, syn::Meta::Path(_)) {
return Ok(Some(InspectArgs {
name: None,
mode: InspectMode::Clone,
}));
}
let mut ret: Option<Type> = None;
let mut with: Option<Expr> = None;
let mut name: Option<String> = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("ret") {
ret = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("with") {
with = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("name") {
let lit: syn::LitStr = meta.value()?.parse()?;
name = Some(lit.value());
Ok(())
} else if meta.path.is_ident("snapshot") {
Err(meta.error(
"`snapshot = T` was removed in 0.3.0; use \
`#[inspect(ret = T, with = <projector>)]` where \
`<projector>: Fn(&FieldType) -> T`",
))
} else {
Err(meta.error(
"unsupported `inspect` argument. Use bare `#[inspect]` to clone \
the field, or `#[inspect(ret = T, with = <projector>)]` to \
project it. The positional projection type `#[inspect(T)]` and \
`snapshot = T` were removed in 0.3.0.",
))
}
})?;
let mode = match (ret, with) {
(None, None) => InspectMode::Clone,
(Some(ret), Some(with)) => InspectMode::Project(Box::new(Projection { ret, with })),
(Some(_), None) => {
return Err(syn::Error::new_spanned(
attr,
"projection mode requires `with = <projector>` alongside `ret = T`",
));
}
(None, Some(_)) => {
return Err(syn::Error::new_spanned(
attr,
"projection mode requires `ret = T` alongside `with = <projector>`",
));
}
};
Ok(Some(InspectArgs { name, mode }))
}
#[cfg(test)]
mod tests {
use super::derive_impl;
use quote::quote;
#[test]
fn generated_impl_carries_type_complexity_allow() {
let input = quote! {
struct Db {
#[inspect(
ret = Vec<Vec<(Vec<u8>, Vec<u8>, u32)>>,
with = |s| s.clone()
)]
ssts: Vec<Vec<(Vec<u8>, Vec<u8>, u32)>>,
}
};
let out = derive_impl(input).to_string();
assert!(
out.contains("allow") && out.contains("type_complexity"),
"generated impl must carry #[allow(clippy::type_complexity)]; got:\n{out}"
);
}
}