// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
-import { ITERATOR, toFunctionName, toLength } from "./value.js";
+import { ITERATOR, toFunctionName, toLength, type } from "./value.js";
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,
} 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.
"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,
+ );
+ }
+ },
};
})();
createArrowFunction,
createCallableFunction,
createIllegalConstructor,
+ createProxyConstructor,
identity,
isCallable,
isConstructor,
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(
constructorPrototype,
);
assert(Object.hasOwn(constructor, "prototype"));
- assertEquals(
+ assertStrictEquals(
constructor.prototype,
constructorPrototype.prototype,
);
.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(() => {
});
});
});
+});
+
+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,
+ },
);
});
});