use quote::quote;
use std::ops::Deref;
use proc_macro2::{Span, TokenStream};
use syn::{punctuated::Punctuated, token::Comma, Block, FnArg, Ident, ImplItemFn, ItemFn, Meta, Type};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelfReceiver {
Instance,
Static,
}
type ProcessFunctionParametersResult = syn::Result<(
SelfReceiver,
Punctuated<FnArg, Comma>,
Punctuated<FnArg, Comma>,
)>;
pub enum FunctionType<'a> {
Method(&'a ImplItemFn),
Standalone(&'a ItemFn),
}
pub struct WasmExportFunctionBuilderConfig {
pub forward_attrs: Vec<Meta>,
pub return_type: Type,
pub preserve_js_class: Option<Span>,
}
pub struct WasmExportFunctionBuilder;
impl WasmExportFunctionBuilder {
pub fn build_export_method(
method: &ImplItemFn,
config: WasmExportFunctionBuilderConfig,
) -> syn::Result<ImplItemFn> {
let WasmExportFunctionBuilderConfig {
forward_attrs,
return_type,
preserve_js_class,
} = config;
let mut export_method = method.clone();
export_method.sig.ident = Self::populate_name(&method.sig.ident);
let (_, processed_params, _) = Self::process_function_parameters(&method.sig.inputs)?;
export_method.sig.inputs = processed_params;
let doc_comments = Self::extract_doc_comments(&method.attrs);
export_method.attrs = Vec::new();
export_method.attrs.extend(doc_comments);
export_method
.attrs
.push(syn::parse_quote!(#[allow(non_snake_case)]));
if !forward_attrs.is_empty() {
export_method.attrs.push(syn::parse_quote!(
#[wasm_bindgen(#(#forward_attrs),*)]
));
}
if preserve_js_class.is_some() {
export_method.sig.output = syn::parse_quote!(-> JsValue);
} else {
export_method.sig.output = syn::parse_quote!(-> WasmEncodedResult<#return_type>);
}
export_method.block =
Self::build_fn_body_unified(FunctionType::Method(method), preserve_js_class.is_some());
Ok(export_method)
}
pub fn build_export_function(
func: &ItemFn,
config: WasmExportFunctionBuilderConfig,
) -> syn::Result<ItemFn> {
let WasmExportFunctionBuilderConfig {
forward_attrs,
return_type,
preserve_js_class,
} = config;
let mut export_fn = func.clone();
export_fn.sig.ident = Self::populate_name(&func.sig.ident);
let (_, processed_params, _) = Self::process_function_parameters(&func.sig.inputs)?;
export_fn.sig.inputs = processed_params;
let doc_comments = Self::extract_doc_comments(&func.attrs);
export_fn.attrs = Vec::new();
export_fn.attrs.extend(doc_comments);
export_fn
.attrs
.push(syn::parse_quote!(#[allow(non_snake_case)]));
if !forward_attrs.is_empty() {
export_fn.attrs.push(syn::parse_quote!(
#[wasm_bindgen(#(#forward_attrs),*)]
));
} else {
export_fn.attrs.push(syn::parse_quote!(#[wasm_bindgen]));
}
if preserve_js_class.is_some() {
export_fn.sig.output = syn::parse_quote!(-> JsValue);
} else {
export_fn.sig.output = syn::parse_quote!(-> WasmEncodedResult<#return_type>);
}
export_fn.block = Box::new(Self::build_fn_body_unified(
FunctionType::Standalone(func),
preserve_js_class.is_some(),
));
Ok(export_fn)
}
pub fn build_fn_body_unified(function_type: FunctionType, preserve_js_class: bool) -> Block {
let (call_expr, is_async) = match function_type {
FunctionType::Method(method) => {
let fn_name = &method.sig.ident;
let (self_receiver, args) = Self::collect_function_arguments(&method.sig.inputs);
let call_expr = match self_receiver {
SelfReceiver::Instance => {
quote! { self.#fn_name(#(#args),*) }
}
SelfReceiver::Static => {
quote! { Self::#fn_name(#(#args),*) }
}
};
(call_expr, method.sig.asyncness.is_some())
}
FunctionType::Standalone(function) => {
let fn_name = &function.sig.ident;
let (_, args) = Self::collect_function_arguments(&function.sig.inputs);
(
quote! { #fn_name(#(#args),*) },
function.sig.asyncness.is_some(),
)
}
};
let call_expr = if is_async {
quote!( #call_expr.await.into() )
} else {
quote!( #call_expr.into() )
};
if preserve_js_class {
syn::parse_quote!({
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = #call_expr;
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED).unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into()).unwrap();
}
};
obj.into()
})
} else {
syn::parse_quote!({
#call_expr
})
}
}
pub fn collect_function_arguments(
inputs: &Punctuated<FnArg, Comma>,
) -> (SelfReceiver, Vec<TokenStream>) {
let mut self_receiver = SelfReceiver::Static;
let args = inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Receiver(_) => {
self_receiver = SelfReceiver::Instance;
None
}
FnArg::Typed(pat_type) => {
let pat = pat_type.pat.deref();
Some(quote! { #pat })
}
})
.collect();
(self_receiver, args)
}
pub fn process_function_parameters(
inputs: &Punctuated<FnArg, Comma>,
) -> ProcessFunctionParametersResult {
let mut self_receiver = SelfReceiver::Static;
let mut processed_inputs = Punctuated::new();
let mut cleaned_inputs = Punctuated::new();
for input in inputs {
match input {
FnArg::Receiver(receiver) => {
self_receiver = SelfReceiver::Instance;
for attr in &receiver.attrs {
if attr.path().is_ident("wasm_export") {
return Err(syn::Error::new_spanned(
attr,
"wasm_export parameter attributes cannot be used on receiver parameters (self, &self, &mut self)"
));
}
}
processed_inputs.push(input.clone());
cleaned_inputs.push(input.clone());
}
FnArg::Typed(pat_type) => {
let mut new_pat_type = pat_type.clone();
let mut cleaned_pat_type = pat_type.clone();
let mut wasm_bindgen_attrs = Vec::new();
let mut other_attrs = Vec::new();
for attr in &pat_type.attrs {
if attr.path().is_ident("wasm_export") {
let processed_attrs = Self::process_parameter_wasm_export_attr(attr)?;
wasm_bindgen_attrs.extend(processed_attrs);
} else {
other_attrs.push(attr.clone());
}
}
new_pat_type.attrs.clone_from(&other_attrs);
if !wasm_bindgen_attrs.is_empty() {
new_pat_type
.attrs
.push(syn::parse_quote!(#[wasm_bindgen(#(#wasm_bindgen_attrs),*)]));
}
cleaned_pat_type.attrs = other_attrs;
processed_inputs.push(FnArg::Typed(new_pat_type));
cleaned_inputs.push(FnArg::Typed(cleaned_pat_type));
}
}
}
Ok((self_receiver, processed_inputs, cleaned_inputs))
}
fn process_parameter_wasm_export_attr(attr: &syn::Attribute) -> syn::Result<Vec<syn::Meta>> {
use syn::{punctuated::Punctuated, token::Comma, Meta};
use super::{error::extend_err_msg, attrs::AttrKeys};
let mut wasm_bindgen_metas = Vec::new();
let mut seen_param_description = false;
let mut seen_unchecked_param_type = false;
let mut seen_js_name = false;
if matches!(attr.meta, Meta::Path(_)) {
return Ok(wasm_bindgen_metas);
}
let nested_metas = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
for meta in nested_metas {
if let Some(ident) = meta.path().get_ident() {
match ident.to_string().as_str() {
"param_description" => {
if seen_param_description {
return Err(syn::Error::new_spanned(
meta,
"duplicate `param_description` attribute",
));
}
seen_param_description = true;
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(_),
..
}) = &meta
.require_name_value()
.map_err(extend_err_msg(" and it must be a string literal"))?
.value
{
wasm_bindgen_metas.push(meta);
} else {
return Err(syn::Error::new_spanned(meta, "expected string literal"));
}
}
AttrKeys::UNCHECKED_PARAM_TYPE => {
if seen_unchecked_param_type {
return Err(syn::Error::new_spanned(
meta,
"duplicate `unchecked_param_type` attribute",
));
}
seen_unchecked_param_type = true;
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(_),
..
}) = &meta
.require_name_value()
.map_err(extend_err_msg(" and it must be a string literal"))?
.value
{
wasm_bindgen_metas.push(meta);
} else {
return Err(syn::Error::new_spanned(meta, "expected string literal"));
}
}
AttrKeys::JS_NAME => {
if seen_js_name {
return Err(syn::Error::new_spanned(
meta,
"duplicate `js_name` attribute",
));
}
seen_js_name = true;
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(_),
..
}) = &meta
.require_name_value()
.map_err(extend_err_msg(" and it must be a string literal"))?
.value
{
wasm_bindgen_metas.push(meta);
} else {
return Err(syn::Error::new_spanned(meta, "expected string literal"));
}
}
_ => {
}
}
}
}
Ok(wasm_bindgen_metas)
}
pub fn clean_parameter_attributes(inputs: &mut Punctuated<FnArg, Comma>) {
for input in inputs.iter_mut() {
if let FnArg::Typed(pat_type) = input {
pat_type
.attrs
.retain(|attr| !attr.path().is_ident("wasm_export"));
}
}
}
pub fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
attrs
.iter()
.filter(|attr| {
attr.path().is_ident("doc")
})
.cloned()
.collect()
}
pub fn populate_name(org_fn_ident: &Ident) -> Ident {
Ident::new(
&format!("{}__wasm_export", org_fn_ident),
org_fn_ident.span(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use proc_macro2::{Span, TokenStream};
use syn::{parse::Parser, parse_quote};
use quote::ToTokens;
#[test]
fn test_from_method() {
let method: ImplItemFn = parse_quote!(
pub async fn some_fn(arg1: String) -> Result<SomeType, Error> {}
);
let wasm_export_fn_config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![parse_quote!(some_forward_attr)],
return_type: parse_quote!(SomeType),
preserve_js_class: None,
};
let result =
WasmExportFunctionBuilder::build_export_method(&method, wasm_export_fn_config).unwrap();
let expected = parse_quote!(
#[allow(non_snake_case)]
#[wasm_bindgen(some_forward_attr)]
pub async fn some_fn__wasm_export(arg1: String) -> WasmEncodedResult<SomeType> {
Self::some_fn(arg1).await.into()
}
);
assert_eq!(result, expected);
let method: ImplItemFn = parse_quote!(
pub async fn some_fn(arg1: String) -> Result<SomeType, Error> {}
);
let wasm_export_fn_config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![parse_quote!(some_forward_attr)],
return_type: parse_quote!(SomeType),
preserve_js_class: Some(Span::call_site()),
};
let result =
WasmExportFunctionBuilder::build_export_method(&method, wasm_export_fn_config).unwrap();
let expected = parse_quote!(
#[allow(non_snake_case)]
#[wasm_bindgen(some_forward_attr)]
pub async fn some_fn__wasm_export(arg1: String) -> JsValue {
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = Self::some_fn(arg1).await.into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED)
.unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED)
.unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into())
.unwrap();
}
};
obj.into()
}
);
assert_eq!(result, expected);
}
#[test]
fn test_from_standalone() {
let func: ItemFn = parse_quote!(
pub async fn some_fn(arg1: String) -> Result<SomeType, Error> {}
);
let wasm_export_fn_config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![parse_quote!(some_forward_attr)],
return_type: parse_quote!(SomeType),
preserve_js_class: None,
};
let result =
WasmExportFunctionBuilder::build_export_function(&func, wasm_export_fn_config).unwrap();
let expected = parse_quote!(
#[allow(non_snake_case)]
#[wasm_bindgen(some_forward_attr)]
pub async fn some_fn__wasm_export(arg1: String) -> WasmEncodedResult<SomeType> {
some_fn(arg1).await.into()
}
);
assert_eq!(result, expected);
let func: ItemFn = parse_quote!(
pub async fn some_fn(arg1: String) -> Result<SomeType, Error> {}
);
let wasm_export_fn_config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![parse_quote!(some_forward_attr)],
return_type: parse_quote!(SomeType),
preserve_js_class: Some(Span::call_site()),
};
let result =
WasmExportFunctionBuilder::build_export_function(&func, wasm_export_fn_config).unwrap();
let expected = parse_quote!(
#[allow(non_snake_case)]
#[wasm_bindgen(some_forward_attr)]
pub async fn some_fn__wasm_export(arg1: String) -> JsValue {
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = some_fn(arg1).await.into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED)
.unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED)
.unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into())
.unwrap();
}
};
obj.into()
}
);
assert_eq!(result, expected);
}
#[test]
fn test_build_fn_body_unified_method_async() {
let method: ImplItemFn = parse_quote!(
pub async fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), false);
let expected: Block = parse_quote!({ Self::some_name((arg1, arg2)).await.into() });
assert_eq!(result, expected);
let method: ImplItemFn = parse_quote!(
pub async fn some_name(&self, (arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), false);
let expected: Block = parse_quote!({ self.some_name((arg1, arg2)).await.into() });
assert_eq!(result, expected);
let method: ImplItemFn = parse_quote!(
pub async fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), true);
let expected: Block = parse_quote!({
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = Self::some_name((arg1, arg2)).await.into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED).unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into()).unwrap();
}
};
obj.into()
});
assert_eq!(result, expected);
let method: ImplItemFn = parse_quote!(
pub async fn some_name(&self, (arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), true);
let expected: Block = parse_quote!({
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = self.some_name((arg1, arg2)).await.into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED).unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into()).unwrap();
}
};
obj.into()
});
assert_eq!(result, expected);
}
#[test]
fn test_build_fn_body_unified_method_sync() {
let method: ImplItemFn = parse_quote!(
pub fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), false);
let expected: Block = parse_quote!({ Self::some_name((arg1, arg2)).into() });
assert_eq!(result, expected);
let method: ImplItemFn = parse_quote!(
pub fn some_name(&self, (arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), false);
let expected: Block = parse_quote!({ self.some_name((arg1, arg2)).into() });
assert_eq!(result, expected);
let method: ImplItemFn = parse_quote!(
pub fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), true);
let expected: Block = parse_quote!({
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = Self::some_name((arg1, arg2)).into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED).unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into()).unwrap();
}
};
obj.into()
});
assert_eq!(result, expected);
let method: ImplItemFn = parse_quote!(
pub fn some_name(&self, (arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result =
WasmExportFunctionBuilder::build_fn_body_unified(FunctionType::Method(&method), true);
let expected: Block = parse_quote!({
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = self.some_name((arg1, arg2)).into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED).unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into()).unwrap();
}
};
obj.into()
});
assert_eq!(result, expected);
}
#[test]
fn test_build_fn_body_unified_standalone_function_async() {
let function: ItemFn = parse_quote!(
pub async fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result = WasmExportFunctionBuilder::build_fn_body_unified(
FunctionType::Standalone(&function),
false,
);
let expected: Block = parse_quote!({ some_name((arg1, arg2)).await.into() });
assert_eq!(result, expected);
let function: ItemFn = parse_quote!(
pub async fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result = WasmExportFunctionBuilder::build_fn_body_unified(
FunctionType::Standalone(&function),
true,
);
let expected: Block = parse_quote!({
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = some_name((arg1, arg2)).await.into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED).unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into()).unwrap();
}
};
obj.into()
});
assert_eq!(result, expected);
}
#[test]
fn test_build_fn_body_unified_standalone_function_sync() {
let function: ItemFn = parse_quote!(
pub fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result = WasmExportFunctionBuilder::build_fn_body_unified(
FunctionType::Standalone(&function),
false,
);
let expected: Block = parse_quote!({ some_name((arg1, arg2)).into() });
assert_eq!(result, expected);
let function: ItemFn = parse_quote!(
pub fn some_name((arg1, arg2): (String, u8)) -> Result<SomeType, Error> {
Ok(SomeType::new())
}
);
let result = WasmExportFunctionBuilder::build_fn_body_unified(
FunctionType::Standalone(&function),
true,
);
let expected: Block = parse_quote!({
use js_sys::{Reflect, Object};
let obj = Object::new();
let result = some_name((arg1, arg2)).into();
match result {
Ok(value) => {
Reflect::set(&obj, &JsValue::from_str("value"), &value.into()).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &JsValue::UNDEFINED).unwrap();
}
Err(error) => {
let wasm_error: WasmEncodedError = error.into();
Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::UNDEFINED).unwrap();
Reflect::set(&obj, &JsValue::from_str("error"), &wasm_error.into()).unwrap();
}
};
obj.into()
});
assert_eq!(result, expected);
}
#[test]
fn test_collect_function_arguments() {
let stream = TokenStream::from_str(r#"arg1: String, arg2: u8"#).unwrap();
let inputs = Punctuated::<FnArg, Comma>::parse_terminated
.parse2(stream)
.unwrap();
let result = WasmExportFunctionBuilder::collect_function_arguments(&inputs);
let expected = (
SelfReceiver::Static,
vec![
TokenStream::from_str(r#"arg1"#).unwrap(),
TokenStream::from_str(r#"arg2"#).unwrap(),
],
);
assert_eq!(result.0, expected.0);
assert_eq!(result.1.len(), expected.1.len());
assert!(result
.1
.iter()
.zip(expected.1.iter())
.all(|(res, exp)| { res.to_string() == exp.to_string() }));
let stream = TokenStream::from_str(r#"&self, arg1: String"#).unwrap();
let inputs = Punctuated::<FnArg, Comma>::parse_terminated
.parse2(stream)
.unwrap();
let result = WasmExportFunctionBuilder::collect_function_arguments(&inputs);
let expected = (
SelfReceiver::Instance,
vec![TokenStream::from_str(r#"arg1"#).unwrap()],
);
assert_eq!(result.0, expected.0);
assert_eq!(result.1.len(), expected.1.len());
assert!(result
.1
.iter()
.zip(expected.1.iter())
.all(|(res, exp)| { res.to_string() == exp.to_string() }));
}
#[test]
fn test_populate_name() {
let org_fn_ident = Ident::new("some_name", Span::call_site());
let result = WasmExportFunctionBuilder::populate_name(&org_fn_ident);
assert_eq!(result.to_string(), "some_name__wasm_export");
}
#[test]
fn test_process_function_parameters_basic() {
let stream = TokenStream::from_str(r#"arg1: String, arg2: u32"#).unwrap();
let inputs = Punctuated::<FnArg, Comma>::parse_terminated
.parse2(stream)
.unwrap();
let result = WasmExportFunctionBuilder::process_function_parameters(&inputs).unwrap();
assert_eq!(result.0, SelfReceiver::Static); assert_eq!(result.1.len(), 2); assert_eq!(result.2.len(), 2);
assert_eq!(result.1.len(), result.2.len());
if let (FnArg::Typed(processed), FnArg::Typed(cleaned)) = (&result.1[0], &result.2[0]) {
assert_eq!(processed.attrs.len(), cleaned.attrs.len());
assert_eq!(
processed.pat.to_token_stream().to_string(),
cleaned.pat.to_token_stream().to_string()
);
}
}
#[test]
fn test_process_function_parameters_with_param_description() {
let stream = TokenStream::from_str(
r#"#[wasm_export(param_description = "first param")] arg1: String, arg2: u32"#,
)
.unwrap();
let inputs = Punctuated::<FnArg, Comma>::parse_terminated
.parse2(stream)
.unwrap();
let result = WasmExportFunctionBuilder::process_function_parameters(&inputs).unwrap();
assert_eq!(result.0, SelfReceiver::Static); assert_eq!(result.1.len(), 2); assert_eq!(result.2.len(), 2);
let processed_first = &result.1[0];
let cleaned_first = &result.2[0];
if let (FnArg::Typed(processed_pat), FnArg::Typed(cleaned_pat)) =
(processed_first, cleaned_first)
{
assert!(processed_pat
.attrs
.iter()
.any(|attr| attr.path().is_ident("wasm_bindgen")));
assert!(cleaned_pat.attrs.is_empty());
} else {
panic!("Expected FnArg::Typed");
}
}
#[test]
fn test_process_function_parameters_with_self() {
let stream = TokenStream::from_str(r#"&self, arg1: String"#).unwrap();
let inputs = Punctuated::<FnArg, Comma>::parse_terminated
.parse2(stream)
.unwrap();
let result = WasmExportFunctionBuilder::process_function_parameters(&inputs).unwrap();
assert_eq!(result.0, SelfReceiver::Instance); assert_eq!(result.1.len(), 2); assert_eq!(result.2.len(), 2); }
#[test]
fn test_process_function_parameters_self_with_wasm_export_error() {
let stream = TokenStream::from_str(
r#"#[wasm_export(param_description = "self desc")] &self, arg1: String"#,
)
.unwrap();
let inputs = Punctuated::<FnArg, Comma>::parse_terminated
.parse2(stream)
.unwrap();
let result = WasmExportFunctionBuilder::process_function_parameters(&inputs);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error
.to_string()
.contains("wasm_export parameter attributes cannot be used on receiver parameters"));
}
#[test]
fn test_process_parameter_wasm_export_attr_valid() {
let attr: syn::Attribute =
syn::parse_quote!(#[wasm_export(param_description = "test description")]);
let result = WasmExportFunctionBuilder::process_parameter_wasm_export_attr(&attr).unwrap();
assert_eq!(result.len(), 1);
let meta = &result[0];
assert!(meta.path().is_ident("param_description"));
}
#[test]
fn test_process_parameter_wasm_export_attr_duplicate_error() {
let attr: syn::Attribute = syn::parse_quote!(
#[wasm_export(param_description = "first", param_description = "second")]
);
let result = WasmExportFunctionBuilder::process_parameter_wasm_export_attr(&attr);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error
.to_string()
.contains("duplicate `param_description` attribute"));
}
#[test]
fn test_process_parameter_wasm_export_attr_invalid_value() {
let attr: syn::Attribute = syn::parse_quote!(#[wasm_export(param_description = something)]);
let result = WasmExportFunctionBuilder::process_parameter_wasm_export_attr(&attr);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("expected string literal"));
}
#[test]
fn test_process_parameter_wasm_export_attr_missing_value() {
let attr: syn::Attribute = syn::parse_quote!(#[wasm_export(param_description)]);
let result = WasmExportFunctionBuilder::process_parameter_wasm_export_attr(&attr);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error
.to_string()
.contains("expected a value for this attribute"));
}
#[test]
fn test_clean_parameter_attributes() {
let stream = TokenStream::from_str(
r#"#[wasm_export(param_description = "test")] #[other_attr] arg1: String, arg2: u32"#,
)
.unwrap();
let mut inputs = Punctuated::<FnArg, Comma>::parse_terminated
.parse2(stream)
.unwrap();
if let FnArg::Typed(pat_type) = &inputs[0] {
assert_eq!(pat_type.attrs.len(), 2);
}
WasmExportFunctionBuilder::clean_parameter_attributes(&mut inputs);
if let FnArg::Typed(pat_type) = &inputs[0] {
assert_eq!(pat_type.attrs.len(), 1);
assert!(pat_type.attrs[0].path().is_ident("other_attr"));
}
}
#[test]
fn test_extract_doc_comments() {
let func: ItemFn = parse_quote!(
#[other_attr]
pub fn test_func() -> Result<(), Error> {
Ok(())
}
);
let doc_comments = WasmExportFunctionBuilder::extract_doc_comments(&func.attrs);
assert_eq!(doc_comments.len(), 2);
for comment in &doc_comments {
assert!(comment.path().is_ident("doc"));
}
}
#[test]
fn test_extract_doc_comments_no_docs() {
let func: ItemFn = parse_quote!(
#[other_attr]
#[some_macro]
pub fn test_func() -> Result<(), Error> {
Ok(())
}
);
let doc_comments = WasmExportFunctionBuilder::extract_doc_comments(&func.attrs);
assert_eq!(doc_comments.len(), 0);
}
#[test]
fn test_extract_doc_comments_mixed() {
let func: ItemFn = parse_quote!(
#[cfg(test)]
#[derive(Debug)]
#[allow(dead_code)]
pub fn test_func() -> Result<(), Error> {
Ok(())
}
);
let doc_comments = WasmExportFunctionBuilder::extract_doc_comments(&func.attrs);
assert_eq!(doc_comments.len(), 3);
for comment in &doc_comments {
assert!(comment.path().is_ident("doc"));
}
}
#[test]
fn test_build_export_method_with_doc_comments() {
let method: ImplItemFn = parse_quote!(
pub fn some_method(arg: String) -> Result<String, Error> {
Ok(arg)
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![parse_quote!(js_name = "someMethod")],
return_type: parse_quote!(String),
preserve_js_class: None,
};
let result = WasmExportFunctionBuilder::build_export_method(&method, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 2);
assert!(result
.attrs
.iter()
.any(|attr| attr.path().is_ident("allow")));
assert!(result
.attrs
.iter()
.any(|attr| attr.path().is_ident("wasm_bindgen")));
}
#[test]
fn test_build_export_method_no_doc_comments() {
let method: ImplItemFn = parse_quote!(
#[some_attr]
pub fn some_method(arg: String) -> Result<String, Error> {
Ok(arg)
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![],
return_type: parse_quote!(String),
preserve_js_class: None,
};
let result = WasmExportFunctionBuilder::build_export_method(&method, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 0);
assert!(result
.attrs
.iter()
.any(|attr| attr.path().is_ident("allow")));
}
#[test]
fn test_build_export_function_with_doc_comments() {
let func: ItemFn = parse_quote!(
pub fn add(a: u32, b: u32) -> Result<u32, Error> {
Ok(a + b)
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![parse_quote!(js_name = "add")],
return_type: parse_quote!(u32),
preserve_js_class: None,
};
let result = WasmExportFunctionBuilder::build_export_function(&func, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 5);
assert!(result
.attrs
.iter()
.any(|attr| attr.path().is_ident("allow")));
assert!(result
.attrs
.iter()
.any(|attr| attr.path().is_ident("wasm_bindgen")));
}
#[test]
fn test_build_export_function_no_doc_comments() {
let func: ItemFn = parse_quote!(
pub fn simple_func() -> Result<(), Error> {
Ok(())
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![],
return_type: parse_quote!(()),
preserve_js_class: None,
};
let result = WasmExportFunctionBuilder::build_export_function(&func, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 0);
assert!(result
.attrs
.iter()
.any(|attr| attr.path().is_ident("allow")));
assert!(result
.attrs
.iter()
.any(|attr| attr.path().is_ident("wasm_bindgen")));
}
#[test]
fn test_doc_comments_with_preserve_js_class() {
let func: ItemFn = parse_quote!(
pub fn get_js_instance() -> Result<JsClass, Error> {
Ok(JsClass::new())
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![],
return_type: parse_quote!(JsClass),
preserve_js_class: Some(Span::call_site()),
};
let result = WasmExportFunctionBuilder::build_export_function(&func, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 2);
match &result.sig.output {
syn::ReturnType::Type(_, ty) => {
assert_eq!(quote!(#ty).to_string(), "JsValue");
}
_ => panic!("Expected return type"),
}
}
#[test]
fn test_doc_comments_with_mixed_attributes() {
let func: ItemFn = parse_quote!(
pub fn complex_func(input: String) -> Result<u32, Error> {
Ok(42)
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![
parse_quote!(js_name = "complexFunction"),
parse_quote!(catch),
parse_quote!(return_description = "a magic number"),
],
return_type: parse_quote!(u32),
preserve_js_class: None,
};
let result = WasmExportFunctionBuilder::build_export_function(&func, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 2);
let wasm_bindgen_attrs: Vec<_> = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("wasm_bindgen"))
.collect();
assert_eq!(wasm_bindgen_attrs.len(), 1);
let attr_tokens = quote!(#(#wasm_bindgen_attrs)*).to_string();
assert!(attr_tokens.contains("js_name = \"complexFunction\""));
assert!(attr_tokens.contains("catch"));
assert!(attr_tokens.contains("return_description = \"a magic number\""));
}
#[test]
fn test_doc_comments_special_characters() {
let func: ItemFn = parse_quote!(
pub fn special_func(input: &str) -> Result<String, Error> {
Ok(format!("processed: {}", input))
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![],
return_type: parse_quote!(String),
preserve_js_class: None,
};
let result = WasmExportFunctionBuilder::build_export_function(&func, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 12);
let doc_attrs: Vec<_> = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
let all_docs = quote!(#(#doc_attrs)*).to_string();
assert!(all_docs.contains("🦀 Rust"));
assert!(all_docs.contains("```rust"));
assert!(all_docs.contains("@#$%^&*()"));
}
#[test]
fn test_build_export_method_doc_comments_comprehensive() {
let method: ImplItemFn = parse_quote!(
pub fn advanced_method(&self, input: u64) -> Result<ProcessedValue, Error> {
Ok(ProcessedValue::new(input))
}
);
let config = WasmExportFunctionBuilderConfig {
forward_attrs: vec![parse_quote!(js_name = "advancedMethod")],
return_type: parse_quote!(ProcessedValue),
preserve_js_class: None,
};
let result = WasmExportFunctionBuilder::build_export_method(&method, config).unwrap();
let doc_count = result
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.count();
assert_eq!(doc_count, 11);
assert_eq!(result.sig.ident.to_string(), "advanced_method__wasm_export");
let wasm_bindgen_attr = result
.attrs
.iter()
.find(|attr| attr.path().is_ident("wasm_bindgen"))
.expect("wasm_bindgen attribute should be present");
let attr_tokens = quote!(#wasm_bindgen_attr).to_string();
assert!(attr_tokens.contains("js_name = \"advancedMethod\""));
}
}