Skip to main content

bitwarden_ffi_macro/
wasm_export.rs

1use proc_macro2::TokenStream;
2use quote::ToTokens;
3use syn::{Attribute, ImplItem, ItemImpl, parse2};
4
5/// Processes an impl block, transforming methods marked with `#[wasm_only]`.
6///
7/// For each marked method:
8/// - Strips the `#[wasm_only]` marker attribute
9/// - Renames the method with a `__wasm_only_` prefix (e.g. `subscribe` -> `__wasm_only_subscribe`)
10/// - Adds `#[wasm_bindgen(js_name = "original_name")]` if no `js_name` is already present
11/// - Adds `#[doc(hidden)]` to hide it from Rust documentation
12/// - Adds `#[deprecated]` so it shows with strikethrough in IDE autocomplete
13///
14/// This makes the methods effectively unreachable from Rust (hidden, mangled name) while
15/// preserving the original JS-facing API through `wasm_bindgen`'s `js_name` attribute.
16pub(crate) fn wasm_export(item: TokenStream) -> TokenStream {
17    let mut impl_block = match parse2::<ItemImpl>(item) {
18        Ok(block) => block,
19        Err(err) => return err.to_compile_error(),
20    };
21
22    for item in &mut impl_block.items {
23        let ImplItem::Fn(method) = item else {
24            continue;
25        };
26
27        let wasm_only_idx = method
28            .attrs
29            .iter()
30            .position(|attr| attr.path().is_ident("wasm_only"));
31
32        let Some(idx) = wasm_only_idx else {
33            continue;
34        };
35
36        // Extract optional note from #[wasm_only(note = "custom note")] before removing
37        let custom_note = match extract_wasm_only_note(&method.attrs[idx]) {
38            Ok(note) => note,
39            Err(err) => return err.to_compile_error(),
40        };
41
42        // Remove the #[wasm_only] marker attribute
43        method.attrs.remove(idx);
44
45        let original_name = method.sig.ident.to_string();
46
47        // Add #[wasm_bindgen(js_name = "...")] if not already present,
48        // so JS consumers still see the original name
49        if !has_wasm_bindgen_js_name(&method.attrs) {
50            method
51                .attrs
52                .push(syn::parse_quote!(#[wasm_bindgen(js_name = #original_name)]));
53        }
54
55        // Hide from Rust documentation
56        method.attrs.push(syn::parse_quote!(#[doc(hidden)]));
57
58        // Mark as deprecated so IDEs show strikethrough
59        let note = custom_note.unwrap_or_else(|| {
60            "This is a WASM-only binding. Calling it from Rust is not allowed.".to_string()
61        });
62        method
63            .attrs
64            .push(syn::parse_quote!(#[deprecated(note = #note)]));
65
66        // Suppress the deprecation warning on the definition itself
67        method.attrs.push(syn::parse_quote!(#[allow(deprecated)]));
68
69        // Rename the method with __wasm_only_ prefix to discourage direct Rust usage
70        method.sig.ident = syn::Ident::new(
71            &format!("__wasm_only_{original_name}"),
72            method.sig.ident.span(),
73        );
74    }
75
76    impl_block.into_token_stream()
77}
78
79/// Extracts the optional note from `#[wasm_only(note = "custom note")]`.
80/// Returns `None` for plain `#[wasm_only]`, or `Err` for unknown attributes.
81fn extract_wasm_only_note(attr: &Attribute) -> Result<Option<String>, syn::Error> {
82    // Plain #[wasm_only] with no arguments
83    if attr.meta.require_path_only().is_ok() {
84        return Ok(None);
85    }
86
87    let mut note = None;
88    attr.parse_nested_meta(|meta| {
89        if meta.path.is_ident("note") {
90            note = Some(meta.value()?.parse::<syn::LitStr>()?.value());
91            Ok(())
92        } else {
93            Err(meta.error("unknown attribute, expected `note`"))
94        }
95    })?;
96    Ok(note)
97}
98
99fn has_wasm_bindgen_js_name(attrs: &[Attribute]) -> bool {
100    attrs.iter().any(|attr| {
101        attr.path().is_ident("wasm_bindgen")
102            && attr.to_token_stream().to_string().contains("js_name")
103    })
104}