Bug 1054759 - Part 2: ES6 Symbol.unscopables. r=shu.
authorJason Orendorff <jorendorff@mozilla.com>
Wed, 09 Dec 2015 10:42:40 -0500
changeset 289178 e9e74f6bd12a8c45bb3e20a0ca573db972ed6345
parent 289177 b61921a307e9ed7d94e94290dda4672ad2779bd0
child 289179 fdd1faf35dbfd32211e0f47e8e8d13ecc0e9856f
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersshu
bugs1054759
milestone48.0a1
Bug 1054759 - Part 2: ES6 Symbol.unscopables. r=shu.
js/src/jit-test/tests/debug/Environment-unscopables.js
js/src/jsapi.h
js/src/tests/ecma_6/LexicalEnvironment/unscopables-basics.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-closures.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-const.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-delete.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-getters.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-global.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-ignored.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-miss.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation-frozen.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-mutation.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-proto.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-proxy.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-strict.js
js/src/tests/ecma_6/LexicalEnvironment/unscopables-tdz.js
js/src/tests/ecma_6/Symbol/well-known.js
js/src/vm/CommonPropertyNames.h
js/src/vm/Debugger.cpp
js/src/vm/ScopeObject.cpp
new file mode 100644
--- /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);
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -4714,17 +4714,18 @@ GetSymbolFor(JSContext* cx, HandleString
 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(toPrimitive) \
+    macro(unscopables)
 
 enum class SymbolCode : uint32_t {
     // 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()
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
+
--- a/js/src/tests/ecma_6/Symbol/well-known.js
+++ b/js/src/tests/ecma_6/Symbol/well-known.js
@@ -1,15 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/licenses/publicdomain/ */
 
 var names = [
     "iterator",
     "match",
     "species",
+    "toPrimitive",
+    "unscopables"
 ];
 
 for (var name of names) {
     // Well-known symbols exist.
     assertEq(typeof Symbol[name], "symbol");
 
     // They are never in the registry.
     assertEq(Symbol[name] !== Symbol.for("Symbol." + name), true);
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -288,21 +288,21 @@
     macro(null, null, "null") \
     macro(symbol, symbol, "symbol") \
     /* Well-known atom names must be continuous and ordered, matching \
      * enum JS::SymbolCode in jsapi.h. */ \
     macro(iterator, iterator, "iterator") \
     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") \
     macro(Symbol_iterator, Symbol_iterator, "Symbol.iterator") \
     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]") \
 
 #endif /* vm_CommonPropertyNames_h */
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -8867,16 +8867,24 @@ DebuggerEnv_getVariable(JSContext* cx, u
     RootedValue v(cx);
     {
         Maybe<AutoCompartment> ac;
         ac.emplace(cx, env);
 
         /* 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).
         //
         // See wrapDebuggeeValue for how the sentinel values are wrapped.
         if (env->is<DebugScopeObject>()) {
             if (!env->as<DebugScopeObject>().getMaybeSentinelValue(cx, id, &v))
                 return false;
         } else {
--- a/js/src/vm/ScopeObject.cpp
+++ b/js/src/vm/ScopeObject.cpp
@@ -750,44 +750,84 @@ DynamicWithObject::create(JSContext* cx,
     obj->setEnclosingScope(enclosing);
     obj->setFixedSlot(OBJECT_SLOT, ObjectValue(*object));
     obj->setFixedSlot(THIS_SLOT, thisv);
     obj->setFixedSlot(KIND_SLOT, Int32Value(kind));
 
     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)
 {
     if (JSID_IS_ATOM(id, cx->names().dotThis)) {
         objp.set(nullptr);
         propp.set(nullptr);
         return true;
     }
     RootedObject actual(cx, &obj->as<DynamicWithObject>().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
 with_DefineProperty(JSContext* cx, HandleObject obj, HandleId id, Handle<PropertyDescriptor> desc,
                     ObjectOpResult& result)
 {
     MOZ_ASSERT(!JSID_IS_ATOM(id, cx->names().dotThis));
     RootedObject actual(cx, &obj->as<DynamicWithObject>().object());
     return DefineProperty(cx, actual, id, desc, result);
 }
 
 static bool
 with_HasProperty(JSContext* cx, HandleObject obj, HandleId id, bool* foundp)
 {
     MOZ_ASSERT(!JSID_IS_ATOM(id, cx->names().dotThis));
     RootedObject actual(cx, &obj->as<DynamicWithObject>().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
 with_GetProperty(JSContext* cx, HandleObject obj, HandleValue receiver, HandleId id,
                  MutableHandleValue vp)
 {
     MOZ_ASSERT(!JSID_IS_ATOM(id, cx->names().dotThis));
     RootedObject actual(cx, &obj->as<DynamicWithObject>().object());
@@ -2241,22 +2281,35 @@ class DebugScopeProxy : public BaseProxy
                 return false;
         }
 
         // DynamicWithObject isn't a very good proxy.  It doesn't have a
         // JSNewEnumerateOp implementation, because if it just delegated to the
         // 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<JSObject*> target(cx, (scope->is<DynamicWithObject>()
-                                      ? &scope->as<DynamicWithObject>().object() : scope));
+        // issue: punch a hole through to the with object target, then manually
+        // examine @@unscopables.
+        bool isWith = scope->is<DynamicWithObject>();
+        Rooted<JSObject*> target(cx, (isWith ? &scope->as<DynamicWithObject>().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.
          */
         if (isFunctionScope(*scope)) {
             RootedScript script(cx, scope->as<CallObject>().callee().nonLazyScript());
             for (BindingIter bi(script); bi; bi++) {
                 if (!bi->aliased() && !props.append(NameToId(bi->name())))