#![doc = doctest_example!("fruits")]
#![warn(missing_docs)]
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::ToTokens;
use syn::{
braced, parenthesized,
parse::{Parse, ParseStream, Result},
parse_quote,
punctuated::Punctuated,
token::{Brace, Paren},
Attribute, Error, Expr, Ident, Path, Token, Type,
};
macro_rules! doctest_example {
($file:literal) => {
include_str!(concat!("../tests/doctest_example_", $file, ".rs"))
};
}
#[doc = doctest_example!("assert_result_blocked")]
#[doc = doctest_example!("assert_result_static")]
#[doc = doctest_example!("termination_basic")]
#[doc = doctest_example!("termination_override")]
#[doc = doctest_example!("attributes")]
#[proc_macro]
pub fn test_gen(tokens: TokenStream) -> TokenStream {
syn::parse(tokens)
.map_or_else(Error::into_compile_error, MacroHelper::restructure)
.into()
}
#[derive(Clone)]
struct MacroHelper {
static_attrs: Vec<Attribute>,
separator: Separator, helper: Path,
static_args: Option<FnArgs>,
static_return_type: Option<ReturnType>,
farrow: Token![=>], braces: Brace, cases: Punctuated<TestCase, Token![,]>,
}
impl MacroHelper {
fn restructure(self) -> TokenStream2 {
let Self {
static_attrs,
helper,
mut static_args,
static_return_type,
cases,
..
} = self;
let static_args = static_args.as_mut().map(|FnArgs { args, .. }| {
args.push_punct(Default::default());
args
});
let static_return_type = static_return_type.as_ref();
cases
.into_iter()
.map(|case| -> TokenStream2 {
let TestCase {
fn_name,
args:
CaseArgs {
attrs,
args: FnArgs { args, .. },
return_type,
..
},
..
} = case;
let return_type = return_type.as_ref().or(static_return_type);
parse_quote! {
#(#static_attrs)*
#(#attrs)*
#[test]
fn #fn_name() #return_type {
#helper(#static_args #args)
}
}
})
.collect()
}
}
impl Parse for MacroHelper {
fn parse(input: ParseStream) -> Result<Self> {
let static_attrs = input.call(Attribute::parse_outer)?;
let separator = input.parse().map_err(|err| {
Error::new(err.span(), "expected attributes, `fn`, `struct` or `enum`")
})?;
let helper = input
.parse()
.map_err(|err| Error::new(err.span(), "expected helper function"))?;
let static_args = input.peek(Paren).then(|| input.parse()).transpose()?;
let static_return_type = input.call(ReturnType::try_parse)?;
let farrow = input.parse()?;
let cases;
let braces = braced!(cases in input);
cases
.is_empty()
.then(|| Error::new(cases.span(), "expected test cases"))
.map_or_else(|| cases.parse_terminated(TestCase::parse), Result::Err)
.map(|cases| Self {
static_attrs,
separator,
helper,
static_args,
static_return_type,
farrow,
braces,
cases,
})
}
}
impl ToTokens for MacroHelper {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.static_attrs
.iter()
.for_each(|attr| attr.to_tokens(tokens));
self.separator.to_tokens(tokens);
self.helper.to_tokens(tokens);
self.static_args.to_tokens(tokens);
self.static_return_type.to_tokens(tokens);
self.farrow.to_tokens(tokens);
self.braces
.surround(tokens, |inner| self.cases.to_tokens(inner));
}
}
#[derive(Clone)]
enum Separator {
Fn(Token![fn]),
Struct(Token![struct]),
Enum(Token![enum]),
}
impl Parse for Separator {
fn parse(input: ParseStream) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![fn]) {
input.parse().map(Self::Fn)
} else if lookahead.peek(Token![struct]) {
input.parse().map(Self::Struct)
} else if lookahead.peek(Token![enum]) {
input.parse().map(Self::Enum)
} else {
Err(lookahead.error())
}
}
}
impl ToTokens for Separator {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
Self::Fn(item) => item.to_tokens(tokens),
Self::Struct(item) => item.to_tokens(tokens),
Self::Enum(item) => item.to_tokens(tokens),
}
}
}
#[derive(Clone)]
struct TestCase {
fn_name: Ident,
colon: Token![:], args: CaseArgs,
}
impl Parse for TestCase {
fn parse(input: ParseStream) -> Result<Self> {
let fn_name = input
.parse()
.map_err(|err| Error::new(err.span(), "expected test case name"))?;
let colon = input.parse()?;
input.parse().map(|args| Self {
fn_name,
colon,
args,
})
}
}
impl ToTokens for TestCase {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.fn_name.to_tokens(tokens);
self.colon.to_tokens(tokens);
self.args.to_tokens(tokens);
}
}
#[derive(Clone)]
struct CaseArgs {
braces: Brace, attrs: Vec<Attribute>,
args: FnArgs,
return_type: Option<ReturnType>,
}
impl Parse for CaseArgs {
fn parse(input: ParseStream) -> Result<Self> {
let inner;
let braces = braced!(inner in input);
let attrs = inner.call(Attribute::parse_outer)?;
if !inner.peek(Paren) {
return Err(Error::new(
inner.span(),
"expected attributes or function parameters",
));
}
let args = inner.parse()?;
inner.call(ReturnType::try_parse).map(|return_type| Self {
braces,
attrs,
args,
return_type,
})
}
}
impl ToTokens for CaseArgs {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.braces.surround(tokens, |inner| {
self.attrs.iter().for_each(|attr| attr.to_tokens(inner));
self.args.to_tokens(inner);
self.return_type.to_tokens(inner);
});
}
}
#[derive(Clone)]
struct FnArgs {
parens: Paren, args: Punctuated<Expr, Token![,]>,
}
impl Parse for FnArgs {
fn parse(input: ParseStream) -> Result<Self> {
let inner;
let parens = parenthesized!(inner in input);
inner
.call(Punctuated::parse_separated_nonempty)
.map_err(|err| Error::new(err.span(), "expected function arguments"))
.map(|args| Self { parens, args })
}
}
impl ToTokens for FnArgs {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.parens
.surround(tokens, |inner| self.args.to_tokens(inner))
}
}
#[derive(Clone)]
struct ReturnType {
arrow: Token![->],
return_type: Type,
}
impl ReturnType {
fn try_parse(input: ParseStream) -> Result<Option<Self>> {
input.peek(Token![->]).then(|| input.parse()).transpose()
}
}
impl Parse for ReturnType {
fn parse(input: ParseStream) -> Result<Self> {
let arrow = input.parse()?;
input
.parse()
.map_err(|err| Error::new(err.span(), "expected a return type"))
.map(|return_type| Self { arrow, return_type })
}
}
impl ToTokens for ReturnType {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.arrow.to_tokens(tokens);
self.return_type.to_tokens(tokens);
}
}
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
struct ReadMeDocTestDummy;
#[cfg(test)]
mod tests {
use super::*;
fn parse_to_tokens<P: Parse + ToTokens>(p: &'static str) {
let tokens: TokenStream2 = p.parse().expect("string could not be parsed as tokens");
let parsed = syn::parse_str::<P>(&p)
.expect("tokens could not be parsed as type")
.into_token_stream()
.to_string();
assert_eq!(parsed, tokens.to_string());
}
#[test]
fn return_type_parsing() {
parse_to_tokens::<ReturnType>("-> usize");
}
#[test]
fn fn_args_parsing() {
parse_to_tokens::<FnArgs>("(1, 2)");
}
#[test]
fn case_args_parsing() {
parse_to_tokens::<CaseArgs>("{ #[ignore] (1, 2) -> usize }");
}
#[test]
fn test_case_parsing() {
parse_to_tokens::<TestCase>("test: { #[ignore] (1, 2) -> usize }");
}
#[test]
fn test_helper_parsing() {
parse_to_tokens::<MacroHelper>("#[should_panic] fn Into::into -> (usize, usize) => { test: { #[ignore] (1, 2) -> usize } }");
}
}