]> Lady’s Gitweb - Pisces/commitdiff
Add createProxyConstructor to function.js
authorLady <redacted>
Thu, 23 Nov 2023 02:00:45 +0000 (21:00 -0500)
committerLady <redacted>
Thu, 23 Nov 2023 02:06:37 +0000 (21:06 -0500)
function.js
function.test.js

index d02fe6e85eb6a8ec2a7137dd221ecb227d7640c1..10a3f89fd2bdf6c79257db0307457e0bbec9a0ce 100644 (file)
@@ -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 <https://mozilla.org/MPL/2.0/>.
 
-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,
+        );
+      }
+    },
   };
 })();
 
index 439f9d38c2ad771d0529415c6f5f29d8735f35d3..0a58a367af38433412a700d230bb5995d1da13ae 100644 (file)
@@ -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,
+        },
       );
     });
   });
This page took 0.032599 seconds and 4 git commands to generate.