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 }
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 match token {
154 TokenTree::Punct(op) if op.as_char() == '#' => (),
155 _ => return Ok(None),
156 }
157
158 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 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 match stream.next() {
176 Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
178 let span = group.span();
179 stream = group.stream().into_iter();
180
181 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 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 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 if let Some(TokenTree::Literal(lit)) = stream.next() {
218 span = lit.span();
219 let string = lit.to_string();
220
221 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 match token {
236 TokenTree::Punct(op) if op.as_char() == '#' => (),
237 _ => return Ok(None),
238 }
239
240 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 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 match stream.next() {
258 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 if let Some(TokenTree::Literal(lit)) = stream.next() {
273 span = lit.span();
274 let string = lit.to_string();
275
276 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}