1extern 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 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 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 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 match token {
158 TokenTree::Punct(op) if op.as_char() == '#' => (),
159 _ => return Ok(None),
160 }
161
162 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 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 match stream.next() {
180 Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
182 let span = group.span();
183 stream = group.stream().into_iter();
184
185 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 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 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 if let Some(TokenTree::Literal(lit)) = stream.next() {
222 span = lit.span();
223 let string = lit.to_string();
224
225 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 match token {
240 TokenTree::Punct(op) if op.as_char() == '#' => (),
241 _ => return Ok(None),
242 }
243
244 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 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 match stream.next() {
262 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 if let Some(TokenTree::Literal(lit)) = stream.next() {
277 span = lit.span();
278 let string = lit.to_string();
279
280 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}