diff --git a/CHANGELOG.md b/CHANGELOG.md index 16bd83de8e0..c2a06670873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Added +* Add attributes to overwrite return (``unchecked_return_type`) and parameter types (`unchecked_param_type`), descriptions (`return_description` and `param_description`) as well as parameter names (`js_name`) for exported functions and methods. See the guide for more details. + [#4394](https://github.com/rustwasm/wasm-bindgen/pull/4394) + * Add a `copy_to_uninit()` method to all `TypedArray`s. It takes `&mut [MaybeUninit]` and returns `&mut [T]`. [#4340](https://github.com/rustwasm/wasm-bindgen/pull/4340) diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 0507f055306..0fed5f18c54 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -375,9 +375,9 @@ pub struct Function { /// Whether the function has a js_name attribute pub renamed_via_js_name: bool, /// The arguments to the function - pub arguments: Vec, - /// The return type of the function, if provided - pub ret: Option, + pub arguments: Vec, + /// The data of return type of the function + pub ret: Option, /// Any custom attributes being applied to the function pub rust_attrs: Vec, /// The visibility of this function in Rust @@ -394,6 +394,32 @@ pub struct Function { pub variadic: bool, } +/// Information about a function's return +#[cfg_attr(feature = "extra-traits", derive(Debug))] +#[derive(Clone)] +pub struct FunctionReturnData { + /// Specifies the type of the function's return + pub r#type: syn::Type, + /// Specifies the JS return type override + pub js_type: Option, + /// Specifies the return description + pub desc: Option, +} + +/// Information about a function's argument +#[cfg_attr(feature = "extra-traits", derive(Debug))] +#[derive(Clone)] +pub struct FunctionArgumentData { + /// Specifies the type of the function's argument + pub pat_type: syn::PatType, + /// Specifies the JS argument name override + pub js_name: Option, + /// Specifies the JS function argument type override + pub js_type: Option, + /// Specifies the argument description + pub desc: Option, +} + /// Information about a Struct being exported #[cfg_attr(feature = "extra-traits", derive(Debug))] #[derive(Clone)] diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index c8decb4a136..01450ccb9df 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -637,7 +637,7 @@ impl TryToTokens for ast::Export { let mut argtys = Vec::new(); for (i, arg) in self.function.arguments.iter().enumerate() { - argtys.push(&*arg.ty); + argtys.push(&*arg.pat_type.ty); let i = i + offset; let ident = Ident::new(&format!("arg{}", i), Span::call_site()); fn unwrap_nested_types(ty: &syn::Type) -> &syn::Type { @@ -647,7 +647,7 @@ impl TryToTokens for ast::Export { _ => ty, } } - let ty = unwrap_nested_types(&arg.ty); + let ty = unwrap_nested_types(&arg.pat_type.ty); match &ty { syn::Type::Reference(syn::TypeReference { @@ -720,7 +720,12 @@ impl TryToTokens for ast::Export { elems: Default::default(), paren_token: Default::default(), }); - let syn_ret = self.function.ret.as_ref().unwrap_or(&syn_unit); + let syn_ret = self + .function + .ret + .as_ref() + .map(|ret| &ret.r#type) + .unwrap_or(&syn_unit); if let syn::Type::Reference(_) = syn_ret { bail_span!(syn_ret, "cannot return a borrowed ref with #[wasm_bindgen]",) } @@ -1323,7 +1328,7 @@ impl TryToTokens for ast::ImportFunction { ast::ImportFunctionKind::Normal => {} } let vis = &self.function.rust_vis; - let ret = match &self.function.ret { + let ret = match self.function.ret.as_ref().map(|ret| &ret.r#type) { Some(ty) => quote! { -> #ty }, None => quote!(), }; @@ -1337,8 +1342,8 @@ impl TryToTokens for ast::ImportFunction { let wasm_bindgen_futures = &self.wasm_bindgen_futures; for (i, arg) in self.function.arguments.iter().enumerate() { - let ty = &arg.ty; - let name = match &*arg.pat { + let ty = &arg.pat_type.ty; + let name = match &*arg.pat_type.pat { syn::Pat::Ident(syn::PatIdent { by_ref: None, ident, @@ -1347,7 +1352,7 @@ impl TryToTokens for ast::ImportFunction { }) => ident.clone(), syn::Pat::Wild(_) => syn::Ident::new(&format!("__genarg_{}", i), Span::call_site()), _ => bail_span!( - arg.pat, + arg.pat_type.pat, "unsupported pattern in #[wasm_bindgen] imported function", ), }; @@ -1542,7 +1547,7 @@ impl ToTokens for DescribeImport<'_> { ast::ImportKind::Type(_) => return, ast::ImportKind::Enum(_) => return, }; - let argtys = f.function.arguments.iter().map(|arg| &arg.ty); + let argtys = f.function.arguments.iter().map(|arg| &arg.pat_type.ty); let nargs = f.function.arguments.len() as u32; let inform_ret = match &f.js_ret { Some(ref t) => quote! { <#t as WasmDescribe>::describe(); }, diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index d98fe628450..1285f19fc93 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -215,24 +215,34 @@ fn shared_export<'a>( } fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Function<'a> { - let arg_names = func - .arguments - .iter() - .enumerate() - .map(|(idx, arg)| { - if let syn::Pat::Ident(x) = &*arg.pat { - return x.ident.unraw().to_string(); - } - format!("arg{}", idx) - }) - .collect::>(); + let args = + func.arguments + .iter() + .enumerate() + .map(|(idx, arg)| FunctionArgumentData { + // use argument's "js_name" if it was provided via attributes + // if not use the original Rust argument ident + name: arg.js_name.clone().unwrap_or( + if let syn::Pat::Ident(x) = &*arg.pat_type.pat { + x.ident.unraw().to_string() + } else { + format!("arg{}", idx) + }, + ), + ty_override: arg.js_type.as_deref(), + desc: arg.desc.as_deref(), + }) + .collect::>(); + Function { - arg_names, + args, asyncness: func.r#async, name: &func.name, generate_typescript: func.generate_typescript, generate_jsdoc: func.generate_jsdoc, variadic: func.variadic, + ret_ty_override: func.ret.as_ref().and_then(|v| v.js_type.as_deref()), + ret_desc: func.ret.as_ref().and_then(|v| v.desc.as_deref()), } } diff --git a/crates/cli-support/Cargo.toml b/crates/cli-support/Cargo.toml index 6a4a5d94eee..60e3e6fe6f0 100644 --- a/crates/cli-support/Cargo.toml +++ b/crates/cli-support/Cargo.toml @@ -21,7 +21,6 @@ rustc-demangle = "0.1.13" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tempfile = "3.0" -unicode-ident = "1.0.5" walrus = { version = "0.23", features = ['parallel'] } wasm-bindgen-externref-xform = { path = '../externref-xform', version = '=0.2.99' } wasm-bindgen-multi-value-xform = { path = '../multi-value-xform', version = '=0.2.99' } diff --git a/crates/cli-support/src/descriptor.rs b/crates/cli-support/src/descriptor.rs index d9b7fd63eab..3ddf75405b3 100644 --- a/crates/cli-support/src/descriptor.rs +++ b/crates/cli-support/src/descriptor.rs @@ -1,6 +1,6 @@ use std::char; -use crate::js::identifier::is_valid_ident; +use wasm_bindgen_shared::identifier::is_valid_ident; macro_rules! tys { ($($a:ident)*) => (tys! { @ ($($a)*) 0 }); diff --git a/crates/cli-support/src/js/binding.rs b/crates/cli-support/src/js/binding.rs index 637c3a5edf7..09501a67b40 100644 --- a/crates/cli-support/src/js/binding.rs +++ b/crates/cli-support/src/js/binding.rs @@ -6,7 +6,9 @@ use crate::js::Context; use crate::wit::InstructionData; -use crate::wit::{Adapter, AdapterId, AdapterKind, AdapterType, Instruction}; +use crate::wit::{ + Adapter, AdapterId, AdapterKind, AdapterType, AuxFunctionArgumentData, Instruction, +}; use anyhow::{anyhow, bail, Error}; use std::collections::HashSet; use std::fmt::Write; @@ -70,6 +72,7 @@ pub struct JsFunction { pub code: String, pub ts_sig: String, pub js_doc: String, + pub ts_doc: String, pub ts_arg_tys: Vec, pub ts_ret_ty: Option, pub ts_refs: HashSet, @@ -120,11 +123,13 @@ impl<'a, 'b> Builder<'a, 'b> { &mut self, adapter: &Adapter, instructions: &[InstructionData], - explicit_arg_names: &Option>, + args_data: &Option>, asyncness: bool, variadic: bool, generate_jsdoc: bool, debug_name: &str, + ret_ty_override: &Option, + ret_desc: &Option, ) -> Result { if self .cx @@ -158,11 +163,15 @@ impl<'a, 'b> Builder<'a, 'b> { } } for (i, param) in params.enumerate() { - let arg = match explicit_arg_names { + let arg = match args_data { Some(list) => list[i].clone(), - None => format!("arg{}", i), + None => AuxFunctionArgumentData { + name: format!("arg{}", i), + ty_override: None, + desc: None, + }, }; - js.args.push(arg.clone()); + js.args.push(arg.name.clone()); function_args.push(arg); arg_tys.push(param); } @@ -223,16 +232,16 @@ impl<'a, 'b> Builder<'a, 'b> { let mut code = String::new(); code.push('('); - if variadic { - if let Some((last, non_variadic_args)) = function_args.split_last() { - code.push_str(&non_variadic_args.join(", ")); - if !non_variadic_args.is_empty() { - code.push_str(", "); - } - code.push_str((String::from("...") + last).as_str()) + for (i, v) in function_args.iter().enumerate() { + if i != 0 { + code.push_str(", "); } - } else { - code.push_str(&function_args.join(", ")); + + if variadic && i == function_args.len() - 1 { + code.push_str("..."); + } + + code.push_str(&v.name); } code.push_str(") {\n"); @@ -272,17 +281,34 @@ impl<'a, 'b> Builder<'a, 'b> { &mut might_be_optional_field, asyncness, variadic, + ret_ty_override, ); let js_doc = if generate_jsdoc { - self.js_doc_comments(&function_args, &arg_tys, &ts_ret_ty, variadic) + self.js_doc_comments( + &function_args, + &arg_tys, + &ts_ret_ty, + variadic, + ret_ty_override, + ret_desc, + ) } else { String::new() }; + // generate ts_doc + // ts doc is slightly different than js doc, where there is no + // arguments types followed after @param tag, as well as no special + // casings for arguments names such as "@param {string} [arg]" that + // tags the argument as optional, for ts doc we only need arg names + // and rest are just derived from function ts signature + let ts_doc = self.ts_doc_comments(&function_args, ret_desc); + Ok(JsFunction { code, ts_sig, js_doc, + ts_doc, ts_arg_tys, ts_ret_ty, ts_refs, @@ -299,19 +325,26 @@ impl<'a, 'b> Builder<'a, 'b> { /// return value, it doesn't include the function name in any way. fn typescript_signature( &self, - arg_names: &[String], + args_data: &[AuxFunctionArgumentData], arg_tys: &[&AdapterType], result_tys: &[AdapterType], might_be_optional_field: &mut bool, asyncness: bool, variadic: bool, + ret_ty_override: &Option, ) -> (String, Vec, Option, HashSet) { // Build up the typescript signature as well let mut omittable = true; let mut ts_args = Vec::new(); let mut ts_arg_tys = Vec::new(); let mut ts_refs = HashSet::new(); - for (name, ty) in arg_names.iter().zip(arg_tys).rev() { + for ( + AuxFunctionArgumentData { + name, ty_override, .. + }, + ty, + ) in args_data.iter().zip(arg_tys).rev() + { // In TypeScript, we can mark optional parameters as omittable // using the `?` suffix, but only if they're not followed by // non-omittable parameters. Therefore iterate the parameter list @@ -319,17 +352,23 @@ impl<'a, 'b> Builder<'a, 'b> { // soon as a non-optional parameter is encountered. let mut arg = name.to_string(); let mut ts = String::new(); - match ty { - AdapterType::Option(ty) if omittable => { - // e.g. `foo?: string | null` - arg.push_str("?: "); - adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs)); - ts.push_str(" | null"); - } - ty => { - omittable = false; - arg.push_str(": "); - adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs)); + if let Some(v) = ty_override { + omittable = false; + arg.push_str(": "); + ts.push_str(v); + } else { + match ty { + AdapterType::Option(ty) if omittable => { + // e.g. `foo?: string | null` + arg.push_str("?: "); + adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs)); + ts.push_str(" | null"); + } + ty => { + adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs)); + omittable = false; + arg.push_str(": "); + } } } arg.push_str(&ts); @@ -363,15 +402,19 @@ impl<'a, 'b> Builder<'a, 'b> { if self.constructor.is_none() { ts.push_str(": "); let mut ret = String::new(); - match result_tys.len() { - 0 => ret.push_str("void"), - 1 => adapter2ts( - &result_tys[0], - TypePosition::Return, - &mut ret, - Some(&mut ts_refs), - ), - _ => ret.push_str("[any]"), + if let Some(v) = &ret_ty_override { + ret.push_str(v); + } else { + match result_tys.len() { + 0 => ret.push_str("void"), + 1 => adapter2ts( + &result_tys[0], + TypePosition::Return, + &mut ret, + Some(&mut ts_refs), + ), + _ => ret.push_str("[any]"), + } } if asyncness { ret = format!("Promise<{}>", ret); @@ -386,57 +429,132 @@ impl<'a, 'b> Builder<'a, 'b> { /// and the return value. fn js_doc_comments( &self, - arg_names: &[String], + args_data: &[AuxFunctionArgumentData], arg_tys: &[&AdapterType], ts_ret: &Option, variadic: bool, + ret_ty_override: &Option, + ret_desc: &Option, ) -> String { - let (variadic_arg, fn_arg_names) = match arg_names.split_last() { + let (variadic_arg, fn_arg_names) = match args_data.split_last() { Some((last, args)) if variadic => (Some(last), args), - _ => (None, arg_names), + _ => (None, args_data), }; let mut omittable = true; let mut js_doc_args = Vec::new(); - for (name, ty) in fn_arg_names.iter().zip(arg_tys).rev() { + for ( + AuxFunctionArgumentData { + name, + ty_override, + desc, + }, + ty, + ) in fn_arg_names.iter().zip(arg_tys).rev() + { let mut arg = "@param {".to_string(); - match ty { - AdapterType::Option(ty) if omittable => { - adapter2ts(ty, TypePosition::Argument, &mut arg, None); - arg.push_str(" | null} "); - arg.push('['); - arg.push_str(name); - arg.push(']'); - } - _ => { - omittable = false; - adapter2ts(ty, TypePosition::Argument, &mut arg, None); - arg.push_str("} "); - arg.push_str(name); + if let Some(v) = ty_override { + omittable = false; + arg.push_str(v); + arg.push_str("} "); + arg.push_str(name); + } else { + match ty { + AdapterType::Option(ty) if omittable => { + adapter2ts(ty, TypePosition::Argument, &mut arg, None); + arg.push_str(" | null} "); + arg.push('['); + arg.push_str(name); + arg.push(']'); + } + _ => { + omittable = false; + adapter2ts(ty, TypePosition::Argument, &mut arg, None); + arg.push_str("} "); + arg.push_str(name); + } } } + // append description + if let Some(v) = desc { + arg.push_str(" - "); + arg.push_str(v); + } arg.push('\n'); js_doc_args.push(arg); } let mut ret: String = js_doc_args.into_iter().rev().collect(); - if let (Some(name), Some(ty)) = (variadic_arg, arg_tys.last()) { + if let ( + Some(AuxFunctionArgumentData { + name, + ty_override, + desc, + }), + Some(ty), + ) = (variadic_arg, arg_tys.last()) + { ret.push_str("@param {..."); - adapter2ts(ty, TypePosition::Argument, &mut ret, None); + if let Some(v) = ty_override { + ret.push_str(v); + } else { + adapter2ts(ty, TypePosition::Argument, &mut ret, None); + } ret.push_str("} "); ret.push_str(name); + + // append desc + if let Some(v) = desc { + ret.push_str(" - "); + ret.push_str(v); + } ret.push('\n'); } - if let Some(ts) = ts_ret { - if ts != "void" { + if let Some(ts) = ret_ty_override.as_ref().or(ts_ret.as_ref()) { + // skip if type is void and there is no description + if ts != "void" || ret_desc.is_some() { ret.push_str(&format!("@returns {{{}}}", ts)); } + // append return description + if let Some(v) = ret_desc { + ret.push(' '); + ret.push_str(v); + } } ret } + + /// Returns a helpful TS doc comment which lists all parameters and + /// the return value descriptions. + fn ts_doc_comments( + &self, + args_data: &[AuxFunctionArgumentData], + ret_desc: &Option, + ) -> String { + let mut ts_doc = String::new(); + // ofc we dont need arg type for ts doc, only arg name + for AuxFunctionArgumentData { name, desc, .. } in args_data.iter() { + ts_doc.push_str("@param "); + ts_doc.push_str(name); + + // append desc + if let Some(v) = desc { + ts_doc.push_str(" - "); + ts_doc.push_str(v); + } + ts_doc.push('\n'); + } + + // only if there is return description, as we dont want empty @return tag + if let Some(ret_desc) = ret_desc { + ts_doc.push_str("@returns "); + ts_doc.push_str(ret_desc); + } + ts_doc + } } impl<'a, 'b> JsBuilder<'a, 'b> { diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index c7252852b49..3ba5be3a719 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -10,7 +10,6 @@ use crate::wit::{JsImport, JsImportName, NonstandardWitSection, WasmBindgenAux}; use crate::{reset_indentation, Bindgen, EncodeInto, OutputMode, PLACEHOLDER_MODULE}; use anyhow::{anyhow, bail, Context as _, Error}; use binding::TsReference; -use identifier::is_valid_ident; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt; @@ -18,9 +17,9 @@ use std::fmt::Write; use std::fs; use std::path::{Path, PathBuf}; use walrus::{FunctionId, ImportId, MemoryId, Module, TableId, ValType}; +use wasm_bindgen_shared::identifier::is_valid_ident; mod binding; -pub mod identifier; pub struct Context<'a> { globals: String, @@ -2829,16 +2828,20 @@ __wbg_set_wasm(wasm);" ContextAdapterKind::Import(_) => builder.cx.config.debug, }); builder.catch(catch); - let mut arg_names = &None; + let mut args = &None; let mut asyncness = false; let mut variadic = false; let mut generate_jsdoc = false; + let mut ret_ty_override = &None; + let mut ret_desc = &None; match kind { ContextAdapterKind::Export(export) => { - arg_names = &export.arg_names; + args = &export.args; asyncness = export.asyncness; variadic = export.variadic; generate_jsdoc = export.generate_jsdoc; + ret_ty_override = &export.fn_ret_ty_override; + ret_desc = &export.fn_ret_desc; match &export.kind { AuxExportKind::Function(_) => {} AuxExportKind::Constructor(class) => builder.constructor(class), @@ -2870,6 +2873,7 @@ __wbg_set_wasm(wasm);" ts_ret_ty, ts_refs, js_doc, + ts_doc, code, might_be_optional_field, catch, @@ -2878,11 +2882,13 @@ __wbg_set_wasm(wasm);" .process( adapter, instrs, - arg_names, + args, asyncness, variadic, generate_jsdoc, &debug_name, + ret_ty_override, + ret_desc, ) .with_context(|| "failed to generates bindings for ".to_string() + &debug_name)?; @@ -2897,8 +2903,17 @@ __wbg_set_wasm(wasm);" let ts_sig = export.generate_typescript.then_some(ts_sig.as_str()); + // only include `ts_doc` for format if there were arguments or a return var description + // this is because if there are no arguments or return var description, `ts_doc` + // provides no additional value on top of what `ts_sig` already does + let ts_doc_opts = (ret_desc.is_some() + || args + .as_ref() + .is_some_and(|v| v.iter().any(|arg| arg.desc.is_some()))) + .then_some(ts_doc); + let js_docs = format_doc_comments(&export.comments, Some(js_doc)); - let ts_docs = format_doc_comments(&export.comments, None); + let ts_docs = format_doc_comments(&export.comments, ts_doc_opts); match &export.kind { AuxExportKind::Function(name) => { diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index 930fddca566..a514e875b3d 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -521,18 +521,32 @@ impl<'a> Context<'a> { None => AuxExportKind::Function(export.function.name.to_string()), }; + let args = Some( + export + .function + .args + .into_iter() + .map(|v| AuxFunctionArgumentData { + name: v.name, + ty_override: v.ty_override.map(String::from), + desc: v.desc.map(String::from), + }) + .collect::>(), + ); let id = self.export_adapter(export_id, descriptor)?; self.aux.export_map.insert( id, AuxExport { debug_name: wasm_name, comments: concatenate_comments(&export.comments), - arg_names: Some(export.function.arg_names), + args, asyncness: export.function.asyncness, kind, generate_typescript: export.function.generate_typescript, generate_jsdoc: export.function.generate_jsdoc, variadic: export.function.variadic, + fn_ret_ty_override: export.function.ret_ty_override.map(String::from), + fn_ret_desc: export.function.ret_desc.map(String::from), }, ); Ok(()) @@ -946,7 +960,7 @@ impl<'a> Context<'a> { getter_id, AuxExport { debug_name: format!("getter for `{}::{}`", struct_.name, field.name), - arg_names: None, + args: None, asyncness: false, comments: concatenate_comments(&field.comments), kind: AuxExportKind::Method { @@ -958,6 +972,8 @@ impl<'a> Context<'a> { generate_typescript: field.generate_typescript, generate_jsdoc: field.generate_jsdoc, variadic: false, + fn_ret_ty_override: None, + fn_ret_desc: None, }, ); @@ -978,7 +994,7 @@ impl<'a> Context<'a> { setter_id, AuxExport { debug_name: format!("setter for `{}::{}`", struct_.name, field.name), - arg_names: None, + args: None, asyncness: false, comments: concatenate_comments(&field.comments), kind: AuxExportKind::Method { @@ -990,6 +1006,8 @@ impl<'a> Context<'a> { generate_typescript: field.generate_typescript, generate_jsdoc: field.generate_jsdoc, variadic: false, + fn_ret_ty_override: None, + fn_ret_desc: None, }, ); } diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs index 1e9579dc8ba..2742ddb3262 100644 --- a/crates/cli-support/src/wit/nonstandard.rs +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -73,9 +73,9 @@ pub struct AuxExport { pub debug_name: String, /// Comments parsed in Rust and forwarded here to show up in JS bindings. pub comments: String, - /// Argument names in Rust forwarded here to configure the names that show - /// up in TypeScript bindings. - pub arg_names: Option>, + /// Function's argument info in Rust forwarded here to configure the signature + /// that shows up in bindings. + pub args: Option>, /// Whether this is an async function, to configure the TypeScript return value. pub asyncness: bool, /// What kind of function this is and where it shows up @@ -86,6 +86,21 @@ pub struct AuxExport { pub generate_jsdoc: bool, /// Whether typescript bindings should be generated for this export. pub variadic: bool, + /// Function's return overriding type + pub fn_ret_ty_override: Option, + /// Function's return description + pub fn_ret_desc: Option, +} + +/// Information about a functions' argument +#[derive(Debug, Clone)] +pub struct AuxFunctionArgumentData { + /// Specifies the argument name + pub name: String, + /// Specifies the function argument type override + pub ty_override: Option, + /// Specifies the argument description + pub desc: Option, } /// All possible kinds of exports from a Wasm module. diff --git a/crates/cli/tests/reference/function-attrs.d.ts b/crates/cli/tests/reference/function-attrs.d.ts new file mode 100644 index 00000000000..7ae18c6a5c9 --- /dev/null +++ b/crates/cli/tests/reference/function-attrs.d.ts @@ -0,0 +1,34 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Description for fn_with_attr + * @param firstArg - some number + * @param secondArg + * @returns returns 1 if arg2 is true, or arg1 if arg2 is undefined or false + */ +export function fn_with_attr(firstArg: number, secondArg: boolean | undefined): Promise; +/** + * Description for HoldsNumber + */ +export class HoldsNumber { + private constructor(); + free(): void; + /** + * Description for static_fn_with_attr + * @param firstArg - some number + * @param secondArg + * @returns returns an instance of HoldsNumber, holding arg1 if arg2 is undefined and holding arg2 if not + */ + static static_fn_with_attr(firstArg: number, secondArg: number | undefined): HoldsNumber; + /** + * Description for method_with_attr + * @param firstArg - some number + * @param secondArg + * @returns returns arg1 if arg2 is true, or holding value of self if arg2 is undefined or false + */ + method_with_attr(firstArg: number, secondArg: boolean | undefined): number; + /** + * Inner value + */ + readonly inner: number; +} diff --git a/crates/cli/tests/reference/function-attrs.rs b/crates/cli/tests/reference/function-attrs.rs new file mode 100644 index 00000000000..2621a4cc37d --- /dev/null +++ b/crates/cli/tests/reference/function-attrs.rs @@ -0,0 +1,69 @@ +use wasm_bindgen::prelude::*; + +/// Description for fn_with_attr +#[wasm_bindgen( + unchecked_return_type = "number", + return_description = "returns 1 if arg2 is true, or arg1 if arg2 is undefined or false" +)] +pub async fn fn_with_attr( + #[wasm_bindgen(js_name = "firstArg", param_description = "some number")] arg1: u32, + #[wasm_bindgen(js_name = "secondArg", unchecked_param_type = "boolean | undefined")] arg2: JsValue, +) -> Result { + if arg2.is_undefined() { + Ok(arg1.into()) + } else if arg2.is_truthy() { + Ok(1u32.into()) + } else { + Ok(arg1.into()) + } +} + +/// Description for HoldsNumber +#[wasm_bindgen] +pub struct HoldsNumber { + inner: JsValue, +} + +#[wasm_bindgen] +impl HoldsNumber { + /// Inner value + #[wasm_bindgen(getter = "inner", unchecked_return_type = "number")] + pub fn get_inner(&self) -> JsValue { + self.inner.clone() + } + + /// Description for static_fn_with_attr + #[wasm_bindgen( + return_description = "returns an instance of HoldsNumber, holding arg1 if arg2 is undefined and holding arg2 if not" + )] + pub fn static_fn_with_attr( + #[wasm_bindgen(js_name = "firstArg", param_description = "some number")] arg1: u32, + #[wasm_bindgen(js_name = "secondArg", unchecked_param_type = "number | undefined")] + arg2: JsValue, + ) -> HoldsNumber { + if arg2.is_undefined() { + HoldsNumber { inner: arg1.into() } + } else { + HoldsNumber { inner: arg2 } + } + } + + /// Description for method_with_attr + #[wasm_bindgen( + unchecked_return_type = "number", + return_description = "returns arg1 if arg2 is true, or holding value of self if arg2 is undefined or false" + )] + pub fn method_with_attr( + &self, + #[wasm_bindgen(js_name = "firstArg", param_description = "some number")] arg1: u32, + #[wasm_bindgen(js_name = "secondArg", unchecked_param_type = "boolean | undefined")] arg2: JsValue, + ) -> JsValue { + if arg2.is_undefined() { + self.inner.clone() + } else if arg2.is_truthy() { + arg1.into() + } else { + self.inner.clone() + } + } +} diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 46c76a1ef21..a3cf49bffd2 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -1,7 +1,7 @@ use std::cell::{Cell, RefCell}; -use std::char; use std::collections::HashMap; use std::str::Chars; +use std::{char, iter}; use ast::OperationKind; use backend::ast::{self, ThreadLocal}; @@ -9,6 +9,7 @@ use backend::util::{ident_ty, ShortHash}; use backend::Diagnostic; use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use quote::ToTokens; +use shared::identifier::is_valid_ident; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream, Result as SynResult}; use syn::spanned::Spanned; @@ -103,11 +104,37 @@ fn is_non_value_js_keyword(keyword: &str) -> bool { JS_KEYWORDS.contains(&keyword) && !VALUE_LIKE_JS_KEYWORDS.contains(&keyword) } +/// Return an [`Err`] if the given string contains a comment close syntax (`*/``). +fn check_js_comment_close(str: &str, span: Span) -> Result<(), Diagnostic> { + if str.contains("*/") { + Err(Diagnostic::span_error( + span, + "contains comment close syntax", + )) + } else { + Ok(()) + } +} + +/// Return an [`Err`] if the given string is a JS keyword or contains a comment close syntax (`*/``). +fn check_invalid_type(str: &str, span: Span) -> Result<(), Diagnostic> { + if is_js_keyword(str) { + return Err(Diagnostic::span_error(span, "collides with JS keyword")); + } + check_js_comment_close(str, span)?; + Ok(()) +} + #[derive(Default)] struct AttributeParseState { parsed: Cell, checks: Cell, - unused_attrs: RefCell>, + unused_attrs: RefCell>, +} + +struct UnusedState { + error: bool, + ident: Ident, } /// Parsed attributes from a `#[wasm_bindgen(..)]`. @@ -128,53 +155,57 @@ pub struct JsNamespace(Vec); macro_rules! attrgen { ($mac:ident) => { $mac! { - (catch, Catch(Span)), - (constructor, Constructor(Span)), - (method, Method(Span)), - (static_method_of, StaticMethodOf(Span, Ident)), - (js_namespace, JsNamespace(Span, JsNamespace, Vec)), - (module, Module(Span, String, Span)), - (raw_module, RawModule(Span, String, Span)), - (inline_js, InlineJs(Span, String, Span)), - (getter, Getter(Span, Option)), - (setter, Setter(Span, Option)), - (indexing_getter, IndexingGetter(Span)), - (indexing_setter, IndexingSetter(Span)), - (indexing_deleter, IndexingDeleter(Span)), - (structural, Structural(Span)), - (r#final, Final(Span)), - (readonly, Readonly(Span)), - (js_name, JsName(Span, String, Span)), - (js_class, JsClass(Span, String, Span)), - (inspectable, Inspectable(Span)), - (is_type_of, IsTypeOf(Span, syn::Expr)), - (extends, Extends(Span, syn::Path)), - (no_deref, NoDeref(Span)), - (vendor_prefix, VendorPrefix(Span, Ident)), - (variadic, Variadic(Span)), - (typescript_custom_section, TypescriptCustomSection(Span)), - (skip_typescript, SkipTypescript(Span)), - (skip_jsdoc, SkipJsDoc(Span)), - (main, Main(Span)), - (start, Start(Span)), - (wasm_bindgen, WasmBindgen(Span, syn::Path)), - (js_sys, JsSys(Span, syn::Path)), - (wasm_bindgen_futures, WasmBindgenFutures(Span, syn::Path)), - (skip, Skip(Span)), - (typescript_type, TypeScriptType(Span, String, Span)), - (getter_with_clone, GetterWithClone(Span)), - (static_string, StaticString(Span)), - (thread_local, ThreadLocal(Span)), - (thread_local_v2, ThreadLocalV2(Span)), + (catch, false, Catch(Span)), + (constructor, false, Constructor(Span)), + (method, false, Method(Span)), + (static_method_of, false, StaticMethodOf(Span, Ident)), + (js_namespace, false, JsNamespace(Span, JsNamespace, Vec)), + (module, false, Module(Span, String, Span)), + (raw_module, false, RawModule(Span, String, Span)), + (inline_js, false, InlineJs(Span, String, Span)), + (getter, false, Getter(Span, Option)), + (setter, false, Setter(Span, Option)), + (indexing_getter, false, IndexingGetter(Span)), + (indexing_setter, false, IndexingSetter(Span)), + (indexing_deleter, false, IndexingDeleter(Span)), + (structural, false, Structural(Span)), + (r#final, false, Final(Span)), + (readonly, false, Readonly(Span)), + (js_name, false, JsName(Span, String, Span)), + (js_class, false, JsClass(Span, String, Span)), + (inspectable, false, Inspectable(Span)), + (is_type_of, false, IsTypeOf(Span, syn::Expr)), + (extends, false, Extends(Span, syn::Path)), + (no_deref, false, NoDeref(Span)), + (vendor_prefix, false, VendorPrefix(Span, Ident)), + (variadic, false, Variadic(Span)), + (typescript_custom_section, false, TypescriptCustomSection(Span)), + (skip_typescript, false, SkipTypescript(Span)), + (skip_jsdoc, false, SkipJsDoc(Span)), + (main, false, Main(Span)), + (start, false, Start(Span)), + (wasm_bindgen, false, WasmBindgen(Span, syn::Path)), + (js_sys, false, JsSys(Span, syn::Path)), + (wasm_bindgen_futures, false, WasmBindgenFutures(Span, syn::Path)), + (skip, false, Skip(Span)), + (typescript_type, false, TypeScriptType(Span, String, Span)), + (getter_with_clone, false, GetterWithClone(Span)), + (static_string, false, StaticString(Span)), + (thread_local, false, ThreadLocal(Span)), + (thread_local_v2, false, ThreadLocalV2(Span)), + (unchecked_return_type, true, ReturnType(Span, String, Span)), + (return_description, true, ReturnDesc(Span, String, Span)), + (unchecked_param_type, true, ParamType(Span, String, Span)), + (param_description, true, ParamDesc(Span, String, Span)), // For testing purposes only. - (assert_no_shim, AssertNoShim(Span)), + (assert_no_shim, false, AssertNoShim(Span)), } }; } macro_rules! methods { - ($(($name:ident, $variant:ident($($contents:tt)*)),)*) => { + ($(($name:ident, $invalid_unused:literal, $variant:ident($($contents:tt)*)),)*) => { $(methods!(@method $name, $variant($($contents)*));)* fn enforce_used(self) -> Result<(), Diagnostic> { @@ -206,7 +237,10 @@ macro_rules! methods { .map(|attr| { match attr { $(BindgenAttr::$variant(span, ..) => { - syn::parse_quote_spanned!(*span => $name) + UnusedState { + error: $invalid_unused, + ident: syn::parse_quote_spanned!(*span => $name) + } })* } }) @@ -350,7 +384,7 @@ impl Parse for BindgenAttrs { } macro_rules! gen_bindgen_attr { - ($(($method:ident, $($variants:tt)*),)*) => { + ($(($method:ident, $_:literal, $($variants:tt)*),)*) => { /// The possible attributes in the `#[wasm_bindgen]`. #[cfg_attr(feature = "extra-traits", derive(Debug))] pub enum BindgenAttr { @@ -370,7 +404,7 @@ impl Parse for BindgenAttr { let raw_attr_string = format!("r#{}", attr_string); macro_rules! parsers { - ($(($name:ident, $($contents:tt)*),)*) => { + ($(($name:ident, $_:literal, $($contents:tt)*),)*) => { $( if attr_string == stringify!($name) || raw_attr_string == stringify!($name) { parsers!( @@ -619,6 +653,7 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option &**elem, _ => bail_span!( - class.ty, + class.pat_type.ty, "first argument of method must be a shared reference" ), }; @@ -956,10 +991,13 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option for syn::ItemFn { +impl ConvertToAst<(BindgenAttrs, Vec)> for syn::ItemFn { type Target = ast::Function; - fn convert(self, attrs: BindgenAttrs) -> Result { + fn convert( + self, + (attrs, args_attrs): (BindgenAttrs, Vec), + ) -> Result { match self.vis { syn::Visibility::Public(_) => {} _ if attrs.start().is_some() => {} @@ -979,6 +1017,7 @@ impl ConvertToAst for syn::ItemFn { self.attrs, self.vis, FunctionPosition::Free, + Some(args_attrs), )?; attrs.check_used(); @@ -1027,6 +1066,7 @@ fn function_from_decl( attrs: Vec, vis: syn::Visibility, position: FunctionPosition, + args_attrs: Option>, ) -> Result<(ast::Function, Option), Diagnostic> { if sig.variadic.is_some() { bail_span!(sig.variadic, "can't #[wasm_bindgen] variadic functions"); @@ -1122,10 +1162,44 @@ fn function_from_decl( } } + // process function return data + let ret_ty_override = opts.unchecked_return_type(); + let ret_desc = opts.return_description(); let ret = match output { syn::ReturnType::Default => None, - syn::ReturnType::Type(_, ty) => Some(replace_self(*ty)), + syn::ReturnType::Type(_, ty) => Some(ast::FunctionReturnData { + r#type: replace_self(*ty), + js_type: ret_ty_override + .as_ref() + .map_or::, _>(Ok(None), |(ty, span)| { + check_invalid_type(ty, *span)?; + Ok(Some(ty.to_string())) + })?, + desc: ret_desc.as_ref().map_or::, _>( + Ok(None), + |(desc, span)| { + check_js_comment_close(desc, *span)?; + Ok(Some(desc.to_string())) + }, + )?, + }), }; + // error if there were description or type override specified for + // function return while it doesn't actually return anything + if ret.is_none() && (ret_ty_override.is_some() || ret_desc.is_some()) { + if let Some((_, span)) = ret_ty_override { + return Err(Diagnostic::span_error( + span, + "cannot specify return type for a function that doesn't return", + )); + } + if let Some((_, span)) = ret_desc { + return Err(Diagnostic::span_error( + span, + "cannot specify return description for a function that doesn't return", + )); + } + } let (name, name_span, renamed_via_js_name) = if let Some((js_name, js_name_span)) = opts.js_name() { @@ -1141,11 +1215,9 @@ fn function_from_decl( Ok(( ast::Function { - arguments, name_span, name, renamed_via_js_name, - ret, rust_attrs: attrs, rust_vis: vis, r#unsafe: sig.unsafety.is_some(), @@ -1153,11 +1225,71 @@ fn function_from_decl( generate_typescript: opts.skip_typescript().is_none(), generate_jsdoc: opts.skip_jsdoc().is_none(), variadic: opts.variadic().is_some(), + ret, + arguments: arguments + .into_iter() + .zip( + args_attrs + .into_iter() + .flatten() + .chain(iter::repeat(FnArgAttrs::default())), + ) + .map(|(pat_type, attrs)| ast::FunctionArgumentData { + pat_type, + js_name: attrs.js_name, + js_type: attrs.js_type, + desc: attrs.desc, + }) + .collect(), }, method_self, )) } +/// Helper struct to store extracted function argument attrs +#[derive(Default, Clone)] +struct FnArgAttrs { + js_name: Option, + js_type: Option, + desc: Option, +} + +/// Extracts function arguments attributes +fn extract_args_attrs(sig: &mut syn::Signature) -> Result, Diagnostic> { + let mut args_attrs = vec![]; + for input in sig.inputs.iter_mut() { + if let syn::FnArg::Typed(pat_type) = input { + let attrs = BindgenAttrs::find(&mut pat_type.attrs)?; + let arg_attrs = FnArgAttrs { + js_name: attrs + .js_name() + .map_or(Ok(None), |(js_name_override, span)| { + if is_js_keyword(js_name_override) || !is_valid_ident(js_name_override) { + return Err(Diagnostic::span_error(span, "invalid JS identifier")); + } + Ok(Some(js_name_override.to_string())) + })?, + js_type: attrs + .unchecked_param_type() + .map_or::, _>(Ok(None), |(ty, span)| { + check_invalid_type(ty, span)?; + Ok(Some(ty.to_string())) + })?, + desc: attrs + .param_description() + .map_or::, _>(Ok(None), |(description, span)| { + check_js_comment_close(description, span)?; + Ok(Some(description.to_string())) + })?, + }; + // throw error for any unused attrs + attrs.enforce_used()?; + args_attrs.push(arg_attrs); + } + } + Ok(args_attrs) +} + pub(crate) trait MacroParse { /// Parse the contents of an object into our AST, with a context if necessary. /// @@ -1198,6 +1330,8 @@ impl<'a> MacroParse<(Option, &'a mut TokenStream)> for syn::Item { if let Some((i, _)) = no_mangle { f.attrs.remove(i); } + // extract fn args attributes before parsing to tokens stream + let args_attrs = extract_args_attrs(&mut f.sig)?; let comments = extract_doc_comments(&f.attrs); // If the function isn't used for anything other than being exported to JS, // it'll be unused when not building for the Wasm target and produce a @@ -1218,9 +1352,10 @@ impl<'a> MacroParse<(Option, &'a mut TokenStream)> for syn::Item { }); let rust_name = f.sig.ident.clone(); let start = opts.start().is_some(); + program.exports.push(ast::Export { comments, - function: f.convert(opts)?, + function: f.convert((opts, args_attrs))?, js_class: None, method_kind, method_self: None, @@ -1409,6 +1544,7 @@ impl MacroParse<&ClassMarker> for &mut syn::ImplItemFn { let opts = BindgenAttrs::find(&mut self.attrs)?; let comments = extract_doc_comments(&self.attrs); + let args_attrs: Vec = extract_args_attrs(&mut self.sig)?; let (function, method_self) = function_from_decl( &self.sig.ident, &opts, @@ -1416,6 +1552,7 @@ impl MacroParse<&ClassMarker> for &mut syn::ImplItemFn { self.attrs.clone(), self.vis.clone(), FunctionPosition::Impl { self_ty: class }, + Some(args_attrs), )?; let method_kind = if opts.constructor().is_some() { ast::MethodKind::Constructor @@ -2068,10 +2205,18 @@ pub fn check_unused_attrs(tokens: &mut TokenStream) { assert_eq!(state.parsed.get(), state.checks.get()); let unused_attrs = &*state.unused_attrs.borrow(); if !unused_attrs.is_empty() { + let unused_attrs = unused_attrs.iter().map(|UnusedState { error, ident }| { + if *error { + let text = format!("invalid attribute {} in this position", ident); + quote::quote! { ::core::compile_error!(#text); } + } else { + quote::quote! { let #ident: (); } + } + }); tokens.extend(quote::quote! { // Anonymous scope to prevent name clashes. const _: () = { - #(let #unused_attrs: ();)* + #(#unused_attrs)* }; }); } diff --git a/crates/macro/ui-tests/illegal-char-fn-attrs.rs b/crates/macro/ui-tests/illegal-char-fn-attrs.rs new file mode 100644 index 00000000000..11ba1fa8cda --- /dev/null +++ b/crates/macro/ui-tests/illegal-char-fn-attrs.rs @@ -0,0 +1,34 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn fn_with_illegal_char_attr1( + #[wasm_bindgen(unchecked_param_type = "abcd */firstArg")] arg1: u32, +) -> JsValue { + arg1.into() +} + +#[wasm_bindgen] +pub async fn fn_with_illegal_char_attr2( + #[wasm_bindgen(unchecked_param_type = "num*/ber")] arg1: u32, +) -> JsValue { + arg1.into() +} + +#[wasm_bindgen] +pub async fn fn_with_illegal_char_attr3( + #[wasm_bindgen(param_description = "/* some description */")] arg1: u32, +) -> JsValue { + arg1.into() +} + +#[wasm_bindgen(return_description = "*/ some description")] +pub async fn fn_with_illegal_char_attr4(arg1: u32) -> JsValue { + arg1.into() +} + +#[wasm_bindgen(unchecked_return_type = "number */ abcd")] +pub async fn fn_with_illegal_char_attr5(arg1: u32) -> JsValue { + arg1.into() +} + +fn main() {} diff --git a/crates/macro/ui-tests/illegal-char-fn-attrs.stderr b/crates/macro/ui-tests/illegal-char-fn-attrs.stderr new file mode 100644 index 00000000000..ef9de562305 --- /dev/null +++ b/crates/macro/ui-tests/illegal-char-fn-attrs.stderr @@ -0,0 +1,29 @@ +error: contains comment close syntax + --> ui-tests/illegal-char-fn-attrs.rs:5:43 + | +5 | #[wasm_bindgen(unchecked_param_type = "abcd */firstArg")] arg1: u32, + | ^^^^^^^^^^^^^^^^^ + +error: contains comment close syntax + --> ui-tests/illegal-char-fn-attrs.rs:12:43 + | +12 | #[wasm_bindgen(unchecked_param_type = "num*/ber")] arg1: u32, + | ^^^^^^^^^^ + +error: contains comment close syntax + --> ui-tests/illegal-char-fn-attrs.rs:19:40 + | +19 | #[wasm_bindgen(param_description = "/* some description */")] arg1: u32, + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: contains comment close syntax + --> ui-tests/illegal-char-fn-attrs.rs:24:37 + | +24 | #[wasm_bindgen(return_description = "*/ some description")] + | ^^^^^^^^^^^^^^^^^^^^^ + +error: contains comment close syntax + --> ui-tests/illegal-char-fn-attrs.rs:29:40 + | +29 | #[wasm_bindgen(unchecked_return_type = "number */ abcd")] + | ^^^^^^^^^^^^^^^^ diff --git a/crates/macro/ui-tests/invalid-fn-arg-name.rs b/crates/macro/ui-tests/invalid-fn-arg-name.rs new file mode 100644 index 00000000000..d6e882db168 --- /dev/null +++ b/crates/macro/ui-tests/invalid-fn-arg-name.rs @@ -0,0 +1,93 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn fn_with_invalid_arg_name1( + #[wasm_bindgen(js_name = "*firstArg")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub fn fn_with_invalid_arg_name2( + #[wasm_bindgen(js_name = "#firstArg")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub fn fn_with_invalid_arg_name3( + #[wasm_bindgen(js_name = "firstArg#")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub fn fn_with_invalid_arg_name4( + #[wasm_bindgen(js_name = "first-Arg")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub fn fn_with_invalid_arg_name5( + #[wasm_bindgen(js_name = "--firstArg")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub fn fn_with_invalid_arg_name6( + #[wasm_bindgen(js_name = " first Arg")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub struct A {} + +#[wasm_bindgen] +impl A { + #[wasm_bindgen] + pub async fn method_with_invalid_arg_name1( + #[wasm_bindgen(js_name = "(firstArg)")] arg: u32, + ) -> JsValue { + arg.into() + } + + #[wasm_bindgen] + pub async fn method_with_invalid_arg_name2( + #[wasm_bindgen(js_name = "[firstArg]")] arg: u32, + ) -> JsValue { + arg.into() + } + + #[wasm_bindgen] + pub async fn method_with_invalid_arg_name3( + #[wasm_bindgen(js_name = "")] arg: u32, + ) -> JsValue { + arg.into() + } + + #[wasm_bindgen] + pub async fn method_with_invalid_arg_name4( + #[wasm_bindgen(js_name = "firstArg+")] arg: u32, + ) -> JsValue { + arg.into() + } + + #[wasm_bindgen] + pub async fn method_with_invalid_arg_name5( + #[wasm_bindgen(js_name = "@firstArg")] arg: u32, + ) -> JsValue { + arg.into() + } + + #[wasm_bindgen] + pub async fn method_with_invalid_arg_name6( + #[wasm_bindgen(js_name = "!firstArg")] arg: u32, + ) -> JsValue { + arg.into() + } +} + +fn main() {} diff --git a/crates/macro/ui-tests/invalid-fn-arg-name.stderr b/crates/macro/ui-tests/invalid-fn-arg-name.stderr new file mode 100644 index 00000000000..bee7bd83a04 --- /dev/null +++ b/crates/macro/ui-tests/invalid-fn-arg-name.stderr @@ -0,0 +1,71 @@ +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:5:30 + | +5 | #[wasm_bindgen(js_name = "*firstArg")] arg: u32, + | ^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:12:30 + | +12 | #[wasm_bindgen(js_name = "#firstArg")] arg: u32, + | ^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:19:30 + | +19 | #[wasm_bindgen(js_name = "firstArg#")] arg: u32, + | ^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:26:30 + | +26 | #[wasm_bindgen(js_name = "first-Arg")] arg: u32, + | ^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:33:30 + | +33 | #[wasm_bindgen(js_name = "--firstArg")] arg: u32, + | ^^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:40:30 + | +40 | #[wasm_bindgen(js_name = " first Arg")] arg: u32, + | ^^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:52:34 + | +52 | #[wasm_bindgen(js_name = "(firstArg)")] arg: u32, + | ^^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:59:34 + | +59 | #[wasm_bindgen(js_name = "[firstArg]")] arg: u32, + | ^^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:66:34 + | +66 | #[wasm_bindgen(js_name = "")] arg: u32, + | ^^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:73:34 + | +73 | #[wasm_bindgen(js_name = "firstArg+")] arg: u32, + | ^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:80:34 + | +80 | #[wasm_bindgen(js_name = "@firstArg")] arg: u32, + | ^^^^^^^^^^^ + +error: invalid JS identifier + --> ui-tests/invalid-fn-arg-name.rs:87:34 + | +87 | #[wasm_bindgen(js_name = "!firstArg")] arg: u32, + | ^^^^^^^^^^^ diff --git a/crates/macro/ui-tests/invalid-self-fn-attrs.rs b/crates/macro/ui-tests/invalid-self-fn-attrs.rs new file mode 100644 index 00000000000..628a9a38b49 --- /dev/null +++ b/crates/macro/ui-tests/invalid-self-fn-attrs.rs @@ -0,0 +1,29 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct A { + inner: u32 +} + +#[wasm_bindgen] +impl A { + #[wasm_bindgen] + pub fn method_with_self_attr1( + #[wasm_bindgen(unchecked_param_type = "number")] + &self, + arg: u32 + ) -> JsValue { + (self.inner + arg).into() + } + + #[wasm_bindgen] + pub fn method_with_self_attr2( + #[wasm_bindgen(param_description = "some description")] + &self, + arg: u32 + ) -> JsValue { + (self.inner + arg).into() + } +} + +fn main() {} diff --git a/crates/macro/ui-tests/invalid-self-fn-attrs.stderr b/crates/macro/ui-tests/invalid-self-fn-attrs.stderr new file mode 100644 index 00000000000..885f1465c67 --- /dev/null +++ b/crates/macro/ui-tests/invalid-self-fn-attrs.stderr @@ -0,0 +1,11 @@ +error: expected non-macro attribute, found attribute macro `wasm_bindgen` + --> ui-tests/invalid-self-fn-attrs.rs:12:11 + | +12 | #[wasm_bindgen(unchecked_param_type = "number")] + | ^^^^^^^^^^^^ not a non-macro attribute + +error: expected non-macro attribute, found attribute macro `wasm_bindgen` + --> ui-tests/invalid-self-fn-attrs.rs:21:11 + | +21 | #[wasm_bindgen(param_description = "some description")] + | ^^^^^^^^^^^^ not a non-macro attribute diff --git a/crates/macro/ui-tests/no-ret-fn-attr.rs b/crates/macro/ui-tests/no-ret-fn-attr.rs new file mode 100644 index 00000000000..afd2d764f31 --- /dev/null +++ b/crates/macro/ui-tests/no-ret-fn-attr.rs @@ -0,0 +1,21 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(unchecked_return_type = "number")] +pub fn no_ret_fn1() {} + +#[wasm_bindgen(return_description = "some description")] +pub async fn no_ret_fn2() {} + +#[wasm_bindgen] +pub struct A {} + +#[wasm_bindgen] +impl A { + #[wasm_bindgen(unchecked_return_type = "number")] + pub async fn no_ret_method1() {} + + #[wasm_bindgen(return_description = "some description")] + pub fn no_ret_method2() {} +} + +fn main() {} diff --git a/crates/macro/ui-tests/no-ret-fn-attr.stderr b/crates/macro/ui-tests/no-ret-fn-attr.stderr new file mode 100644 index 00000000000..66a6bdda801 --- /dev/null +++ b/crates/macro/ui-tests/no-ret-fn-attr.stderr @@ -0,0 +1,23 @@ +error: cannot specify return type for a function that doesn't return + --> ui-tests/no-ret-fn-attr.rs:3:40 + | +3 | #[wasm_bindgen(unchecked_return_type = "number")] + | ^^^^^^^^ + +error: cannot specify return description for a function that doesn't return + --> ui-tests/no-ret-fn-attr.rs:6:37 + | +6 | #[wasm_bindgen(return_description = "some description")] + | ^^^^^^^^^^^^^^^^^^ + +error: cannot specify return type for a function that doesn't return + --> ui-tests/no-ret-fn-attr.rs:14:44 + | +14 | #[wasm_bindgen(unchecked_return_type = "number")] + | ^^^^^^^^ + +error: cannot specify return description for a function that doesn't return + --> ui-tests/no-ret-fn-attr.rs:17:41 + | +17 | #[wasm_bindgen(return_description = "some description")] + | ^^^^^^^^^^^^^^^^^^ diff --git a/crates/macro/ui-tests/unused-fn-attrs.rs b/crates/macro/ui-tests/unused-fn-attrs.rs new file mode 100644 index 00000000000..d483273a5cd --- /dev/null +++ b/crates/macro/ui-tests/unused-fn-attrs.rs @@ -0,0 +1,85 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn fn_with_unused_attr1( + #[wasm_bindgen(js_name = "firstArg", js_name = "anotherFirstArg")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub async fn fn_with_unused_attr2( + #[wasm_bindgen(unchecked_param_type = "number", unchecked_param_type = "bigint")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub async fn fn_with_unused_attr3( + #[wasm_bindgen(unchecked_return_type = "number")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub async fn fn_with_unused_attr4( + #[wasm_bindgen(return_description = "some description")] arg: u32, +) -> JsValue { + arg.into() +} + +#[wasm_bindgen] +pub struct A {} + +#[wasm_bindgen] +impl A { + #[wasm_bindgen] + pub async fn fn_with_unused_attr( + #[wasm_bindgen(param_description = "some number")] + #[wasm_bindgen(param_description = "some other description")] arg: u32, + ) -> JsValue { + arg.into() + } +} + +#[wasm_bindgen( + unchecked_return_type = "something", + return_description = "something", + unchecked_param_type = "something", + param_description = "somthing" +)] +struct B {} + +#[wasm_bindgen( + unchecked_return_type = "something", + return_description = "something", + unchecked_param_type = "something", + param_description = "somthing" +)] +impl B { + #[wasm_bindgen] + pub fn foo() {} +} + +#[wasm_bindgen( + unchecked_return_type = "something", + return_description = "something", + unchecked_param_type = "something", + param_description = "somthing" +)] +pub enum C { + Variat +} + +#[wasm_bindgen( + unchecked_return_type = "something", + return_description = "something", + unchecked_param_type = "something", + param_description = "somthing" +)] +impl C { + #[wasm_bindgen] + pub fn foo() {} +} + +fn main() {} diff --git a/crates/macro/ui-tests/unused-fn-attrs.stderr b/crates/macro/ui-tests/unused-fn-attrs.stderr new file mode 100644 index 00000000000..1d53ef47e89 --- /dev/null +++ b/crates/macro/ui-tests/unused-fn-attrs.stderr @@ -0,0 +1,237 @@ +error: unused wasm_bindgen attribute + --> ui-tests/unused-fn-attrs.rs:5:42 + | +5 | #[wasm_bindgen(js_name = "firstArg", js_name = "anotherFirstArg")] arg: u32, + | ^^^^^^^ + +error: unused wasm_bindgen attribute + --> ui-tests/unused-fn-attrs.rs:12:53 + | +12 | #[wasm_bindgen(unchecked_param_type = "number", unchecked_param_type = "bigint")] arg: u32, + | ^^^^^^^^^^^^^^^^^^^^ + +error: unused wasm_bindgen attribute + --> ui-tests/unused-fn-attrs.rs:19:20 + | +19 | #[wasm_bindgen(unchecked_return_type = "number")] arg: u32, + | ^^^^^^^^^^^^^^^^^^^^^ + +error: unused wasm_bindgen attribute + --> ui-tests/unused-fn-attrs.rs:26:20 + | +26 | #[wasm_bindgen(return_description = "some description")] arg: u32, + | ^^^^^^^^^^^^^^^^^^ + +error: unused wasm_bindgen attribute + --> ui-tests/unused-fn-attrs.rs:39:24 + | +39 | #[wasm_bindgen(param_description = "some other description")] arg: u32, + | ^^^^^^^^^^^^^^^^^ + +error: invalid attribute unchecked_return_type in this position + --> ui-tests/unused-fn-attrs.rs:45:1 + | +45 | / #[wasm_bindgen( +46 | | unchecked_return_type = "something", +47 | | return_description = "something", +48 | | unchecked_param_type = "something", +49 | | param_description = "somthing" +50 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute return_description in this position + --> ui-tests/unused-fn-attrs.rs:45:1 + | +45 | / #[wasm_bindgen( +46 | | unchecked_return_type = "something", +47 | | return_description = "something", +48 | | unchecked_param_type = "something", +49 | | param_description = "somthing" +50 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute unchecked_param_type in this position + --> ui-tests/unused-fn-attrs.rs:45:1 + | +45 | / #[wasm_bindgen( +46 | | unchecked_return_type = "something", +47 | | return_description = "something", +48 | | unchecked_param_type = "something", +49 | | param_description = "somthing" +50 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute param_description in this position + --> ui-tests/unused-fn-attrs.rs:45:1 + | +45 | / #[wasm_bindgen( +46 | | unchecked_return_type = "something", +47 | | return_description = "something", +48 | | unchecked_param_type = "something", +49 | | param_description = "somthing" +50 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute unchecked_return_type in this position + --> ui-tests/unused-fn-attrs.rs:53:1 + | +53 | / #[wasm_bindgen( +54 | | unchecked_return_type = "something", +55 | | return_description = "something", +56 | | unchecked_param_type = "something", +57 | | param_description = "somthing" +58 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute return_description in this position + --> ui-tests/unused-fn-attrs.rs:53:1 + | +53 | / #[wasm_bindgen( +54 | | unchecked_return_type = "something", +55 | | return_description = "something", +56 | | unchecked_param_type = "something", +57 | | param_description = "somthing" +58 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute unchecked_param_type in this position + --> ui-tests/unused-fn-attrs.rs:53:1 + | +53 | / #[wasm_bindgen( +54 | | unchecked_return_type = "something", +55 | | return_description = "something", +56 | | unchecked_param_type = "something", +57 | | param_description = "somthing" +58 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute param_description in this position + --> ui-tests/unused-fn-attrs.rs:53:1 + | +53 | / #[wasm_bindgen( +54 | | unchecked_return_type = "something", +55 | | return_description = "something", +56 | | unchecked_param_type = "something", +57 | | param_description = "somthing" +58 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute unchecked_return_type in this position + --> ui-tests/unused-fn-attrs.rs:64:1 + | +64 | / #[wasm_bindgen( +65 | | unchecked_return_type = "something", +66 | | return_description = "something", +67 | | unchecked_param_type = "something", +68 | | param_description = "somthing" +69 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute return_description in this position + --> ui-tests/unused-fn-attrs.rs:64:1 + | +64 | / #[wasm_bindgen( +65 | | unchecked_return_type = "something", +66 | | return_description = "something", +67 | | unchecked_param_type = "something", +68 | | param_description = "somthing" +69 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute unchecked_param_type in this position + --> ui-tests/unused-fn-attrs.rs:64:1 + | +64 | / #[wasm_bindgen( +65 | | unchecked_return_type = "something", +66 | | return_description = "something", +67 | | unchecked_param_type = "something", +68 | | param_description = "somthing" +69 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute param_description in this position + --> ui-tests/unused-fn-attrs.rs:64:1 + | +64 | / #[wasm_bindgen( +65 | | unchecked_return_type = "something", +66 | | return_description = "something", +67 | | unchecked_param_type = "something", +68 | | param_description = "somthing" +69 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute unchecked_return_type in this position + --> ui-tests/unused-fn-attrs.rs:74:1 + | +74 | / #[wasm_bindgen( +75 | | unchecked_return_type = "something", +76 | | return_description = "something", +77 | | unchecked_param_type = "something", +78 | | param_description = "somthing" +79 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute return_description in this position + --> ui-tests/unused-fn-attrs.rs:74:1 + | +74 | / #[wasm_bindgen( +75 | | unchecked_return_type = "something", +76 | | return_description = "something", +77 | | unchecked_param_type = "something", +78 | | param_description = "somthing" +79 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute unchecked_param_type in this position + --> ui-tests/unused-fn-attrs.rs:74:1 + | +74 | / #[wasm_bindgen( +75 | | unchecked_return_type = "something", +76 | | return_description = "something", +77 | | unchecked_param_type = "something", +78 | | param_description = "somthing" +79 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid attribute param_description in this position + --> ui-tests/unused-fn-attrs.rs:74:1 + | +74 | / #[wasm_bindgen( +75 | | unchecked_return_type = "something", +76 | | return_description = "something", +77 | | unchecked_param_type = "something", +78 | | param_description = "somthing" +79 | | )] + | |__^ + | + = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index d69ef902539..be3071b0f38 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -21,3 +21,6 @@ links = "wasm_bindgen" [lints] workspace = true + +[dependencies] +unicode-ident = "1.0.5" diff --git a/crates/cli-support/src/js/identifier.rs b/crates/shared/src/identifier.rs similarity index 100% rename from crates/cli-support/src/js/identifier.rs rename to crates/shared/src/identifier.rs diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index c5d46f1bad1..827d6d51033 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1,12 +1,13 @@ #![doc(html_root_url = "https://docs.rs/wasm-bindgen-shared/0.2")] +pub mod identifier; #[cfg(test)] mod schema_hash_approval; // This gets changed whenever our schema changes. // At this time versions of wasm-bindgen and wasm-bindgen-cli are required to have the exact same // SCHEMA_VERSION in order to work together. -pub const SCHEMA_VERSION: &str = "0.2.98"; +pub const SCHEMA_VERSION: &str = "0.2.99"; #[macro_export] macro_rules! shared_api { @@ -135,12 +136,20 @@ macro_rules! shared_api { } struct Function<'a> { - arg_names: Vec, + args: Vec>, asyncness: bool, name: &'a str, generate_typescript: bool, generate_jsdoc: bool, variadic: bool, + ret_ty_override: Option<&'a str>, + ret_desc: Option<&'a str>, + } + + struct FunctionArgumentData<'a> { + name: String, + ty_override: Option<&'a str>, + desc: Option<&'a str>, } struct Struct<'a> { diff --git a/crates/shared/src/schema_hash_approval.rs b/crates/shared/src/schema_hash_approval.rs index be50d273a1f..0f004f64550 100644 --- a/crates/shared/src/schema_hash_approval.rs +++ b/crates/shared/src/schema_hash_approval.rs @@ -8,7 +8,7 @@ // If the schema in this library has changed then: // 1. Bump the version in `crates/shared/Cargo.toml` // 2. Change the `SCHEMA_VERSION` in this library to this new Cargo.toml version -const APPROVED_SCHEMA_FILE_HASH: &str = "12659088193118507901"; +const APPROVED_SCHEMA_FILE_HASH: &str = "15483723499309648925"; #[test] fn schema_version() { diff --git a/crates/typescript-tests/src/function_attrs.rs b/crates/typescript-tests/src/function_attrs.rs new file mode 100644 index 00000000000..824954f8cc5 --- /dev/null +++ b/crates/typescript-tests/src/function_attrs.rs @@ -0,0 +1,71 @@ +use wasm_bindgen::prelude::*; + +/// Description for fn_with_attr +#[wasm_bindgen( + unchecked_return_type = "number", + return_description = "returns 1 if arg2 is true, or arg1 if arg2 is undefined or false" +)] +pub async fn fn_with_attr( + #[wasm_bindgen(js_name = "firstArg", param_description = "some number")] arg1: u32, + #[wasm_bindgen(js_name = "secondArg", unchecked_param_type = "boolean | undefined")] + arg2: JsValue, +) -> Result { + if arg2.is_undefined() { + Ok(arg1.into()) + } else if arg2.is_truthy() { + Ok(1u32.into()) + } else { + Ok(arg1.into()) + } +} + +/// Description for HoldsNumber +#[wasm_bindgen] +pub struct HoldsNumber { + inner: JsValue, +} + +#[wasm_bindgen] +impl HoldsNumber { + /// Inner value + #[wasm_bindgen(getter = "inner", unchecked_return_type = "number")] + pub fn get_inner(&self) -> JsValue { + self.inner.clone() + } + + /// Description for static_fn_with_attr + #[wasm_bindgen( + return_description = "returns an instance of HoldsNumber, holding arg1 if arg2 is undefined and holding arg2 if not" + )] + pub fn static_fn_with_attr( + #[wasm_bindgen(js_name = "firstArg", param_description = "some number")] arg1: u32, + #[wasm_bindgen(js_name = "secondArg", unchecked_param_type = "number | undefined")] + arg2: JsValue, + ) -> HoldsNumber { + if arg2.is_undefined() { + HoldsNumber { inner: arg1.into() } + } else { + HoldsNumber { inner: arg2 } + } + } + + /// Description for method_with_attr + #[wasm_bindgen( + unchecked_return_type = "number", + return_description = "returns arg1 if arg2 is true, or holding value of self if arg2 is undefined or false" + )] + pub fn method_with_attr( + &self, + #[wasm_bindgen(js_name = "firstArg", param_description = "some number")] arg1: u32, + #[wasm_bindgen(js_name = "secondArg", unchecked_param_type = "boolean | undefined")] + arg2: JsValue, + ) -> JsValue { + if arg2.is_undefined() { + self.inner.clone() + } else if arg2.is_truthy() { + arg1.into() + } else { + self.inner.clone() + } + } +} diff --git a/crates/typescript-tests/src/function_attrs.ts b/crates/typescript-tests/src/function_attrs.ts new file mode 100644 index 00000000000..287edd2fbf7 --- /dev/null +++ b/crates/typescript-tests/src/function_attrs.ts @@ -0,0 +1,37 @@ +import * as wbg from '../pkg/typescript_tests'; +import * as wasm from '../pkg/typescript_tests_bg.wasm'; +import { expect, test } from "@jest/globals"; + +const wasm_fn_with_attr: (a: number, b: number) => number = wasm.fn_with_attr; +const wbg_fn_with_attr: (a: number, b: boolean) => Promise = wbg.fn_with_attr; +const wasm_holdsnumber_static_fn_with_attr: (a: number, b: number) => number = wasm.holdsnumber_static_fn_with_attr; +const wbg_holdsnumber_static_fn_with_attr: (a1: number, b: number) => wbg.HoldsNumber = wbg.HoldsNumber.static_fn_with_attr; +const wasm_holdsnumber_method_with_attr: (a: number, b: number, c: number) => number = wasm.holdsnumber_method_with_attr; +const wbg_holdsnumber_method_with_attr: (a: number, b: boolean) => number = wbg.HoldsNumber.static_fn_with_attr(1, undefined).method_with_attr; + +test("async function fn_with_attr", async () => { + let result = await wbg.fn_with_attr(4, undefined); + expect(result).toEqual(4); + + result = await wbg.fn_with_attr(5, false); + expect(result).toEqual(5); + + result = await wbg.fn_with_attr(6, true); + expect(result).toEqual(1); +}); + +test("HoldsNumber methods", () => { + const num1 = wbg.HoldsNumber.static_fn_with_attr(4, undefined).inner; + const num2 = wbg.HoldsNumber.static_fn_with_attr(3, 4).inner; + expect(num1).toEqual(num2); + + const holdsNumber = wbg.HoldsNumber.static_fn_with_attr(8, undefined); + let result = holdsNumber.method_with_attr(4, undefined); + expect(result).toEqual(8); + + result = holdsNumber.method_with_attr(5, false); + expect(result).toEqual(8); + + result = holdsNumber.method_with_attr(6, true); + expect(result).toEqual(6); +}); \ No newline at end of file diff --git a/crates/typescript-tests/src/lib.rs b/crates/typescript-tests/src/lib.rs index 96fc3e2107c..b4edbadb4aa 100644 --- a/crates/typescript-tests/src/lib.rs +++ b/crates/typescript-tests/src/lib.rs @@ -3,6 +3,7 @@ pub mod custom_section; pub mod enums; +pub mod function_attrs; pub mod getters_setters; pub mod inspectable; pub mod omit_definition; diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index c3b58139d00..1795bf6f011 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -96,6 +96,8 @@ - [`inspectable`](./reference/attributes/on-rust-exports/inspectable.md) - [`skip_typescript`](./reference/attributes/on-rust-exports/skip_typescript.md) - [`getter_with_clone`](./reference/attributes/on-rust-exports/getter_with_clone.md) + - [`unchecked_return_type` and `unchecked_param_type`](./reference/attributes/on-rust-exports/unchecked_type.md) + - [`return_description` and `param_description`](./reference/attributes/on-rust-exports/description.md) - [`web-sys`](./web-sys/index.md) - [Using `web-sys`](./web-sys/using-web-sys.md) diff --git a/guide/src/reference/attributes/on-rust-exports/description.md b/guide/src/reference/attributes/on-rust-exports/description.md new file mode 100644 index 00000000000..831cbb81815 --- /dev/null +++ b/guide/src/reference/attributes/on-rust-exports/description.md @@ -0,0 +1,60 @@ +# `return_description` and `param_description` + +Descriptions to return and parameter documentation can be added with `#[wasm_bindgen(return_description)]` and `#[wasm_bindgen(param_description)]`. + +```rust +/// Adds `arg1` and `arg2`. +#[wasm_bindgen(return_description = "the result of the addition of `arg1` and `arg2`")] +pub fn add( + #[wasm_bindgen(param_description = "the first number")] + arg1: u32, + #[wasm_bindgen(param_description = "the second number")] + arg2: u32, +) -> u32 { + arg1 + arg2 +} + +#[wasm_bindgen] +pub struct FooList { + // properties +} + +#[wasm_bindgen] +impl FooList { + /// Returns the number at the given index. + #[wasm_bindgen(return_description = "the number at the given index")] + pub fn number( + &self, + #[wasm_bindgen(param_description = "the index of the number to be returned")] + index: u32, + ) -> u32 { + // function body + } +} +``` + +Which will generate the following JS bindings: +```js +/** + * Adds `arg1` and `arg2`. + * + * @param {number} arg1 - the first number + * @param {number} arg2 - the second number + * @returns {number} the result of the addition of `arg1` and `arg2` + */ +export function add(arg1, arg2) { + // ... +} + +export class FooList { + /** + * Returns the number at the given index. + * + * @param {number} index - the index of the number to be returned + * @returns {number} the number at the given index + */ + number(index) { + // ... + } +} +``` diff --git a/guide/src/reference/attributes/on-rust-exports/js_name.md b/guide/src/reference/attributes/on-rust-exports/js_name.md index b6a883857f1..876f4106582 100644 --- a/guide/src/reference/attributes/on-rust-exports/js_name.md +++ b/guide/src/reference/attributes/on-rust-exports/js_name.md @@ -52,3 +52,51 @@ impl JsFoo { // ... } ``` + +It can also be used to rename parameters of exported functions and methods: + +```rust +#[wasm_bindgen] +pub fn foo( + #[wasm_bindgen(js_name = "firstArg")] + arg1: String, +) { + // function body +} + +#[wasm_bindgen] +pub struct Foo { + // properties +} + +#[wasm_bindgen] +impl Foo { + pub fn foo( + &self, + #[wasm_bindgen(js_name = "firstArg")] + arg1: u32, + ) { + // function body + } +} +``` + +Which will generate the following JS bindings: + +```js +/** + * @param {string} firstArg + */ +export function foo(firstArg) { + // ... +} + +export class Foo { + /** + * @param {number} firstArg + */ + foo(firstArg) { + // ... + } +} +``` diff --git a/guide/src/reference/attributes/on-rust-exports/unchecked_type.md b/guide/src/reference/attributes/on-rust-exports/unchecked_type.md new file mode 100644 index 00000000000..a1ac509bb9e --- /dev/null +++ b/guide/src/reference/attributes/on-rust-exports/unchecked_type.md @@ -0,0 +1,53 @@ +# `unchecked_return_type` and `unchecked_param_type` + +Return and parameter types of exported functions and methods can be overwritten with `#[wasm_bindgen(unchecked_return_type)]` and `#[wasm_bindgen(unchecked_param_type)]`. + +> **Note**: Types that are provided using `#[wasm_bindgen(unchecked_return_type)]` and `#[wasm_bindgen(unchecked_param_type)]` aren't checked for their contents. They will end up in a function signature and JSDoc exactly as they have been specified. E.g. `#[wasm_bindgen(unchecked_return_type = "number")]` on a function returning `String` will return a `string`, not a `number`, even if the TS signature and JSDoc will say otherwise. + +```rust +#[wasm_bindgen(unchecked_return_type = "Foo")] +pub fn foo( + #[wasm_bindgen(unchecked_param_type = "Bar")] + arg1: JsValue, +) -> JsValue { + // function body +} + +#[wasm_bindgen] +pub struct Foo { + // properties +} + +#[wasm_bindgen] +impl Foo { + #[wasm_bindgen(unchecked_return_type = "Baz")] + pub fn foo( + &self, + #[wasm_bindgen(unchecked_param_type = "Bar")] + arg1: JsValue, + ) -> JsValue { + // function body + } +} +``` + +Which will generate the following JS bindings: +```js +/** + * @param {Bar} arg1 + * @returns {Foo} + */ +export function foo(arg1) { + // ... +} + +export class Foo { + /** + * @param {Bar} arg1 + * @returns {Baz} + */ + foo(arg1) { + // ... + } +} +```