wasm_bindgen_test_macro/
lib.rs

1//! See the README for `wasm-bindgen-test` for a bit more info about what's
2//! going on here.
3
4extern crate proc_macro;
5
6use proc_macro2::*;
7use quote::quote;
8use quote::quote_spanned;
9
10#[proc_macro_attribute]
11pub fn wasm_bindgen_test(
12    attr: proc_macro::TokenStream,
13    body: proc_macro::TokenStream,
14) -> proc_macro::TokenStream {
15    let mut attributes = Attributes::default();
16    let attribute_parser = syn::meta::parser(|meta| attributes.parse(meta));
17
18    syn::parse_macro_input!(attr with attribute_parser);
19    let mut should_panic = None;
20    let mut ignore = None;
21
22    let mut body = TokenStream::from(body).into_iter().peekable();
23
24    // Skip over other attributes to `fn #ident ...`, and extract `#ident`
25    let mut leading_tokens = Vec::new();
26    while let Some(token) = body.next() {
27        match parse_should_panic(&mut body, &token) {
28            Ok(Some((new_should_panic, span))) => {
29                if should_panic.replace(new_should_panic).is_some() {
30                    return compile_error(span, "duplicate `should_panic` attribute");
31                }
32
33                // If we found a `should_panic`, we should skip the `#` and `[...]`.
34                // The `[...]` is skipped here, the current `#` is skipped by using `continue`.
35                body.next();
36                continue;
37            }
38            Ok(None) => (),
39            Err(error) => return error,
40        }
41
42        match parse_ignore(&mut body, &token) {
43            Ok(Some((new_ignore, span))) => {
44                if ignore.replace(new_ignore).is_some() {
45                    return compile_error(span, "duplicate `ignore` attribute");
46                }
47
48                // If we found a `new_ignore`, we should skip the `#` and `[...]`.
49                // The `[...]` is skipped here, the current `#` is skipped by using `continue`.
50                body.next();
51                continue;
52            }
53            Ok(None) => (),
54            Err(error) => return error,
55        }
56
57        leading_tokens.push(token.clone());
58        if let TokenTree::Ident(token) = token {
59            if token == "async" {
60                attributes.r#async = true;
61            }
62            if token == "fn" {
63                break;
64            }
65        }
66    }
67    let ident = find_ident(&mut body).expect("expected a function name");
68
69    let mut tokens = Vec::<TokenTree>::new();
70
71    let should_panic_par = match &should_panic {
72        Some(Some(lit)) => {
73            quote! { ::core::option::Option::Some(::core::option::Option::Some(#lit)) }
74        }
75        Some(None) => quote! { ::core::option::Option::Some(::core::option::Option::None) },
76        None => quote! { ::core::option::Option::None },
77    };
78
79    let ignore_par = match &ignore {
80        Some(Some(lit)) => {
81            quote! { ::core::option::Option::Some(::core::option::Option::Some(#lit)) }
82        }
83        Some(None) => quote! { ::core::option::Option::Some(::core::option::Option::None) },
84        None => quote! { ::core::option::Option::None },
85    };
86
87    let test_body = if attributes.r#async {
88        quote! { cx.execute_async(test_name, #ident, #should_panic_par, #ignore_par); }
89    } else {
90        quote! { cx.execute_sync(test_name, #ident, #should_panic_par, #ignore_par); }
91    };
92
93    let ignore_name = if ignore.is_some() { "$" } else { "" };
94
95    let wasm_bindgen_path = attributes.wasm_bindgen_path;
96    tokens.extend(
97        quote! {
98            const _: () = {
99                #wasm_bindgen_path::__rt::wasm_bindgen::__wbindgen_coverage! {
100                #[export_name = ::core::concat!("__wbgt_", #ignore_name, "_", ::core::module_path!(), "::", ::core::stringify!(#ident))]
101                #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
102                extern "C" fn __wbgt_test(cx: &#wasm_bindgen_path::__rt::Context) {
103                    let test_name = ::core::concat!(::core::module_path!(), "::", ::core::stringify!(#ident));
104                    #test_body
105                }
106                }
107            };
108        },
109    );
110
111    if let Some(path) = attributes.unsupported {
112        tokens.extend(
113            quote! { #[cfg_attr(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))), #path)] },
114        );
115
116        if let Some(should_panic) = should_panic {
117            let should_panic = if let Some(lit) = should_panic {
118                quote! { should_panic = #lit }
119            } else {
120                quote! { should_panic }
121            };
122
123            tokens.extend(
124                quote! { #[cfg_attr(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))), #should_panic)] }
125            )
126        }
127
128        if let Some(ignore) = ignore {
129            let ignore = if let Some(lit) = ignore {
130                quote! { ignore = #lit }
131            } else {
132                quote! { ignore }
133            };
134
135            tokens.extend(
136                quote! { #[cfg_attr(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))), #ignore)] }
137            )
138        }
139    }
140
141    tokens.extend(leading_tokens);
142    tokens.push(ident.into());
143    tokens.extend(body);
144
145    tokens.into_iter().collect::<TokenStream>().into()
146}
147
148fn parse_should_panic(
149    body: &mut std::iter::Peekable<token_stream::IntoIter>,
150    token: &TokenTree,
151) -> Result<Option<(Option<Literal>, Span)>, proc_macro::TokenStream> {
152    // Start by parsing the `#`
153    match token {
154        TokenTree::Punct(op) if op.as_char() == '#' => (),
155        _ => return Ok(None),
156    }
157
158    // Parse `[...]`
159    let group = match body.peek() {
160        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
161        _ => return Ok(None),
162    };
163
164    let mut stream = group.stream().into_iter();
165
166    // Parse `should_panic`
167    let mut span = match stream.next() {
168        Some(TokenTree::Ident(token)) if token == "should_panic" => token.span(),
169        _ => return Ok(None),
170    };
171
172    let should_panic = span;
173
174    // We are interested in the `expected` attribute or string if there is any
175    match stream.next() {
176        // Parse the `(...)` in `#[should_panic(...)]`
177        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
178            let span = group.span();
179            stream = group.stream().into_iter();
180
181            // Parse `expected`
182            match stream.next() {
183                Some(TokenTree::Ident(token)) if token == "expected" => (),
184                _ => {
185                    return Err(compile_error(
186                        span,
187                        "malformed `#[should_panic(...)]` attribute",
188                    ))
189                }
190            }
191
192            // Parse `=`
193            match stream.next() {
194                Some(TokenTree::Punct(op)) if op.as_char() == '=' => (),
195                _ => {
196                    return Err(compile_error(
197                        span,
198                        "malformed `#[should_panic(...)]` attribute",
199                    ))
200                }
201            }
202        }
203        // Parse `=`
204        Some(TokenTree::Punct(op)) if op.as_char() == '=' => (),
205        Some(token) => {
206            return Err(compile_error(
207                token.span(),
208                "malformed `#[should_panic = \"...\"]` attribute",
209            ))
210        }
211        None => {
212            return Ok(Some((None, should_panic)));
213        }
214    }
215
216    // Parse string in `#[should_panic(expected = "string")]` or `#[should_panic = "string"]`
217    if let Some(TokenTree::Literal(lit)) = stream.next() {
218        span = lit.span();
219        let string = lit.to_string();
220
221        // Verify it's a string.
222        if string.starts_with('"') && string.ends_with('"') {
223            return Ok(Some((Some(lit), should_panic)));
224        }
225    }
226
227    Err(compile_error(span, "malformed `#[should_panic]` attribute"))
228}
229
230fn parse_ignore(
231    body: &mut std::iter::Peekable<token_stream::IntoIter>,
232    token: &TokenTree,
233) -> Result<Option<(Option<Literal>, Span)>, proc_macro::TokenStream> {
234    // Start by parsing the `#`
235    match token {
236        TokenTree::Punct(op) if op.as_char() == '#' => (),
237        _ => return Ok(None),
238    }
239
240    // Parse `[...]`
241    let group = match body.peek() {
242        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
243        _ => return Ok(None),
244    };
245
246    let mut stream = group.stream().into_iter();
247
248    // Parse `ignore`
249    let mut span = match stream.next() {
250        Some(TokenTree::Ident(token)) if token == "ignore" => token.span(),
251        _ => return Ok(None),
252    };
253
254    let ignore = span;
255
256    // We are interested in the reason string if there is any
257    match stream.next() {
258        // Parse `=`
259        Some(TokenTree::Punct(op)) if op.as_char() == '=' => (),
260        Some(token) => {
261            return Err(compile_error(
262                token.span(),
263                "malformed `#[ignore = \"...\"]` attribute",
264            ))
265        }
266        None => {
267            return Ok(Some((None, ignore)));
268        }
269    }
270
271    // Parse string in `#[ignore = "string"]`
272    if let Some(TokenTree::Literal(lit)) = stream.next() {
273        span = lit.span();
274        let string = lit.to_string();
275
276        // Verify it's a string.
277        if string.starts_with('"') && string.ends_with('"') {
278            return Ok(Some((Some(lit), ignore)));
279        }
280    }
281
282    Err(compile_error(span, "malformed `#[ignore]` attribute"))
283}
284
285fn find_ident(iter: &mut impl Iterator<Item = TokenTree>) -> Option<Ident> {
286    match iter.next()? {
287        TokenTree::Ident(i) => Some(i),
288        TokenTree::Group(g) if g.delimiter() == Delimiter::None => {
289            find_ident(&mut g.stream().into_iter())
290        }
291        _ => None,
292    }
293}
294
295fn compile_error(span: Span, msg: &str) -> proc_macro::TokenStream {
296    quote_spanned! { span => compile_error!(#msg); }.into()
297}
298
299struct Attributes {
300    r#async: bool,
301    wasm_bindgen_path: syn::Path,
302    unsupported: Option<syn::Meta>,
303}
304
305impl Default for Attributes {
306    fn default() -> Self {
307        Self {
308            r#async: false,
309            wasm_bindgen_path: syn::parse_quote!(::wasm_bindgen_test),
310            unsupported: None,
311        }
312    }
313}
314
315impl Attributes {
316    fn parse(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::parse::Result<()> {
317        if meta.path.is_ident("async") {
318            self.r#async = true;
319        } else if meta.path.is_ident("crate") {
320            self.wasm_bindgen_path = meta.value()?.parse::<syn::Path>()?;
321        } else if meta.path.is_ident("unsupported") {
322            self.unsupported = Some(meta.value()?.parse::<syn::Meta>()?);
323        } else {
324            return Err(meta.error("unknown attribute"));
325        }
326        Ok(())
327    }
328}