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    } else {
140        tokens.extend(quote! {
141            #[cfg_attr(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))), allow(dead_code))]
142        });
143    }
144
145    tokens.extend(leading_tokens);
146    tokens.push(ident.into());
147    tokens.extend(body);
148
149    tokens.into_iter().collect::<TokenStream>().into()
150}
151
152fn parse_should_panic(
153    body: &mut std::iter::Peekable<token_stream::IntoIter>,
154    token: &TokenTree,
155) -> Result<Option<(Option<Literal>, Span)>, proc_macro::TokenStream> {
156    // Start by parsing the `#`
157    match token {
158        TokenTree::Punct(op) if op.as_char() == '#' => (),
159        _ => return Ok(None),
160    }
161
162    // Parse `[...]`
163    let group = match body.peek() {
164        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
165        _ => return Ok(None),
166    };
167
168    let mut stream = group.stream().into_iter();
169
170    // Parse `should_panic`
171    let mut span = match stream.next() {
172        Some(TokenTree::Ident(token)) if token == "should_panic" => token.span(),
173        _ => return Ok(None),
174    };
175
176    let should_panic = span;
177
178    // We are interested in the `expected` attribute or string if there is any
179    match stream.next() {
180        // Parse the `(...)` in `#[should_panic(...)]`
181        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
182            let span = group.span();
183            stream = group.stream().into_iter();
184
185            // Parse `expected`
186            match stream.next() {
187                Some(TokenTree::Ident(token)) if token == "expected" => (),
188                _ => {
189                    return Err(compile_error(
190                        span,
191                        "malformed `#[should_panic(...)]` attribute",
192                    ))
193                }
194            }
195
196            // Parse `=`
197            match stream.next() {
198                Some(TokenTree::Punct(op)) if op.as_char() == '=' => (),
199                _ => {
200                    return Err(compile_error(
201                        span,
202                        "malformed `#[should_panic(...)]` attribute",
203                    ))
204                }
205            }
206        }
207        // Parse `=`
208        Some(TokenTree::Punct(op)) if op.as_char() == '=' => (),
209        Some(token) => {
210            return Err(compile_error(
211                token.span(),
212                "malformed `#[should_panic = \"...\"]` attribute",
213            ))
214        }
215        None => {
216            return Ok(Some((None, should_panic)));
217        }
218    }
219
220    // Parse string in `#[should_panic(expected = "string")]` or `#[should_panic = "string"]`
221    if let Some(TokenTree::Literal(lit)) = stream.next() {
222        span = lit.span();
223        let string = lit.to_string();
224
225        // Verify it's a string.
226        if string.starts_with('"') && string.ends_with('"') {
227            return Ok(Some((Some(lit), should_panic)));
228        }
229    }
230
231    Err(compile_error(span, "malformed `#[should_panic]` attribute"))
232}
233
234fn parse_ignore(
235    body: &mut std::iter::Peekable<token_stream::IntoIter>,
236    token: &TokenTree,
237) -> Result<Option<(Option<Literal>, Span)>, proc_macro::TokenStream> {
238    // Start by parsing the `#`
239    match token {
240        TokenTree::Punct(op) if op.as_char() == '#' => (),
241        _ => return Ok(None),
242    }
243
244    // Parse `[...]`
245    let group = match body.peek() {
246        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
247        _ => return Ok(None),
248    };
249
250    let mut stream = group.stream().into_iter();
251
252    // Parse `ignore`
253    let mut span = match stream.next() {
254        Some(TokenTree::Ident(token)) if token == "ignore" => token.span(),
255        _ => return Ok(None),
256    };
257
258    let ignore = span;
259
260    // We are interested in the reason string if there is any
261    match stream.next() {
262        // Parse `=`
263        Some(TokenTree::Punct(op)) if op.as_char() == '=' => (),
264        Some(token) => {
265            return Err(compile_error(
266                token.span(),
267                "malformed `#[ignore = \"...\"]` attribute",
268            ))
269        }
270        None => {
271            return Ok(Some((None, ignore)));
272        }
273    }
274
275    // Parse string in `#[ignore = "string"]`
276    if let Some(TokenTree::Literal(lit)) = stream.next() {
277        span = lit.span();
278        let string = lit.to_string();
279
280        // Verify it's a string.
281        if string.starts_with('"') && string.ends_with('"') {
282            return Ok(Some((Some(lit), ignore)));
283        }
284    }
285
286    Err(compile_error(span, "malformed `#[ignore]` attribute"))
287}
288
289fn find_ident(iter: &mut impl Iterator<Item = TokenTree>) -> Option<Ident> {
290    match iter.next()? {
291        TokenTree::Ident(i) => Some(i),
292        TokenTree::Group(g) if g.delimiter() == Delimiter::None => {
293            find_ident(&mut g.stream().into_iter())
294        }
295        _ => None,
296    }
297}
298
299fn compile_error(span: Span, msg: &str) -> proc_macro::TokenStream {
300    quote_spanned! { span => compile_error!(#msg); }.into()
301}
302
303struct Attributes {
304    r#async: bool,
305    wasm_bindgen_path: syn::Path,
306    unsupported: Option<syn::Meta>,
307}
308
309impl Default for Attributes {
310    fn default() -> Self {
311        Self {
312            r#async: false,
313            wasm_bindgen_path: syn::parse_quote!(::wasm_bindgen_test),
314            unsupported: None,
315        }
316    }
317}
318
319impl Attributes {
320    fn parse(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::parse::Result<()> {
321        if meta.path.is_ident("async") {
322            self.r#async = true;
323        } else if meta.path.is_ident("crate") {
324            self.wasm_bindgen_path = meta.value()?.parse::<syn::Path>()?;
325        } else if meta.path.is_ident("unsupported") {
326            self.unsupported = Some(meta.value()?.parse::<syn::Meta>()?);
327        } else {
328            return Err(meta.error("unknown attribute"));
329        }
330        Ok(())
331    }
332}