#![doc = include_str!("../README.md")]
use proc_macro::TokenStream;
use proc_macro2::{Ident, Punct, Spacing, Span, TokenStream as TokenStream2, TokenTree};
use quote::quote;
use syn::{parse_macro_input, parse_quote, FnArg, ImplItem, ItemImpl, Visibility};
fn dollar_ident(name: &str) -> TokenStream2 {
let mut ts = TokenStream2::new();
ts.extend([
TokenTree::Punct(Punct::new('$', Spacing::Alone)),
TokenTree::Ident(Ident::new(name, Span::call_site())),
]);
ts
}
fn extract_struct_name(ty: &syn::Type) -> Ident {
match ty {
syn::Type::Path(type_path) => type_path
.path
.segments
.last()
.expect("generate_test_macro: expected at least one path segment in self type")
.ident
.clone(),
_ => panic!("generate_test_macro: self type must be a path (e.g. `Struct<T>`)"),
}
}
struct TestMethod {
name: Ident,
has_self: bool,
extra_attrs: Vec<syn::Attribute>,
}
struct QuickcheckMethod {
name: Ident,
arity: usize,
cfg_attrs: Vec<syn::Attribute>,
}
#[proc_macro_attribute]
pub fn generate_test_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
let macro_name = parse_macro_input!(attr as Ident);
let mut impl_block = parse_macro_input!(item as ItemImpl);
let type_params: Vec<Ident> = impl_block
.generics
.type_params()
.map(|tp| tp.ident.clone())
.collect();
let struct_name = extract_struct_name(&impl_block.self_ty);
let mut test_methods: Vec<TestMethod> = Vec::new();
let mut quickcheck_methods: Vec<QuickcheckMethod> = Vec::new();
for impl_item in &mut impl_block.items {
let ImplItem::Fn(method) = impl_item else {
continue;
};
let method_name = method.sig.ident.clone();
let mut is_test = false;
let mut is_quickcheck = false;
let mut extra_attrs: Vec<syn::Attribute> = Vec::new();
method.attrs.retain(|attr| {
if attr.path().is_ident("test") {
is_test = true;
false
} else if attr.path().is_ident("quickcheck") {
is_quickcheck = true;
false
} else if attr.path().is_ident("should_panic") || attr.path().is_ident("ignore") {
extra_attrs.push(attr.clone());
false
} else if attr.path().is_ident("cfg") {
extra_attrs.push(attr.clone());
true
} else {
true
}
});
if is_test || is_quickcheck {
method.vis = Visibility::Public(Default::default());
method.attrs.push(parse_quote!(#[doc(hidden)]));
method.attrs.push(parse_quote!(#[inline(never)]));
}
if is_test {
let has_self = method
.sig
.inputs
.first()
.map(|arg| matches!(arg, FnArg::Receiver(_)))
.unwrap_or(false);
test_methods.push(TestMethod {
name: method_name,
has_self,
extra_attrs,
});
} else if is_quickcheck {
let qm = build_quickcheck_method(method, &type_params);
quickcheck_methods.push(qm);
let _ = (&is_quickcheck, &method_name, &type_params);
}
}
let has_test_methods = !test_methods.is_empty();
let has_self_test_methods = test_methods.iter().any(|tm| tm.has_self);
let has_quickcheck_methods = !quickcheck_methods.is_empty();
if !has_test_methods && !has_quickcheck_methods {
return quote! {
compile_error!("generate_test_macro: the impl block must contain at least one #[test] or #[quickcheck] method");
#impl_block
}
.into();
}
let dollar_mod_name = dollar_ident("mod_name");
let dollar_type = dollar_ident("type");
let dollar_expr = dollar_ident("expr");
let test_fn_items: TokenStream2 = test_methods
.iter()
.map(|tm| {
let name = &tm.name;
let extra_attrs = &tm.extra_attrs;
let call = if tm.has_self {
quote! {
#[allow(unused_mut)]
let mut instance : #dollar_type = #dollar_expr;
instance . #name ();
}
} else {
quote! {
< #dollar_type > :: #name ();
}
};
quote! {
#(#extra_attrs)*
#[test]
fn #name () {
#call
}
}
})
.collect();
let quickcheck_fn_items: TokenStream2 = quickcheck_methods
.iter()
.map(|qm| {
let name = &qm.name;
let cfg_attrs = &qm.cfg_attrs;
let underscores: TokenStream2 = (0..qm.arity)
.enumerate()
.map(|(i, _)| {
if i == 0 {
quote! { _ }
} else {
quote! { , _ }
}
})
.collect();
quote! {
#(#cfg_attrs)*
#[test]
pub fn #name() {
quickcheck::quickcheck(
< #dollar_type > :: #name as fn( #underscores ) -> _
);
}
}
})
.collect();
let (main_pat, supporting_arms) = if has_self_test_methods {
let pat = quote! {
#dollar_mod_name : ident : #dollar_type : ty = #dollar_expr : expr
};
let default_arm = quote! {
($mod_name:ident : $type:ty) => {
#macro_name!($mod_name : $type = <$type as ::core::default::Default>::default());
};
};
let turbofish_arm = quote! {
($mod_name:ident = #struct_name :: <$($tparam:ty),* $(,)?> $($rest:tt)*) => {
#macro_name!($mod_name : #struct_name<$($tparam),*> = #struct_name::<$($tparam),*> $($rest)*);
};
};
let has_static_test_methods =
test_methods.iter().any(|tm| !tm.has_self) || !quickcheck_methods.is_empty();
let plain_arm: TokenStream2 = if type_params.is_empty() || !has_static_test_methods {
let wildcard_params = if type_params.is_empty() {
quote! {}
} else {
let wildcards = type_params
.iter()
.fold(quote! {}, |acc, _| quote! { #acc _, });
quote! { < #wildcards > }
};
quote! {
($mod_name:ident = #struct_name $($rest:tt)*) => {
#macro_name!($mod_name : #struct_name #wildcard_params = #struct_name $($rest)*);
};
}
} else {
quote! {
($mod_name:ident = #struct_name $($rest:tt)*) => {
compile_error!(concat!(stringify!(#macro_name), ": type parameters for ",
stringify!(#struct_name), " cannot be inferred; use the turbofish form instead"));
};
}
};
let arms = quote! {
#default_arm
#turbofish_arm
#plain_arm
};
(pat, arms)
} else {
let pat = quote! {
#dollar_mod_name : ident : #dollar_type : ty
};
(pat, TokenStream2::new())
};
let macro_rules_def = quote! {
#[macro_export]
macro_rules! #macro_name {
#supporting_arms
( #main_pat ) => {
mod #dollar_mod_name {
#[allow(unused_imports)]
use super::*;
#test_fn_items
#quickcheck_fn_items
}
}
}
};
quote! {
#impl_block
#macro_rules_def
}
.into()
}
#[deprecated(
since = "0.1.2",
note = "The `#[test_suite_macro]` attribute is now named `#[generate_test_macro]`"
)]
#[proc_macro_attribute]
pub fn test_suite_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
generate_test_macro(attr, item)
}
fn build_quickcheck_method(method: &syn::ImplItemFn, _type_params: &[Ident]) -> QuickcheckMethod {
let name = method.sig.ident.clone();
let arity = method
.sig
.inputs
.iter()
.filter(|arg| matches!(arg, FnArg::Typed(_)))
.count();
let cfg_attrs = method
.attrs
.iter()
.filter(|a| a.path().is_ident("cfg"))
.cloned()
.collect();
QuickcheckMethod {
name,
arity,
cfg_attrs,
}
}