Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 51 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
16 changes: 8 additions & 8 deletions ubo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down