From 82f1d5895cd99110d51b9777961688ef4534fb61 Mon Sep 17 00:00:00 2001 From: Cameron Kaiser Date: Sat, 16 Dec 2017 22:44:22 -0800 Subject: [PATCH] #399: ChildNode-ParentNode DOM4 M911477 M1301777 M1308922 M1104955 M1054759 M1258163 --- dom/base/nsINode.cpp | 189 ++++++++++++++++++ dom/base/nsINode.h | 14 ++ dom/bindings/BindingUtils.cpp | 31 ++- dom/bindings/BindingUtils.h | 5 +- dom/bindings/Codegen.py | 31 ++- dom/bindings/parser/WebIDL.py | 16 ++ dom/webidl/ChildNode.webidl | 11 +- dom/webidl/ParentNode.webidl | 7 +- .../tests/debug/Environment-unscopables.js | 37 ++++ js/src/jsapi.h | 18 +- js/src/jsarray.cpp | 30 ++- js/src/jsarray.h | 3 - js/src/tests/ecma_6/Array/unscopables.js | 51 +++++ .../LexicalEnvironment/unscopables-basics.js | 22 ++ .../unscopables-closures.js | 23 +++ .../LexicalEnvironment/unscopables-const.js | 8 + .../LexicalEnvironment/unscopables-delete.js | 27 +++ .../LexicalEnvironment/unscopables-getters.js | 41 ++++ .../LexicalEnvironment/unscopables-global.js | 18 ++ .../LexicalEnvironment/unscopables-ignored.js | 22 ++ .../LexicalEnvironment/unscopables-miss.js | 7 + .../unscopables-mutation-frozen.js | 18 ++ .../unscopables-mutation.js | 44 ++++ .../LexicalEnvironment/unscopables-proto.js | 39 ++++ .../LexicalEnvironment/unscopables-proxy.js | 46 +++++ .../LexicalEnvironment/unscopables-strict.js | 32 +++ .../LexicalEnvironment/unscopables-tdz.js | 9 + js/src/vm/CommonPropertyNames.h | 8 +- js/src/vm/Debugger.cpp | 8 + js/src/vm/Runtime.h | 7 +- js/src/vm/ScopeObject.cpp | 63 +++++- 31 files changed, 850 insertions(+), 35 deletions(-) create mode 100644 js/src/jit-test/tests/debug/Environment-unscopables.js create mode 100644 js/src/tests/ecma_6/Array/unscopables.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-basics.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-closures.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-const.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-delete.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-getters.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-global.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-ignored.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-miss.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation-frozen.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-proto.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-proxy.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-strict.js create mode 100644 js/src/tests/ecma_6/LexicalEnvironment/unscopables-tdz.js diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp index 205bd1450..c6982976c 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp @@ -1650,6 +1650,168 @@ nsINode::GetNextElementSibling() const return nullptr; } +static already_AddRefed +GetNodeFromNodeOrString(const OwningNodeOrString& aNode, + nsIDocument* aDocument) +{ + if (aNode.IsNode()) { + nsCOMPtr node = aNode.GetAsNode(); + return node.forget(); + } + + if (aNode.IsString()){ + RefPtr textNode = + aDocument->CreateTextNode(aNode.GetAsString()); + return textNode.forget(); + } + + MOZ_CRASH("Impossible type"); +} + +/** + * Implement the algorithm specified at + * https://dom.spec.whatwg.org/#converting-nodes-into-a-node for |prepend()|, + * |append()|, |before()|, |after()|, and |replaceWith()| APIs. + */ +static already_AddRefed +ConvertNodesOrStringsIntoNode(const Sequence& aNodes, + nsIDocument* aDocument, + ErrorResult& aRv) +{ + if (aNodes.Length() == 1) { + return GetNodeFromNodeOrString(aNodes[0], aDocument); + } + + nsCOMPtr fragment = aDocument->CreateDocumentFragment(); + + for (const auto& node : aNodes) { + nsCOMPtr childNode = GetNodeFromNodeOrString(node, aDocument); + fragment->AppendChild(*childNode, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + return fragment.forget(); +} + +static void +InsertNodesIntoHashset(const Sequence& aNodes, + nsTHashtable>& aHashset) +{ + for (const auto& node : aNodes) { + if (node.IsNode()) { + aHashset.PutEntry(node.GetAsNode()); + } + } +} + +static nsINode* +FindViablePreviousSibling(const nsINode& aNode, + const Sequence& aNodes) +{ + nsTHashtable> nodeSet(16); + InsertNodesIntoHashset(aNodes, nodeSet); + + nsINode* viablePreviousSibling = nullptr; + for (nsINode* sibling = aNode.GetPreviousSibling(); sibling; + sibling = sibling->GetPreviousSibling()) { + if (!nodeSet.Contains(sibling)) { + viablePreviousSibling = sibling; + break; + } + } + + return viablePreviousSibling; +} + +static nsINode* +FindViableNextSibling(const nsINode& aNode, + const Sequence& aNodes) +{ + nsTHashtable> nodeSet(16); + InsertNodesIntoHashset(aNodes, nodeSet); + + nsINode* viableNextSibling = nullptr; + for (nsINode* sibling = aNode.GetNextSibling(); sibling; + sibling = sibling->GetNextSibling()) { + if (!nodeSet.Contains(sibling)) { + viableNextSibling = sibling; + break; + } + } + + return viableNextSibling; +} + +void +nsINode::Before(const Sequence& aNodes, + ErrorResult& aRv) +{ + nsCOMPtr parent = GetParentNode(); + if (!parent) { + return; + } + + nsCOMPtr viablePreviousSibling = + FindViablePreviousSibling(*this, aNodes); + + nsCOMPtr node = + ConvertNodesOrStringsIntoNode(aNodes, OwnerDoc(), aRv); + if (aRv.Failed()) { + return; + } + + viablePreviousSibling = viablePreviousSibling ? + viablePreviousSibling->GetNextSibling() : parent->GetFirstChild(); + + parent->InsertBefore(*node, viablePreviousSibling, aRv); +} + +void +nsINode::After(const Sequence& aNodes, + ErrorResult& aRv) +{ + nsCOMPtr parent = GetParentNode(); + if (!parent) { + return; + } + + nsCOMPtr viableNextSibling = FindViableNextSibling(*this, aNodes); + + nsCOMPtr node = + ConvertNodesOrStringsIntoNode(aNodes, OwnerDoc(), aRv); + if (aRv.Failed()) { + return; + } + + parent->InsertBefore(*node, viableNextSibling, aRv); +} + +void +nsINode::ReplaceWith(const Sequence& aNodes, + ErrorResult& aRv) +{ + nsCOMPtr parent = GetParentNode(); + if (!parent) { + return; + } + + nsCOMPtr viableNextSibling = FindViableNextSibling(*this, aNodes); + + nsCOMPtr node = + ConvertNodesOrStringsIntoNode(aNodes, OwnerDoc(), aRv); + if (aRv.Failed()) { + return; + } + + if (parent == GetParentNode()) { + parent->ReplaceChild(*node, *this, aRv); + } else { + parent->InsertBefore(*node, viableNextSibling, aRv); + } +} + void nsINode::Remove() { @@ -1693,6 +1855,33 @@ nsINode::GetLastElementChild() const return nullptr; } +void +nsINode::Prepend(const Sequence& aNodes, + ErrorResult& aRv) +{ + nsCOMPtr node = + ConvertNodesOrStringsIntoNode(aNodes, OwnerDoc(), aRv); + if (aRv.Failed()) { + return; + } + + nsCOMPtr refNode = mFirstChild; + InsertBefore(*node, refNode, aRv); +} + +void +nsINode::Append(const Sequence& aNodes, + ErrorResult& aRv) +{ + nsCOMPtr node = + ConvertNodesOrStringsIntoNode(aNodes, OwnerDoc(), aRv); + if (aRv.Failed()) { + return; + } + + AppendChild(*node, aRv); +} + void nsINode::doRemoveChildAt(uint32_t aIndex, bool aNotify, nsIContent* aKid, nsAttrAndChildArray& aChildArray) diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h index 9b1b5de2b..a9457fa1f 100644 --- a/dom/base/nsINode.h +++ b/dom/base/nsINode.h @@ -75,6 +75,8 @@ class DOMRectReadOnly; class Element; class EventHandlerNonNull; template class Optional; +class OwningNodeOrString; +template class Sequence; class Text; class TextOrElementOrDocument; struct DOMPointInit; @@ -267,9 +269,13 @@ public: typedef mozilla::dom::DOMPointInit DOMPointInit; typedef mozilla::dom::DOMQuad DOMQuad; typedef mozilla::dom::DOMRectReadOnly DOMRectReadOnly; + typedef mozilla::dom::OwningNodeOrString OwningNodeOrString; typedef mozilla::dom::TextOrElementOrDocument TextOrElementOrDocument; typedef mozilla::ErrorResult ErrorResult; + template + using Sequence = mozilla::dom::Sequence; + NS_DECLARE_STATIC_IID_ACCESSOR(NS_INODE_IID) // Among the sub-classes that inherit (directly or indirectly) from nsINode, @@ -1807,6 +1813,11 @@ public: // ChildNode methods mozilla::dom::Element* GetPreviousElementSibling() const; mozilla::dom::Element* GetNextElementSibling() const; + + void Before(const Sequence& aNodes, ErrorResult& aRv); + void After(const Sequence& aNodes, ErrorResult& aRv); + void ReplaceWith(const Sequence& aNodes, + ErrorResult& aRv); /** * Remove this node from its parent, if any. */ @@ -1816,6 +1827,9 @@ public: mozilla::dom::Element* GetFirstElementChild() const; mozilla::dom::Element* GetLastElementChild() const; + void Prepend(const Sequence& aNodes, ErrorResult& aRv); + void Append(const Sequence& aNodes, ErrorResult& aRv); + void GetBoxQuads(const BoxQuadOptions& aOptions, nsTArray >& aResult, mozilla::ErrorResult& aRv); diff --git a/dom/bindings/BindingUtils.cpp b/dom/bindings/BindingUtils.cpp index 3549303c3..517dae97f 100644 --- a/dom/bindings/BindingUtils.cpp +++ b/dom/bindings/BindingUtils.cpp @@ -769,7 +769,8 @@ CreateInterfacePrototypeObject(JSContext* cx, JS::Handle global, JS::Handle parentProto, const js::Class* protoClass, const NativeProperties* properties, - const NativeProperties* chromeOnlyProperties) + const NativeProperties* chromeOnlyProperties, + const char* const* unscopableNames) { JS::Rooted ourProto(cx, JS_NewObjectWithUniqueType(cx, Jsvalify(protoClass), parentProto)); @@ -778,6 +779,28 @@ CreateInterfacePrototypeObject(JSContext* cx, JS::Handle global, return nullptr; } + if (unscopableNames) { + JS::Rooted unscopableObj(cx, JS_NewPlainObject(cx)); + if (!unscopableObj) { + return nullptr; + } + + for (; *unscopableNames; ++unscopableNames) { + if (!JS_DefineProperty(cx, unscopableObj, *unscopableNames, + JS::TrueHandleValue, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + JS::Rooted unscopableId(cx, + SYMBOL_TO_JSID(JS::GetWellKnownSymbol(cx, JS::SymbolCode::unscopables))); + // Readonly and non-enumerable to match Array.prototype. + if (!JS_DefinePropertyById(cx, ourProto, unscopableId, unscopableObj, + JSPROP_READONLY)) { + return nullptr; + } + } + return ourProto; } @@ -833,7 +856,8 @@ CreateInterfaceObjects(JSContext* cx, JS::Handle global, JS::Heap* constructorCache, const NativeProperties* properties, const NativeProperties* chromeOnlyProperties, - const char* name, bool defineOnGlobal) + const char* name, bool defineOnGlobal, + const char* const* unscopableNames) { MOZ_ASSERT(protoClass || constructorClass || constructor, "Need at least one class or a constructor!"); @@ -864,7 +888,8 @@ CreateInterfaceObjects(JSContext* cx, JS::Handle global, if (protoClass) { proto = CreateInterfacePrototypeObject(cx, global, protoProto, protoClass, - properties, chromeOnlyProperties); + properties, chromeOnlyProperties, + unscopableNames); if (!proto) { return; } diff --git a/dom/bindings/BindingUtils.h b/dom/bindings/BindingUtils.h index 138b6a26a..704760942 100644 --- a/dom/bindings/BindingUtils.h +++ b/dom/bindings/BindingUtils.h @@ -611,6 +611,8 @@ struct NamedConstructor * false in situations where we want the properties to only * appear on privileged Xrays but not on the unprivileged * underlying global. + * unscopableNames if not null it points to a null-terminated list of const + * char* names of the unscopable properties for this interface. * * At least one of protoClass, constructorClass or constructor should be * non-null. If constructorClass or constructor are non-null, the resulting @@ -627,7 +629,8 @@ CreateInterfaceObjects(JSContext* cx, JS::Handle global, JS::Heap* constructorCache, const NativeProperties* regularProperties, const NativeProperties* chromeOnlyProperties, - const char* name, bool defineOnGlobal); + const char* name, bool defineOnGlobal, + const char* const* unscopableNames); /** * Define the properties (regular and chrome-only) on obj. diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index 66d66c9fb..b4b083a91 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -2691,13 +2691,14 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): properties should be a PropertyArrays instance. """ - def __init__(self, descriptor, properties): + def __init__(self, descriptor, properties, haveUnscopables): args = [Argument('JSContext*', 'aCx'), Argument('JS::Handle', 'aGlobal'), Argument('ProtoAndIfaceCache&', 'aProtoAndIfaceCache'), Argument('bool', 'aDefineOnGlobal')] CGAbstractMethod.__init__(self, descriptor, 'CreateInterfaceObjects', 'void', args) self.properties = properties + self.haveUnscopables = haveUnscopables def definition_body(self): (protoGetter, protoHandleGetter) = InterfacePrototypeObjectProtoGetter(self.descriptor) @@ -2837,7 +2838,8 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): interfaceCache, ${properties}, ${chromeProperties}, - ${name}, aDefineOnGlobal); + ${name}, aDefineOnGlobal, + ${unscopableNames}); """, protoClass=protoClass, parentProto=parentProto, @@ -2849,7 +2851,8 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): interfaceCache=interfaceCache, properties=properties, chromeProperties=chromeProperties, - name='"' + self.descriptor.interface.identifier.name + '"' if needInterfaceObject else "nullptr") + name='"' + self.descriptor.interface.identifier.name + '"' if needInterfaceObject else "nullptr", + unscopableNames="unscopableNames" if self.haveUnscopables else "nullptr") # If we fail after here, we must clear interface and prototype caches # using this code: intermediate failure must not expose the interface in @@ -11762,6 +11765,7 @@ class CGDescriptor(CGThing): hasPromiseReturningMethod) = False, False, False, False, False, False jsonifierMethod = None crossOriginMethods, crossOriginGetters, crossOriginSetters = set(), set(), set() + unscopableNames = list() for n in descriptor.interface.namedConstructors: cgThings.append(CGClassConstructor(descriptor, n, NamedConstructorName(n))) @@ -11774,6 +11778,9 @@ class CGDescriptor(CGThing): props = memberProperties(m, descriptor) if m.isMethod(): + if m.getExtendedAttribute("Unscopable"): + assert not m.isStatic() + unscopableNames.append(m.identifier.name) if props.isJsonifier: jsonifierMethod = m elif not m.isIdentifierLess() or m == descriptor.operations['Stringifier']: @@ -11799,6 +11806,9 @@ class CGDescriptor(CGThing): raise TypeError("Stringifier attributes not supported yet. " "See bug 824857.\n" "%s" % m.location) + if m.getExtendedAttribute("Unscopable"): + assert not m.isStatic() + unscopableNames.append(m.identifier.name) if m.isStatic(): assert descriptor.interface.hasInterfaceObject() cgThings.append(CGStaticGetter(descriptor, m)) @@ -11986,9 +11996,20 @@ class CGDescriptor(CGThing): cgThings.extend(CGClearCachedValueMethod(descriptor, m) for m in clearableCachedAttrs(descriptor)) + haveUnscopables = (len(unscopableNames) != 0 and + descriptor.interface.hasInterfacePrototypeObject()) + if haveUnscopables: + cgThings.append( + CGList([CGGeneric("static const char* const unscopableNames[] = {"), + CGIndenter(CGList([CGGeneric('"%s"' % name) for + name in unscopableNames] + + [CGGeneric("nullptr")], ",\n")), + CGGeneric("};\n")], "\n")) + # CGCreateInterfaceObjectsMethod needs to come after our - # CGDOMJSClass, if any. - cgThings.append(CGCreateInterfaceObjectsMethod(descriptor, properties)) + # CGDOMJSClass and unscopables, if any. + cgThings.append(CGCreateInterfaceObjectsMethod(descriptor, properties, + haveUnscopables)) # CGGetProtoObjectMethod and CGGetConstructorObjectMethod need # to come after CGCreateInterfaceObjectsMethod. diff --git a/dom/bindings/parser/WebIDL.py b/dom/bindings/parser/WebIDL.py index bee6af4d9..1e3055127 100644 --- a/dom/bindings/parser/WebIDL.py +++ b/dom/bindings/parser/WebIDL.py @@ -4011,6 +4011,14 @@ class IDLAttribute(IDLInterfaceMember): raise WebIDLError("[UseCounter] must not be used on a " "stringifier attribute", [attr.location, self.location]) + elif identifier == "Unscopable": + if not attr.noArguments(): + raise WebIDLError("[Unscopable] must take no arguments", + [attr.location]) + if self.isStatic(): + raise WebIDLError("[Unscopable] is only allowed on non-static " + "attributes and operations", + [attr.location, self.location]) elif (identifier == "Pref" or identifier == "Deprecated" or identifier == "SetterThrows" or @@ -4711,6 +4719,14 @@ class IDLMethod(IDLInterfaceMember, IDLScope): raise WebIDLError("[UseCounter] must not be used on a special " "operation", [attr.location, self.location]) + elif identifier == "Unscopable": + if not attr.noArguments(): + raise WebIDLError("[Unscopable] must take no arguments", + [attr.location]) + if self.isStatic(): + raise WebIDLError("[Unscopable] is only allowed on non-static " + "attributes and operations", + [attr.location, self.location]) elif (identifier == "Throws" or identifier == "NewObject" or identifier == "ChromeOnly" or diff --git a/dom/webidl/ChildNode.webidl b/dom/webidl/ChildNode.webidl index 0895011fe..fcf388059 100644 --- a/dom/webidl/ChildNode.webidl +++ b/dom/webidl/ChildNode.webidl @@ -9,10 +9,13 @@ [NoInterfaceObject] interface ChildNode { -// Not implemented yet: -// void before((Node or DOMString)... nodes); -// void after((Node or DOMString)... nodes); -// void replace((Node or DOMString)... nodes); + [Throws, Unscopable] + void before((Node or DOMString)... nodes); + [Throws, Unscopable] + void after((Node or DOMString)... nodes); + [Throws, Unscopable] + void replaceWith((Node or DOMString)... nodes); + [Unscopable] void remove(); }; diff --git a/dom/webidl/ParentNode.webidl b/dom/webidl/ParentNode.webidl index da08b7497..5834b9be3 100644 --- a/dom/webidl/ParentNode.webidl +++ b/dom/webidl/ParentNode.webidl @@ -18,7 +18,8 @@ interface ParentNode { [Pure] readonly attribute unsigned long childElementCount; - // Not implemented yet - // void prepend((Node or DOMString)... nodes); - // void append((Node or DOMString)... nodes); + [Throws, Unscopable] + void prepend((Node or DOMString)... nodes); + [Throws, Unscopable] + void append((Node or DOMString)... nodes); }; diff --git a/js/src/jit-test/tests/debug/Environment-unscopables.js b/js/src/jit-test/tests/debug/Environment-unscopables.js new file mode 100644 index 000000000..0075f5072 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-unscopables.js @@ -0,0 +1,37 @@ +// An Environment for a `with` statement does not observe bindings ruled out by @@unscopables. + +load(libdir + "asserts.js"); + +let g = newGlobal(); +g.eval(` + let x = 'global'; + function f() { + let obj = { + x: 'obj', + y: 'obj', + [Symbol.unscopables]: {x: 1}, + }; + with (obj) + debugger; + } +`); +let dbg = Debugger(g); +let hits = 0; +dbg.onDebuggerStatement = function (frame) { + let env = frame.environment; + + assertEq(env.find("x") !== env, true); + assertEq(env.names().indexOf("x"), -1); + assertEq(env.getVariable("x"), undefined); + assertThrowsInstanceOf(() => env.setVariable("x", 7), TypeError); + + assertEq(env.find("y") === env, true); + assertEq(env.getVariable("y"), "obj"); + env.setVariable("y", 8); + + assertEq(frame.eval("x").return, "global"); + assertEq(frame.eval("y").return, 8); + hits++; +}; +g.f(); +assertEq(hits, 1); diff --git a/js/src/jsapi.h b/js/src/jsapi.h index c094a7af0..76e6fb495 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -4556,17 +4556,25 @@ JS_PUBLIC_API(JSString*) GetSymbolDescription(HandleSymbol symbol); /* Well-known symbols. */ +#define JS_FOR_EACH_WELL_KNOWN_SYMBOL(macro) \ + macro(iterator) \ + macro(match) \ + macro(species) \ + macro(toPrimitive) \ + macro(unscopables) + enum class SymbolCode : uint32_t { - iterator, // well-known symbols - match, - species, - toPrimitive, + // There is one SymbolCode for each well-known symbol. +#define JS_DEFINE_SYMBOL_ENUM(name) name, + JS_FOR_EACH_WELL_KNOWN_SYMBOL(JS_DEFINE_SYMBOL_ENUM) // SymbolCode::iterator, etc. +#undef JS_DEFINE_SYMBOL_ENUM + Limit, InSymbolRegistry = 0xfffffffe, // created by Symbol.for() or JS::GetSymbolFor() UniqueSymbol = 0xffffffff // created by Symbol() or JS::NewSymbol() }; /* For use in loops that iterate over the well-known symbols. */ -const size_t WellKnownSymbolLimit = 4; +const size_t WellKnownSymbolLimit = size_t(SymbolCode::Limit); /** * Return the SymbolCode telling what sort of symbol `symbol` is. diff --git a/js/src/jsarray.cpp b/js/src/jsarray.cpp index daba74dd0..05849cd3c 100644 --- a/js/src/jsarray.cpp +++ b/js/src/jsarray.cpp @@ -3278,6 +3278,32 @@ CreateArrayPrototype(JSContext* cx, JSProtoKey key) return arrayProto; } +static bool +array_proto_finish(JSContext* cx, JS::HandleObject ctor, JS::HandleObject proto) +{ + // Add Array.prototype[@@unscopables]. ECMA-262 draft (2016 Mar 19) 22.1.3.32. + RootedObject unscopables(cx, NewObjectWithGivenProto(cx, nullptr, TenuredObject)); + if (!unscopables) + return false; + + RootedValue value(cx, BooleanValue(true)); + if (!DefineProperty(cx, unscopables, cx->names().copyWithin, value) || + !DefineProperty(cx, unscopables, cx->names().entries, value) || + !DefineProperty(cx, unscopables, cx->names().fill, value) || + !DefineProperty(cx, unscopables, cx->names().find, value) || + !DefineProperty(cx, unscopables, cx->names().findIndex, value) || + !DefineProperty(cx, unscopables, cx->names().includes, value) || + !DefineProperty(cx, unscopables, cx->names().keys, value) || + !DefineProperty(cx, unscopables, cx->names().values, value)) + { + return false; + } + + RootedId id(cx, SYMBOL_TO_JSID(cx->wellKnownSymbols().get(JS::SymbolCode::unscopables))); + value.setObject(*unscopables); + return DefineProperty(cx, proto, id, value, nullptr, nullptr, JSPROP_READONLY); +} + const Class ArrayObject::class_ = { "Array", JSCLASS_HAS_CACHED_PROTO(JSProto_Array) | JSCLASS_DELAY_METADATA_CALLBACK, @@ -3298,7 +3324,9 @@ const Class ArrayObject::class_ = { CreateArrayPrototype, array_static_methods, nullptr, - array_methods + array_methods, + nullptr, + array_proto_finish } }; diff --git a/js/src/jsarray.h b/js/src/jsarray.h index 9e200c6d6..82e16b718 100644 --- a/js/src/jsarray.h +++ b/js/src/jsarray.h @@ -38,9 +38,6 @@ IdIsIndex(jsid id, uint32_t* indexp) return js::StringIsArrayIndex(atom, indexp); } -extern JSObject* -InitArrayClass(JSContext* cx, js::HandleObject obj); - // The methods below only create dense boxed arrays. /* Create a dense array with no capacity allocated, length set to 0. */ diff --git a/js/src/tests/ecma_6/Array/unscopables.js b/js/src/tests/ecma_6/Array/unscopables.js new file mode 100644 index 000000000..6685309a0 --- /dev/null +++ b/js/src/tests/ecma_6/Array/unscopables.js @@ -0,0 +1,51 @@ +let Array_unscopables = Array.prototype[Symbol.unscopables]; + +let desc = Reflect.getOwnPropertyDescriptor(Array.prototype, Symbol.unscopables); +assertDeepEq(desc, { + value: Array_unscopables, + writable: false, + enumerable: false, + configurable: true +}); + +assertEq(Reflect.getPrototypeOf(Array_unscopables), null); + +let desc2 = Object.getOwnPropertyDescriptor(Array_unscopables, "values"); +assertDeepEq(desc2, { + value: true, + writable: true, + enumerable: true, + configurable: true +}); + +let keys = Reflect.ownKeys(Array_unscopables); +print(uneval(keys)); +assertDeepEq(keys, [ + "copyWithin", + "entries", + "fill", + "find", + "findIndex", + "includes", + "keys", + "values" +]); + +for (let key of keys) + assertEq(Array_unscopables[key], true); + +// Test that it actually works +assertThrowsInstanceOf(() => { + with ([]) { + return entries; + } +}, ReferenceError); + +{ + let fill = 33; + with (Array.prototype) { + assertEq(fill, 33); + } +} + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-basics.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-basics.js new file mode 100644 index 000000000..4032b2774 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-basics.js @@ -0,0 +1,22 @@ +// Basics of @@unscopables support. + +// In with(obj), if obj[@@unscopables][id] is truthy, then the identifier id +// is not present as a binding in the with-block's scope. +var x = "global"; +with ({x: "with", [Symbol.unscopables]: {x: true}}) + assertEq(x, "global"); + +// But if obj[@@unscopables][id] is false or not present, there is a binding. +with ({y: "with", z: "with", [Symbol.unscopables]: {y: false}}) { + assertEq(y, "with"); + assertEq(z, "with"); +} + +// ToBoolean(obj[@@unscopables][id]) determines whether there's a binding. +let someValues = [0, -0, NaN, "", undefined, null, "x", {}, []]; +for (let v of someValues) { + with ({x: "with", [Symbol.unscopables]: {x: v}}) + assertEq(x, v ? "global" : "with"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-closures.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-closures.js new file mode 100644 index 000000000..bdade1f11 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-closures.js @@ -0,0 +1,23 @@ +// @@unscopables continues to work after exiting the relevant `with` block, +// if the environment is captured by a closure. + +let env = { + x: 9000, + [Symbol.unscopables]: {x: true} +}; + +function make_adder(x) { + with (env) + return function (y) { return x + y; }; +} +assertEq(make_adder(3)(10), 13); + +// Same test, but with a bunch of different parts for bad luck +let x = 500; +function make_adder_with_eval() { + with (env) + return eval('y => eval("x + y")'); +} +assertEq(make_adder_with_eval()(10), 510); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-const.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-const.js new file mode 100644 index 000000000..7e1d0e07c --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-const.js @@ -0,0 +1,8 @@ +// @@unscopables prevents a property from having any effect on assigning to a +// const binding (which is an error). + +const x = 1; +with ({x: 1, [Symbol.unscopables]: {x: true}}) + assertThrowsInstanceOf(() => {x = 2;}, TypeError); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-delete.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-delete.js new file mode 100644 index 000000000..3cd296f4b --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-delete.js @@ -0,0 +1,27 @@ +// If obj[@@unscopables][id], then `delete id` works across `with (obj)` scope. + +this.niche = 7; +let obj = { niche: 8, [Symbol.unscopables]: { niche: true } }; +with (obj) { + delete niche; +} + +assertEq(obj.niche, 8); +assertEq("niche" in this, false); + +// Same thing, but delete a variable introduced by sloppy direct eval. +this.niche = 9; +function f() { + eval("var niche = 10;"); + with (obj) { + assertEq(niche, 10); + delete niche; + } + assertEq(niche, 9); +} + +// Of course none of this affects a qualified delete. +assertEq(delete this.niche, true); +assertEq("niche" in this, false); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-getters.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-getters.js new file mode 100644 index 000000000..136078798 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-getters.js @@ -0,0 +1,41 @@ +// @@unscopables checks can call getters. + +// The @@unscopables property itself can be a getter. +let hit1 = 0; +let x = "global x"; +let env1 = { + x: "env1.x", + get [Symbol.unscopables]() { + hit1++; + return {x: true}; + } +}; +with (env1) + assertEq(x, "global x"); +assertEq(hit1, 1); + +// It can throw; the exception is propagated out. +function Fit() {} +with ({x: 0, get [Symbol.unscopables]() { throw new Fit; }}) + assertThrowsInstanceOf(() => x, Fit); + +// Individual properties on the @@unscopables object can have getters. +let hit2 = 0; +let env2 = { + x: "env2.x", + [Symbol.unscopables]: { + get x() { + hit2++; + return true; + } + } +}; +with (env2) + assertEq(x, "global x"); +assertEq(hit2, 1); + +// And they can throw. +with ({x: 0, [Symbol.unscopables]: {get x() { throw new Fit; }}}) + assertThrowsInstanceOf(() => x, Fit); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-global.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-global.js new file mode 100644 index 000000000..1aa4a52bd --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-global.js @@ -0,0 +1,18 @@ +// @@unscopables does not affect the global environment. + +this.x = "global property x"; +let y = "global lexical y"; +this[Symbol.unscopables] = {x: true, y: true}; +assertEq(x, "global property x"); +assertEq(y, "global lexical y"); +assertEq(eval("x"), "global property x"); +assertEq(eval("y"), "global lexical y"); + +// But it does affect `with` statements targeting the global object. +{ + let x = "local x"; + with (this) + assertEq(x, "local x"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-ignored.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-ignored.js new file mode 100644 index 000000000..08b042a3b --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-ignored.js @@ -0,0 +1,22 @@ +// In these cases, @@unscopables should not be consulted. + +// Because obj has no properties `assertEq` or `x`, +// obj[@@unscopables] is not checked here: +var obj = { + get [Symbol.unscopables]() { + throw "tried to read @@unscopables"; + } +}; +var x = 3; +with (obj) + assertEq(x, 3); + +// If @@unscopables is present but not an object, it is ignored: +for (let nonObject of [undefined, null, "nothing", Symbol.for("moon")]) { + let y = 4; + let obj2 = {[Symbol.unscopables]: nonObject, y: 5}; + with (obj2) + assertEq(y, 5); +} + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-miss.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-miss.js new file mode 100644 index 000000000..b86d51078 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-miss.js @@ -0,0 +1,7 @@ +// Trying to access a binding that doesn't exist due to @@unscopables +// is a ReferenceError. + +with ({x: 1, [Symbol.unscopables]: {x: true}}) + assertThrowsInstanceOf(() => x, ReferenceError); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation-frozen.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation-frozen.js new file mode 100644 index 000000000..632785c05 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation-frozen.js @@ -0,0 +1,18 @@ +// When env[@@unscopables].x changes, bindings can appear even if env is inextensible. + +let x = "global"; +let unscopables = {x: true}; +let env = Object.create(null); +env[Symbol.unscopables] = unscopables; +env.x = "object"; +Object.freeze(env); + +for (let i = 0; i < 1004; i++) { + if (i === 1000) + unscopables.x = false; + with (env) { + assertEq(x, i < 1000 ? "global" : "object"); + } +} + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation.js new file mode 100644 index 000000000..2f35e1dd3 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation.js @@ -0,0 +1,44 @@ +// When obj[@@unscopables].x changes, bindings appear and disappear accordingly. + +let x = "global"; +function getX() { return x; } + +let unscopables = {x: true}; +let obj = {x: "obj", [Symbol.unscopables]: unscopables}; + +with (obj) { + assertEq(x, "global"); + x = "global-1"; + assertEq(x, "global-1"); + assertEq(obj.x, "obj"); + + unscopables.x = false; // suddenly x appears in the with-environment + + assertEq(x, "obj"); + x = "obj-1"; + assertEq(getX(), "global-1"); // unchanged + assertEq(obj.x, "obj-1"); + + unscopables.x = true; // *poof* + + assertEq(x, "global-1"); + x = "global-2"; + assertEq(getX(), "global-2"); + assertEq(obj.x, "obj-1"); // unchanged + + // The determination of which binding is assigned happens when the LHS of + // assignment is evaluated, before the RHS. This is observable if we make + // the binding appear or disappear during evaluation of the RHS, before + // assigning. + x = (unscopables.x = false, "global-3"); + assertEq(getX(), "global-3"); + assertEq(obj.x, "obj-1"); + + x = (unscopables.x = true, "obj-2"); + assertEq(getX(), "global-3"); + assertEq(obj.x, "obj-2"); +} + +assertEq(x, "global-3"); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-proto.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-proto.js new file mode 100644 index 000000000..dbbfb712d --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-proto.js @@ -0,0 +1,39 @@ +// @@unscopables treats properties found on prototype chains the same as other +// properties. + +const x = "global x"; +const y = "global y"; + +// obj[@@unscopables].x works when obj.x is inherited via the prototype chain. +let proto = {x: "object x", y: "object y"}; +let env = Object.create(proto); +env[Symbol.unscopables] = {x: true, y: false}; +with (env) { + assertEq(x, "global x"); + assertEq(delete x, false); + assertEq(y, "object y"); +} +assertEq(env.x, "object x"); + +// @@unscopables works if is inherited via the prototype chain. +env = { + x: "object", + [Symbol.unscopables]: {x: true, y: true} +}; +for (let i = 0; i < 50; i++) + env = Object.create(env); +env.y = 1; +with (env) { + assertEq(x, "global x"); + assertEq(y, "global y"); +} + +// @@unscopables works if the obj[@@unscopables][id] property is inherited. +env = { + x: "object", + [Symbol.unscopables]: Object.create({x: true}) +}; +with (env) + assertEq(x, "global x"); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-proxy.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-proxy.js new file mode 100644 index 000000000..fcf241ee4 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-proxy.js @@ -0,0 +1,46 @@ +// Object operations are performed in the right order, as observed by proxies. + +let log = []; +function LoggingProxyHandlerWrapper(name, handler={}) { + return new Proxy(handler, { + get(t, id) { + let method = handler[id]; + return function (...args) { + log.push([name + "." + id, ...args.filter(v => typeof v !== "object")]); + if (method === undefined) + return Reflect[id].apply(null, args); + return method.apply(this, args); + }; + } + }); +} + +function LoggingProxy(name, target) { + return new Proxy(target, new LoggingProxyHandlerWrapper(name)); +} + +let proto = {x: 44}; +let proto_proxy = new LoggingProxy("proto", proto); +let unscopables = {x: true}; +let unscopables_proxy = new LoggingProxy("unscopables", {x: true}); +let env = Object.create(proto_proxy, { + [Symbol.unscopables]: { value: unscopables_proxy } +}); +let env_proxy = new LoggingProxy("env", env); + +let x = 11; +function f() { + with (env_proxy) + return x; +} + +assertEq(f(), 11); + +assertDeepEq(log, [ + ["env.has", "x"], + ["proto.has", "x"], + ["env.get", Symbol.unscopables], + ["unscopables.get", "x"] +]); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-strict.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-strict.js new file mode 100644 index 000000000..fd0413ed7 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-strict.js @@ -0,0 +1,32 @@ +// Strict assignment to the name of a property that's masked by @@unscopables +// throws a ReferenceError. + +let env = {k: 1}; +let f; +with (env) { + f = function () { + "use strict"; + k = 2; + }; +} + +f(); +assertEq(env.k, 2); + +env[Symbol.unscopables] = {k: true}; +assertThrowsInstanceOf(f, ReferenceError); + +// @@unscopables is tested when the LHS of assignment is evaluated, so there is +// no effect on the assignment if it is changed while evaluating the RHS. +let g; +with (env) { + g = function () { + "use strict"; + k = (env[Symbol.unscopables].k = true, 3); + } +} +env[Symbol.unscopables].k = false; +g(); +assertEq(env.k, 3); + +reportCompare(0, 0); diff --git a/js/src/tests/ecma_6/LexicalEnvironment/unscopables-tdz.js b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-tdz.js new file mode 100644 index 000000000..ce6b1df92 --- /dev/null +++ b/js/src/tests/ecma_6/LexicalEnvironment/unscopables-tdz.js @@ -0,0 +1,9 @@ +// Accessing an uninitialized variable due to @@unscopables is still a ReferenceError. + +with ({x: 1, [Symbol.unscopables]: {x: true}}) + assertThrowsInstanceOf(() => x, ReferenceError); + +let x; + +reportCompare(0, 0); + diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 6a764ff63..9828268a8 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -49,6 +49,7 @@ macro(construct, construct, "construct") \ macro(constructor, constructor, "constructor") \ macro(ConvertAndCopyTo, ConvertAndCopyTo, "ConvertAndCopyTo") \ + macro(copyWithin, copyWithin, "copyWithin") \ macro(count, count, "count") \ macro(currency, currency, "currency") \ macro(currencyDisplay, currencyDisplay, "currencyDisplay") \ @@ -73,6 +74,7 @@ macro(encodeURI, encodeURI, "encodeURI") \ macro(encodeURIComponent, encodeURIComponent, "encodeURIComponent") \ macro(endTimestamp, endTimestamp, "endTimestamp") \ + macro(entries, entries, "entries") \ macro(enumerable, enumerable, "enumerable") \ macro(enumerate, enumerate, "enumerate") \ macro(escape, escape, "escape") \ @@ -81,6 +83,9 @@ macro(fieldOffsets, fieldOffsets, "fieldOffsets") \ macro(fieldTypes, fieldTypes, "fieldTypes") \ macro(fileName, fileName, "fileName") \ + macro(fill, fill, "fill") \ + macro(find, find, "find") \ + macro(findIndex, findIndex, "findIndex") \ macro(fix, fix, "fix") \ macro(flags, flags, "flags") \ macro(float32, float32, "float32") \ @@ -106,6 +111,7 @@ macro(hasOwnProperty, hasOwnProperty, "hasOwnProperty") \ macro(ignoreCase, ignoreCase, "ignoreCase") \ macro(ignorePunctuation, ignorePunctuation, "ignorePunctuation") \ + macro(includes, includes, "includes") \ macro(index, index, "index") \ macro(InitializeCollator, InitializeCollator, "InitializeCollator") \ macro(InitializeDateTimeFormat, InitializeDateTimeFormat, "InitializeDateTimeFormat") \ @@ -270,6 +276,7 @@ macro(match, match, "match") \ macro(species, species, "species") \ macro(toPrimitive, toPrimitive, "toPrimitive") \ + macro(unscopables, unscopables, "unscopables") \ /* Same goes for the descriptions of the well-known symbols. */ \ macro(Symbol_hasInstance, Symbol_hasInstance, "Symbol.hasInstance") \ macro(Symbol_isConcatSpreadable, Symbol_isConcatSpreadable, "Symbol.isConcatSpreadable") \ @@ -277,7 +284,6 @@ macro(Symbol_match, Symbol_match, "Symbol.match") \ macro(Symbol_species, Symbol_species, "Symbol.species") \ macro(Symbol_toPrimitive, Symbol_toPrimitive, "Symbol.toPrimitive") \ - macro(Symbol_toStringTag, Symbol_toStringTag, "Symbol.toStringTag") \ macro(Symbol_unscopables, Symbol_unscopables, "Symbol.unscopables") \ /* Function names for properties named by symbols. */ \ macro(Symbol_iterator_fun, Symbol_iterator_fun, "[Symbol.iterator]") \ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp index d583c9c52..19512d921 100644 --- a/js/src/vm/Debugger.cpp +++ b/js/src/vm/Debugger.cpp @@ -8174,6 +8174,14 @@ DebuggerEnv_getVariable(JSContext* cx, unsigned argc, Value* vp) /* This can trigger getters. */ ErrorCopier ec(ac); + bool found; + if (!HasProperty(cx, env, id, &found)) + return false; + if (!found) { + args.rval().setUndefined(); + return true; + } + // For DebugScopeObjects, we get sentinel values for optimized out // slots and arguments instead of throwing (the default behavior). // diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index d91e9ceb0..3e75b3836 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -443,10 +443,9 @@ namespace js { */ struct WellKnownSymbols { - js::ImmutableSymbolPtr iterator; - js::ImmutableSymbolPtr match; - js::ImmutableSymbolPtr species; - js::ImmutableSymbolPtr toPrimitive; +#define DECLARE_SYMBOL(name) js::ImmutableSymbolPtr name; + JS_FOR_EACH_WELL_KNOWN_SYMBOL(DECLARE_SYMBOL) +#undef DECLARE_SYMBOL const ImmutableSymbolPtr& get(size_t u) const { MOZ_ASSERT(u < JS::WellKnownSymbolLimit); diff --git a/js/src/vm/ScopeObject.cpp b/js/src/vm/ScopeObject.cpp index 7ff9dc24a..4e0bfbfed 100644 --- a/js/src/vm/ScopeObject.cpp +++ b/js/src/vm/ScopeObject.cpp @@ -652,6 +652,26 @@ DynamicWithObject::create(JSContext* cx, HandleObject object, HandleObject enclo return obj; } +/* Implements ES6 8.1.1.2.1 HasBinding steps 7-9. */ +static bool +CheckUnscopables(JSContext *cx, HandleObject obj, HandleId id, bool *scopable) +{ + RootedId unscopablesId(cx, SYMBOL_TO_JSID(cx->wellKnownSymbols() + .get(JS::SymbolCode::unscopables))); + RootedValue v(cx); + if (!GetProperty(cx, obj, obj, unscopablesId, &v)) + return false; + if (v.isObject()) { + RootedObject unscopablesObj(cx, &v.toObject()); + if (!GetProperty(cx, unscopablesObj, unscopablesObj, id, &v)) + return false; + *scopable = !ToBoolean(v); + } else { + *scopable = true; + } + return true; +} + static bool with_LookupProperty(JSContext* cx, HandleObject obj, HandleId id, MutableHandleObject objp, MutableHandleShape propp) @@ -662,7 +682,19 @@ with_LookupProperty(JSContext* cx, HandleObject obj, HandleId id, return true; } RootedObject actual(cx, &obj->as().object()); - return LookupProperty(cx, actual, id, objp, propp); + if (!LookupProperty(cx, actual, id, objp, propp)) + return false; + + if (propp) { + bool scopable; + if (!CheckUnscopables(cx, actual, id, &scopable)) + return false; + if (!scopable) { + objp.set(nullptr); + propp.set(nullptr); + } + } + return true; } static bool @@ -679,7 +711,15 @@ with_HasProperty(JSContext* cx, HandleObject obj, HandleId id, bool* foundp) { MOZ_ASSERT(!JSID_IS_ATOM(id, cx->names().dotThis)); RootedObject actual(cx, &obj->as().object()); - return HasProperty(cx, actual, id, foundp); + + // ES 8.1.1.2.1 step 3-5. + if (!HasProperty(cx, actual, id, foundp)) + return false; + if (!*foundp) + return true; + + // Steps 7-10. (Step 6 is a no-op.) + return CheckUnscopables(cx, actual, id, foundp); } static bool @@ -2173,12 +2213,25 @@ class DebugScopeProxy : public BaseProxyHandler // target object, the object would indicate that native enumeration is // the thing to do, but native enumeration over the DynamicWithObject // wrapper yields no properties. So instead here we hack around the - // issue, and punch a hole through to the with object target. - Rooted target(cx, (scope->is() - ? &scope->as().object() : scope)); + // issue: punch a hole through to the with object target, then manually + // examine @@unscopables. + bool isWith = scope->is(); + Rooted target(cx, (isWith ? &scope->as().object() : scope)); if (!GetPropertyKeys(cx, target, JSITER_OWNONLY, &props)) return false; + if (isWith) { + size_t j = 0; + for (size_t i = 0; i < props.length(); i++) { + bool inScope; + if (!CheckUnscopables(cx, scope, props[i], &inScope)) + return false; + if (inScope) + props[j++].set(props[i]); + } + props.resize(j); + } + /* * Function scopes are optimized to not contain unaliased variables so * they must be manually appended here.