diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs
index c54a27be..78e26af4 100644
--- a/crates/backend/src/ast.rs
+++ b/crates/backend/src/ast.rs
@@ -34,19 +34,18 @@ pub struct Program {
 #[cfg_attr(feature = "extra-traits", derive(Debug))]
 #[derive(Clone)]
 pub struct Export {
-    /// 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
-    /// instance of the `impl` type
-    pub is_constructor: bool,
-    /// The rust function
-    pub function: Function,
     /// Comments extracted from the rust source.
     pub comments: Vec<String>,
+    /// The rust function
+    pub function: Function,
+    /// The class name in JS this is attached to
+    pub js_class: Option<String>,
+    /// The kind (static, named, regular)
+    pub method_kind: MethodKind,
+    /// The type of `self` (either `self`, `&self`, or `&mut self`)
+    pub method_self: Option<MethodSelf>,
+    /// The struct name, in Rust, this is attached to
+    pub rust_class: Option<Ident>,
     /// The name of the rust function/method on the rust side.
     pub rust_name: Ident,
     /// Whether or not this function should be flagged as the wasm start
@@ -342,28 +341,28 @@ impl ImportKind {
     }
 }
 
-impl ImportFunction {
+impl Function {
     /// If the rust object has a `fn xxx(&self) -> MyType` method, get the name for a getter in
     /// javascript (in this case `xxx`, so you can write `val = obj.xxx`)
     pub fn infer_getter_property(&self) -> &str {
-        &self.function.name
+        &self.name
     }
 
     /// If the rust object has a `fn set_xxx(&mut self, MyType)` style method, get the name
     /// for a setter in javascript (in this case `xxx`, so you can write `obj.xxx = val`)
     pub fn infer_setter_property(&self) -> Result<String, Diagnostic> {
-        let name = self.function.name.to_string();
+        let name = self.name.to_string();
 
         // if `#[wasm_bindgen(js_name = "...")]` is used then that explicitly
         // because it was hand-written anyway.
-        if self.function.renamed_via_js_name {
+        if self.renamed_via_js_name {
             return Ok(name);
         }
 
         // Otherwise we infer names based on the Rust function name.
         if !name.starts_with("set_") {
             bail_span!(
-                syn::token::Pub(self.function.name_span),
+                syn::token::Pub(self.name_span),
                 "setters must start with `set_`, found: {}",
                 name,
             );
diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs
index ae8d4cd3..66711f1f 100644
--- a/crates/backend/src/encode.rs
+++ b/crates/backend/src/encode.rs
@@ -125,7 +125,7 @@ fn shared_program<'a>(
             .exports
             .iter()
             .map(|a| shared_export(a, intern))
-            .collect(),
+            .collect::<Result<Vec<_>, _>>()?,
         structs: prog
             .structs
             .iter()
@@ -172,21 +172,23 @@ fn shared_program<'a>(
     })
 }
 
-fn shared_export<'a>(export: &'a ast::Export, intern: &'a Interner) -> Export<'a> {
-    let (method, consumed) = match export.method_self {
-        Some(ast::MethodSelf::ByValue) => (true, true),
-        Some(_) => (true, false),
-        None => (false, false),
+fn shared_export<'a>(
+    export: &'a ast::Export,
+    intern: &'a Interner,
+) -> Result<Export<'a>, Diagnostic> {
+    let consumed = match export.method_self {
+        Some(ast::MethodSelf::ByValue) => true,
+        _ => false,
     };
-    Export {
+    let method_kind = from_ast_method_kind(&export.function, intern, &export.method_kind)?;
+    Ok(Export {
         class: export.js_class.as_ref().map(|s| &**s),
-        method,
-        consumed,
-        is_constructor: export.is_constructor,
-        function: shared_function(&export.function, intern),
         comments: export.comments.iter().map(|s| &**s).collect(),
+        consumed,
+        function: shared_function(&export.function, intern),
+        method_kind,
         start: export.start,
-    }
+    })
 }
 
 fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Function<'a> {
@@ -203,8 +205,8 @@ fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Functi
         })
         .collect::<Vec<_>>();
     Function {
-        name: &func.name,
         arg_names,
+        name: &func.name,
     }
 }
 
@@ -260,30 +262,7 @@ fn shared_import_function<'a>(
 ) -> Result<ImportFunction<'a>, Diagnostic> {
     let method = match &i.kind {
         ast::ImportFunctionKind::Method { class, kind, .. } => {
-            let kind = match kind {
-                ast::MethodKind::Constructor => MethodKind::Constructor,
-                ast::MethodKind::Operation(ast::Operation { is_static, kind }) => {
-                    let is_static = *is_static;
-                    let kind = match kind {
-                        ast::OperationKind::Regular => OperationKind::Regular,
-                        ast::OperationKind::Getter(g) => {
-                            let g = g.as_ref().map(|g| intern.intern(g));
-                            OperationKind::Getter(g.unwrap_or_else(|| i.infer_getter_property()))
-                        }
-                        ast::OperationKind::Setter(s) => {
-                            let s = s.as_ref().map(|s| intern.intern(s));
-                            OperationKind::Setter(match s {
-                                Some(s) => s,
-                                None => intern.intern_str(&i.infer_setter_property()?),
-                            })
-                        }
-                        ast::OperationKind::IndexingGetter => OperationKind::IndexingGetter,
-                        ast::OperationKind::IndexingSetter => OperationKind::IndexingSetter,
-                        ast::OperationKind::IndexingDeleter => OperationKind::IndexingDeleter,
-                    };
-                    MethodKind::Operation(Operation { is_static, kind })
-                }
-            };
+            let kind = from_ast_method_kind(&i.function, intern, kind)?;
             Some(MethodData { class, kind })
         }
         ast::ImportFunctionKind::Normal => None,
@@ -510,3 +489,34 @@ macro_rules! encode_api {
     );
 }
 wasm_bindgen_shared::shared_api!(encode_api);
+
+fn from_ast_method_kind<'a>(
+    function: &'a ast::Function,
+    intern: &'a Interner,
+    method_kind: &'a ast::MethodKind,
+) -> Result<MethodKind<'a>, Diagnostic> {
+    Ok(match method_kind {
+        ast::MethodKind::Constructor => MethodKind::Constructor,
+        ast::MethodKind::Operation(ast::Operation { is_static, kind }) => {
+            let is_static = *is_static;
+            let kind = match kind {
+                ast::OperationKind::Getter(g) => {
+                    let g = g.as_ref().map(|g| intern.intern(g));
+                    OperationKind::Getter(g.unwrap_or_else(|| function.infer_getter_property()))
+                }
+                ast::OperationKind::Regular => OperationKind::Regular,
+                ast::OperationKind::Setter(s) => {
+                    let s = s.as_ref().map(|s| intern.intern(s));
+                    OperationKind::Setter(match s {
+                        Some(s) => s,
+                        None => intern.intern_str(&function.infer_setter_property()?),
+                    })
+                }
+                ast::OperationKind::IndexingGetter => OperationKind::IndexingGetter,
+                ast::OperationKind::IndexingSetter => OperationKind::IndexingSetter,
+                ast::OperationKind::IndexingDeleter => OperationKind::IndexingDeleter,
+            };
+            MethodKind::Operation(Operation { is_static, kind })
+        }
+    })
+}
diff --git a/crates/cli-support/src/js/js2rust.rs b/crates/cli-support/src/js/js2rust.rs
index de6d17bb..c75b0e7a 100644
--- a/crates/cli-support/src/js/js2rust.rs
+++ b/crates/cli-support/src/js/js2rust.rs
@@ -103,26 +103,24 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
 
     /// Flag this shim as a method call into Rust, so the first Rust argument
     /// passed should be `this.ptr`.
-    pub fn method(&mut self, method: bool, consumed: bool) -> &mut Self {
-        if method {
-            if self.cx.config.debug {
-                self.prelude(
-                    "if (this.ptr === 0) {
-                         throw new Error('Attempt to use a moved value');
-                    }",
-                );
-            }
-            if consumed {
-                self.prelude(
-                    "\
-                     const ptr = this.ptr;\n\
-                     this.ptr = 0;\n\
-                     ",
-                );
-                self.rust_arguments.insert(0, "ptr".to_string());
-            } else {
-                self.rust_arguments.insert(0, "this.ptr".to_string());
-            }
+    pub fn method(&mut self, consumed: bool) -> &mut Self {
+        if self.cx.config.debug {
+            self.prelude(
+                "if (this.ptr === 0) {
+                        throw new Error('Attempt to use a moved value');
+                }",
+            );
+        }
+        if consumed {
+            self.prelude(
+                "\
+                 const ptr = this.ptr;\n\
+                 this.ptr = 0;\n\
+                 ",
+            );
+            self.rust_arguments.insert(0, "ptr".to_string());
+        } else {
+            self.rust_arguments.insert(0, "this.ptr".to_string());
         }
         self
     }
diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs
index 42f6a6e2..c0018551 100644
--- a/crates/cli-support/src/js/mod.rs
+++ b/crates/cli-support/src/js/mod.rs
@@ -1,18 +1,24 @@
-use crate::decode;
-use crate::descriptor::{Descriptor, VectorKind};
-use crate::{Bindgen, EncodeInto, OutputMode};
-use failure::{bail, Error, ResultExt};
-use std::collections::{BTreeMap, HashMap, HashSet};
-use std::env;
-use std::fs;
-use walrus::{MemoryId, Module};
-use wasm_bindgen_wasm_interpreter::Interpreter;
-
-mod js2rust;
-use self::js2rust::{ExportedShim, Js2Rust};
-mod rust2js;
-use self::rust2js::Rust2Js;
 mod closures;
+mod js2rust;
+mod rust2js;
+
+use self::{
+    js2rust::{ExportedShim, Js2Rust},
+    rust2js::Rust2Js,
+};
+use crate::{
+    decode,
+    descriptor::{Descriptor, VectorKind},
+    Bindgen, EncodeInto, OutputMode,
+};
+use failure::{bail, Error, ResultExt};
+use std::{
+    collections::{BTreeMap, HashMap, HashSet},
+    env, fs,
+};
+use walrus::{MemoryId, Module};
+use wasm_bindgen_shared::struct_function_export_name;
+use wasm_bindgen_wasm_interpreter::Interpreter;
 
 pub struct Context<'a> {
     pub globals: String,
@@ -2189,13 +2195,8 @@ impl<'a> Context<'a> {
         }
     }
 
-    fn require_class_wrap(&mut self, class: &str) {
-        self.exported_classes
-            .as_mut()
-            .expect("classes already written")
-            .entry(class.to_string())
-            .or_insert_with(ExportedClass::default)
-            .wrap_needed = true;
+    fn require_class_wrap(&mut self, name: &str) {
+        require_class(&mut self.exported_classes, name).wrap_needed = true;
     }
 
     fn import_identifier(&mut self, import: Import<'a>) -> String {
@@ -2634,63 +2635,75 @@ impl<'a, 'b> SubContext<'a, 'b> {
         class_name: &'b str,
         export: &decode::Export,
     ) -> Result<(), Error> {
-        let wasm_name =
-            wasm_bindgen_shared::struct_function_export_name(class_name, &export.function.name);
-
+        let mut fn_name = export.function.name;
+        let wasm_name = struct_function_export_name(class_name, fn_name);
         let descriptor = match self.cx.describe(&wasm_name) {
             None => return Ok(()),
             Some(d) => d,
         };
-
-        let function_name = if export.is_constructor {
-            "constructor"
-        } else {
-            &export.function.name
+        let docs = |raw_docs| format_doc_comments(&export.comments, Some(raw_docs));
+        let method = |class: &mut ExportedClass, docs, fn_name, fn_prfx, js, ts| {
+            class.contents.push_str(docs);
+            class.contents.push_str(fn_prfx);
+            class.contents.push_str(fn_name);
+            class.contents.push_str(js);
+            class.contents.push_str("\n");
+            class.typescript.push_str(docs);
+            class.typescript.push_str("  "); // Indentation
+            class.typescript.push_str(fn_prfx);
+            class.typescript.push_str(ts);
+            class.typescript.push_str("\n");
         };
-        let (js, ts, js_doc) = Js2Rust::new(function_name, self.cx)
-            .method(export.method, export.consumed)
-            .constructor(if export.is_constructor {
-                Some(class_name)
-            } else {
-                None
-            })
-            .process(descriptor.unwrap_function(), &export.function.arg_names)?
-            .finish(
-                "",
-                &format!("wasm.{}", wasm_name),
-                ExportedShim::Named(&wasm_name),
-            );
-
-        let class = self
-            .cx
-            .exported_classes
-            .as_mut()
-            .expect("classes already written")
-            .entry(class_name.to_string())
-            .or_insert(ExportedClass::default());
-
-        let doc_comments = format_doc_comments(&export.comments, Some(js_doc));
-        class.contents.push_str(&doc_comments);
-        class.typescript.push_str(&doc_comments);
-
-        class.typescript.push_str("  "); // Indentation
-
-        if export.is_constructor {
-            if class.has_constructor {
-                bail!("found duplicate constructor `{}`", export.function.name);
+        let finish_j2r = |mut j2r: Js2Rust| -> Result<(_, _, _), Error> {
+            Ok(j2r
+                .process(descriptor.unwrap_function(), &export.function.arg_names)?
+                .finish(
+                    "",
+                    &format!("wasm.{}", wasm_name),
+                    ExportedShim::Named(&wasm_name),
+                ))
+        };
+        match &export.method_kind {
+            decode::MethodKind::Constructor => {
+                fn_name = "constructor";
+                let mut j2r = Js2Rust::new(fn_name, self.cx);
+                j2r.constructor(Some(class_name));
+                let (js, ts, raw_docs) = finish_j2r(j2r)?;
+                let class = require_class(&mut self.cx.exported_classes, class_name);
+                if class.has_constructor {
+                    bail!("found duplicate constructor `{}`", export.function.name);
+                }
+                class.has_constructor = true;
+                let docs = docs(raw_docs);
+                method(class, &docs, fn_name, "", &js, &ts);
+                Ok(())
+            }
+            decode::MethodKind::Operation(operation) => {
+                let mut j2r = Js2Rust::new(fn_name, self.cx);
+                let mut fn_prfx = "";
+                if operation.is_static {
+                    fn_prfx = "static ";
+                } else {
+                    j2r.method(export.consumed);
+                }
+                let (js, ts, raw_docs) = finish_j2r(j2r)?;
+                let class = require_class(&mut self.cx.exported_classes, class_name);
+                let docs = docs(raw_docs);
+                match operation.kind {
+                    decode::OperationKind::Getter(getter_name) => {
+                        fn_name = getter_name;
+                        fn_prfx = "get ";
+                    }
+                    decode::OperationKind::Setter(setter_name) => {
+                        fn_name = setter_name;
+                        fn_prfx = "set ";
+                    }
+                    _ => {}
+                }
+                method(class, &docs, fn_name, fn_prfx, &js, &ts);
+                Ok(())
             }
-            class.has_constructor = true;
-        } else if !export.method {
-            class.contents.push_str("static ");
-            class.typescript.push_str("static ");
         }
-
-        class.contents.push_str(function_name);
-        class.contents.push_str(&js);
-        class.contents.push_str("\n");
-        class.typescript.push_str(&ts);
-        class.typescript.push_str("\n");
-        Ok(())
     }
 
     fn generate_import(&mut self, import: &decode::Import<'b>) -> Result<(), Error> {
@@ -2764,7 +2777,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
         // the class if there's a method call.
         let name = match &import.method {
             Some(data) => self.determine_import(info, &data.class)?,
-            None => self.determine_import(info, &import.function.name)?,
+            None => self.determine_import(info, import.function.name)?,
         };
 
         // Build up our shim's state, and we'll use that to guide whether we
@@ -2872,7 +2885,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
             let set = {
                 let setter = ExportedShim::Named(&wasm_setter);
                 let mut cx = Js2Rust::new(&field.name, self.cx);
-                cx.method(true, false)
+                cx.method(false)
                     .argument(&descriptor, None)?
                     .ret(&Descriptor::Unit)?;
                 ts_dst.push_str(&format!(
@@ -2885,7 +2898,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
             };
             let getter = ExportedShim::Named(&wasm_getter);
             let (get, _ts, js_doc) = Js2Rust::new(&field.name, self.cx)
-                .method(true, false)
+                .method(false)
                 .ret(&descriptor)?
                 .finish("", &format!("wasm.{}", wasm_getter), getter);
             if !dst.ends_with("\n") {
@@ -2903,13 +2916,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
             }
         }
 
-        let class = self
-            .cx
-            .exported_classes
-            .as_mut()
-            .expect("classes already written")
-            .entry(struct_.name.to_string())
-            .or_insert_with(Default::default);
+        let class = require_class(&mut self.cx.exported_classes, struct_.name);
         class.comments = format_doc_comments(&struct_.comments, None);
         class.contents.push_str(&dst);
         class.contents.push_str("\n");
@@ -3183,6 +3190,17 @@ fn format_doc_comments(comments: &[&str], js_doc_comments: Option<String>) -> St
     format!("/**\n{}{}*/\n", body, doc)
 }
 
+fn require_class<'a>(
+    exported_classes: &'a mut Option<BTreeMap<String, ExportedClass>>,
+    name: &str,
+) -> &'a mut ExportedClass {
+    exported_classes
+        .as_mut()
+        .expect("classes already written")
+        .entry(name.to_string())
+        .or_insert_with(ExportedClass::default)
+}
+
 #[test]
 fn test_generate_identifier() {
     let mut used_names: HashMap<String, usize> = HashMap::new();
diff --git a/crates/js-sys/tests/wasm/JsString.rs b/crates/js-sys/tests/wasm/JsString.rs
index 652cb7d0..f2a3d48a 100644
--- a/crates/js-sys/tests/wasm/JsString.rs
+++ b/crates/js-sys/tests/wasm/JsString.rs
@@ -92,15 +92,15 @@ fn from_char_code() {
         "½+¾="
     );
 
-    let codes_u16: Vec<u16> = codes.into_iter().map(|code| {
-        assert!(code <= u32::from(u16::max_value()));
-        code as u16
-    }).collect();
+    let codes_u16: Vec<u16> = codes
+        .into_iter()
+        .map(|code| {
+            assert!(code <= u32::from(u16::max_value()));
+            code as u16
+        })
+        .collect();
 
-    assert_eq!(
-        JsString::from_char_code(&codes_u16),
-        "½+¾="
-    );
+    assert_eq!(JsString::from_char_code(&codes_u16), "½+¾=");
 }
 
 #[wasm_bindgen_test]
@@ -121,10 +121,7 @@ fn from_code_point() {
         JsString::from_code_point4(codes[0], codes[1], codes[2], codes[3]).unwrap(),
         "☃★♲你"
     );
-    assert_eq!(
-        JsString::from_code_point(&codes).unwrap(),
-        "☃★♲你"
-    );
+    assert_eq!(JsString::from_code_point(&codes).unwrap(), "☃★♲你");
 
     assert!(!JsString::from_code_point1(0x10FFFF).is_err());
     assert!(JsString::from_code_point1(0x110000).is_err());
diff --git a/crates/js-sys/tests/wasm/SharedArrayBuffer.rs b/crates/js-sys/tests/wasm/SharedArrayBuffer.rs
index 0509f490..fdfd55d9 100644
--- a/crates/js-sys/tests/wasm/SharedArrayBuffer.rs
+++ b/crates/js-sys/tests/wasm/SharedArrayBuffer.rs
@@ -1,6 +1,6 @@
 use js_sys::*;
-use wasm_bindgen::JsCast;
 use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsCast;
 use wasm_bindgen_test::*;
 
 #[wasm_bindgen(module = "tests/wasm/SharedArrayBuffer.js")]
diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs
index 165e76f4..d00802ce 100644
--- a/crates/macro-support/src/parser.rs
+++ b/crates/macro-support/src/parser.rs
@@ -384,22 +384,7 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a ast::ImportModule)> for syn::ForeignIte
             wasm.ret.clone()
         };
 
-        let mut operation_kind = ast::OperationKind::Regular;
-        if let Some(g) = opts.getter() {
-            operation_kind = ast::OperationKind::Getter(g.clone());
-        }
-        if let Some(s) = opts.setter() {
-            operation_kind = ast::OperationKind::Setter(s.clone());
-        }
-        if opts.indexing_getter().is_some() {
-            operation_kind = ast::OperationKind::IndexingGetter;
-        }
-        if opts.indexing_setter().is_some() {
-            operation_kind = ast::OperationKind::IndexingSetter;
-        }
-        if opts.indexing_deleter().is_some() {
-            operation_kind = ast::OperationKind::IndexingDeleter;
-        }
+        let operation_kind = operation_kind(&opts)?;
 
         let kind = if opts.method().is_some() {
             let class = wasm.arguments.get(0).ok_or_else(|| {
@@ -699,18 +684,21 @@ fn function_from_decl(
         syn::ReturnType::Type(_, ty) => Some(replace_self(*ty)),
     };
 
-    let js_name = opts.js_name();
+    let (name, name_span, renamed_via_js_name) =
+        if let Some((js_name, js_name_span)) = opts.js_name() {
+            (js_name.to_string(), js_name_span, true)
+        } else {
+            (decl_name.to_string(), decl_name.span(), false)
+        };
     Ok((
         ast::Function {
-            name: js_name
-                .map(|s| s.0.to_string())
-                .unwrap_or(decl_name.to_string()),
-            name_span: js_name.map(|s| s.1).unwrap_or(decl_name.span()),
-            renamed_via_js_name: js_name.is_some(),
             arguments,
+            name_span,
+            name,
+            renamed_via_js_name,
             ret,
-            rust_vis: vis,
             rust_attrs: attrs,
+            rust_vis: vis,
         },
         method_self,
     ))
@@ -755,15 +743,21 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
                         bail_span!(&f.decl.inputs, "the start function cannot have arguments",);
                     }
                 }
+                let method_kind = ast::MethodKind::Operation(ast::Operation {
+                    is_static: true,
+                    kind: operation_kind(&opts)?,
+                });
+                let rust_name = f.ident.clone();
+                let start = opts.start().is_some();
                 program.exports.push(ast::Export {
-                    rust_class: None,
-                    js_class: None,
-                    method_self: None,
-                    is_constructor: false,
                     comments,
-                    rust_name: f.ident.clone(),
-                    start: opts.start().is_some(),
                     function: f.convert(opts)?,
+                    js_class: None,
+                    method_kind,
+                    method_self: None,
+                    rust_class: None,
+                    rust_name,
+                    start,
                 });
             }
             syn::Item::Struct(mut s) => {
@@ -942,7 +936,6 @@ impl<'a, 'b> MacroParse<(&'a Ident, &'a str)> for &'b mut syn::ImplItemMethod {
 
         let opts = BindgenAttrs::find(&mut self.attrs)?;
         let comments = extract_doc_comments(&self.attrs);
-        let is_constructor = opts.constructor().is_some();
         let (function, method_self) = function_from_decl(
             &self.sig.ident,
             &opts,
@@ -952,16 +945,22 @@ impl<'a, 'b> MacroParse<(&'a Ident, &'a str)> for &'b mut syn::ImplItemMethod {
             true,
             Some(class),
         )?;
-
+        let method_kind = if opts.constructor().is_some() {
+            ast::MethodKind::Constructor
+        } else {
+            let is_static = method_self.is_none();
+            let kind = operation_kind(&opts)?;
+            ast::MethodKind::Operation(ast::Operation { is_static, kind })
+        };
         program.exports.push(ast::Export {
-            rust_class: Some(class.clone()),
-            js_class: Some(js_class.to_string()),
-            method_self,
-            is_constructor,
-            function,
             comments,
-            start: false,
+            function,
+            js_class: Some(js_class.to_string()),
+            method_kind,
+            method_self,
+            rust_class: Some(class.clone()),
             rust_name: self.sig.ident.clone(),
+            start: false,
         });
         opts.check_used()?;
         Ok(())
@@ -1294,3 +1293,23 @@ pub fn assert_all_attrs_checked() {
         assert_eq!(state.parsed.get(), state.checks.get());
     })
 }
+
+fn operation_kind(opts: &BindgenAttrs) -> Result<ast::OperationKind, Diagnostic> {
+    let mut operation_kind = ast::OperationKind::Regular;
+    if let Some(g) = opts.getter() {
+        operation_kind = ast::OperationKind::Getter(g.clone());
+    }
+    if let Some(s) = opts.setter() {
+        operation_kind = ast::OperationKind::Setter(s.clone());
+    }
+    if opts.indexing_getter().is_some() {
+        operation_kind = ast::OperationKind::IndexingGetter;
+    }
+    if opts.indexing_setter().is_some() {
+        operation_kind = ast::OperationKind::IndexingSetter;
+    }
+    if opts.indexing_deleter().is_some() {
+        operation_kind = ast::OperationKind::IndexingDeleter;
+    }
+    Ok(operation_kind)
+}
diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs
index 6bbfefb6..845b384f 100644
--- a/crates/shared/src/lib.rs
+++ b/crates/shared/src/lib.rs
@@ -88,11 +88,10 @@ macro_rules! shared_api {
 
         struct Export<'a> {
             class: Option<&'a str>,
-            method: bool,
-            consumed: bool,
-            is_constructor: bool,
-            function: Function<'a>,
             comments: Vec<&'a str>,
+            consumed: bool,
+            function: Function<'a>,
+            method_kind: MethodKind<'a>,
             start: bool,
         }
 
diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md
index 11c3d18d..fe32f7c4 100644
--- a/guide/src/SUMMARY.md
+++ b/guide/src/SUMMARY.md
@@ -78,6 +78,7 @@
       - [`skip`](./reference/attributes/on-rust-exports/skip.md)
       - [`start`](./reference/attributes/on-rust-exports/start.md)
       - [`typescript_custom_section`](./reference/attributes/on-rust-exports/typescript_custom_section.md)
+      - [`getter` and `setter`](./reference/attributes/on-rust-exports/getter-and-setter.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/getter-and-setter.md b/guide/src/reference/attributes/on-rust-exports/getter-and-setter.md
new file mode 100644
index 00000000..79da30a0
--- /dev/null
+++ b/guide/src/reference/attributes/on-rust-exports/getter-and-setter.md
@@ -0,0 +1,64 @@
+# `getter` and `setter`
+
+The `getter` and `setter` attributes can be used in Rust `impl` blocks to define
+properties in JS that act like getters and setters of a field. For example:
+
+```rust
+#[wasm_bindgen]
+pub struct Baz {
+    field: i32,
+}
+
+#[wasm_bindgen]
+impl Baz {
+    #[wasm_bindgen(constructor)]
+    pub fn new(field: i32) -> Baz {
+        Baz { field }
+    }
+
+    #[wasm_bindgen(getter)]
+    pub fn field(&self) -> i32 {
+        self.field
+    }
+
+    #[wasm_bindgen(setter)]
+    pub fn set_field(&mut self, field: i32) {
+        self.field = field;
+    }
+}
+```
+
+Can be combined in `JavaScript` like in this snippet:
+
+```js
+const obj = new Baz(3);
+assert.equal(obj.field, 3);
+obj.field = 4;
+assert.equal(obj.field, 4);
+```
+
+You can also configure the name of the property that is exported in JS like so:
+
+```rust
+#[wasm_bindgen]
+impl Baz {
+    #[wasm_bindgen(getter = anotherName)]
+    pub fn field(&self) -> i32 {
+        self.field
+    }
+
+    #[wasm_bindgen(setter = anotherName)]
+    pub fn set_field(&mut self, field: i32) {
+        self.field = field;
+    }
+}
+```
+
+Getters are expected to take no arguments other than `&self` and return the
+field's type. Setters are expected to take one argument other than `&mut self`
+(or `&self`) and return no values.
+
+The name for a `getter` is by default inferred from the function name it's
+attached to. The default name for a `setter` is the function's name minus the
+`set_` prefix, and if `set_` isn't a prefix of the function it's an error to not
+provide the name explicitly.
diff --git a/tests/wasm/getters_and_setters.js b/tests/wasm/getters_and_setters.js
new file mode 100644
index 00000000..def62b39
--- /dev/null
+++ b/tests/wasm/getters_and_setters.js
@@ -0,0 +1,58 @@
+const wasm = require('wasm-bindgen-test.js');
+const assert = require('assert');
+
+exports._1_js = (rules) => {
+    assert.equal(rules.field, 1);
+    rules.field *= 2;
+    return rules;
+}
+
+exports._2_js = (rules) => {
+    let value = rules.no_js_name__no_getter_with_name__no_getter_without_name();
+    assert.equal(value, 2);
+    rules.set_no_js_name__no_setter_with_name__no_setter_without_name(value * 2);
+    return rules;
+}
+
+exports._3_js = (rules) => {
+    let value = rules.no_js_name__no_getter_with_name__getter_without_name;
+    assert.equal(value, 3);
+    rules.no_js_name__no_setter_with_name__setter_without_name = value * 2;
+    return rules;
+}
+
+exports._4_js = (rules) => {
+    let value = rules.new_no_js_name__getter_with_name__getter_without_name;
+    assert.equal(value, 4);
+    rules.new_no_js_name__setter_with_name__setter_without_name = value * 2;
+    return rules;
+}
+
+exports._5_js = (rules) => {
+    let value = rules.new_js_name__no_getter_with_name__no_getter_without_name();
+    assert.equal(value, 5);
+    rules.new_js_name__no_setter_with_name__no_setter_without_name(value * 2);
+    return rules;
+}
+
+exports._6_js = (rules) => {
+    let value = rules.new_js_name__no_getter_with_name__getter_without_name;
+    assert.equal(value, 6);
+    rules.new_js_name__no_setter_with_name__setter_without_name = value * 2;
+    return rules;
+}
+
+exports._7_js = (rules) => {
+    let value = rules.new_js_name__getter_with_name__no_getter_without_name_for_field;
+    assert.equal(value, 7);
+    rules.new_js_name__setter_with_name__no_setter_without_name_for_field = value * 2;
+    return rules;
+}
+
+exports.test_getter_compute = x => {
+  assert.equal(x.foo, 3)
+};
+
+exports.test_setter_compute = x => {
+  x.foo = 97;
+};
diff --git a/tests/wasm/getters_and_setters.rs b/tests/wasm/getters_and_setters.rs
new file mode 100644
index 00000000..cb693e11
--- /dev/null
+++ b/tests/wasm/getters_and_setters.rs
@@ -0,0 +1,163 @@
+use std::rc::Rc;
+use std::cell::Cell;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen_test::*;
+
+#[wasm_bindgen(module = "tests/wasm/getters_and_setters.js")]
+extern "C" {
+    fn _1_js(rules: Rules) -> Rules;
+    fn _2_js(rules: Rules) -> Rules;
+    fn _3_js(rules: Rules) -> Rules;
+    fn _4_js(rules: Rules) -> Rules;
+    fn _5_js(rules: Rules) -> Rules;
+    fn _6_js(rules: Rules) -> Rules;
+    fn _7_js(rules: Rules) -> Rules;
+
+    fn test_getter_compute(x: GetterCompute);
+    fn test_setter_compute(x: SetterCompute);
+}
+
+// Each getter/setter combination is derived
+// from https://github.com/rustwasm/wasm-bindgen/pull/1440#issuecomment-487113564
+#[wasm_bindgen]
+pub struct Rules {
+    pub field: i32,
+}
+
+#[wasm_bindgen]
+#[allow(non_snake_case)]
+impl Rules {
+    #[wasm_bindgen]
+    pub fn no_js_name__no_getter_with_name__no_getter_without_name(&self) -> i32 {
+        self.field
+    }
+    #[wasm_bindgen]
+    pub fn set_no_js_name__no_setter_with_name__no_setter_without_name(&mut self, field: i32) {
+        self.field = field;
+    }
+
+    #[wasm_bindgen(getter)]
+    pub fn no_js_name__no_getter_with_name__getter_without_name(&self) -> i32 {
+        self.field
+    }
+    #[wasm_bindgen(setter)]
+    pub fn set_no_js_name__no_setter_with_name__setter_without_name(&mut self, field: i32) {
+        self.field = field;
+    }
+
+    #[wasm_bindgen(getter = new_no_js_name__getter_with_name__getter_without_name)]
+    pub fn no_js_name__getter_with_name__getter_without_name(&self) -> i32 {
+        self.field
+    }
+    #[wasm_bindgen(setter = new_no_js_name__setter_with_name__setter_without_name)]
+    pub fn set_no_js_name__setter_with_name__setter_without_name(&mut self, field: i32) {
+        self.field = field;
+    }
+
+    #[wasm_bindgen(js_name = new_js_name__no_getter_with_name__no_getter_without_name)]
+    pub fn js_name__no_getter_with_name__no_getter_without_name(&self) -> i32 {
+        self.field
+    }
+    #[wasm_bindgen(js_name = new_js_name__no_setter_with_name__no_setter_without_name)]
+    pub fn set_js_name__no_setter_with_name__no_setter_without_name(&mut self, field: i32) {
+        self.field = field;
+    }
+
+    #[wasm_bindgen(getter, js_name = new_js_name__no_getter_with_name__getter_without_name)]
+    pub fn js_name__no_getter_with_name__getter_without_name(&self) -> i32 {
+        self.field
+    }
+    #[wasm_bindgen(js_name = new_js_name__no_setter_with_name__setter_without_name, setter)]
+    pub fn set_js_name__no_setter_with_name__setter_without_name(&mut self, field: i32) {
+        self.field = field;
+    }
+
+    #[wasm_bindgen(
+        getter = new_js_name__getter_with_name__no_getter_without_name_for_field,
+        js_name = new_js_name__getter_with_name__no_getter_without_name_for_method
+    )]
+    pub fn js_name__getter_with_name__no_getter_without_name(&self) -> i32 {
+        self.field
+    }
+    #[wasm_bindgen(
+        js_name = new_js_name__setter_with_name__no_setter_without_name_for_method,
+        setter = new_js_name__setter_with_name__no_setter_without_name_for_field
+    )]
+    pub fn set_js_name__setter_with_name__no_setter_without_name_for_field(&mut self, field: i32) {
+        self.field = field;
+    }
+}
+
+#[wasm_bindgen_test]
+fn _1_rust() {
+    let rules = _1_js(Rules { field: 1 });
+    assert_eq!(rules.field, 2);
+}
+
+#[wasm_bindgen_test]
+fn _2_rust() {
+    let rules = _2_js(Rules { field: 2 });
+    assert_eq!(rules.field, 4);
+}
+
+#[wasm_bindgen_test]
+fn _3_rust() {
+    let rules = _3_js(Rules { field: 3 });
+    assert_eq!(rules.field, 6);
+}
+
+#[wasm_bindgen_test]
+fn _4_rust() {
+    let rules = _4_js(Rules { field: 4 });
+    assert_eq!(rules.field, 8);
+}
+
+#[wasm_bindgen_test]
+fn _5_rust() {
+    let rules = _5_js(Rules { field: 5 });
+    assert_eq!(rules.field, 10);
+}
+
+#[wasm_bindgen_test]
+fn _6_rust() {
+    let rules = _6_js(Rules { field: 6 });
+    assert_eq!(rules.field, 12);
+}
+
+#[wasm_bindgen_test]
+fn _7_rust() {
+    let rules = _7_js(Rules { field: 7 });
+    assert_eq!(rules.field, 14);
+}
+
+#[wasm_bindgen]
+struct GetterCompute;
+
+#[wasm_bindgen]
+impl GetterCompute {
+    #[wasm_bindgen(getter)]
+    pub fn foo(&self) -> u32 { 3 }
+}
+
+#[wasm_bindgen_test]
+fn getter_compute() {
+    test_getter_compute(GetterCompute);
+}
+
+#[wasm_bindgen]
+struct SetterCompute(Rc<Cell<u32>>);
+
+#[wasm_bindgen]
+impl SetterCompute {
+    #[wasm_bindgen(setter)]
+    pub fn set_foo(&self, x: u32) {
+        self.0.set(x + 3);
+    }
+}
+
+#[wasm_bindgen_test]
+fn setter_compute() {
+    let r = Rc::new(Cell::new(3));
+    test_setter_compute(SetterCompute(r.clone()));
+    assert_eq!(r.get(), 100);
+}
diff --git a/tests/wasm/main.rs b/tests/wasm/main.rs
index fff18228..65f47f86 100644
--- a/tests/wasm/main.rs
+++ b/tests/wasm/main.rs
@@ -23,6 +23,7 @@ pub mod duplicates;
 pub mod enums;
 #[path = "final.rs"]
 pub mod final_;
+pub mod getters_and_setters;
 pub mod import_class;
 pub mod imports;
 pub mod js_objects;