1#![warn(missing_docs)]
36
37extern crate proc_macro;
38
39use proc_macro::TokenStream;
40use proc_macro2::TokenStream as Tokens;
41
42use quote::quote;
43use quote::quote_spanned;
44
45use syn::parse::Parse;
46use syn::parse::Parser as _;
47use syn::punctuated::Punctuated;
48use syn::spanned::Spanned as _;
49use syn::Attribute;
50use syn::Error;
51use syn::Ident;
52use syn::ItemFn;
53use syn::Meta;
54use syn::MetaNameValue;
55use syn::PathArguments;
56use syn::PathSegment;
57use syn::Result;
58use syn::Token;
59
60
61type Tags = Punctuated<Ident, Token![,]>;
63
64
65#[proc_macro_attribute]
83pub fn tag(attrs: TokenStream, item: TokenStream) -> TokenStream {
84 try_tag(attrs, item)
85 .unwrap_or_else(Error::into_compile_error)
86 .into()
87}
88
89
90fn try_tag(attrs: TokenStream, item: TokenStream) -> Result<Tokens> {
107 let mut tags = parse_tags(attrs)?;
110 let input = ItemFn::parse.parse(item)?;
111 let ItemFn {
112 attrs,
113 vis,
114 mut sig,
115 block,
116 } = input;
117
118 let (more_tags, mut attrs) = parse_fn_attrs(attrs)?;
122 let () = tags.extend(more_tags);
123 let () = rewrite_test_attrs(&mut attrs);
124
125 let test_name = sig.ident.clone();
126 sig.ident = Ident::new("test", sig.ident.span());
130
131 let mut result = quote! {
132 #(#attrs)*
133 pub #sig {
134 #block
135 }
136 };
137
138 let mut import = None;
139 for tag in tags.into_iter().rev() {
140 import = if let Some(import) = &import {
141 Some(quote! { #tag::#import })
142 } else {
143 Some(quote! { #tag })
144 };
145
146 result = quote! {
147 pub mod #tag {
148 use super::*;
149 #result
150 }
151 };
152 }
153
154 result = quote! {
165 use ::core::prelude::v1::*;
166 #[allow(unused_imports)]
167 #vis use #test_name::#import::test as #test_name;
168 #[doc(hidden)]
169 pub mod #test_name {
170 use super::*;
171 #result
172 }
173 };
174 Ok(result)
175}
176
177
178fn parse_tags(attrs: TokenStream) -> Result<Tags> {
182 let tags = Tags::parse_terminated.parse(attrs)?;
183 if !tags.is_empty() {
184 Ok(tags)
185 } else {
186 Err(Error::new_spanned(
187 &tags,
188 "at least one tag is required: #[test_tag::tag(<tags...>)]",
189 ))
190 }
191}
192
193
194fn parse_fn_attrs(attrs: Vec<Attribute>) -> Result<(Tags, Vec<Attribute>)> {
199 let mut tags = Tags::new();
200 let mut passthrough_attrs = Vec::new();
201
202 for attr in attrs {
203 if is_test_tag_attr(&attr) {
204 let tokens = match attr.meta {
205 Meta::Path(..) => {
206 quote_spanned!(attr.meta.span() => {})
209 },
210 Meta::List(list) => list.tokens,
211 Meta::NameValue(..) => {
212 return Err(Error::new_spanned(
213 &attr,
214 "encountered unexpected argument to `tag` attribute; expected list of tags",
215 ))
216 },
217 };
218
219 let attr_tags = parse_tags(tokens.into())?;
220 let () = tags.extend(attr_tags);
221 } else {
222 let () = passthrough_attrs.push(attr);
223 }
224 }
225
226 Ok((tags, passthrough_attrs))
227}
228
229
230fn is_test_tag_attr(attr: &Attribute) -> bool {
232 let path = match &attr.meta {
233 Meta::Path(path) => path,
236 Meta::List(list) => &list.path,
237 _ => return false,
238 };
239
240 let segments = ["test_tag", "tag"];
241 if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "tag" {
242 true
243 } else if path.segments.len() != segments.len() {
244 false
245 } else {
246 path
247 .segments
248 .iter()
249 .zip(segments)
250 .all(|(segment, path)| segment.ident == path)
251 }
252}
253
254
255fn rewrite_test_attrs(attrs: &mut [Attribute]) {
262 for attr in attrs.iter_mut() {
263 let span = attr.meta.span();
264 let path = match &mut attr.meta {
265 Meta::Path(path) => path,
266 Meta::List(list) => &mut list.path,
267 Meta::NameValue(MetaNameValue { path, .. }) => path,
268 };
269
270 if path.leading_colon.is_none() && path.segments.len() == 1 && path.segments[0].ident == "test"
271 {
272 let segment = PathSegment {
273 ident: Ident::new("self", span),
274 arguments: PathArguments::None,
275 };
276 let () = path.segments.insert(0, segment);
277 }
278 }
279}
280
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286
287 #[test]
290 fn test_tag_attr_recognition() {
291 #[track_caller]
292 fn test(func: Tokens) {
293 let attrs = ItemFn::parse.parse2(func).unwrap().attrs;
294 assert!(is_test_tag_attr(&attrs[0]));
295 assert!(!is_test_tag_attr(&attrs[1]));
296 }
297
298
299 let func = quote! {
300 #[tag(xxx)]
301 #[test]
302 fn foobar() {}
303 };
304 let () = test(func);
305
306 let func = quote! {
307 #[test_tag::tag(xxx)]
308 #[test]
309 fn foobar() {}
310 };
311 let () = test(func);
312
313 let func = quote! {
314 #[::test_tag::tag(xxx)]
315 #[test]
316 fn foobar() {}
317 };
318 let () = test(func);
319
320 let func = quote! {
321 #[::test_tag::tag]
322 #[test]
323 fn foobar() {}
324 };
325 let () = test(func);
326
327 let func = quote! {
328 #[test_tag::tag]
329 #[test]
330 fn foobar() {}
331 };
332 let () = test(func);
333 }
334}