diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 0991dac66cf..7669583a87a 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -361,8 +361,6 @@ pub struct Function { pub name: String, /// The span of the function's name in Rust code pub name_span: Span, - /// 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 diff --git a/crates/cli/tests/reference/rename.d.ts b/crates/cli/tests/reference/rename.d.ts new file mode 100644 index 00000000000..726bd010a7b --- /dev/null +++ b/crates/cli/tests/reference/rename.d.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +export function export_from_rust(a: number): number; +export class RustStruct { + free(): void; + static i_dont_get_renamed(): void; + incrementFoo(amount?: number): void; + setFoo(foo: number): void; + get_another(): number; + static staticMethod(a: number): void; + static IHaveA_funky_name(a: number): void; + foo: number; + someCoolField: number; + another_field_for_you: number; + someOtherProp: number; + my_own_name: number; + my_unique_name: number; + static someStaticProp: number; +} diff --git a/crates/cli/tests/reference/rename.js b/crates/cli/tests/reference/rename.js new file mode 100644 index 00000000000..2a065afc443 --- /dev/null +++ b/crates/cli/tests/reference/rename.js @@ -0,0 +1,247 @@ +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} + + +const heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder; + +let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +function getObject(idx) { return heap[idx]; } + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} +/** + * @param {number} a + * @returns {number} + */ +export function export_from_rust(a) { + const ret = wasm.export_from_rust(a); + return ret >>> 0; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +const RustStructFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_ruststruct_free(ptr >>> 0, 1)); + +export class RustStruct { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + RustStructFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_ruststruct_free(ptr, 0); + } + /** + * @returns {number} + */ + get foo() { + const ret = wasm.__wbg_get_ruststruct_foo(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {number} arg0 + */ + set foo(arg0) { + wasm.__wbg_set_ruststruct_foo(this.__wbg_ptr, arg0); + } + /** + * @returns {number} + */ + get someCoolField() { + const ret = wasm.__wbg_get_ruststruct_someCoolField(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {number} arg0 + */ + set someCoolField(arg0) { + wasm.__wbg_set_ruststruct_someCoolField(this.__wbg_ptr, arg0); + } + /** + * @returns {number} + */ + get another_field_for_you() { + const ret = wasm.__wbg_get_ruststruct_another_field_for_you(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {number} arg0 + */ + set another_field_for_you(arg0) { + wasm.__wbg_set_ruststruct_another_field_for_you(this.__wbg_ptr, arg0); + } + static i_dont_get_renamed() { + wasm.ruststruct_i_dont_get_renamed(); + } + /** + * @param {number | undefined} [amount] + */ + incrementFoo(amount) { + wasm.ruststruct_incrementFoo(this.__wbg_ptr, isLikeNone(amount) ? 0x100000001 : (amount) >>> 0); + } + /** + * @param {number} foo + */ + setFoo(foo) { + wasm.ruststruct_setFoo(this.__wbg_ptr, foo); + } + /** + * @returns {number} + */ + get_another() { + const ret = wasm.ruststruct_get_another(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {number} a + */ + static staticMethod(a) { + wasm.ruststruct_staticMethod(a); + } + /** + * @param {number} a + */ + static IHaveA_funky_name(a) { + wasm.ruststruct_IHaveA_funky_name(a); + } + /** + * @returns {number} + */ + get someOtherProp() { + const ret = wasm.ruststruct_someOtherProp(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {number} value + */ + set someOtherProp(value) { + wasm.ruststruct_set_someOtherProp(this.__wbg_ptr, value); + } + /** + * @returns {number} + */ + get my_own_name() { + const ret = wasm.ruststruct_someDifferentProp(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {number} value + */ + set my_own_name(value) { + wasm.ruststruct_set_someDifferentProp(this.__wbg_ptr, value); + } + /** + * @returns {number} + */ + get my_unique_name() { + const ret = wasm.ruststruct_my_unique_name(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @param {number} value + */ + set my_unique_name(value) { + wasm.ruststruct_set_my_unique_name(this.__wbg_ptr, value); + } + /** + * @returns {number} + */ + static get someStaticProp() { + const ret = wasm.ruststruct_someStaticProp(); + return ret >>> 0; + } + /** + * @param {number} value + */ + static set someStaticProp(value) { + wasm.ruststruct_set_someStaticProp(value); + } +} + +export function __wbg_documentElement_17a3f0d4e04c6241() { + const ret = document.documentElement(); + return addHeapObject(ret); +}; + +export function __wbg_foobar_aa5072d28246f9cb() { + foo_bar(); +}; + +export function __wbg_querySelector_ab6b6886c63dca45(arg0, arg1) { + const ret = document.querySelector(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}; + +export function __wbg_quxCorge_d8ec2d56c00b013f() { + quxCorge(); +}; + +export function __wbg_static_accessor_MAX_SAFE_INTEGER_37128e65405df998() { + const ret = Number.MAX_SAFE_INTEGER; + return ret; +}; + +export function __wbg_static_accessor_i_do_not_exist_344091339f70bf24() { + const ret = Number.i_do_not_exist; + return ret; +}; + +export function __wbindgen_object_drop_ref(arg0) { + takeObject(arg0); +}; + +export function __wbindgen_throw(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + diff --git a/crates/cli/tests/reference/rename.rs b/crates/cli/tests/reference/rename.rs new file mode 100644 index 00000000000..78d01fec4fd --- /dev/null +++ b/crates/cli/tests/reference/rename.rs @@ -0,0 +1,90 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(auto_rename)] +extern "C" { + #[wasm_bindgen(js_name = foo_bar)] + fn foo_bar(); + fn qux_corge(); + #[wasm_bindgen(js_namespace = document)] + fn query_selector(query: &str) -> JsValue; + #[wasm_bindgen(getter, js_namespace = document)] + fn document_element() -> JsValue; + #[wasm_bindgen(getter, js_namespace = document)] + fn DOCUMENT_NODE() -> u32; + + #[wasm_bindgen(thread_local, js_namespace = Number)] + static MAX_SAFE_INTEGER: f64; + #[wasm_bindgen(thread_local, js_namespace = Number)] + static i_do_not_exist: f64; +} + +#[wasm_bindgen] +pub fn export_from_rust(a: u32) -> u32 { + foo_bar(); + qux_corge(); + query_selector(".class"); + document_element(); + assert!(MAX_SAFE_INTEGER.with(Clone::clone) + i_do_not_exist.with(Clone::clone) > 100.0); + + a +} + +#[wasm_bindgen(auto_rename)] +pub struct RustStruct { + pub foo: u32, + pub some_cool_field: u32, + #[wasm_bindgen(js_name = "another_field_for_you")] + pub another_field: u32, +} + +#[wasm_bindgen] +impl RustStruct { + pub fn i_dont_get_renamed() {} +} + +#[wasm_bindgen(auto_rename)] +impl RustStruct { + // methods + + pub fn increment_foo(&mut self, amount: Option) {} + pub fn set_foo(&mut self, foo: u32) {} + + #[wasm_bindgen(js_name = get_another)] + pub fn another(&self) -> u32 { + self.another_field + } + + pub fn static_method(a: u32) {} + + pub fn IHaveA_funky_name(a: u32) {} + + // getters/setters + + #[wasm_bindgen(getter)] + pub fn some_other_prop(&self) -> u32 { + 0 + } + #[wasm_bindgen(setter)] + pub fn set_some_other_prop(&self, value: u32) {} + + #[wasm_bindgen(getter = my_own_name)] + pub fn some_different_prop(&self) -> u32 { + 0 + } + #[wasm_bindgen(setter = my_own_name)] + pub fn set_some_different_prop(&self, value: u32) {} + + #[wasm_bindgen(getter, js_name = my_unique_name)] + pub fn some_unique_prop(&self) -> u32 { + 0 + } + #[wasm_bindgen(setter, js_name = my_unique_name)] + pub fn set_some_unique_prop(&self, value: u32) {} + + #[wasm_bindgen(getter)] + pub fn some_static_prop() -> u32 { + 0 + } + #[wasm_bindgen(setter)] + pub fn set_some_static_prop(value: u32) {} +} diff --git a/crates/cli/tests/reference/rename.wat b/crates/cli/tests/reference/rename.wat new file mode 100644 index 00000000000..fa7161af741 --- /dev/null +++ b/crates/cli/tests/reference/rename.wat @@ -0,0 +1,56 @@ +(module $reference_test.wasm + (type (;0;) (func)) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32))) + (type (;3;) (func (param i32) (result i32))) + (type (;4;) (func (param i32 i32))) + (type (;5;) (func (param i32 f64))) + (func $__wbg_get_ruststruct_foo (;0;) (type 3) (param i32) (result i32)) + (func $__wbg_get_ruststruct_someCoolField (;1;) (type 3) (param i32) (result i32)) + (func $__wbg_get_ruststruct_another_field_for_you (;2;) (type 3) (param i32) (result i32)) + (func $ruststruct_incrementFoo (;3;) (type 5) (param i32 f64)) + (func $__wbg_set_ruststruct_foo (;4;) (type 4) (param i32 i32)) + (func $__wbg_set_ruststruct_someCoolField (;5;) (type 4) (param i32 i32)) + (func $__wbg_set_ruststruct_another_field_for_you (;6;) (type 4) (param i32 i32)) + (func $ruststruct_get_another (;7;) (type 3) (param i32) (result i32)) + (func $ruststruct_someOtherProp (;8;) (type 3) (param i32) (result i32)) + (func $ruststruct_someDifferentProp (;9;) (type 3) (param i32) (result i32)) + (func $ruststruct_my_unique_name (;10;) (type 3) (param i32) (result i32)) + (func $ruststruct_setFoo (;11;) (type 4) (param i32 i32)) + (func $ruststruct_set_someOtherProp (;12;) (type 4) (param i32 i32)) + (func $ruststruct_set_someDifferentProp (;13;) (type 4) (param i32 i32)) + (func $ruststruct_set_my_unique_name (;14;) (type 4) (param i32 i32)) + (func $export_from_rust (;15;) (type 3) (param i32) (result i32)) + (func $__wbg_ruststruct_free (;16;) (type 4) (param i32 i32)) + (func $ruststruct_someStaticProp (;17;) (type 1) (result i32)) + (func $ruststruct_staticMethod (;18;) (type 2) (param i32)) + (func $ruststruct_IHaveA_funky_name (;19;) (type 2) (param i32)) + (func $ruststruct_set_someStaticProp (;20;) (type 2) (param i32)) + (func $ruststruct_i_dont_get_renamed (;21;) (type 0)) + (memory (;0;) 17) + (export "memory" (memory 0)) + (export "export_from_rust" (func $export_from_rust)) + (export "__wbg_ruststruct_free" (func $__wbg_ruststruct_free)) + (export "__wbg_get_ruststruct_foo" (func $__wbg_get_ruststruct_foo)) + (export "__wbg_set_ruststruct_foo" (func $__wbg_set_ruststruct_foo)) + (export "__wbg_get_ruststruct_someCoolField" (func $__wbg_get_ruststruct_someCoolField)) + (export "__wbg_set_ruststruct_someCoolField" (func $__wbg_set_ruststruct_someCoolField)) + (export "__wbg_get_ruststruct_another_field_for_you" (func $__wbg_get_ruststruct_another_field_for_you)) + (export "__wbg_set_ruststruct_another_field_for_you" (func $__wbg_set_ruststruct_another_field_for_you)) + (export "ruststruct_i_dont_get_renamed" (func $ruststruct_i_dont_get_renamed)) + (export "ruststruct_incrementFoo" (func $ruststruct_incrementFoo)) + (export "ruststruct_setFoo" (func $ruststruct_setFoo)) + (export "ruststruct_get_another" (func $ruststruct_get_another)) + (export "ruststruct_staticMethod" (func $ruststruct_staticMethod)) + (export "ruststruct_IHaveA_funky_name" (func $ruststruct_IHaveA_funky_name)) + (export "ruststruct_someOtherProp" (func $ruststruct_someOtherProp)) + (export "ruststruct_set_someOtherProp" (func $ruststruct_set_someOtherProp)) + (export "ruststruct_someDifferentProp" (func $ruststruct_someDifferentProp)) + (export "ruststruct_set_someDifferentProp" (func $ruststruct_set_someDifferentProp)) + (export "ruststruct_my_unique_name" (func $ruststruct_my_unique_name)) + (export "ruststruct_set_my_unique_name" (func $ruststruct_set_my_unique_name)) + (export "ruststruct_someStaticProp" (func $ruststruct_someStaticProp)) + (export "ruststruct_set_someStaticProp" (func $ruststruct_set_someStaticProp)) + (@custom "target_features" (after code) "\04+\0amultivalue+\0fmutable-globals+\0freference-types+\08sign-ext") +) + diff --git a/crates/macro-support/src/lib.rs b/crates/macro-support/src/lib.rs index dd609f42260..ea5c33dcb21 100644 --- a/crates/macro-support/src/lib.rs +++ b/crates/macro-support/src/lib.rs @@ -104,9 +104,62 @@ pub fn expand_class_marker( Ok(tokens) } +/// Turns the given string from lower_snake_case to camelCase. +/// +/// If the given string is not in lower_snake_case, this function returns None. +pub fn snake_case_to_camel_case(name: &str) -> Option { + // TODO: test + + let mut out = String::new(); + + let mut is_leading = true; + let mut seen_underscore = false; + + for c in name.chars() { + match c { + '_' => { + if is_leading { + // we want to keep leading underscores. + // e.g. "_foo_bar" -> "_fooBar" + out.push('_'); + } else if seen_underscore { + // double underscores are not allowed except for leading + // underscores. + return None; + } + + seen_underscore = true; + } + _ => { + if !c.is_ascii_alphanumeric() { + // We currently only support ASCII. + // This might change in the future. + return None; + } + if c.is_ascii_uppercase() { + // we only support lower_snake_case + return None; + } + + if seen_underscore { + out.push(c.to_ascii_uppercase()); + } else { + out.push(c); + } + + is_leading = false; + seen_underscore = false; + } + } + } + + Some(out) +} + struct ClassMarker { class: syn::Ident, js_class: String, + auto_rename: bool, wasm_bindgen: syn::Path, wasm_bindgen_futures: syn::Path, } @@ -121,6 +174,9 @@ impl Parse for ClassMarker { .map(String::from) .unwrap_or(js_class); + input.parse::>()?; + let auto_rename = input.parse::()?.value(); + let mut wasm_bindgen = None; let mut wasm_bindgen_futures = None; @@ -162,6 +218,7 @@ impl Parse for ClassMarker { Ok(ClassMarker { class, js_class, + auto_rename, wasm_bindgen: wasm_bindgen.unwrap_or_else(|| syn::parse_quote! { wasm_bindgen }), wasm_bindgen_futures: wasm_bindgen_futures .unwrap_or_else(|| syn::parse_quote! { wasm_bindgen_futures }), diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 72bd849d859..bb441c6778f 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -15,7 +15,7 @@ use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ItemFn, Lit, MacroDelimiter, ReturnType}; -use crate::ClassMarker; +use crate::{snake_case_to_camel_case, ClassMarker}; thread_local!(static ATTRS: AttributeParseState = Default::default()); @@ -77,6 +77,7 @@ macro_rules! attrgen { (readonly, Readonly(Span)), (js_name, JsName(Span, String, Span)), (js_class, JsClass(Span, String, Span)), + (auto_rename, AutoRename(Span)), (inspectable, Inspectable(Span)), (is_type_of, IsTypeOf(Span, syn::Expr)), (extends, Extends(Span, syn::Path)), @@ -424,6 +425,7 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs)> for &'a mut syn::ItemStruct .map(|s| s.0.to_string()) .unwrap_or(self.ident.unraw().to_string()); let is_inspectable = attrs.inspectable().is_some(); + let auto_rename = attrs.auto_rename().is_some(); let getter_with_clone = attrs.getter_with_clone(); for (i, field) in self.fields.iter_mut().enumerate() { match field.vis { @@ -443,7 +445,7 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs)> for &'a mut syn::ItemStruct let js_field_name = match attrs.js_name() { Some((name, _)) => name.to_string(), - None => js_field_name, + None => camel_caseify(js_field_name, auto_rename), }; let comments = extract_doc_comments(&field.attrs); @@ -481,6 +483,15 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs)> for &'a mut syn::ItemStruct } } +fn camel_caseify(name: String, auto_rename: bool) -> String { + if auto_rename { + if let Some(camel_case) = snake_case_to_camel_case(&name) { + return camel_case; + } + } + name +} + fn get_ty(mut ty: &syn::Type) -> &syn::Type { while let syn::Type::Group(g) = ty { ty = &g.elem; @@ -497,14 +508,12 @@ fn get_expr(mut expr: &syn::Expr) -> &syn::Expr { expr } -impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option)> - for syn::ForeignItemFn -{ +impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a ForeignItemCtx)> for syn::ForeignItemFn { type Target = ast::ImportKind; fn convert( self, - (program, opts, module): (&ast::Program, BindgenAttrs, &'a Option), + (program, opts, context): (&ast::Program, BindgenAttrs, &'a ForeignItemCtx), ) -> Result { let mut wasm = function_from_decl( &self.sig.ident, @@ -514,6 +523,7 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option (0, "n"), ast::ImportFunctionKind::Method { ref class, .. } => (1, &class[..]), }; - let data = (ns, &self.sig.ident, module); + let data = (ns, &self.sig.ident, &context.module); format!( "__wbg_{}_{}", wasm.name @@ -876,6 +886,7 @@ impl ConvertToAst for syn::ItemFn { false, None, false, + false, Some(&["default"]), )?; attrs.check_used(); @@ -920,6 +931,7 @@ fn function_from_decl( vis: syn::Visibility, allow_self: bool, self_ty: Option<&Ident>, + auto_rename: bool, is_from_impl: bool, skip_keywords: Option<&[&str]>, ) -> Result<(ast::Function, Option), Diagnostic> { @@ -1004,39 +1016,35 @@ fn function_from_decl( syn::ReturnType::Type(_, ty) => Some(replace_self(*ty)), }; - let (name, name_span, renamed_via_js_name) = - if let Some((js_name, js_name_span)) = opts.js_name() { - let kind = operation_kind(opts); - let prefix = match kind { - OperationKind::Setter(_) => "set_", - _ => "", - }; - let name = if prefix.is_empty() - && opts.method().is_none() - && is_js_keyword(js_name, skip_keywords) - { - format!("_{}", js_name) - } else { - format!("{}{}", prefix, js_name) - }; - (name, js_name_span, true) + // To ensure unique function names in the binary, we need the names of + // setters to start with `set_`. This prefix will be removed in later + // stages os the JS side never sees it. + let is_setter = matches!(operation_kind(opts), OperationKind::Setter(_)); + let prefix = if is_setter { "set_" } else { "" }; + let (mut name, name_span) = if let Some((js_name, js_name_span)) = opts.js_name() { + let name = format!("{}{}", prefix, js_name); + (name, js_name_span) + } else { + let ident = decl_name.unraw().to_string(); + let name = if is_setter && ident.starts_with("set_") { + let ident = camel_caseify(ident[4..].to_string(), auto_rename); + format!("set_{}", ident) } else { - let name = if !is_from_impl - && opts.method().is_none() - && is_js_keyword(&decl_name.to_string(), skip_keywords) - { - format!("_{}", decl_name.unraw()) - } else { - decl_name.unraw().to_string() - }; - (name, decl_name.span(), false) + camel_caseify(ident, auto_rename) }; + (name, decl_name.span()) + }; + + // add underscore if the name is a keyword + if !is_from_impl && opts.method().is_none() && is_js_keyword(&name, skip_keywords) { + name = format!("_{}", name); + } + Ok(( ast::Function { arguments, name_span, name, - renamed_via_js_name, ret, rust_attrs: attrs, rust_vis: vis, @@ -1256,6 +1264,8 @@ fn prepare_for_impl_recursion( .map(|s| s.0.to_string()) .unwrap_or(ident.to_string()); + let auto_rename = impl_opts.auto_rename().is_some(); + let wasm_bindgen = &program.wasm_bindgen; let wasm_bindgen_futures = &program.wasm_bindgen_futures; method.attrs.insert( @@ -1264,7 +1274,7 @@ fn prepare_for_impl_recursion( pound_token: Default::default(), style: syn::AttrStyle::Outer, bracket_token: Default::default(), - meta: syn::parse_quote! { #wasm_bindgen::prelude::__wasm_bindgen_class_marker(#class = #js_class, wasm_bindgen = #wasm_bindgen, wasm_bindgen_futures = #wasm_bindgen_futures) }, + meta: syn::parse_quote! { #wasm_bindgen::prelude::__wasm_bindgen_class_marker(#class = #js_class, #auto_rename, wasm_bindgen = #wasm_bindgen, wasm_bindgen_futures = #wasm_bindgen_futures) }, }, ); @@ -1278,6 +1288,7 @@ impl<'a> MacroParse<&ClassMarker> for &'a mut syn::ImplItemFn { ClassMarker { class, js_class, + auto_rename: auto_camel_case, wasm_bindgen, wasm_bindgen_futures, }: &ClassMarker, @@ -1309,6 +1320,7 @@ impl<'a> MacroParse<&ClassMarker> for &'a mut syn::ImplItemFn { self.vis.clone(), true, Some(class), + *auto_camel_case, true, None, )?; @@ -1623,10 +1635,12 @@ impl MacroParse for syn::ItemForeignMod { let module = module_from_opts(program, &opts) .map_err(|e| errors.push(e)) .unwrap_or_default(); + let auto_rename = opts.auto_rename().is_some(); for item in self.items.into_iter() { let ctx = ForeignItemCtx { module: module.clone(), js_namespace: js_namespace.clone(), + auto_rename, }; if let Err(e) = item.macro_parse(program, ctx) { errors.push(e); @@ -1641,6 +1655,7 @@ impl MacroParse for syn::ItemForeignMod { struct ForeignItemCtx { module: Option, js_namespace: Option>, + auto_rename: bool, } impl MacroParse for syn::ForeignItem { @@ -1676,18 +1691,17 @@ impl MacroParse for syn::ForeignItem { let js_namespace = item_opts .js_namespace() .map(|(s, _)| s.to_owned()) - .or(ctx.js_namespace); - let module = ctx.module; + .or_else(|| ctx.js_namespace.clone()); let kind = match self { - syn::ForeignItem::Fn(f) => f.convert((program, item_opts, &module))?, + syn::ForeignItem::Fn(f) => f.convert((program, item_opts, &ctx))?, syn::ForeignItem::Type(t) => t.convert((program, item_opts))?, - syn::ForeignItem::Static(s) => s.convert((program, item_opts, &module))?, + syn::ForeignItem::Static(s) => s.convert((program, item_opts, &ctx.module))?, _ => panic!("only foreign functions/types allowed for now"), }; program.imports.push(ast::Import { - module, + module: ctx.module, js_namespace, kind, }); diff --git a/crates/macro/ui-tests/rename.rs b/crates/macro/ui-tests/rename.rs new file mode 100644 index 00000000000..54bd37336af --- /dev/null +++ b/crates/macro/ui-tests/rename.rs @@ -0,0 +1,29 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(auto_rename)] +fn foo_bar() {} + +#[wasm_bindgen(auto_rename)] +extern "C" { + #[wasm_bindgen(auto_rename)] + fn foo_bar_again(); + + #[wasm_bindgen(auto_rename)] + type A; +} + +#[wasm_bindgen(auto_rename)] +pub struct Foo { + #[wasm_bindgen(auto_rename)] + pub foo: u32, +} + +#[wasm_bindgen(auto_rename)] +impl Foo { + #[wasm_bindgen(auto_rename)] + pub fn foo(&self) {} + + pub fn isShouldCauseAWarning(&self) {} +} + +fn main() {} diff --git a/crates/macro/ui-tests/rename.stderr b/crates/macro/ui-tests/rename.stderr new file mode 100644 index 00000000000..45a998fe56d --- /dev/null +++ b/crates/macro/ui-tests/rename.stderr @@ -0,0 +1,31 @@ +error: can only #[wasm_bindgen] public functions + --> ui-tests/rename.rs:4:1 + | +4 | fn foo_bar() {} + | ^^^^^^^^^^^^^^^ + +warning: unused variable: `auto_rename` + --> ui-tests/rename.rs:8:20 + | +8 | #[wasm_bindgen(auto_rename)] + | ^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_auto_rename` + | + = note: `#[warn(unused_variables)]` on by default + +warning: unused variable: `auto_rename` + --> ui-tests/rename.rs:11:20 + | +11 | #[wasm_bindgen(auto_rename)] + | ^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_auto_rename` + +warning: unused variable: `auto_rename` + --> ui-tests/rename.rs:17:20 + | +17 | #[wasm_bindgen(auto_rename)] + | ^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_auto_rename` + +warning: unused variable: `auto_rename` + --> ui-tests/rename.rs:23:20 + | +23 | #[wasm_bindgen(auto_rename)] + | ^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_auto_rename` diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 9ef51ede23a..0f3a309c193 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -39,6 +39,7 @@ - [Receiving JS Closures in Rust](./reference/receiving-js-closures-in-rust.md) - [`Promise`s and `Future`s](./reference/js-promises-and-rust-futures.md) - [Iterating over JS Values](./reference/iterating-over-js-values.md) + - [Automatic renaming](./reference/automatic-renaming.md) - [Arbitrary Data with Serde](./reference/arbitrary-data-with-serde.md) - [Accessing Properties of Untyped JS Values](./reference/accessing-properties-of-untyped-js-values.md) - [Working with Duck-Typed Interfaces](./reference/working-with-duck-typed-interfaces.md) @@ -65,6 +66,7 @@ - [`Result`](./reference/types/result.md) - [`#[wasm_bindgen]` Attributes](./reference/attributes/index.md) - [On JavaScript Imports](./reference/attributes/on-js-imports/index.md) + - [`auto_rename`](./reference/attributes/on-js-imports/auto_rename.md) - [`catch`](./reference/attributes/on-js-imports/catch.md) - [`constructor`](./reference/attributes/on-js-imports/constructor.md) - [`extends`](./reference/attributes/on-js-imports/extends.md) @@ -84,6 +86,7 @@ - [`variadic`](./reference/attributes/on-js-imports/variadic.md) - [`vendor_prefix`](./reference/attributes/on-js-imports/vendor_prefix.md) - [On Rust Exports](./reference/attributes/on-rust-exports/index.md) + - [`auto_rename`](./reference/attributes/on-rust-exports/auto_rename.md) - [`constructor`](./reference/attributes/on-rust-exports/constructor.md) - [`js_name = Blah`](./reference/attributes/on-rust-exports/js_name.md) - [`js_class = Blah`](./reference/attributes/on-rust-exports/js_class.md) diff --git a/guide/src/reference/attributes/on-js-imports/auto_rename.md b/guide/src/reference/attributes/on-js-imports/auto_rename.md new file mode 100644 index 00000000000..7b2a567be00 --- /dev/null +++ b/guide/src/reference/attributes/on-js-imports/auto_rename.md @@ -0,0 +1,34 @@ +# `auto_rename` + +> **Note**: This attribute is only available `wasm-bindgen` in versions 0.2.96 and later. + +> **Note**: See the page about [automatic renaming](../../automatic-renaming.md.md) for more general information about this attribute. + +`wasm-bindgen` generally encourages users to follow Rust naming conventions and use `js_name` to specify the JavaScript name of functions, types, etc. However, this can be very tedious since JavaScript typically uses camelCase for function names, requiring the user to specify the `js_name` attribute for almost every function. + +The `auto_rename` attribute makes this easier by automatically setting the `js_name` of functions to the camelCase version of the Rust function name. This is useful when the Rust function name is in snake_case and the JavaScript function name is in camelCase. + +Example: + +```rust +#[wasm_bindgen(auto_rename, js_namespace = document)] +extern "C" { + // the JS name will be inferred as `createElement` + fn create_element(tag_name: String) -> JsValue; + + // the JS name will be inferred as `createElementWithOptions`, so it has + // to be set explicitly using the `js_name` attribute + #[wasm_bindgen(js_name = createElement)] + fn create_element_with_options(tag_name: String, options: JsValue) -> JsValue; + + // since the function name is not in snake_case, the `auto_rename` attribute + // has no effect, and the JS name will be inferred as `documentElement` + // + // Note: This is *NOT* recommended, as it is not idiomatic Rust. Always use + // snake_case for function names in Rust. + #[wasm_bindgen(getter)] + fn documentElement() -> JsValue; +} +``` + +> **Note**: The `auto_rename` attribute is valid on `extern` blocks, but not on individual functions inside `extern` blocks. diff --git a/guide/src/reference/attributes/on-rust-exports/auto_rename.md b/guide/src/reference/attributes/on-rust-exports/auto_rename.md new file mode 100644 index 00000000000..523ea9cbbf3 --- /dev/null +++ b/guide/src/reference/attributes/on-rust-exports/auto_rename.md @@ -0,0 +1,46 @@ +# `auto_rename` + +> **Note**: This attribute is only available `wasm-bindgen` in versions 0.2.96 and later. + +> **Note**: See the page about [automatic renaming](../../automatic-renaming.md.md) for more general information about this attribute. + +`wasm-bindgen` generally encourages users to follow Rust naming conventions and use `js_name` to specify the JavaScript name of functions, types, etc. However, this can be very tedious since JavaScript typically uses camelCase for function names, requiring the user to specify the `js_name` attribute for almost every function. + +The `auto_rename` attribute makes this easier by automatically setting the `js_name` of functions/methods/field to the camelCase version of the Rust function name. This is useful when the Rust function name is in snake_case and the JavaScript function name is in camelCase. + +Example: + +```rust +#[wasm_bindgen(auto_rename)] +pub struct MetalHead { + // the JS name will be inferred as `songsWritten` + pub songs_written: u32, + // the JS name will be inferred as `headbangsPerMinute` + pub headbangs_per_minute: f64, +} + +#[wasm_bindgen(auto_rename)] +impl MetalHead { + // the JS name will be inferred as `powerLevel` + #[wasm_bindgen(getter)] + pub fn power_level(&self) -> f64 { + self.songs_written as f64 * self.headbangs_per_minute + } + + // the JS name will be inferred as `enterMoshpit` + pub fn enter_moshpit(&self) { + // ... + } + + // since the function name is not in snake_case, the `auto_rename` attribute + // has no effect, and the JS name will be inferred as `SCREAM` + // + // Note: This is *NOT* recommended, as it is not idiomatic Rust. Always use + // snake_case for function names in Rust. + pub fn SCREAM(&self) { + // ... + } +} +``` + +> **Note**: The `auto_rename` attribute is valid on `struct`s and `impl` blocks, but not on individual functions or fields. diff --git a/guide/src/reference/automatic-renaming.md.md b/guide/src/reference/automatic-renaming.md.md new file mode 100644 index 00000000000..50e8a3bd011 --- /dev/null +++ b/guide/src/reference/automatic-renaming.md.md @@ -0,0 +1,20 @@ +# Automatic renaming + +`wasm-bindgen` encourages users to follow [Rust naming conventions](https://rust-lang.github.io/api-guidelines/naming.html) in their projects. However, while JavaScript has no official naming conventions, the JavaScript community typically uses camelCase for function, method, and field/property names. This makes it quite tedious to define JavaScript APIs in Rust (both importing and exporting), since a lot of functions and properties require a `js_name` attribute to explicitly specify the camelCase JavaScript name. + +Here's an overview comparing Rust and JavaScript naming conventions: + +| Feature | Rust | JavaScript | +| --------------------------------------- | ---------------------- | ----------------------------------- | +| `class`/`struct` | `PascalCase` | `PascalCase` | +| Anything type-like, e.g. `type`, `enum` | `PascalCase` | `PascalCase` | +| Enum variants | `PascalCase` | `PascalCase` (in TS) | +| Functions | `snake_case` | `camelCase` | +| Methods | `snake_case` | `camelCase` | +| Fields, properties (getter/setter) | `snake_case` | `camelCase` | +| Local variables | `snake_case` | `camelCase` | +| Constants | `SCREAMING_SNAKE_CASE` | `SCREAMING_SNAKE_CASE` (very often) | + +(While some JavaScript libraries may use different conventions, this table still holds true for most JavaScript code.) + +TODO: write docs