diff --git a/proxy.js b/proxy.js index adcf180..7189c41 100644 --- a/proxy.js +++ b/proxy.js @@ -24,19 +24,19 @@ export function createProxy(NativeProxy) { }; return function Proxy(target, handler) { - // Compose with the handler's own setPrototypeOf trap, if any: read it once, as an own - // property only, so page code cannot inject one through Object.prototype. It runs after - // the cycle check below, so it only ever sees non-cyclic prototypes. - const trap = apply(hasOwnProperty, handler, ['setPrototypeOf']) ? handler.setPrototypeOf : undefined; - - // Inherit from the handler rather than copy it, so other traps added to it later still apply. - // A setPrototypeOf trap added to the handler after construction stays shadowed by the composed one. + // Inherit from the handler rather than copy it, so traps added to it later still apply. const proxy = new NativeProxy(target, objectCreate(handler, { setPrototypeOf: { value(target, proto) { // Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf // return false, as both do for a native function. if (reaches(proto, proxy, target)) return false; - return trap === undefined ? setPrototypeOf(target, proto) : apply(trap, this, [target, proto]); + + // The handler's trap only ever sees non-cyclic prototypes. Own property only, + // so page code cannot inject a trap through Object.prototype. + const trap = apply(hasOwnProperty, handler, ['setPrototypeOf']) ? handler.setPrototypeOf : undefined; + + // Same `this` as the handler's other traps get, so state they share through it lines up. + return trap == null ? setPrototypeOf(target, proto) : apply(trap, this, [target, proto]); } }, })); return proxy; diff --git a/test.js b/test.js index 52a1360..38525c6 100644 --- a/test.js +++ b/test.js @@ -172,4 +172,55 @@ suite("createProxy", () => { assert.strictEqual(Reflect.setPrototypeOf(proxy, Object.create(proxy)), false); assert.deepStrictEqual(calls, [proto, "reject"]); }); + + test("shares this between the handler's traps, like a native Proxy", () => { + const Proxy = createProxy(globalThis.Proxy); + const proxy = new Proxy(function fn() {}, { + apply() { + this.locked = true; + }, + setPrototypeOf(t, proto) { + return this.locked ? false : Reflect.setPrototypeOf(t, proto); + }, + }); + assert.strictEqual(Reflect.setPrototypeOf(proxy, Object.create(Function.prototype)), true); + proxy(); + assert.strictEqual(Reflect.setPrototypeOf(proxy, Object.create(Function.prototype)), false); + }); + + test("treats a null trap as absent, like a native Proxy", () => { + const Proxy = createProxy(globalThis.Proxy); + const target = function fn() {}; + const proto = Object.create(Function.prototype); + assert.strictEqual(Reflect.setPrototypeOf(new Proxy(target, { setPrototypeOf: null }), proto), true); + assert.strictEqual(Object.getPrototypeOf(target), proto); + }); + + test("applies a trap added to the handler after construction, like a native Proxy", () => { + const Proxy = createProxy(globalThis.Proxy); + const target = function fn() {}; + const handler = {}; + const proxy = new Proxy(target, handler); + handler.setPrototypeOf = () => false; + assert.strictEqual(Reflect.setPrototypeOf(proxy, Object.create(Function.prototype)), false); + assert.strictEqual(Object.getPrototypeOf(target), Function.prototype); + + // The cycle check still comes first. + handler.setPrototypeOf = () => true; + assert.strictEqual(Reflect.setPrototypeOf(proxy, Object.create(proxy)), false); + }); + + test("ignores a trap injected through Object.prototype", () => { + const Proxy = createProxy(globalThis.Proxy); + const target = function fn() {}; + const proxy = new Proxy(target, {}); + const proto = Object.create(Function.prototype); + Object.prototype.setPrototypeOf = () => false; + try { + assert.strictEqual(Reflect.setPrototypeOf(proxy, proto), true); + } finally { + delete Object.prototype.setPrototypeOf; + } + assert.strictEqual(Object.getPrototypeOf(target), proto); + }); }); diff --git a/ubo.js b/ubo.js index 186705a..e0112bf 100644 --- a/ubo.js +++ b/ubo.js @@ -831,19 +831,19 @@ const Proxy = (function createProxy(NativeProxy) { }; return function Proxy(target, handler) { - // Compose with the handler's own setPrototypeOf trap, if any: read it once, as an own - // property only, so page code cannot inject one through Object.prototype. It runs after - // the cycle check below, so it only ever sees non-cyclic prototypes. - const trap = apply(hasOwnProperty, handler, ['setPrototypeOf']) ? handler.setPrototypeOf : undefined; - - // Inherit from the handler rather than copy it, so other traps added to it later still apply. - // A setPrototypeOf trap added to the handler after construction stays shadowed by the composed one. + // Inherit from the handler rather than copy it, so traps added to it later still apply. const proxy = new NativeProxy(target, objectCreate(handler, { setPrototypeOf: { value(target, proto) { // Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf // return false, as both do for a native function. if (reaches(proto, proxy, target)) return false; - return trap === undefined ? setPrototypeOf(target, proto) : apply(trap, this, [target, proto]); + + // The handler's trap only ever sees non-cyclic prototypes. Own property only, + // so page code cannot inject a trap through Object.prototype. + const trap = apply(hasOwnProperty, handler, ['setPrototypeOf']) ? handler.setPrototypeOf : undefined; + + // Same `this` as the handler's other traps get, so state they share through it lines up. + return trap == null ? setPrototypeOf(target, proto) : apply(trap, this, [target, proto]); } }, })); return proxy;