#![warn(missing_docs)]
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as Tokens;
use quote::quote;
use quote::quote_spanned;
use syn::parse::Parse;
use syn::parse::Parser as _;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned as _;
use syn::Attribute;
use syn::Error;
use syn::Ident;
use syn::ItemFn;
use syn::Meta;
use syn::MetaNameValue;
use syn::PathArguments;
use syn::PathSegment;
use syn::Result;
use syn::Token;
type Tags = Punctuated<Ident, Token![,]>;
#[proc_macro_attribute]
pub fn tag(attrs: TokenStream, item: TokenStream) -> TokenStream {
try_tag(attrs, item)
.unwrap_or_else(Error::into_compile_error)
.into()
}
fn try_tag(attrs: TokenStream, item: TokenStream) -> Result<Tokens> {
let mut tags = parse_tags(attrs)?;
let input = ItemFn::parse.parse(item)?;
let ItemFn {
attrs,
vis,
mut sig,
block,
} = input;
let (more_tags, mut attrs) = parse_fn_attrs(attrs)?;
let () = tags.extend(more_tags);
let () = rewrite_test_attrs(&mut attrs);
let test_name = sig.ident.clone();
sig.ident = Ident::new("test", sig.ident.span());
let mut result = quote! {
#(#attrs)*
pub #sig {
#block
}
};
let mut import = None;
for tag in tags.into_iter().rev() {
import = if let Some(import) = &import {
Some(quote! { #tag::#import })
} else {
Some(quote! { #tag })
};
result = quote! {
pub mod #tag {
use super::*;
#result
}
};
}
result = quote! {
use ::std::prelude::v1::*;
#[allow(unused_imports)]
#vis use #test_name::#import::test as #test_name;
#[doc(hidden)]
pub mod #test_name {
use super::*;
#result
}
};
Ok(result)
}
fn parse_tags(attrs: TokenStream) -> Result<Tags> {
let tags = Tags::parse_terminated.parse(attrs)?;
if !tags.is_empty() {
Ok(tags)
} else {
Err(Error::new_spanned(
&tags,
"at least one tag is required: #[test_tag::tag(<tags...>)]",
))
}
}
fn parse_fn_attrs(attrs: Vec<Attribute>) -> Result<(Tags, Vec<Attribute>)> {
let mut tags = Tags::new();
let mut passthrough_attrs = Vec::new();
for attr in attrs {
if is_test_tag_attr(&attr) {
let tokens = match attr.meta {
Meta::Path(..) => {
quote_spanned!(attr.meta.span() => {})
},
Meta::List(list) => list.tokens,
Meta::NameValue(..) => {
return Err(Error::new_spanned(
&attr,
"encountered unexpected argument to `tag` attribute; expected list of tags",
))
},
};
let attr_tags = parse_tags(tokens.into())?;
let () = tags.extend(attr_tags);
} else {
let () = passthrough_attrs.push(attr);
}
}
Ok((tags, passthrough_attrs))
}
fn is_test_tag_attr(attr: &Attribute) -> bool {
let path = match &attr.meta {
Meta::Path(path) => path,
Meta::List(list) => &list.path,
_ => return false,
};
let segments = ["test_tag", "tag"];
if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "tag" {
true
} else if path.segments.len() != segments.len() {
false
} else {
path
.segments
.iter()
.zip(segments)
.all(|(segment, path)| segment.ident == path)
}
}
fn rewrite_test_attrs(attrs: &mut [Attribute]) {
for attr in attrs.iter_mut() {
let span = attr.meta.span();
let path = match &mut attr.meta {
Meta::Path(path) => path,
Meta::List(list) => &mut list.path,
Meta::NameValue(MetaNameValue { path, .. }) => path,
};
if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "test"
{
let segment = PathSegment {
ident: Ident::new("self", span),
arguments: PathArguments::None,
};
let () = path.segments.insert(0, segment);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tag_attr_recognition() {
#[track_caller]
fn test(func: Tokens) {
let attrs = ItemFn::parse.parse2(func).unwrap().attrs;
assert!(is_test_tag_attr(&attrs[0]));
assert!(!is_test_tag_attr(&attrs[1]));
}
let func = quote! {
#[tag(xxx)]
#[test]
fn foobar() {}
};
let () = test(func);
let func = quote! {
#[test_tag::tag(xxx)]
#[test]
fn foobar() {}
};
let () = test(func);
let func = quote! {
#[::test_tag::tag(xxx)]
#[test]
fn foobar() {}
};
let () = test(func);
let func = quote! {
#[::test_tag::tag]
#[test]
fn foobar() {}
};
let () = test(func);
let func = quote! {
#[test_tag::tag]
#[test]
fn foobar() {}
};
let () = test(func);
}
}