1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::{Attribute, ImplItem, ItemFn, ItemImpl, Signature, Visibility, parse2, spanned::Spanned};
4
5use crate::attrs;
6
7pub(crate) fn wasm_export(attr: TokenStream, item: TokenStream) -> TokenStream {
10 match parse2::<ItemImpl>(item.clone()) {
11 Ok(impl_block) => wasm_export_impl(attr, impl_block),
12 Err(impl_err) => match parse2::<ItemFn>(item) {
15 Ok(function) => wasm_export_fn(attr, function),
16 Err(_) => impl_err.to_compile_error(),
17 },
18 }
19}
20
21fn wasm_export_impl(attr: TokenStream, mut impl_block: ItemImpl) -> TokenStream {
24 let forwarded = match attrs::parse_args(attr) {
25 Ok(args) => args,
26 Err(err) => return err.to_compile_error(),
27 };
28
29 if let Err(err) = check_exportable(&impl_block.generics, &impl_block.attrs, "impl blocks") {
30 return err.to_compile_error();
31 }
32
33 for item in &mut impl_block.items {
34 let ImplItem::Fn(method) = item else { continue };
35
36 let renamed = match apply_wasm_only(&mut method.attrs, &mut method.sig) {
39 Ok(renamed) => renamed,
40 Err(err) => return err.to_compile_error(),
41 };
42
43 if !matches!(method.vis, Visibility::Public(_)) {
46 continue;
47 }
48
49 if !method.sig.generics.params.is_empty() {
50 return syn::Error::new(
51 method.sig.generics.span(),
52 "#[wasm_export] does not support generic methods; wasm_bindgen cannot export them",
53 )
54 .to_compile_error();
55 }
56
57 if let Some(name) = renamed {
58 let named = method
60 .attrs
61 .iter()
62 .filter_map(attrs::wasm_bindgen_args)
63 .flatten()
64 .any(|meta| meta.path().is_ident("js_name") || meta.path().is_ident("constructor"));
65 if !named {
66 method.attrs.push(syn::parse_quote!(
67 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = #name))]
68 ));
69 }
70 }
71 }
72
73 let bindgen = attrs::wasm_bindgen_attr(&forwarded);
74 quote! {
75 #bindgen
76 #impl_block
77 }
78}
79
80fn wasm_export_fn(attr: TokenStream, mut function: ItemFn) -> TokenStream {
82 let mut forwarded = match attrs::parse_args(attr) {
83 Ok(args) => args,
84 Err(err) => return err.to_compile_error(),
85 };
86
87 if let Err(err) = check_exportable(&function.sig.generics, &function.attrs, "functions") {
88 return err.to_compile_error();
89 }
90
91 let renamed = match apply_wasm_only(&mut function.attrs, &mut function.sig) {
92 Ok(renamed) => renamed,
93 Err(err) => return err.to_compile_error(),
94 };
95
96 if let Some(name) = renamed {
98 let named = forwarded.iter().any(|meta| meta.path().is_ident("js_name"));
99 if !named {
100 forwarded.push(syn::parse_quote!(js_name = #name));
101 }
102 }
103
104 let bindgen = attrs::wasm_bindgen_attr(&forwarded);
105 quote! {
106 #bindgen
107 #function
108 }
109}
110
111fn check_exportable(
115 generics: &syn::Generics,
116 item_attrs: &[Attribute],
117 kind: &str,
118) -> syn::Result<()> {
119 if !generics.params.is_empty() {
120 return Err(syn::Error::new(
121 generics.span(),
122 format!(
123 "#[wasm_export] does not support generic {kind}; wasm_bindgen cannot export them"
124 ),
125 ));
126 }
127
128 if let Some(attr) = attrs::find_wasm_bindgen(item_attrs) {
129 return Err(syn::Error::new_spanned(
130 attr,
131 "#[wasm_export] replaces #[wasm_bindgen]; pass its arguments to #[wasm_export] \
132 instead, as in #[wasm_export(js_class = Foo)]",
133 ));
134 }
135
136 Ok(())
137}
138
139fn apply_wasm_only(attrs: &mut Vec<Attribute>, sig: &mut Signature) -> syn::Result<Option<String>> {
145 let idx = attrs.iter().position(|a| a.path().is_ident("wasm_only"));
146 let Some(idx) = idx else { return Ok(None) };
147 let note = wasm_only_note(&attrs.remove(idx))?;
148
149 let original = sig.ident.to_string();
150 attrs.push(syn::parse_quote!(#[doc(hidden)]));
151 attrs.push(syn::parse_quote!(#[deprecated(note = #note)]));
152 attrs.push(syn::parse_quote!(#[allow(deprecated)]));
153 sig.ident = syn::Ident::new(&format!("__wasm_only_{original}"), sig.ident.span());
154
155 Ok(Some(original))
156}
157
158fn wasm_only_note(attr: &Attribute) -> syn::Result<String> {
160 const DEFAULT: &str = "This is a WASM-only binding. Calling it from Rust is not allowed.";
161 if attr.meta.require_path_only().is_ok() {
162 return Ok(DEFAULT.to_owned());
163 }
164
165 let mut note = None;
166 attr.parse_nested_meta(|meta| {
167 if meta.path.is_ident("note") {
168 note = Some(meta.value()?.parse::<syn::LitStr>()?.value());
169 Ok(())
170 } else {
171 Err(meta.error("unknown attribute, expected `note`"))
172 }
173 })?;
174 Ok(note.unwrap_or_else(|| DEFAULT.to_owned()))
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 fn expand(attr: TokenStream, item: TokenStream) -> String {
184 wasm_export(attr, item).to_string().replace(' ', "")
185 }
186
187 #[test]
188 fn applies_wasm_bindgen_to_the_block_under_the_wasm_feature() {
189 let out = expand(
190 TokenStream::new(),
191 quote! {
192 impl Canvas {
193 pub fn translate(&self, point: Point) -> Point { point }
194 }
195 },
196 );
197
198 assert!(!out.contains("compile_error!"), "{out}");
199 assert!(
200 out.contains("#[cfg_attr(feature=\"wasm\",::bitwarden_ffi::_macro::wasm_bindgen)]"),
201 "{out}"
202 );
203 assert!(
205 out.contains("pubfntranslate(&self,point:Point)->Point{point}"),
206 "{out}"
207 );
208 }
209
210 #[test]
211 fn forwards_its_arguments_to_wasm_bindgen() {
212 let out = expand(
213 quote!(js_class = IpcClient),
214 quote! {
215 impl JsIpcClient {
216 pub fn is_running(&self) -> bool { true }
217 }
218 },
219 );
220
221 assert!(
222 out.contains("::bitwarden_ffi::_macro::wasm_bindgen(js_class=IpcClient)"),
223 "{out}"
224 );
225 }
226
227 #[test]
228 fn renames_a_wasm_only_method_and_keeps_its_js_name() {
229 let out = expand(
230 TokenStream::new(),
231 quote! {
232 impl JsIpcClient {
233 #[wasm_only]
234 pub fn is_running(&self) -> bool { true }
235 }
236 },
237 );
238
239 assert!(out.contains("fn__wasm_only_is_running"), "{out}");
240 assert!(
241 out.contains("#[cfg_attr(feature=\"wasm\",wasm_bindgen(js_name=\"is_running\"))]"),
242 "{out}"
243 );
244 assert!(out.contains("#[doc(hidden)]"), "{out}");
245 assert!(
246 out.contains(
247 "#[deprecated(note=\"ThisisaWASM-onlybinding.CallingitfromRustisnotallowed.\")]"
248 ),
249 "{out}"
250 );
251 assert!(out.contains("#[allow(deprecated)]"), "{out}");
252 }
253
254 #[test]
255 fn carries_a_custom_note() {
256 let out = expand(
257 TokenStream::new(),
258 quote! {
259 impl JsIpcClient {
260 #[wasm_only(note = "Use `IpcClient::start`.")]
261 pub fn start(&self) {}
262 }
263 },
264 );
265
266 assert!(
267 out.contains("#[deprecated(note=\"Use`IpcClient::start`.\")]"),
268 "{out}"
269 );
270 }
271
272 #[test]
273 fn keeps_a_js_name_the_method_already_declares() {
274 let out = expand(
275 TokenStream::new(),
276 quote! {
277 impl JsIpcClient {
278 #[wasm_only]
279 #[wasm_bindgen(js_name = isRunning)]
280 pub fn is_running(&self) -> bool { true }
281 }
282 },
283 );
284
285 assert!(out.contains("#[wasm_bindgen(js_name=isRunning)]"), "{out}");
286 assert!(!out.contains("js_name=\"is_running\""), "{out}");
287 }
288
289 #[test]
290 fn keeps_a_js_name_the_method_declares_under_a_cfg_attr() {
291 let out = expand(
292 TokenStream::new(),
293 quote! {
294 impl PolicyClient {
295 #[wasm_only]
296 #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "get_enforced"))]
297 pub fn get_enforced(&self) {}
298 }
299 },
300 );
301
302 assert_eq!(out.matches("js_name=").count(), 1, "{out}");
303 assert!(out.contains("js_name=\"get_enforced\""), "{out}");
304 }
305
306 #[test]
307 fn leaves_a_wasm_only_constructor_for_wasm_bindgen_to_name() {
308 let out = expand(
309 TokenStream::new(),
310 quote! {
311 impl JsIpcClient {
312 #[wasm_only]
313 #[wasm_bindgen(constructor)]
314 pub fn new() -> JsIpcClient { todo!() }
315 }
316 },
317 );
318
319 assert!(!out.contains("js_name"), "{out}");
320 assert!(out.contains("fn__wasm_only_new"), "{out}");
321 }
322
323 #[test]
324 fn renames_a_private_wasm_only_method_without_naming_it_for_javascript() {
325 let out = expand(
326 TokenStream::new(),
327 quote! {
328 impl JsIpcClient {
329 #[wasm_only]
330 fn internal(&self) {}
331 }
332 },
333 );
334
335 assert!(out.contains("fn__wasm_only_internal"), "{out}");
336 assert!(!out.contains("js_name"), "{out}");
337 }
338
339 #[test]
340 fn leaves_a_private_generic_method_alone() {
341 let out = expand(
343 TokenStream::new(),
344 quote! {
345 impl Canvas {
346 fn draw<T: Shape>(&self, shape: T) {}
347 }
348 },
349 );
350
351 assert!(!out.contains("compile_error!"), "{out}");
352 }
353
354 #[test]
355 fn rejects_a_generic_impl_block() {
356 let out = expand(
357 TokenStream::new(),
358 quote! {
359 impl<T> Canvas<T> {
360 pub fn translate(&self, point: Point) -> Point { point }
361 }
362 },
363 );
364
365 assert!(out.contains("compile_error!"), "{out}");
366 assert!(out.contains("doesnotsupportgenericimplblocks"), "{out}");
367 }
368
369 #[test]
370 fn rejects_a_generic_method() {
371 let out = expand(
372 TokenStream::new(),
373 quote! {
374 impl Canvas {
375 pub fn draw<T: Shape>(&self, shape: T) {}
376 }
377 },
378 );
379
380 assert!(out.contains("compile_error!"), "{out}");
381 assert!(out.contains("doesnotsupportgenericmethods"), "{out}");
382 }
383
384 #[test]
385 fn rejects_a_block_that_still_carries_wasm_bindgen() {
386 let out = expand(
389 TokenStream::new(),
390 quote! {
391 #[wasm_bindgen(js_class = IpcClient)]
392 impl JsIpcClient {
393 pub fn is_running(&self) -> bool { true }
394 }
395 },
396 );
397
398 assert!(out.contains("compile_error!"), "{out}");
399 assert!(out.contains("replaces#[wasm_bindgen]"), "{out}");
400 }
401
402 #[test]
403 fn exports_a_free_function() {
404 let out = expand(
405 quote!(js_name = doThing),
406 quote! {
407 pub fn do_thing(point: Point) -> Point { point }
408 },
409 );
410
411 assert!(!out.contains("compile_error!"), "{out}");
412 assert!(
413 out.contains("::bitwarden_ffi::_macro::wasm_bindgen(js_name=doThing)"),
414 "{out}"
415 );
416 assert!(out.contains("pubfndo_thing"), "{out}");
417 }
418
419 #[test]
420 fn names_a_wasm_only_free_function_in_its_own_arguments() {
421 let out = expand(
422 TokenStream::new(),
423 quote! {
424 #[wasm_only]
425 pub fn do_thing() {}
426 },
427 );
428
429 assert!(
430 out.contains("::bitwarden_ffi::_macro::wasm_bindgen(js_name=\"do_thing\")"),
431 "{out}"
432 );
433 assert!(out.contains("pubfn__wasm_only_do_thing"), "{out}");
434 }
435
436 #[test]
437 fn rejects_a_generic_free_function() {
438 let out = expand(
439 TokenStream::new(),
440 quote! {
441 pub fn do_thing<T>(value: T) {}
442 },
443 );
444
445 assert!(out.contains("compile_error!"), "{out}");
446 assert!(out.contains("doesnotsupportgenericfunctions"), "{out}");
447 }
448
449 #[test]
450 fn rejects_an_item_that_is_neither() {
451 let out = expand(TokenStream::new(), quote! { pub struct Canvas; });
452
453 assert!(out.contains("compile_error!"), "{out}");
454 }
455}