From: Lady Date: Thu, 23 Nov 2023 02:00:45 +0000 (-0500) Subject: Add createProxyConstructor to function.js X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/e59e8454940d56a20e22c8eb5154ca542e478a0a?ds=inline;hp=aa1ae4c089cec40d79c3087ed7d6007dade77815 Add createProxyConstructor to function.js --- diff --git a/function.js b/function.js index d02fe6e..10a3f89 100644 --- a/function.js +++ b/function.js @@ -7,7 +7,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { ITERATOR, toFunctionName, toLength } from "./value.js"; +import { ITERATOR, toFunctionName, toLength, type } from "./value.js"; export const { /** @@ -52,24 +52,57 @@ export const { * be used to override `length`, `name`, and `prototype`. */ createIllegalConstructor, + + /** + * Returns a constructor which produces a new constructor which wraps + * the provided constructor, but returns a proxy of the result using + * the provided handler. + * + * The resulting constructor inherits from, and has the same basic + * shape as, `Proxy`. + * + * If a base constructor is not provided, `Object` will be used. + * + * If a third argument is provided, it is used as the target for the + * provided constructor when it is constructed. This can be used to + * prevent leakage of the provided constructor to superclasses + * through `new.target`. + * + * The `length` of the provided function will be preserved in the new + * one. A fourth argument may be used to override `length` and + * `name`. + * + * ※ `.prototype` will be present, but undefined, on the resulting + * constructor. This differs from the behaviour of `Proxy`, for which + * `.prototype` is not present at all. It is not presently possible + * to create a constructor with no `.prototype` property in + * Ecmascript code. + */ + createProxyConstructor, } = (() => { const { prototype: functionPrototype } = Function; const { bind: functionBind, call: functionCall, } = functionPrototype; - const { apply: reflectApply } = Reflect; + const objectConstructor = Object; + const proxyConstructor = Proxy; const { create: objectCreate, defineProperty: defineOwnProperty, defineProperties: defineOwnProperties, - hasOwn: hasOwnProperty, + getOwnPropertyDescriptor, getPrototypeOf: getPrototype, setPrototypeOf: setPrototype, } = Object; + const { + apply: reflectApply, + construct: reflectConstruct, + } = Reflect; const callBind = reflectApply(functionBind, functionCall, [ functionBind, ]); + const { revocable } = Proxy; const { [ITERATOR]: arrayIterator } = Array.prototype; const { next: arrayIteratorNext, @@ -113,9 +146,12 @@ export const { } else { // Properties were provided; apply them. const { length, name, prototype } = override; - if (!hasOwnProperty($, "prototype") || prototype === undefined) { - // The provided function has no `.prototype` or no prototype - // value was provided. + if ( + !getOwnPropertyDescriptor($, "prototype")?.writable || + prototype === undefined + ) { + // The provided function has no `.prototype`, its prototype is + // not writable, or no prototype value was provided. // // Do not modify the prototype property of the provided // function. @@ -189,6 +225,87 @@ export const { "prototype", { writable: false }, ), + createProxyConstructor: ( + handler, + $, + newTarget = undefined, + propertyOverride = undefined, + ) => { + const constructor = $ === undefined ? objectConstructor : $; + const target = newTarget === undefined ? constructor : newTarget; + const len = toLength(constructor.length); + if (!(type(handler) === "object")) { + throw new TypeError( + `Piscēs: Proxy handler must be an object, but got: ${handler}.`, + ); + } else if (!isConstructor(constructor)) { + throw new TypeError( + "Piscēs: Cannot create proxy constructor from nonconstructible value.", + ); + } else if (!isConstructor(target)) { + throw new TypeError( + "Piscēs: New target must be a constructor.", + ); + } else { + return applyProperties( + defineOwnProperties( + setPrototype( + function C(...$s) { + if (new.target === undefined) { + throw new TypeError( + `Piscēs: ${ + C.name ?? "Proxy" + } must be called with new.`, + ); + } else { + const O = reflectConstruct( + constructor, + $s, + target, + ); + return new proxyConstructor(O, handler); + } + }, + proxyConstructor, + ), + { + length: { value: len }, + name: { + value: `${ + toFunctionName(constructor.name ?? "") + }Proxy`, + }, + prototype: { + configurable: false, + enumerable: false, + value: undefined, + writable: false, + }, + revocable: { + configurable: true, + enumerable: false, + value: defineOwnProperties( + (...$s) => { + const O = reflectConstruct( + constructor, + $s, + target, + ); + return revocable(O, handler); + }, + { + length: { value: len }, + name: { value: "revocable" }, + }, + ), + writable: true, + }, + }, + ), + propertyOverride, + ); + } + }, }; })(); diff --git a/function.test.js b/function.test.js index 439f9d3..0a58a36 100644 --- a/function.test.js +++ b/function.test.js @@ -23,6 +23,7 @@ import { createArrowFunction, createCallableFunction, createIllegalConstructor, + createProxyConstructor, identity, isCallable, isConstructor, @@ -488,6 +489,7 @@ describe("createIllegalConstructor", () => { it("[[Call]] returns a correctly‐formed constructor when provided one argument", () => { const constructorPrototype = Object.create(null, { name: { value: "etaoin" }, + length: { value: "3" }, prototype: { value: {} }, }); const constructor = createIllegalConstructor( @@ -499,7 +501,7 @@ describe("createIllegalConstructor", () => { constructorPrototype, ); assert(Object.hasOwn(constructor, "prototype")); - assertEquals( + assertStrictEquals( constructor.prototype, constructorPrototype.prototype, ); @@ -508,12 +510,59 @@ describe("createIllegalConstructor", () => { .writable, ); assertStrictEquals(constructor.name, "etaoin"); + assertStrictEquals(constructor.length, 3); + }); + + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createIllegalConstructor( + function etaoin() {}, + { length: 100 }, + ).length, + 100, + ); + }); + + it("[[Call]] allows the name to be overridden", () => { + assertStrictEquals( + createIllegalConstructor( + function etaoin() {}, + { name: "shrdlu" }, + ).name, + "shrdlu", + ); + }); + + it("[[Call]] allows the prototype to be overridden", () => { + const obj = {}; + assertStrictEquals( + createIllegalConstructor( + function etaoin() {}, + { prototype: obj }, + ).prototype, + obj, + ); }); it("[[Construct]] throws an error", () => { assertThrows(() => new createIllegalConstructor(function () {})); }); + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(createIllegalConstructor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + createIllegalConstructor.name, + "createIllegalConstructor", + ); + }); + }); + describe("~", () => { it("[[Call]] throws an error", () => { assertThrows(() => { @@ -527,18 +576,243 @@ describe("createIllegalConstructor", () => { }); }); }); +}); + +describe("createProxyConstructor", () => { + it("[[Call]] returns a constructor", () => { + assert(isConstructor(createProxyConstructor({}))); + }); + + it("[[Call]] throws with no arguments", () => { + assertThrows(() => createProxyConstructor()); + }); + + it("[[Call]] throws if the second argument is not a constructor or undefined", () => { + assertThrows(() => createProxyConstructor({}, () => {})); + }); + + it("[[Call]] throws if the third argument is not a constructor or undefined", () => { + assertThrows(() => + createProxyConstructor({}, undefined, () => {}) + ); + }); + + it("[[Call]] creates a proper proxy constructor", () => { + const constructorPrototype = function etaoin(_) {}; + const constructor = class Constructor + extends constructorPrototype { + constructor(_1, _2, _3) {} + }; + const proxyConstructor = createProxyConstructor( + {}, + constructor, + ); + assert(isConstructor(proxyConstructor)); + assertStrictEquals( + Object.getPrototypeOf(proxyConstructor), + Proxy, + ); + assertStrictEquals(proxyConstructor.prototype, undefined); + assertStrictEquals(proxyConstructor.name, "ConstructorProxy"); + assertStrictEquals(proxyConstructor.length, 3); + }); + + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createProxyConstructor({}, undefined, undefined, { + length: 100, + }).length, + 100, + ); + }); + + it("[[Call]] allows the name to be overridden", () => { + assertStrictEquals( + createProxyConstructor({}, function etaoin() {}, undefined, { + name: "shrdlu", + }).name, + "shrdlu", + ); + }); + + it("[[Call]] does not allow the prototype to be overridden", () => { + assertStrictEquals( + createProxyConstructor({}, undefined, undefined, { + prototype: {}, + }).prototype, + undefined, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new createProxyConstructor({})); + }); describe(".length", () => { it("[[Get]] returns the correct length", () => { - assertStrictEquals(createIllegalConstructor.length, 1); + assertStrictEquals(createProxyConstructor.length, 2); }); }); describe(".name", () => { it("[[Get]] returns the correct name", () => { assertStrictEquals( - createIllegalConstructor.name, - "createIllegalConstructor", + createProxyConstructor.name, + "createProxyConstructor", + ); + }); + }); + + describe("~", () => { + it("[[Call]] throws an error", () => { + assertThrows(() => { + createProxyConstructor({})(); + }); + }); + + it("[[Construct]] produces a proxy", () => { + const obj = {}; + const proxyConstructor = createProxyConstructor({ + get(O, P, Receiver) { + if (P === "etaoin") { + return Reflect.get(O, P, Receiver) ?? "success"; + } else { + return Reflect.get(O, P, Receiver); + } + }, + }, function () { + return obj; + }); + const proxy = new proxyConstructor(); + assertStrictEquals(proxy.etaoin, "success"); + obj.etaoin = "shrdlu"; + assertStrictEquals(proxy.etaoin, "shrdlu"); + }); + + it("[[Construct]] receives the expected new.target", () => { + const constructor = function Constructor() { + return { name: new.target.name }; + }; + assertStrictEquals( + new (createProxyConstructor({}, constructor))().name, + "Constructor", + ); + assertStrictEquals( + new (createProxyConstructor( + {}, + constructor, + function NewTarget() {}, + ))().name, + "NewTarget", + ); + }); + }); + + describe("~length", () => { + it("[[GetOwnProperty]] has the correct descriptor", () => { + assertEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}), + "length", + ), + { + configurable: true, + enumerable: false, + value: 1, + writable: false, + }, + ); + }); + }); + + describe("~name", () => { + it("[[GetOwnProperty]] has the correct descriptor", () => { + assertEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}), + "name", + ), + { + configurable: true, + enumerable: false, + value: "ObjectProxy", + writable: false, + }, + ); + }); + }); + + describe("~prototype", () => { + it("[[GetOwnProperty]] has the correct descriptor", () => { + assertEquals( + Object.getOwnPropertyDescriptor( + createProxyConstructor({}), + "prototype", + ), + { + configurable: false, + enumerable: false, + value: undefined, + writable: false, + }, + ); + }); + }); + + describe("~revocable", () => { + it("[[Call]] produces a revocable proxy", () => { + const obj = {}; + const proxyConstructor = createProxyConstructor({ + get(O, P, Receiver) { + if (P === "etaoin") { + return Reflect.get(O, P, Receiver) ?? "success"; + } else { + return Reflect.get(O, P, Receiver); + } + }, + }, function () { + return obj; + }); + const { proxy, revoke } = proxyConstructor.revocable(); + assertStrictEquals(proxy.etaoin, "success"); + obj.etaoin = "shrdlu"; + assertStrictEquals(proxy.etaoin, "shrdlu"); + revoke(); + assertThrows(() => proxy.etaoin); + }); + + it("[[Call]] receives the expected new.target", () => { + const constructor = function Constructor() { + return { name: new.target.name }; + }; + assertStrictEquals( + createProxyConstructor({}, constructor).revocable().proxy.name, + "Constructor", + ); + assertStrictEquals( + createProxyConstructor( + {}, + constructor, + function NewTarget() {}, + ).revocable().proxy.name, + "NewTarget", + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new (createProxyConstructor({})).revocable()); + }); + + it("[[GetOwnProperty]] has the correct descriptor", () => { + const proxyConstructor = createProxyConstructor({}); + assertEquals( + Object.getOwnPropertyDescriptor(proxyConstructor, "revocable"), + { + configurable: true, + enumerable: false, + value: proxyConstructor.revocable, + writable: true, + }, ); }); });