diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 852595c4..96c1e3cc 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -105,8 +105,11 @@ pub struct Operation { #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] pub enum OperationKind { Regular, - Setter(Option), Getter(Option), + Setter(Option), + IndexingGetter, + IndexingSetter, + IndexingDeleter, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] @@ -369,6 +372,9 @@ impl ImportFunction { s.unwrap_or_else(|| self.infer_setter_property()), ) } + OperationKind::IndexingGetter => shared::OperationKind::IndexingGetter, + OperationKind::IndexingSetter => shared::OperationKind::IndexingSetter, + OperationKind::IndexingDeleter => shared::OperationKind::IndexingDeleter, }; shared::MethodKind::Operation(shared::Operation { is_static, kind }) } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index e1ad5939..93b818a2 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -1808,18 +1808,6 @@ impl<'a, 'b> SubContext<'a, 'b> { let location = if *is_static { &class } else { "this" }; match kind { - shared::OperationKind::Getter(g) => format!( - "function() {{ - return {}.{}; - }}", - location, g - ), - shared::OperationKind::Setter(s) => format!( - "function(y) {{ - {}.{} = y; - }}", - location, s - ), shared::OperationKind::Regular => { let nargs = descriptor.unwrap_function().arguments.len(); let mut s = format!("function("); @@ -1841,11 +1829,44 @@ impl<'a, 'b> SubContext<'a, 'b> { s.push_str(");\n}"); s } + shared::OperationKind::Getter(g) => format!( + "function() {{ + return {}.{}; + }}", + location, g + ), + shared::OperationKind::Setter(s) => format!( + "function(y) {{ + {}.{} = y; + }}", + location, s + ), + shared::OperationKind::IndexingGetter => format!( + "function(y) {{ + return {}[y]; + }}", + location + ), + shared::OperationKind::IndexingSetter => format!( + "function(y, z) {{ + {}[y] = z; + }}", + location + ), + shared::OperationKind::IndexingDeleter => format!( + "function(y) {{ + delete {}[y]; + }}", + location + ), } } else { let location = if *is_static { "" } else { ".prototype" }; match kind { + shared::OperationKind::Regular => { + format!("{}{}.{}", class, location, import.function.name) + } shared::OperationKind::Getter(g) => { self.cx.expose_get_inherited_descriptor(); format!( @@ -1860,9 +1881,9 @@ impl<'a, 'b> SubContext<'a, 'b> { class, location, s, ) } - shared::OperationKind::Regular => { - format!("{}{}.{}", class, location, import.function.name) - } + shared::OperationKind::IndexingGetter => panic!("indexing getter should be structural"), + shared::OperationKind::IndexingSetter => panic!("indexing setter should be structural"), + shared::OperationKind::IndexingDeleter => panic!("indexing deleter should be structural"), } }; diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 9c5bc23c..2258c88f 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -121,6 +121,30 @@ impl BindgenAttrs { .next() } + /// Whether the indexing getter attributes is present + fn indexing_getter(&self) -> bool { + self.attrs.iter().any(|a| match *a { + BindgenAttr::IndexingGetter => true, + _ => false, + }) + } + + /// Whether the indexing setter attributes is present + fn indexing_setter(&self) -> bool { + self.attrs.iter().any(|a| match *a { + BindgenAttr::IndexingSetter => true, + _ => false, + }) + } + + /// Whether the indexing deleter attributes is present + fn indexing_deleter(&self) -> bool { + self.attrs.iter().any(|a| match *a { + BindgenAttr::IndexingDeleter => true, + _ => false, + }) + } + /// Whether the structural attributes is present fn structural(&self) -> bool { self.attrs.iter().any(|a| match *a { @@ -186,6 +210,9 @@ pub enum BindgenAttr { Module(String), Getter(Option), Setter(Option), + IndexingGetter, + IndexingSetter, + IndexingDeleter, Structural, Readonly, JsName(String), @@ -227,6 +254,12 @@ impl syn::synom::Synom for BindgenAttr { (val) )=> { BindgenAttr::Setter } | + call!(term, "indexing_getter") => { |_| BindgenAttr::IndexingGetter } + | + call!(term, "indexing_setter") => { |_| BindgenAttr::IndexingSetter } + | + call!(term, "indexing_deleter") => { |_| BindgenAttr::IndexingDeleter } + | call!(term, "structural") => { |_| BindgenAttr::Structural } | call!(term, "readonly") => { |_| BindgenAttr::Readonly } @@ -381,6 +414,15 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a Option)> for syn::ForeignItemFn if let Some(s) = opts.setter() { operation_kind = ast::OperationKind::Setter(s); } + if opts.indexing_getter() { + operation_kind = ast::OperationKind::IndexingGetter; + } + if opts.indexing_setter() { + operation_kind = ast::OperationKind::IndexingSetter; + } + if opts.indexing_deleter() { + operation_kind = ast::OperationKind::IndexingDeleter; + } let kind = if opts.method() { let class = wasm diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index bfdf34dc..c40ad81c 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -69,6 +69,9 @@ pub enum OperationKind { Regular, Getter(String), Setter(String), + IndexingGetter, + IndexingSetter, + IndexingDeleter, } #[derive(Deserialize, Serialize)] diff --git a/crates/webidl-tests/simple.js b/crates/webidl-tests/simple.js index 2f6b3876..7dbd45b4 100644 --- a/crates/webidl-tests/simple.js +++ b/crates/webidl-tests/simple.js @@ -93,6 +93,20 @@ global.GlobalMethod = class GlobalMethod { } }; +global.Indexing = function () { + return new Proxy({}, { + get(obj, prop) { + return obj.hasOwnProperty(prop) ? obj[prop] : -1; + }, + set(obj, prop, value) { + obj[prop] = value; + }, + deleteProperty(obj, prop) { + delete obj[prop]; + }, + }); +}; + global.PartialInterface = class PartialInterface { get un() { return 1; diff --git a/crates/webidl-tests/simple.rs b/crates/webidl-tests/simple.rs index 914643be..1230bfbc 100644 --- a/crates/webidl-tests/simple.rs +++ b/crates/webidl-tests/simple.rs @@ -65,7 +65,17 @@ fn optional_method() { #[wasm_bindgen_test] fn global_method() { let f = GlobalMethod::new().unwrap(); - assert!(f.m() == 123); + assert_eq!(f.m(), 123); +} + +#[wasm_bindgen_test] +fn indexing() { + let f = Indexing::new().unwrap(); + assert_eq!(f.get(123), -1); + f.set(123, 456); + assert_eq!(f.get(123), 456); + f.delete(123); + assert_eq!(f.get(123), -1); } #[wasm_bindgen_test] diff --git a/crates/webidl-tests/simple.webidl b/crates/webidl-tests/simple.webidl index 810aba0c..8435c024 100644 --- a/crates/webidl-tests/simple.webidl +++ b/crates/webidl-tests/simple.webidl @@ -40,6 +40,13 @@ interface GlobalMethod { octet m(); }; +[Constructor()] +interface Indexing { + getter short (unsigned long index); + setter void (unsigned long index, short value); + deleter void (unsigned long index); +}; + [Constructor()] interface Unforgeable { [Unforgeable] readonly attribute short uno; diff --git a/crates/webidl/src/first_pass.rs b/crates/webidl/src/first_pass.rs index 5b9a39be..7d65d2b8 100644 --- a/crates/webidl/src/first_pass.rs +++ b/crates/webidl/src/first_pass.rs @@ -41,7 +41,10 @@ pub(crate) struct InterfaceData<'src> { #[derive(PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum OperationId<'src> { Constructor, - Operation(Option<&'src str>) + Operation(Option<&'src str>), + IndexingGetter, + IndexingSetter, + IndexingDeleter, } #[derive(Default)] @@ -243,7 +246,7 @@ impl<'src> FirstPass<'src, &'src str> for weedle::interface::InterfaceMember<'sr impl<'src> FirstPass<'src, &'src str> for weedle::interface::OperationInterfaceMember<'src> { fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, self_name: &'src str) -> Result<()> { - if self.specials.len() > 0 { + if !self.specials.is_empty() && self.specials.len() != 1 { warn!("Unsupported webidl operation {:?}", self); return Ok(()) } @@ -254,7 +257,16 @@ impl<'src> FirstPass<'src, &'src str> for weedle::interface::OperationInterfaceM first_pass_operation( record, self_name, - OperationId::Operation(self.identifier.map(|s| s.0)), + match self.identifier.map(|s| s.0) { + None => match self.specials.get(0) { + None => OperationId::Operation(None), + Some(weedle::interface::Special::Getter(weedle::term::Getter)) => OperationId::IndexingGetter, + Some(weedle::interface::Special::Setter(weedle::term::Setter)) => OperationId::IndexingSetter, + Some(weedle::interface::Special::Deleter(weedle::term::Deleter)) => OperationId::IndexingDeleter, + Some(weedle::interface::Special::LegacyCaller(weedle::term::LegacyCaller)) => return Ok(()), + }, + Some(ref name) => OperationId::Operation(Some(name.clone())), + }, &self.args.body.list, ) } diff --git a/crates/webidl/src/lib.rs b/crates/webidl/src/lib.rs index 4cdb457f..bf749c00 100644 --- a/crates/webidl/src/lib.rs +++ b/crates/webidl/src/lib.rs @@ -299,7 +299,7 @@ impl<'src> WebidlParse<'src, &'src weedle::InterfaceDefinition<'src>> for Extend let mut add_constructor = |arguments: &[Argument], class: &str| { let (overloaded, same_argument_names) = first_pass.get_operation_overloading( arguments, - ::first_pass::OperationId::Constructor, + &::first_pass::OperationId::Constructor, interface.identifier.0, ); @@ -644,18 +644,32 @@ fn member_operation<'src>( Some(Static(_)) => true, None => false, }; - if specials.len() > 0 { - warn!("Unsupported specials on type {:?}", (self_name, identifier)); - return Ok(()) - } first_pass .create_basic_method( args, - identifier.map(|s| s.0), + match identifier.map(|s| s.0) { + None if specials.is_empty() => ::first_pass::OperationId::Operation(None), + None if specials.len() == 1 => match specials[0] { + weedle::interface::Special::Getter(weedle::term::Getter) => ::first_pass::OperationId::IndexingGetter, + weedle::interface::Special::Setter(weedle::term::Setter) => ::first_pass::OperationId::IndexingSetter, + weedle::interface::Special::Deleter(weedle::term::Deleter) => ::first_pass::OperationId::IndexingDeleter, + weedle::interface::Special::LegacyCaller(weedle::term::LegacyCaller) => return Ok(()), + }, + Some(ref name) if specials.is_empty() => ::first_pass::OperationId::Operation(Some(name.clone())), + _ => { + warn!("Unsupported specials on type {:?}", (self_name, identifier)); + return Ok(()) + } + }, return_type, self_name, statik, + specials.len() == 1 || first_pass + .interfaces + .get(self_name) + .map(|interface_data| interface_data.global) + .unwrap_or(false), util::throws(attrs), ) .map(wrap_import_function) diff --git a/crates/webidl/src/util.rs b/crates/webidl/src/util.rs index 50c05901..554f90f0 100644 --- a/crates/webidl/src/util.rs +++ b/crates/webidl/src/util.rs @@ -910,24 +910,31 @@ impl<'src> FirstPassRecord<'src> { pub fn create_basic_method( &self, arguments: &[weedle::argument::Argument], - name: Option<&str>, + operation_id: ::first_pass::OperationId, return_type: &weedle::types::ReturnType, self_name: &str, is_static: bool, + structural: bool, catch: bool, ) -> Option { let (overloaded, same_argument_names) = self.get_operation_overloading( arguments, - ::first_pass::OperationId::Operation(name), + &operation_id, self_name, ); - let name = match name { - None => { - warn!("Operations without a name are unsupported"); - return None; - } - Some(ref name) => name, + let name = match &operation_id { + ::first_pass::OperationId::Constructor => panic!("constructors are unsupported"), + ::first_pass::OperationId::Operation(name) => match name { + None => { + warn!("Operations without a name are unsupported"); + return None; + } + Some(name) => name.to_string(), + }, + ::first_pass::OperationId::IndexingGetter => "get".to_string(), + ::first_pass::OperationId::IndexingSetter => "set".to_string(), + ::first_pass::OperationId::IndexingDeleter => "delete".to_string(), }; let kind = backend::ast::ImportFunctionKind::Method { @@ -935,7 +942,13 @@ impl<'src> FirstPassRecord<'src> { ty: ident_ty(rust_ident(camel_case_ident(&self_name).as_str())), kind: backend::ast::MethodKind::Operation(backend::ast::Operation { is_static, - kind: backend::ast::OperationKind::Regular, + kind: match &operation_id { + ::first_pass::OperationId::Constructor => panic!("constructors are unsupported"), + ::first_pass::OperationId::Operation(_) => backend::ast::OperationKind::Regular, + ::first_pass::OperationId::IndexingGetter => backend::ast::OperationKind::IndexingGetter, + ::first_pass::OperationId::IndexingSetter => backend::ast::OperationKind::IndexingSetter, + ::first_pass::OperationId::IndexingDeleter => backend::ast::OperationKind::IndexingDeleter, + }, }), }; @@ -951,7 +964,19 @@ impl<'src> FirstPassRecord<'src> { } } }; - let doc_comment = Some(format!("The `{}()` method\n\n{}", name, mdn_doc(self_name, Some(name)))); + let doc_comment = match &operation_id { + ::first_pass::OperationId::Constructor => panic!("constructors are unsupported"), + ::first_pass::OperationId::Operation(_) => Some( + format!( + "The `{}()` method\n\n{}", + name, + mdn_doc(self_name, Some(&name)) + ) + ), + ::first_pass::OperationId::IndexingGetter => Some("The indexing getter\n\n".to_string()), + ::first_pass::OperationId::IndexingSetter => Some("The indexing setter\n\n".to_string()), + ::first_pass::OperationId::IndexingDeleter => Some("The indexing deleter\n\n".to_string()), + }; self.create_function( &name, @@ -960,11 +985,7 @@ impl<'src> FirstPassRecord<'src> { arguments, ret, kind, - self - .interfaces - .get(self_name) - .map(|interface_data| interface_data.global) - .unwrap_or(false), + structural, catch, doc_comment, ) @@ -975,14 +996,14 @@ impl<'src> FirstPassRecord<'src> { pub fn get_operation_overloading( &self, arguments: &[weedle::argument::Argument], - id: ::first_pass::OperationId, + id: &::first_pass::OperationId, self_name: &str, ) -> (bool, bool) { let data = match self.interfaces.get(self_name) { Some(data) => data, None => return (false, false), }; - let data = match data.operations.get(&id) { + let data = match data.operations.get(id) { Some(data) => data, None => return (false, false), }; diff --git a/guide/src/design/import-customization.md b/guide/src/design/import-customization.md index 4f1b6d9a..7deafd68 100644 --- a/guide/src/design/import-customization.md +++ b/guide/src/design/import-customization.md @@ -126,7 +126,30 @@ possibilities! Properties in JS are accessed through `Object.getOwnPropertyDescriptor`. Note that this typically only works for class-like-defined properties which aren't just attached properties on any old object. For accessing any old property on - an object we can use... + an object we can use the `structural` flag. + +* `indexing_getter`, `indexing_setter` and `indexing_deleter` - these three + attributes can be combined with `method` to indicate that this is a indexing + getter, indexing setter or indexing deleter method. They are different from + `getter` and `setter` in a way that `getter` and `setter` can only access + properties that have a name corresponding to the function name or their + argument, but `indexing_getter`, `indexing_setter` and `indexing_deleter` + work in a dynamic manner, similarly to the indexing syntax in JS + (`object[propertyName]`), hence the name. Should always be used together with + the `structural` flag. For example: + + ```rust + #[wasm_bindgen] + extern { + type Foo; + #[wasm_bindgen(method, structural, indexing_getter)] + fn get(this: &Foo, prop: &str) -> u32; + #[wasm_bindgen(method, structural, indexing_setter)] + fn set(this: &Foo, prop: &str, val: u32); + #[wasm_bindgen(method, structural, indexing_deleter)] + fn delete(this: &Foo, prop: &str); + } + ``` * `structural` - this is a flag to `method` annotations which indicates that the method being accessed (or property with getters/setters) should be accessed in