From e0a6281e7375468f367aadf398d5a836512d9df6 Mon Sep 17 00:00:00 2001 From: Samson <16504129+sagudev@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:08:45 +0200 Subject: [PATCH] Impl Setlike and Maplike (#30237) * MallocSizeOf for Index{Set, Map} * like as iterable in WebIDL * Codegen magic for like interfaces * TestBinding for like * Test for Setlike and Maplike test bindings * Some fixes * Switch to any.js * nit * Keep order --- Cargo.lock | 1 + components/malloc_size_of/Cargo.toml | 1 + components/malloc_size_of/lib.rs | 2 + .../dom/bindings/codegen/CodegenRust.py | 77 ++++- .../dom/bindings/codegen/Configuration.py | 10 +- components/script/dom/bindings/like.rs | 298 +++++++++++++++++ components/script/dom/bindings/mod.rs | 1 + components/script/dom/mod.rs | 2 + components/script/dom/testbindingmaplike.rs | 90 +++++ components/script/dom/testbindingsetlike.rs | 63 ++++ .../dom/webidls/TestBindingMaplike.webidl | 20 ++ .../dom/webidls/TestBindingSetlike.webidl | 14 + .../mozilla/like.any.js.ini | 23 ++ tests/wpt/mozilla/meta/MANIFEST.json | 25 ++ .../wpt/mozilla/meta/mozilla/like.any.js.ini | 23 ++ tests/wpt/mozilla/tests/mozilla/like.any.js | 308 ++++++++++++++++++ third_party/WebIDL/WebIDL.py | 61 ++++ third_party/WebIDL/like-as-iterable.patch | 72 ++++ third_party/WebIDL/update.sh | 1 + 19 files changed, 1088 insertions(+), 4 deletions(-) create mode 100644 components/script/dom/bindings/like.rs create mode 100644 components/script/dom/testbindingmaplike.rs create mode 100644 components/script/dom/testbindingsetlike.rs create mode 100644 components/script/dom/webidls/TestBindingMaplike.webidl create mode 100644 components/script/dom/webidls/TestBindingSetlike.webidl create mode 100644 tests/wpt/mozilla/meta-legacy-layout/mozilla/like.any.js.ini create mode 100644 tests/wpt/mozilla/meta/mozilla/like.any.js.ini create mode 100644 tests/wpt/mozilla/tests/mozilla/like.any.js create mode 100644 third_party/WebIDL/like-as-iterable.patch diff --git a/Cargo.lock b/Cargo.lock index 6c67e44b037..09b2768575a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3464,6 +3464,7 @@ dependencies = [ "euclid", "http", "hyper_serde", + "indexmap", "keyboard-types", "selectors", "serde", diff --git a/components/malloc_size_of/Cargo.toml b/components/malloc_size_of/Cargo.toml index e90b36b5104..feb219f3c83 100644 --- a/components/malloc_size_of/Cargo.toml +++ b/components/malloc_size_of/Cargo.toml @@ -36,6 +36,7 @@ cssparser = { workspace = true } euclid = { workspace = true } http = { workspace = true, optional = true } hyper_serde = { workspace = true, optional = true } +indexmap = { workspace = true } keyboard-types = { workspace = true, optional = true } selectors = { path = "../selectors", features = ["shmem"] } serde = { workspace = true, optional = true } diff --git a/components/malloc_size_of/lib.rs b/components/malloc_size_of/lib.rs index 8a2b0f19fd8..f33a41aa341 100644 --- a/components/malloc_size_of/lib.rs +++ b/components/malloc_size_of/lib.rs @@ -436,6 +436,7 @@ macro_rules! malloc_size_of_hash_set { } malloc_size_of_hash_set!(std::collections::HashSet); +malloc_size_of_hash_set!(indexmap::IndexSet); macro_rules! malloc_size_of_hash_map { ($ty:ty) => { @@ -475,6 +476,7 @@ macro_rules! malloc_size_of_hash_map { } malloc_size_of_hash_map!(std::collections::HashMap); +malloc_size_of_hash_map!(indexmap::IndexMap); impl MallocShallowSizeOf for std::collections::BTreeMap where diff --git a/components/script/dom/bindings/codegen/CodegenRust.py b/components/script/dom/bindings/codegen/CodegenRust.py index bc7f2ceb9a8..f7280ffba22 100644 --- a/components/script/dom/bindings/codegen/CodegenRust.py +++ b/components/script/dom/bindings/codegen/CodegenRust.py @@ -3805,7 +3805,9 @@ class CGPerSignatureCall(CGThing): if idlNode.isMethod() and idlNode.isMaplikeOrSetlikeOrIterableMethod(): if idlNode.maplikeOrSetlikeOrIterable.isMaplike() or \ idlNode.maplikeOrSetlikeOrIterable.isSetlike(): - raise TypeError('Maplike/Setlike methods are not supported yet') + cgThings.append(CGMaplikeOrSetlikeMethodGenerator(descriptor, + idlNode.maplikeOrSetlikeOrIterable, + idlNode.identifier.name)) else: cgThings.append(CGIterableMethodGenerator(descriptor, idlNode.maplikeOrSetlikeOrIterable, @@ -6490,6 +6492,8 @@ def generate_imports(config, cgthings, descriptors, callbacks=None, dictionaries 'crate::dom::bindings::htmlconstructor::push_new_element_queue', 'crate::dom::bindings::iterable::Iterable', 'crate::dom::bindings::iterable::IteratorType', + 'crate::dom::bindings::like::Setlike', + 'crate::dom::bindings::like::Maplike', 'crate::dom::bindings::namespace::NamespaceObjectClass', 'crate::dom::bindings::namespace::create_namespace_object', 'crate::dom::bindings::reflector::MutDomObject', @@ -7862,6 +7866,77 @@ class CallbackOperation(CallbackOperationBase): descriptor, descriptor.interface.isSingleOperationInterface()) +class CGMaplikeOrSetlikeMethodGenerator(CGGeneric): + """ + Creates methods for *like interfaces. Unwrapping/wrapping + will be taken care of by the usual method generation machinery in + CGMethodCall/CGPerSignatureCall. Functionality is filled in here instead of + using CGCallGenerator. + """ + def __init__(self, descriptor, likeable, methodName): + trait: str + if likeable.isSetlike(): + trait = "Setlike" + elif likeable.isMaplike(): + trait = "Maplike" + else: + raise TypeError("CGMaplikeOrSetlikeMethodGenerator is only for Setlike/Maplike") + """ + setlike: + fn size(&self) -> usize; + fn add(&self, key: Self::Key); + fn has(&self, key: &Self::Key) -> bool; + fn clear(&self); + fn delete(&self, key: &Self::Key) -> bool; + maplike: + fn get(&self, key: Self::Key) -> Self::Value; + fn size(&self) -> usize; + fn set(&self, key: Self::Key, value: Self::Value); + fn has(&self, key: &Self::Key) -> bool; + fn clear(&self); + fn delete(&self, key: &Self::Key) -> bool; + like iterable: + keys/values/entries/forEach + """ + # like iterables are implemented seperatly as we are actually implementing them + if methodName in ["keys", "values", "entries", "forEach"]: + CGIterableMethodGenerator.__init__(self, descriptor, likeable, methodName) + elif methodName in ["size", "clear"]: # zero arguments + CGGeneric.__init__(self, fill( + """ + let result = ${trt}::${method}(&*this); + """, + trt=trait, + method=methodName.lower())) + elif methodName == "add": # special case one argumet + CGGeneric.__init__(self, fill( + """ + ${trt}::${method}(&*this, arg0); + // Returns itself per https://webidl.spec.whatwg.org/#es-set-add + let result = this; + """, + trt=trait, + method=methodName)) + elif methodName in ["has", "delete", "get"]: # one argument + CGGeneric.__init__(self, fill( + """ + let result = ${trt}::${method}(&*this, arg0); + """, + trt=trait, + method=methodName)) + elif methodName == "set": # two arguments + CGGeneric.__init__(self, fill( + """ + ${trt}::${method}(&*this, arg0, arg1); + // Returns itself per https://webidl.spec.whatwg.org/#es-map-set + let result = this; + """, + trt=trait, + method=methodName)) + else: + raise TypeError(f"Do not know how to impl *like method: {methodName}") + + class CGIterableMethodGenerator(CGGeneric): """ Creates methods for iterable interfaces. Unwrapping/wrapping diff --git a/components/script/dom/bindings/codegen/Configuration.py b/components/script/dom/bindings/codegen/Configuration.py index bd455f5288e..aa32093772f 100644 --- a/components/script/dom/bindings/codegen/Configuration.py +++ b/components/script/dom/bindings/codegen/Configuration.py @@ -516,7 +516,11 @@ def getUnwrappedType(type): def iteratorNativeType(descriptor, infer=False): - assert descriptor.interface.isIterable() iterableDecl = descriptor.interface.maplikeOrSetlikeOrIterable - assert iterableDecl.isPairIterator() - return "IterableIterator%s" % ("" if infer else '<%s>' % descriptor.interface.identifier.name) + assert (iterableDecl.isIterable() and iterableDecl.isPairIterator()) \ + or iterableDecl.isSetlike() or iterableDecl.isMaplike() + res = "IterableIterator%s" % ("" if infer else '<%s>' % descriptor.interface.identifier.name) + # todo: this hack is telling us that something is still wrong in codegen + if iterableDecl.isSetlike() or iterableDecl.isMaplike(): + res = f"crate::dom::bindings::iterable::{res}" + return res diff --git a/components/script/dom/bindings/like.rs b/components/script/dom/bindings/like.rs new file mode 100644 index 00000000000..ad14e453655 --- /dev/null +++ b/components/script/dom/bindings/like.rs @@ -0,0 +1,298 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! Implementation of `setlike<...>` and `maplike<..., ...>` WebIDL declarations. + +use crate::dom::bindings::cell::DomRefCell; +use indexmap::{IndexMap, IndexSet}; +use js::conversions::ToJSValConvertible; +use std::cmp::Eq; +use std::hash::Hash; + +use super::iterable::Iterable; + +/// Every Setlike dom_struct must implement this to provide access to underlying storage +/// so codegen can automatically generate all setlike methods +/// +/// In case you use a type that implements Setlike as underlying storage it's recommended to use `setlike` macro. +// In webidl: `setlike` +pub trait Setlike { + /// The type of the key of the set. + type Key: ToJSValConvertible + Clone; // clone is for impl Maplike for T + + fn get_index(&self, index: u32) -> Option; + + fn size(&self) -> u32; + fn add(&self, key: Self::Key); + fn has(&self, key: Self::Key) -> bool; + fn clear(&self); + fn delete(&self, key: Self::Key) -> bool; +} + +// we can only have one iterable for T +// so we have impl Iterable for T +// and minimal: +impl Maplike for T { + type Key = ::Key; + + type Value = ::Key; + + #[inline] + fn get_index(&self, index: u32) -> Option<(Self::Key, Self::Value)> { + self.get_index(index).map(|k| (k.clone(), k)) + } + + fn get(&self, _key: Self::Key) -> Option { + unimplemented!() + } + + #[inline] + fn size(&self) -> u32 { + self.size() + } + + fn set(&self, _key: Self::Key, _value: Self::Value) { + unimplemented!() + } + + fn has(&self, _key: Self::Key) -> bool { + unimplemented!() + } + + fn clear(&self) { + unimplemented!() + } + + fn delete(&self, _key: Self::Key) -> bool { + unimplemented!() + } +} + +impl Setlike for DomRefCell> +where + K: ToJSValConvertible + Eq + PartialEq + Hash + Clone, +{ + type Key = K; + + #[inline(always)] + fn get_index(&self, index: u32) -> Option { + self.borrow().get_index(index as usize).cloned() + } + + #[inline(always)] + fn size(&self) -> u32 { + self.borrow().len() as u32 + } + + #[inline(always)] + fn add(&self, key: Self::Key) { + self.borrow_mut().insert(key); + } + + #[inline(always)] + fn has(&self, key: Self::Key) -> bool { + self.borrow().contains(&key) + } + + #[inline(always)] + fn clear(&self) { + self.borrow_mut().clear() + } + + #[inline(always)] + fn delete(&self, key: Self::Key) -> bool { + self.borrow_mut().shift_remove(&key) + } +} + +/// Usage: +/// ```rust +/// pub struct TestBindingSetlike { +/// // setlike +/// internal: DomRefCell>, +/// } +/// impl Setlike for TestBindingSetlike { +/// type Key = DOMString; +/// setlike!(self, internal); +/// } +/// ``` +#[macro_export] +macro_rules! setlike { + ( $self:ident, $x:ident ) => { + #[inline(always)] + fn get_index(&$self, index: u32) -> Option { + $self.$x.get_index(index) + } + + #[inline(always)] + fn size(&$self) -> u32 { + $self.$x.size() + } + + #[inline(always)] + fn add(&$self, key: Self::Key) { + $self.$x.add(key) + } + + #[inline(always)] + fn has(&$self, key: Self::Key) -> bool { + $self.$x.has(key) + } + + #[inline(always)] + fn clear(&$self) { + $self.$x.clear() + } + + #[inline(always)] + fn delete(&$self, key: Self::Key) -> bool { + $self.$x.delete(key) + } + }; +} + +/// Every Maplike dom_struct must implement this +/// to provide access to underlying storage +/// so codegen can automatically generate all maplike methods +/// +/// In case you use a type that implements Maplike as underlying storage it's recommended to use `maplike` macro. +// In webidl: `maplike` +pub trait Maplike { + /// The type of the key of the map. + type Key: ToJSValConvertible; + /// The type of the value of the map. + type Value: ToJSValConvertible; + + fn get_index(&self, index: u32) -> Option<(Self::Key, Self::Value)>; + + fn get(&self, key: Self::Key) -> Option; + fn size(&self) -> u32; + fn set(&self, key: Self::Key, value: Self::Value); + fn has(&self, key: Self::Key) -> bool; + fn clear(&self); + fn delete(&self, key: Self::Key) -> bool; +} + +impl Iterable for T { + type Key = T::Key; + + type Value = T::Value; + + #[inline] + fn get_iterable_length(&self) -> u32 { + self.size() + } + + #[inline] + fn get_value_at_index(&self, index: u32) -> Self::Value { + // SAFETY: we are checking bounds manually + self.get_index(index).unwrap().1 + } + + #[inline] + fn get_key_at_index(&self, index: u32) -> Self::Key { + // SAFETY: we are checking bounds manually + self.get_index(index).unwrap().0 + } +} + +impl Maplike for DomRefCell> +where + K: ToJSValConvertible + Eq + PartialEq + Hash + Clone, + V: ToJSValConvertible + Clone, +{ + type Key = K; + type Value = V; + + #[inline(always)] + fn get_index(&self, index: u32) -> Option<(Self::Key, Self::Value)> { + self.borrow() + .get_index(index as usize) + .map(|(k, v)| (k.to_owned(), v.to_owned())) + } + + #[inline(always)] + fn get(&self, key: Self::Key) -> Option { + self.borrow().get(&key).cloned() + } + + #[inline(always)] + fn size(&self) -> u32 { + self.borrow().len() as u32 + } + + #[inline(always)] + fn set(&self, key: Self::Key, value: Self::Value) { + self.borrow_mut().insert(key, value); + } + + #[inline(always)] + fn has(&self, key: Self::Key) -> bool { + self.borrow().contains_key(&key) + } + + #[inline(always)] + fn clear(&self) { + self.borrow_mut().clear() + } + + #[inline(always)] + fn delete(&self, key: Self::Key) -> bool { + self.borrow_mut().shift_remove(&key).is_some() + } +} + +/// Usage: +/// ```rust +/// pub struct TestBindingMaplike { +/// // maplike +/// internal: DomRefCell>, +/// } +/// impl Maplike for TestBindingSetlike { +/// type Key = DOMString; +/// type Value = i32; +/// maplike!(self, internal); +/// } +/// ``` +#[macro_export] +macro_rules! maplike { + ( $self:ident, $internal:ident ) => { + #[inline(always)] + fn get_index(&$self, index: u32) -> Option<(Self::Key, Self::Value)> { + $self.$internal.get_index(index) + } + + #[inline(always)] + fn get(&$self, key: Self::Key) -> Option { + $self.$internal.get(key) + } + + #[inline(always)] + fn size(&$self) -> u32 { + $self.$internal.size() + } + + #[inline(always)] + fn set(&$self, key: Self::Key, value: Self::Value) { + $self.$internal.set(key, value) + } + + #[inline(always)] + fn has(&$self, key: Self::Key) -> bool { + $self.$internal.has(key) + } + + #[inline(always)] + fn clear(&$self) { + $self.$internal.clear() + } + + #[inline(always)] + fn delete(&$self, key: Self::Key) -> bool { + $self.$internal.delete(key) + } + }; +} diff --git a/components/script/dom/bindings/mod.rs b/components/script/dom/bindings/mod.rs index e446af3693f..9263fca2ba6 100644 --- a/components/script/dom/bindings/mod.rs +++ b/components/script/dom/bindings/mod.rs @@ -143,6 +143,7 @@ pub mod htmlconstructor; pub mod inheritance; pub mod interface; pub mod iterable; +pub mod like; pub mod namespace; pub mod num; pub mod principals; diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 95d1821b87a..ae92d11de8f 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -532,8 +532,10 @@ pub mod svggraphicselement; pub mod svgsvgelement; pub mod testbinding; pub mod testbindingiterable; +pub mod testbindingmaplike; pub mod testbindingpairiterable; pub mod testbindingproxy; +pub mod testbindingsetlike; pub mod testrunner; pub mod testworklet; pub mod testworkletglobalscope; diff --git a/components/script/dom/testbindingmaplike.rs b/components/script/dom/testbindingmaplike.rs new file mode 100644 index 00000000000..83ebcce4646 --- /dev/null +++ b/components/script/dom/testbindingmaplike.rs @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// check-tidy: no specs after this line + +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::TestBindingMaplikeBinding::TestBindingMaplikeMethods; +use crate::dom::bindings::error::Fallible; +use crate::dom::bindings::like::Maplike; +use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, Reflector}; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::str::DOMString; +use crate::dom::globalscope::GlobalScope; +use crate::maplike; +use dom_struct::dom_struct; +use indexmap::IndexMap; +use js::rust::HandleObject; + +use super::bindings::error::Error; + +/// maplike +#[dom_struct] +pub struct TestBindingMaplike { + reflector: Reflector, + #[custom_trace] + internal: DomRefCell>, +} + +impl TestBindingMaplike { + fn new(global: &GlobalScope, proto: Option) -> DomRoot { + reflect_dom_object_with_proto( + Box::new(TestBindingMaplike { + reflector: Reflector::new(), + internal: DomRefCell::new(IndexMap::new()), + }), + global, + proto, + ) + } + + #[allow(non_snake_case)] + pub fn Constructor( + global: &GlobalScope, + proto: Option, + ) -> Fallible> { + Ok(TestBindingMaplike::new(global, proto)) + } +} + +impl TestBindingMaplikeMethods for TestBindingMaplike { + fn SetInternal(&self, key: DOMString, value: i32) { + self.internal.set(key, value) + } + + fn ClearInternal(&self) { + self.internal.clear() + } + + fn DeleteInternal(&self, key: DOMString) -> bool { + self.internal.delete(key) + } + + fn HasInternal(&self, key: DOMString) -> bool { + self.internal.has(key) + } + + fn GetInternal(&self, key: DOMString) -> Fallible { + // TODO: error type? + self.internal + .borrow() + .get(&key) + .ok_or_else(|| Error::Type(format!("No entry for key {key}"))) + .copied() + } + + fn Size(&self) -> u32 { + self.internal.size() + } +} + +// this error is wrong because if we inline Self::Key and Self::Value all errors are gone +// TODO: FIX THIS +#[allow(unrooted_must_root)] +impl Maplike for TestBindingMaplike { + type Key = DOMString; + type Value = i32; + + maplike!(self, internal); +} diff --git a/components/script/dom/testbindingsetlike.rs b/components/script/dom/testbindingsetlike.rs new file mode 100644 index 00000000000..9d62afaf653 --- /dev/null +++ b/components/script/dom/testbindingsetlike.rs @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// check-tidy: no specs after this line + +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::TestBindingSetlikeBinding::TestBindingSetlikeMethods; +use crate::dom::bindings::error::Fallible; +use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, Reflector}; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::str::DOMString; +use crate::dom::globalscope::GlobalScope; +use crate::setlike; +use dom_struct::dom_struct; +use indexmap::IndexSet; +use js::rust::HandleObject; + +use super::bindings::like::Setlike; + +// setlike +#[dom_struct] +pub struct TestBindingSetlike { + reflector: Reflector, + #[custom_trace] + internal: DomRefCell>, +} + +impl TestBindingSetlike { + fn new(global: &GlobalScope, proto: Option) -> DomRoot { + reflect_dom_object_with_proto( + Box::new(TestBindingSetlike { + reflector: Reflector::new(), + internal: DomRefCell::new(IndexSet::new()), + }), + global, + proto, + ) + } + + #[allow(non_snake_case)] + pub fn Constructor( + global: &GlobalScope, + proto: Option, + ) -> Fallible> { + Ok(TestBindingSetlike::new(global, proto)) + } +} + +impl TestBindingSetlikeMethods for TestBindingSetlike { + fn Size(&self) -> u32 { + self.internal.size() + } +} + +// this error is wrong because if we inline Self::Key and Self::Value all errors are gone +// TODO: FIX THIS +#[allow(unrooted_must_root)] +impl Setlike for TestBindingSetlike { + type Key = DOMString; + + setlike!(self, internal); +} diff --git a/components/script/dom/webidls/TestBindingMaplike.webidl b/components/script/dom/webidls/TestBindingMaplike.webidl new file mode 100644 index 00000000000..f9712d63a8a --- /dev/null +++ b/components/script/dom/webidls/TestBindingMaplike.webidl @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// This interface is entirely internal to Servo, and should not be accessible to +// web pages. + +[Pref="dom.testbinding.enabled", Exposed=(Window,Worker)] +interface TestBindingMaplike { + [Throws] + constructor(); + + maplike; + undefined setInternal(DOMString aKey, long aValue); + undefined clearInternal(); + boolean deleteInternal(DOMString aKey); + boolean hasInternal(DOMString aKey); + [Throws] + long getInternal(DOMString aKey); +}; diff --git a/components/script/dom/webidls/TestBindingSetlike.webidl b/components/script/dom/webidls/TestBindingSetlike.webidl new file mode 100644 index 00000000000..c8cfcf73b86 --- /dev/null +++ b/components/script/dom/webidls/TestBindingSetlike.webidl @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// This interface is entirely internal to Servo, and should not be accessible to +// web pages. + +[Pref="dom.testbinding.enabled", Exposed=(Window,Worker)] +interface TestBindingSetlike { + [Throws] + constructor(); + + setlike; +}; diff --git a/tests/wpt/mozilla/meta-legacy-layout/mozilla/like.any.js.ini b/tests/wpt/mozilla/meta-legacy-layout/mozilla/like.any.js.ini new file mode 100644 index 00000000000..626c7c904f2 --- /dev/null +++ b/tests/wpt/mozilla/meta-legacy-layout/mozilla/like.any.js.ini @@ -0,0 +1,23 @@ +[like.any.html] + type: testharness + prefs: [dom.testbinding.enabled:true] + [Test defaulting arguments on setlike to undefined] + expected: FAIL + + [Simple map creation and functionality test] + expected: FAIL + + [Test defaulting arguments on maplike to undefined] + expected: FAIL + +[like.any.worker.html] + type: testharness + prefs: [dom.testbinding.enabled:true] + [Test defaulting arguments on setlike to undefined] + expected: FAIL + + [Simple map creation and functionality test] + expected: FAIL + + [Test defaulting arguments on maplike to undefined] + expected: FAIL diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index a360705ea8c..d6e7863afe8 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -13451,6 +13451,31 @@ {} ] ], + "like.any.js": [ + "11e4d86325ce6216754a0e0b760c59beb812db85", + [ + "mozilla/like.any.html", + { + "script_metadata": [ + [ + "title", + "Setlike and Maplike bindings" + ] + ] + } + ], + [ + "mozilla/like.any.worker.html", + { + "script_metadata": [ + [ + "title", + "Setlike and Maplike bindings" + ] + ] + } + ] + ], "link_rel_crash.html": [ "9fa5a1dc04fff0f879d88ce95f4eb5181e2f9b92", [ diff --git a/tests/wpt/mozilla/meta/mozilla/like.any.js.ini b/tests/wpt/mozilla/meta/mozilla/like.any.js.ini new file mode 100644 index 00000000000..626c7c904f2 --- /dev/null +++ b/tests/wpt/mozilla/meta/mozilla/like.any.js.ini @@ -0,0 +1,23 @@ +[like.any.html] + type: testharness + prefs: [dom.testbinding.enabled:true] + [Test defaulting arguments on setlike to undefined] + expected: FAIL + + [Simple map creation and functionality test] + expected: FAIL + + [Test defaulting arguments on maplike to undefined] + expected: FAIL + +[like.any.worker.html] + type: testharness + prefs: [dom.testbinding.enabled:true] + [Test defaulting arguments on setlike to undefined] + expected: FAIL + + [Simple map creation and functionality test] + expected: FAIL + + [Test defaulting arguments on maplike to undefined] + expected: FAIL diff --git a/tests/wpt/mozilla/tests/mozilla/like.any.js b/tests/wpt/mozilla/tests/mozilla/like.any.js new file mode 100644 index 00000000000..11e4d86325c --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/like.any.js @@ -0,0 +1,308 @@ +// META: title=Setlike and Maplike bindings + +function collect(iter) { + var collection = []; + for (element of iter) { + collection.push(element); + } + return collection; +} + +// mochitest does not do strict true test +function ok(t, desc) { + if (t) { + return true + } + return false; +} + +// tests ported from https://searchfox.org/mozilla-central/source/dom/bindings/test/test_bug1123516_maplikesetlike.html + +var base_properties = [["has", "function", 1], +["entries", "function", 0], +["keys", "function", 0], +["values", "function", 0], +["forEach", "function", 1], +["size", "number"]]; +var maplike_properties = base_properties.concat([["set", "function", 2]]); +var rw_properties = [["clear", "function", 0], +["delete", "function", 1]]; +var setlike_rw_properties = base_properties.concat(rw_properties).concat([["add", "function", 1]]); +var maplike_rw_properties = maplike_properties.concat(rw_properties).concat([["get", "function", 1]]); +var testExistence = function testExistence(prefix, obj, properties) { + for (var [name, type, args] of properties) { + // Properties are somewhere up the proto chain, hasOwnProperty won't work + assert_not_equals(obj[name], undefined, + `${prefix} object has property ${name}`); + + assert_equals(typeof obj[name], type, + `${prefix} object property ${name} is a ${type}`); + // Check function length + if (type == "function") { + assert_equals(obj[name].length, args, + `${prefix} object property ${name} is length ${args}`); + assert_equals(obj[name].name, name, + `${prefix} object method name is ${name}`); + } + + // Find where property is on proto chain, check for enumerablility there. + var owner = obj; + while (owner) { + var propDesc = Object.getOwnPropertyDescriptor(owner, name); + if (propDesc) { + assert_true(ok(propDesc.enumerable), + `${prefix} object property ${name} should be enumerable`); + break; + } + owner = Object.getPrototypeOf(owner); + } + } +}; + +test(function () { + var m = new TestBindingSetlike(); + assert_true(ok(m), "SimpleSet: got a TestBindingSetlike object"); + testExistence("SimpleSet: ", m, setlike_rw_properties); + assert_equals(m.size, 0, "SimpleSet: size should be zero"); + assert_true(!m.has("test"), "SimpleSet: maplike has should return false"); + m1 = m.add("test"); + assert_equals(m, m1, "SimpleSet: return from set should be map object"); + assert_equals(m.size, 1, "SimpleSet: size should be 1"); + assert_true(m.has("test"), "SimpleSet: maplike has should return true"); + m.add("test2"); + assert_equals(m.size, 2, "SimpleSet: size should be 2"); + testSet = ["test", "test2"]; + testIndex = 0; + m.forEach(function (v, k, o) { + "use strict"; + assert_equals(o, m, "SimpleSet: foreach obj is correct"); + assert_equals(k, testSet[testIndex], "SimpleSet: foreach set key: " + k + " = " + testSet[testIndex]); + testIndex += 1; + }); + assert_equals(testIndex, 2, "SimpleSet: foreach ran correct number of times"); + assert_true(m.has("test2"), "SimpleSet: maplike has should return true"); + assert_equals(m.delete("test2"), true, "SimpleSet: maplike deletion should return true"); + assert_equals(m.size, 1, "SimpleSet: size should be 1"); + iterable = false; + for (let e of m) { + iterable = true; + assert_equals(e, "test", "SimpleSet: iterable first array element should be key"); + } + assert_equals(m[Symbol.iterator].length, 0, "SimpleSet: @@iterator symbol is correct length"); + assert_equals(m[Symbol.iterator].name, "values", "SimpleSet: @@iterator symbol has correct name"); + assert_equals(m[Symbol.iterator], m.values, 'SimpleSet: @@iterator is an alias for "values"'); + assert_true(ok(iterable), "SimpleSet: @@iterator symbol resolved correctly"); + for (let k of m.keys()) { + assert_equals(k, "test", "SimpleSet: first keys element should be 'test'"); + } + for (let v of m.values()) { + assert_equals(v, "test", "SimpleSet: first values elements should be 'test'"); + } + for (let e of m.entries()) { + assert_equals(e[0], "test", "SimpleSet: Entries first array element should be 'test'"); + assert_equals(e[1], "test", "SimpleSet: Entries second array element should be 'test'"); + } + m.clear(); + assert_equals(m.size, 0, "SimpleSet: size should be 0 after clear"); +}, "Simple set creation and functionality"); + +test(function () { + var m = new TestBindingSetlike(); + m.add(); + assert_equals(m.size, 1, "SetArgsDefault: should have 1 entry"); + m.forEach(function (v, k) { + "use strict"; + assert_equals(typeof k, "string", "SetArgsDefault: key is a string"); + assert_equals(k, "undefined", "SetArgsDefault: key is the string undefined"); + }); + m.delete(); + assert_equals(m.size, 0, "SetArgsDefault: should have 0 entries"); +}, "Test defaulting arguments on setlike to undefined"); + +test(function () { + // Simple map creation and functionality test + m = new TestBindingMaplike(); + assert_true(ok(m), "SimpleMap: got a TestBindingMaplike object"); + testExistence("SimpleMap: ", m, maplike_rw_properties); + assert_equals(m.size, 0, "SimpleMap: size should be zero"); + assert_true(!m.has("test"), "SimpleMap: maplike has should return false"); + assert_equals(m.get("test"), undefined, "SimpleMap: maplike get should return undefined on bogus lookup"); + var m1 = m.set("test", 1); + assert_equals(m, m1, "SimpleMap: return from set should be map object"); + assert_equals(m.size, 1, "SimpleMap: size should be 1"); + assert_true(m.has("test"), "SimpleMap: maplike has should return true"); + assert_equals(m.get("test"), 1, "SimpleMap: maplike get should return value entered"); + m.set("test2", 2); + assert_equals(m.size, 2, "SimpleMap: size should be 2"); + testSet = [["test", 1], ["test2", 2]]; + testIndex = 0; + m.forEach(function (v, k, o) { + "use strict"; + assert_equals(o, m, "SimpleMap: foreach obj is correct"); + assert_equals(k, testSet[testIndex][0], "SimpleMap: foreach map key: " + k + " = " + testSet[testIndex][0]); + assert_equals(v, testSet[testIndex][1], "SimpleMap: foreach map value: " + v + " = " + testSet[testIndex][1]); + testIndex += 1; + }); + assert_equals(testIndex, 2, "SimpleMap: foreach ran correct number of times"); + assert_true(m.has("test2"), "SimpleMap: maplike has should return true"); + assert_equals(m.get("test2"), 2, "SimpleMap: maplike get should return value entered"); + assert_equals(m.delete("test2"), true, "SimpleMap: maplike deletion should return boolean"); + assert_equals(m.size, 1, "SimpleMap: size should be 1"); + var iterable = false; + for (let e of m) { + iterable = true; + assert_equals(e[0], "test", "SimpleMap: iterable first array element should be key"); + assert_equals(e[1], 1, "SimpleMap: iterable second array element should be value"); + } + assert_equals(m[Symbol.iterator].length, 0, "SimpleMap: @@iterator symbol is correct length"); + assert_equals(m[Symbol.iterator].name, "entries", "SimpleMap: @@iterator symbol has correct name"); + assert_equals(m[Symbol.iterator], m.entries, 'SimpleMap: @@iterator is an alias for "entries"'); + assert_true(ok(iterable), "SimpleMap: @@iterator symbol resolved correctly"); + for (let k of m.keys()) { + assert_equals(k, "test", "SimpleMap: first keys element should be 'test'"); + } + for (let v of m.values()) { + assert_equals(v, 1, "SimpleMap: first values elements should be 1"); + } + for (let e of m.entries()) { + assert_equals(e[0], "test", "SimpleMap: entries first array element should be 'test'"); + assert_equals(e[1], 1, "SimpleMap: entries second array element should be 1"); + } + m.clear(); + assert_equals(m.size, 0, "SimpleMap: size should be 0 after clear"); +}, "Simple map creation and functionality test"); + +test(function () { + // Map convenience function test + m = new TestBindingMaplike(); + assert_true(ok(m), "MapConvenience: got a TestBindingMaplike object"); + assert_equals(m.size, 0, "MapConvenience: size should be zero"); + assert_true(!m.hasInternal("test"), "MapConvenience: maplike hasInternal should return false"); + // It's fine to let getInternal to return 0 if the key doesn't exist + // because this API can only be used internally in C++ and we'd throw + // an error if the key doesn't exist. + //SimpleTest.doesThrow(() => m.getInternal("test"), 0, "MapConvenience: maplike getInternal should throw if the key doesn't exist"); + m.setInternal("test", 1); + assert_equals(m.size, 1, "MapConvenience: size should be 1"); + assert_true(m.hasInternal("test"), "MapConvenience: maplike hasInternal should return true"); + assert_equals(m.get("test"), 1, "MapConvenience: maplike get should return value entered"); + assert_equals(m.getInternal("test"), 1, "MapConvenience: maplike getInternal should return value entered"); + m.setInternal("test2", 2); + assert_equals(m.size, 2, "size should be 2"); + assert_true(m.hasInternal("test2"), "MapConvenience: maplike hasInternal should return true"); + assert_equals(m.get("test2"), 2, "MapConvenience: maplike get should return value entered"); + assert_equals(m.getInternal("test2"), 2, "MapConvenience: maplike getInternal should return value entered"); + assert_equals(m.deleteInternal("test2"), true, "MapConvenience: maplike deleteInternal should return true"); + assert_equals(m.size, 1, "MapConvenience: size should be 1"); + m.clearInternal(); + assert_equals(m.size, 0, "MapConvenience: size should be 0 after clearInternal"); +}, "Map convenience function test"); + +// JS implemented map creation convenience function test +test(function () { + // Test this override for forEach + m = new TestBindingMaplike(); + m.set("test", 1); + m.forEach(function (v, k, o) { + "use strict"; + assert_equals(o, m, "ForEachThisOverride: foreach obj is correct"); + assert_equals(this, 5, "ForEachThisOverride: 'this' value should be correct"); + }, 5); +}, "Test this override for forEach"); + +test(function () { + // Test defaulting arguments on maplike to undefined + m = new TestBindingMaplike(); + m.set(); + assert_equals(m.size, 1, "MapArgsDefault: should have 1 entry"); + m.forEach(function (v, k) { + "use strict"; + assert_equals(typeof k, "string", "MapArgsDefault: key is a string"); + assert_equals(k, "undefined", "MapArgsDefault: key is the string undefined"); + assert_equals(v, 0, "MapArgsDefault: value is 0"); + }); + assert_equals(m.get(), 0, "MapArgsDefault: no argument to get() returns correct value"); + m.delete(); + assert_equals(m.size, 0, "MapArgsDefault: should have 0 entries"); +}, "Test defaulting arguments on maplike to undefined"); + +// some iterable test ported to *like interfaces +test(function () { + var t = new TestBindingSetlike(); + var empty = true; + t.forEach(function () { empty = false; }); + assert_true(empty); +}, "Empty setlike"); + +test(function () { + var t = new TestBindingSetlike(); + function is_iterator(o) { + return o[Symbol.iterator]() === o; + } + assert_true(is_iterator(t.keys())); + assert_true(is_iterator(t.values())); + assert_true(is_iterator(t.entries())); +}, "Setlike are iterators"); + +test(function () { + var t = new TestBindingSetlike(); + t.add("first"); + t.add("second"); + t.add("third"); + assert_array_equals(collect(t.keys()), collect(t.values())); + assert_array_equals(collect(t.values()), ["first", "second", "third"]); + var expected = [["first", "first"], ["second", "second"], ["third", "third"]]; + var i = 0; + for (entry of t.entries()) { + assert_array_equals(entry, expected[i++]); + } + + t.add("fourth"); + assert_array_equals(collect(t.keys()), collect(t.values())); + assert_array_equals(collect(t.values()), ["first", "second", "third", "fourth"]); + var expected = [["first", "first"], ["second", "second"], ["third", "third"], ["fourth", "fourth"]]; + var i = 0; + for (entry of t.entries()) { + assert_array_equals(entry, expected[i++]); + } +}, "Iterators iterate over values"); + +test(function () { + var t = new TestBindingMaplike(); + var empty = true; + t.forEach(function () { empty = false; }); + assert_true(empty); +}, "Empty maplike"); + +test(function () { + var t = new TestBindingMaplike(); + function is_iterator(o) { + return o[Symbol.iterator]() === o; + } + assert_true(is_iterator(t.keys())); + assert_true(is_iterator(t.values())); + assert_true(is_iterator(t.entries())); +}, "Maplike are iterators"); + +test(function () { + var t = new TestBindingMaplike(); + t.set("first", 0); + t.set("second", 1); + t.set("third", 2); + assert_array_equals(collect(t.keys()), ["first", "second", "third"]); + assert_array_equals(collect(t.values()), [0, 1, 2]); + var expected = [["first", 0], ["second", 1], ["third", 2]]; + var i = 0; + for (entry of t.entries()) { + assert_array_equals(entry, expected[i++]); + } + + t.set("fourth", 3); + assert_array_equals(collect(t.keys()), ["first", "second", "third", "fourth"]); + assert_array_equals(collect(t.values()), [0, 1, 2, 3]); + var expected = [["first", 0], ["second", 1], ["third", 2], ["fourth", 3]]; + var i = 0; + for (entry of t.entries()) { + assert_array_equals(entry, expected[i++]); + } +}, "Maplike iterate over key/value pairs"); diff --git a/third_party/WebIDL/WebIDL.py b/third_party/WebIDL/WebIDL.py index 2366e3f7027..e1d973f5fe6 100644 --- a/third_party/WebIDL/WebIDL.py +++ b/third_party/WebIDL/WebIDL.py @@ -9022,6 +9022,67 @@ class Parser(Tokenizer): itr_iface.asyncIterableInterface = iface self._productions.append(itr_iface) iterable.iteratorType = IDLWrapperType(iface.location, itr_iface) + if not iterable: + # We haven't run finish() on the interface yet, so we don't know + # whether our interface is maplike/setlike/iterable or not. This + # means we have to loop through the members to see if we have an + # iterable member. + for m in iface.members: + if isinstance(m, IDLMaplikeOrSetlike): + iterable = m + break + if iterable and (iterable.isSetlike() or iterable.isMaplike()): + + def simpleExtendedAttr(str): + return IDLExtendedAttribute(iface.location, (str,)) + + if isinstance(iterable, IDLAsyncIterable): + nextReturnType = IDLPromiseType( + iterable.location, BuiltinTypes[IDLBuiltinType.Types.any] + ) + else: + nextReturnType = BuiltinTypes[IDLBuiltinType.Types.object] + nextMethod = IDLMethod( + iterable.location, + IDLUnresolvedIdentifier(iterable.location, "next"), + nextReturnType, + [], + ) + nextMethod.addExtendedAttributes([simpleExtendedAttr("Throws")]) + + methods = [nextMethod] + + if iterable.isSetlike(): + itr_suffix = "Setlike" + else: + itr_suffix = "Maplike" + itr_ident = IDLUnresolvedIdentifier( + iface.location, iface.identifier.name + itr_suffix + ) + classNameOverride = iface.identifier.name + " " + itr_suffix + itr_iface = IDLInterface( + iface.location, + self.globalScope(), + itr_ident, + None, + methods, + isKnownNonPartial=True, + classNameOverride=classNameOverride, + ) + itr_iface.addExtendedAttributes( + [simpleExtendedAttr("LegacyNoInterfaceObject")] + ) + # Make sure the exposure set for the iterator interface is the + # same as the exposure set for the iterable interface, because + # we're going to generate methods on the iterable that return + # instances of the iterator. + itr_iface._exposureGlobalNames = set(iface._exposureGlobalNames) + # Always append generated iterable interfaces after the + # interface they're a member of, otherwise nativeType generation + # won't work correctly. + itr_iface.iterableInterface = iface + self._productions.append(itr_iface) + iterable.iteratorType = IDLWrapperType(iface.location, itr_iface) # Make sure we finish IDLIncludesStatements before we finish the # IDLInterfaces. diff --git a/third_party/WebIDL/like-as-iterable.patch b/third_party/WebIDL/like-as-iterable.patch new file mode 100644 index 00000000000..4ba3c3628ed --- /dev/null +++ b/third_party/WebIDL/like-as-iterable.patch @@ -0,0 +1,72 @@ +diff --git a/third_party/WebIDL/WebIDL.py b/third_party/WebIDL/WebIDL.py +index 2366e3f702..e1d973f5fe 100644 +--- a/third_party/WebIDL/WebIDL.py ++++ b/third_party/WebIDL/WebIDL.py +@@ -9022,6 +9022,67 @@ class Parser(Tokenizer): + itr_iface.asyncIterableInterface = iface + self._productions.append(itr_iface) + iterable.iteratorType = IDLWrapperType(iface.location, itr_iface) ++ if not iterable: ++ # We haven't run finish() on the interface yet, so we don't know ++ # whether our interface is maplike/setlike/iterable or not. This ++ # means we have to loop through the members to see if we have an ++ # iterable member. ++ for m in iface.members: ++ if isinstance(m, IDLMaplikeOrSetlike): ++ iterable = m ++ break ++ if iterable and (iterable.isSetlike() or iterable.isMaplike()): ++ ++ def simpleExtendedAttr(str): ++ return IDLExtendedAttribute(iface.location, (str,)) ++ ++ if isinstance(iterable, IDLAsyncIterable): ++ nextReturnType = IDLPromiseType( ++ iterable.location, BuiltinTypes[IDLBuiltinType.Types.any] ++ ) ++ else: ++ nextReturnType = BuiltinTypes[IDLBuiltinType.Types.object] ++ nextMethod = IDLMethod( ++ iterable.location, ++ IDLUnresolvedIdentifier(iterable.location, "next"), ++ nextReturnType, ++ [], ++ ) ++ nextMethod.addExtendedAttributes([simpleExtendedAttr("Throws")]) ++ ++ methods = [nextMethod] ++ ++ if iterable.isSetlike(): ++ itr_suffix = "Setlike" ++ else: ++ itr_suffix = "Maplike" ++ itr_ident = IDLUnresolvedIdentifier( ++ iface.location, iface.identifier.name + itr_suffix ++ ) ++ classNameOverride = iface.identifier.name + " " + itr_suffix ++ itr_iface = IDLInterface( ++ iface.location, ++ self.globalScope(), ++ itr_ident, ++ None, ++ methods, ++ isKnownNonPartial=True, ++ classNameOverride=classNameOverride, ++ ) ++ itr_iface.addExtendedAttributes( ++ [simpleExtendedAttr("LegacyNoInterfaceObject")] ++ ) ++ # Make sure the exposure set for the iterator interface is the ++ # same as the exposure set for the iterable interface, because ++ # we're going to generate methods on the iterable that return ++ # instances of the iterator. ++ itr_iface._exposureGlobalNames = set(iface._exposureGlobalNames) ++ # Always append generated iterable interfaces after the ++ # interface they're a member of, otherwise nativeType generation ++ # won't work correctly. ++ itr_iface.iterableInterface = iface ++ self._productions.append(itr_iface) ++ iterable.iteratorType = IDLWrapperType(iface.location, itr_iface) + + # Make sure we finish IDLIncludesStatements before we finish the + # IDLInterfaces. diff --git a/third_party/WebIDL/update.sh b/third_party/WebIDL/update.sh index cec4d6a378e..e4d1c2d6d32 100755 --- a/third_party/WebIDL/update.sh +++ b/third_party/WebIDL/update.sh @@ -5,6 +5,7 @@ patch < callback-location.patch patch < union-typedef.patch patch < inline.patch patch < readable-stream.patch +patch < like-as-iterable.patch wget https://hg.mozilla.org/mozilla-central/archive/tip.zip/dom/bindings/parser/tests/ -O tests.zip rm -r tests