From 16d5243362df82aa63018a8ea39344f79f79b780 Mon Sep 17 00:00:00 2001
From: Alex Crichton <alex@alexcrichton.com>
Date: Mon, 5 Nov 2018 12:29:14 -0800
Subject: [PATCH] Implement support for `js_class` on exported types

Allow defining types which have different names in Rust than they have
in JS! (just like can be done with imported types)

Closes #1010
---
 crates/backend/src/ast.rs                     | 17 ++++---
 crates/backend/src/codegen.rs                 | 12 ++---
 crates/backend/src/encode.rs                  |  4 +-
 crates/macro-support/src/parser.rs            | 48 ++++++++++++-------
 .../attributes/on-rust-exports/js_class.md    | 28 +++++++++++
 .../attributes/on-rust-exports/js_name.md     | 30 ++++++++++++
 tests/wasm/classes.js                         |  7 +++
 tests/wasm/classes.rs                         | 27 +++++++++++
 8 files changed, 142 insertions(+), 31 deletions(-)
 create mode 100644 guide/src/reference/attributes/on-rust-exports/js_class.md

diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs
index ff6f3db1..6dcf799a 100644
--- a/crates/backend/src/ast.rs
+++ b/crates/backend/src/ast.rs
@@ -29,8 +29,10 @@ pub struct Program {
 #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
 #[derive(Clone)]
 pub struct Export {
-    /// The javascript class name.
-    pub class: Option<Ident>,
+    /// The struct name, in Rust, this is attached to
+    pub rust_class: Option<Ident>,
+    /// The class name in JS this is attached to
+    pub js_class: Option<String>,
     /// The type of `self` (either `self`, `&self`, or `&mut self`)
     pub method_self: Option<MethodSelf>,
     /// Whether or not this export is flagged as a constructor, returning an
@@ -176,7 +178,8 @@ pub struct Function {
 #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
 #[derive(Clone)]
 pub struct Struct {
-    pub name: Ident,
+    pub rust_name: Ident,
+    pub js_name: String,
     pub fields: Vec<StructField>,
     pub comments: Vec<String>,
 }
@@ -264,9 +267,9 @@ impl Export {
     /// name and class name, if the function belongs to a javascript class.
     pub(crate) fn rust_symbol(&self) -> Ident {
         let mut generated_name = String::from("__wasm_bindgen_generated");
-        if let Some(class) = &self.class {
+        if let Some(class) = &self.js_class {
             generated_name.push_str("_");
-            generated_name.push_str(&class.to_string());
+            generated_name.push_str(class);
         }
         generated_name.push_str("_");
         generated_name.push_str(&self.function.name.to_string());
@@ -278,8 +281,8 @@ impl Export {
     /// "high level" form before calling the actual function.
     pub(crate) fn export_name(&self) -> String {
         let fn_name = self.function.name.to_string();
-        match &self.class {
-            Some(class) => shared::struct_function_export_name(&class.to_string(), &fn_name),
+        match &self.js_class {
+            Some(class) => shared::struct_function_export_name(class, &fn_name),
             None => shared::free_function_export_name(&fn_name),
         }
     }
diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs
index c9d1f63c..3df58c40 100644
--- a/crates/backend/src/codegen.rs
+++ b/crates/backend/src/codegen.rs
@@ -118,8 +118,8 @@ impl TryToTokens for ast::Program {
 
 impl ToTokens for ast::Struct {
     fn to_tokens(&self, tokens: &mut TokenStream) {
-        let name = &self.name;
-        let name_str = name.to_string();
+        let name = &self.rust_name;
+        let name_str = self.js_name.to_string();
         let name_len = name_str.len() as u32;
         let name_chars = name_str.chars().map(|c| c as u32);
         let new_fn = Ident::new(&shared::new_function(&name_str), Span::call_site());
@@ -328,7 +328,7 @@ impl TryToTokens for ast::Export {
         let name = &self.rust_name;
         let receiver = match self.method_self {
             Some(ast::MethodSelf::ByValue) => {
-                let class = self.class.as_ref().unwrap();
+                let class = self.rust_class.as_ref().unwrap();
                 arg_conversions.push(quote! {
                     let me = unsafe {
                         <#class as ::wasm_bindgen::convert::FromWasmAbi>::from_abi(
@@ -340,7 +340,7 @@ impl TryToTokens for ast::Export {
                 quote! { me.#name }
             }
             Some(ast::MethodSelf::RefMutable) => {
-                let class = self.class.as_ref().unwrap();
+                let class = self.rust_class.as_ref().unwrap();
                 arg_conversions.push(quote! {
                     let mut me = unsafe {
                         <#class as ::wasm_bindgen::convert::RefMutFromWasmAbi>
@@ -354,7 +354,7 @@ impl TryToTokens for ast::Export {
                 quote! { me.#name }
             }
             Some(ast::MethodSelf::RefShared) => {
-                let class = self.class.as_ref().unwrap();
+                let class = self.rust_class.as_ref().unwrap();
                 arg_conversions.push(quote! {
                     let me = unsafe {
                         <#class as ::wasm_bindgen::convert::RefFromWasmAbi>
@@ -367,7 +367,7 @@ impl TryToTokens for ast::Export {
                 });
                 quote! { me.#name }
             }
-            None => match &self.class {
+            None => match &self.rust_class {
                 Some(class) => quote! { #class::#name },
                 None => quote! { #name },
             },
diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs
index 04d92436..b82ca2a7 100644
--- a/crates/backend/src/encode.rs
+++ b/crates/backend/src/encode.rs
@@ -58,7 +58,7 @@ fn shared_export<'a>(export: &'a ast::Export, intern: &'a Interner) -> Export<'a
         None => (false, false),
     };
     Export {
-        class: export.class.as_ref().map(|s| intern.intern(s)),
+        class: export.js_class.as_ref().map(|s| &**s),
         method,
         consumed,
         is_constructor: export.is_constructor,
@@ -187,7 +187,7 @@ fn shared_import_enum<'a>(_i: &'a ast::ImportEnum, _intern: &'a Interner)
 
 fn shared_struct<'a>(s: &'a ast::Struct, intern: &'a Interner) -> Struct<'a> {
     Struct {
-        name: intern.intern(&s.name),
+        name: &s.js_name,
         fields: s.fields.iter().map(|s| shared_struct_field(s, intern)).collect(),
         comments: s.comments.iter().map(|s| &**s).collect(),
     }
diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs
index 459bbd1f..23dc483f 100644
--- a/crates/macro-support/src/parser.rs
+++ b/crates/macro-support/src/parser.rs
@@ -304,7 +304,11 @@ impl Parse for BindgenAttr {
         }
         if attr == "js_class" {
             input.parse::<Token![=]>()?;
-            return Ok(BindgenAttr::JsClass(input.parse::<syn::LitStr>()?.value()));
+            let val = match input.parse::<syn::LitStr>() {
+                Ok(str) => str.value(),
+                Err(_) => input.parse::<AnyIdent>()?.0.to_string(),
+            };
+            return Ok(BindgenAttr::JsClass(val));
         }
         if attr == "js_name" {
             input.parse::<Token![=]>()?;
@@ -346,10 +350,10 @@ trait ConvertToAst<Ctx> {
     fn convert(self, context: Ctx) -> Result<Self::Target, Diagnostic>;
 }
 
-impl<'a> ConvertToAst<()> for &'a mut syn::ItemStruct {
+impl<'a> ConvertToAst<BindgenAttrs> for &'a mut syn::ItemStruct {
     type Target = ast::Struct;
 
-    fn convert(self, (): ()) -> Result<Self::Target, Diagnostic> {
+    fn convert(self, opts: BindgenAttrs) -> Result<Self::Target, Diagnostic> {
         if self.generics.params.len() > 0 {
             bail_span!(
                 self.generics,
@@ -358,6 +362,9 @@ impl<'a> ConvertToAst<()> for &'a mut syn::ItemStruct {
             );
         }
         let mut fields = Vec::new();
+        let js_name = opts.js_name()
+            .map(|s| s.0.to_string())
+            .unwrap_or(self.ident.to_string());
         if let syn::Fields::Named(names) = &mut self.fields {
             for field in names.named.iter_mut() {
                 match field.vis {
@@ -368,10 +375,9 @@ impl<'a> ConvertToAst<()> for &'a mut syn::ItemStruct {
                     Some(n) => n,
                     None => continue,
                 };
-                let ident = self.ident.to_string();
                 let name_str = name.to_string();
-                let getter = shared::struct_field_get(&ident, &name_str);
-                let setter = shared::struct_field_set(&ident, &name_str);
+                let getter = shared::struct_field_get(&js_name, &name_str);
+                let setter = shared::struct_field_set(&js_name, &name_str);
                 let opts = BindgenAttrs::find(&mut field.attrs)?;
                 assert_not_variadic(&opts, &field)?;
                 let comments = extract_doc_comments(&field.attrs);
@@ -388,7 +394,8 @@ impl<'a> ConvertToAst<()> for &'a mut syn::ItemStruct {
         }
         let comments: Vec<String> = extract_doc_comments(&self.attrs);
         Ok(ast::Struct {
-            name: self.ident.clone(),
+            rust_name: self.ident.clone(),
+            js_name,
             fields,
             comments,
         })
@@ -755,7 +762,8 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
                 f.to_tokens(tokens);
                 let opts = opts.unwrap_or_default();
                 program.exports.push(ast::Export {
-                    class: None,
+                    rust_class: None,
+                    js_class: None,
                     method_self: None,
                     is_constructor: false,
                     comments,
@@ -764,11 +772,13 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
                 });
             }
             syn::Item::Struct(mut s) => {
-                program.structs.push((&mut s).convert(())?);
+                let opts = opts.unwrap_or_default();
+                program.structs.push((&mut s).convert(opts)?);
                 s.to_tokens(tokens);
             }
             syn::Item::Impl(mut i) => {
-                (&mut i).macro_parse(program, ())?;
+                let opts = opts.unwrap_or_default();
+                (&mut i).macro_parse(program, opts)?;
                 i.to_tokens(tokens);
             }
             syn::Item::ForeignMod(mut f) => {
@@ -793,8 +803,8 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
     }
 }
 
-impl<'a> MacroParse<()> for &'a mut syn::ItemImpl {
-    fn macro_parse(self, program: &mut ast::Program, (): ()) -> Result<(), Diagnostic> {
+impl<'a> MacroParse<BindgenAttrs> for &'a mut syn::ItemImpl {
+    fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> {
         if self.defaultness.is_some() {
             bail_span!(
                 self.defaultness,
@@ -828,7 +838,7 @@ impl<'a> MacroParse<()> for &'a mut syn::ItemImpl {
         };
         let mut errors = Vec::new();
         for item in self.items.iter_mut() {
-            if let Err(e) = (&name, item).macro_parse(program, ()) {
+            if let Err(e) = (&name, item).macro_parse(program, &opts) {
                 errors.push(e);
             }
         }
@@ -836,8 +846,10 @@ impl<'a> MacroParse<()> for &'a mut syn::ItemImpl {
     }
 }
 
-impl<'a, 'b> MacroParse<()> for (&'a Ident, &'b mut syn::ImplItem) {
-    fn macro_parse(self, program: &mut ast::Program, (): ()) -> Result<(), Diagnostic> {
+impl<'a, 'b> MacroParse<&'a BindgenAttrs> for (&'a Ident, &'b mut syn::ImplItem) {
+    fn macro_parse(self, program: &mut ast::Program, impl_opts: &'a BindgenAttrs)
+        -> Result<(), Diagnostic>
+    {
         let (class, item) = self;
         let method = match item {
             syn::ImplItem::Method(ref mut m) => m,
@@ -889,9 +901,13 @@ impl<'a, 'b> MacroParse<()> for (&'a Ident, &'b mut syn::ImplItem) {
             true,
             Some(class),
         )?;
+        let js_class = impl_opts.js_class()
+            .map(|s| s.to_string())
+            .unwrap_or(class.to_string());
 
         program.exports.push(ast::Export {
-            class: Some(class.clone()),
+            rust_class: Some(class.clone()),
+            js_class: Some(js_class),
             method_self,
             is_constructor,
             function,
diff --git a/guide/src/reference/attributes/on-rust-exports/js_class.md b/guide/src/reference/attributes/on-rust-exports/js_class.md
new file mode 100644
index 00000000..1952b1e3
--- /dev/null
+++ b/guide/src/reference/attributes/on-rust-exports/js_class.md
@@ -0,0 +1,28 @@
+# `js_class = Blah`
+
+The `js_class` attribute is used to indicate that all the methods inside an
+`impl` block should be attached to the specified JS class instead of inferring
+it from the self type in the `impl` block. The `js_class` attribute is most
+frequently paired with [the `js_name` attribute](js_name.html) on structs:
+
+```rust
+#[wasm_bindgen(js_name = Foo)]
+pub struct JsFoo { /* ... */ }
+
+#[wasm_bindgen(js_class = Foo)]
+impl JsFoo {
+    #[wasm_bindgen(constructor)]
+    pub fn new() -> JsFoo { /* ... */ }
+
+    pub fn foo(&self) { /* ... */ }
+}
+```
+
+which is accessed like:
+
+```rust
+import { Foo } from './my_module';
+
+const x = new Foo();
+x.foo();
+```
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 564d8cd0..b6a88385 100644
--- a/guide/src/reference/attributes/on-rust-exports/js_name.md
+++ b/guide/src/reference/attributes/on-rust-exports/js_name.md
@@ -22,3 +22,33 @@ import { doTheThing } from './my_module';
 const x = doTheThing();
 console.log(x);
 ```
+
+Like imports, `js_name` can also be used to rename types exported to JS:
+
+```rust
+#[wasm_bindgen(js_name = Foo)]
+pub struct JsFoo {
+    // ..
+}
+```
+
+to be accessed like:
+
+```js
+import { Foo } from './my_module';
+
+// ...
+```
+
+Note that attaching methods to the JS class `Foo` should be done via the
+[`js_class` attribute](js_class.html):
+
+```rust
+#[wasm_bindgen(js_name = Foo)]
+pub struct JsFoo { /* ... */ }
+
+#[wasm_bindgen(js_class = Foo)]
+impl JsFoo {
+    // ...
+}
+```
diff --git a/tests/wasm/classes.js b/tests/wasm/classes.js
index 4e0408bf..1b47d378 100644
--- a/tests/wasm/classes.js
+++ b/tests/wasm/classes.js
@@ -136,3 +136,10 @@ exports.js_js_rename = () => {
 exports.js_access_fields = () => {
     assert.ok((new wasm.AccessFieldFoo()).bar instanceof wasm.AccessFieldBar);
 };
+
+exports.js_renamed_export = () => {
+    const x = new wasm.JsRenamedExport();
+    assert.ok(x.x === 3);
+    x.foo();
+    x.bar(x);
+};
diff --git a/tests/wasm/classes.rs b/tests/wasm/classes.rs
index b01763d7..10b14009 100644
--- a/tests/wasm/classes.rs
+++ b/tests/wasm/classes.rs
@@ -21,6 +21,7 @@ extern "C" {
     fn js_double_consume();
     fn js_js_rename();
     fn js_access_fields();
+    fn js_renamed_export();
 }
 
 #[wasm_bindgen_test]
@@ -379,3 +380,29 @@ impl AccessFieldFoo {
 fn access_fields() {
     js_access_fields();
 }
+
+#[wasm_bindgen(js_name = JsRenamedExport)]
+pub struct RenamedExport {
+    pub x: u32,
+}
+
+#[wasm_bindgen(js_class = JsRenamedExport)]
+impl RenamedExport {
+    #[wasm_bindgen(constructor)]
+    pub fn new() -> RenamedExport{
+        RenamedExport {
+            x: 3,
+        }
+    }
+    pub fn foo(&self) {
+    }
+
+    pub fn bar(&self, other: &RenamedExport) {
+        drop(other);
+    }
+}
+
+#[wasm_bindgen_test]
+fn renamed_export() {
+    js_renamed_export();
+}