From 9fce41e5c08808c01d2d4b5e981c4d45dc02f3b6 Mon Sep 17 00:00:00 2001 From: Evelyn Hathaway Date: Sun, 3 May 2020 15:42:44 -0700 Subject: [PATCH] New: Proxify meta traps, mutation property tracing, new traps WIP: getOwnPropertyDescriptor accessors, read-only props, triggeredByFunction stack tracing --- plugin/__tests__/proxy.js | 53 ++++++++++++------- plugin/proxy.js | 106 ++++++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 59 deletions(-) diff --git a/plugin/__tests__/proxy.js b/plugin/__tests__/proxy.js index 5ff3e18..3bab24e 100644 --- a/plugin/__tests__/proxy.js +++ b/plugin/__tests__/proxy.js @@ -1,33 +1,48 @@ -const proxify = require('../proxy'); +const proxify = require("../proxy"); describe("Proxy util", () => { - let foo; + let target; beforeEach(() => { - foo = proxify( - Object.assign(Object.create({ - protoTest: "old" - }), { - test: "old", - foo: { - test: "old" + const nakedTarget = Object.assign( + Object.create({protoProp: "old"}), + { + prop: "old", + obj: { + prop: "old" + }, + classy: class { + constructor() {} } - }), { - deep: true, - prototype: true } ); + Object.defineProperty(nakedTarget, "accessor", { + set() {}, + get() {return {};} + }); + target = proxify( + nakedTarget, + {deep: true, prototype: true} + ); }) test("to throw when assigning a prop", async () => { expect(() => { - foo.test = "New" - }).toThrow(); + target.prop = "New"; + }).toThrow(); // TODO: assert exact error }); }); -// foo.test = "new"; // Errors -// delete foo.test; // Errors -// foo.foo.test = "new"; // Errors when `deep` -// Object.getPrototypeOf(foo).test = "foo"; // Errors when `prototype` -// Object.setPrototypeOf(foo, {}); // Errors when `prototype` +// new target.classy(); // TODO: SHOULD NOT ERROR (read-only prop issues) +// target.accessor = "new"; // Errors +// delete target.prop; // Errors +// target.obj.prop = "new"; // Errors when `deep` +// Object.defineProperty(target, "prop", {value: "new"}); // Errors +// Object.getPrototypeOf(target).protoProp = "new"; // Errors when `prototype` +// Object.getOwnPropertyDescriptor(target, "obj").value.prop = "new"; // Errors when `deep`; +// Object.getOwnPropertyDescriptor(target, "accessor"); // TODO: SHOULD NOT ERROR (read-only prop issues) +// Object.getOwnPropertyDescriptor(target, "accessor").set("new"); // TODO: make error (accessor apply trap proxying) +// Object.getOwnPropertyDescriptor(target, "accessor").set.prop = "new"; // TODO: make error (accessor apply trap proxying) +// Object.getOwnPropertyDescriptor(target, "accessor").get().prop = "new"; // TODO: make error (accessor apply trap proxying) +// Object.setPrototypeOf(target, {}); // Errors when `prototype` +// Object.preventExtensions(target.prop); // Errors diff --git a/plugin/proxy.js b/plugin/proxy.js index 84fe85b..e991bbd 100644 --- a/plugin/proxy.js +++ b/plugin/proxy.js @@ -1,49 +1,75 @@ -const proxify = (object, options = {}) => { - const { - deep = false, prototype = false +const proxify = (target, options = {}) => { + // Early return for non-objects + if (!(target instanceof Object)) return target; + + // Options + const {deep = false, prototype = false} = options; + + // Naming properties for mutation tracing in errors + let { + name = (typeof target.name === "string" && target.name), + path = "target" } = options; - if (!(object instanceof Object)) return object; + if (name !== "undefined" && name !== false) path += `.${name}`; - const triggeredByFunction = true; // TODO: this is for stack trace shit + // If the proxy trap was triggered by the function to test + // TODO: implement, possibly make optional? + const triggeredByFunction = true; - return new Proxy(object, { - // Other proxy traps have other edgecases + // Proxy handler + const handler = { + // Accessor edge case traps + getOwnPropertyDescriptor(target, prop) { + const descriptor = old = Reflect.getOwnPropertyDescriptor(...arguments); + if (!descriptor) return; + const isValueDesc = "value" in descriptor; - /* - Get - Deep *lennyface* - */ - // TODO: This has a BUNCH of edgecases involving getting/setting from the result of the desc. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor - getOwnPropertyDescriptor() { - const realDescriptor = Reflect.getOwnPropertyDescriptor(...arguments); - return deep ? proxify(realDescriptor, options) : realDescriptor; - }, - getPrototypeOf() { - const realPrototypeOf = Reflect.getPrototypeOf(...arguments); - return prototype ? proxify(realPrototypeOf, options) : realPrototypeOf; - }, - get() { - const realGet = Reflect.get(...arguments); - return deep ? proxify(realGet, options) : realGet; + if (deep) { + if (isValueDesc) { + descriptor.value = proxify(descriptor.value, {...options, path, name: prop}); + } else { + // descriptor.set = proxify(descriptor.set, {...options, path, name: prop}); // TODO: apply traps + // descriptor.get = proxify(descriptor.get, {...options, path, name: prop}); // TODO: apply traps + } + } else if (!isValueDesc) { + // descriptor.set = descriptor.set && new Proxy(descriptor.set, descriptorSetHandler); // TODO: apply traps + } + + return descriptor; // TODO: make able to return read-only props }, + }; + // Getting traps for deep mutation assertions + const addDeepGetTrap = (trap) => { + handler[trap] = function (target, prop) { + if (trap === "getPrototypeOf") prop = "__proto__"; + const real = Reflect[trap](...arguments); + return proxify(real, {...options, path, name: prop}); + }; + }; + deep && addDeepGetTrap("get"); // Covered by getOwnPropertyDescriptor, but is more specific // TODO: interfering when read-olny + prototype && addDeepGetTrap("getPrototypeOf"); - /* - Set - Errors *sadface* - */ - set() { - if (triggeredByFunction) throw new Error('Darn!'); - return Reflect.set(...arguments); - }, - setPrototypeOf() { - if (prototype && triggeredByFunction) throw new Error('Darn!'); - return Reflect.setPrototypeOf(...arguments); - }, - deleteProperty() { - if (triggeredByFunction) throw new Error('Darn!'); - return Reflect.deleteProperty(...arguments); - } - }); + // Mutation traps for erroring + const addSetTrap = (trap) => { + handler[trap] = function (target, prop) { + // Naming properties for mutation tracing in errors + if (trap !== "preventExtensions") { + if (trap === "setPrototypeOf") prop = "__proto__"; + path += `.${prop}`; + } + + if (triggeredByFunction) throw new Error(`Mutation assertion failed. \`${trap}\` trap triggered on \`${path}\`.`); + return Reflect[trap](...arguments); + }; + }; + addSetTrap("set"); // Covered by defineProperty, but is more specific + addSetTrap("defineProperty"); + addSetTrap("deleteProperty"); + prototype && addSetTrap("setPrototypeOf"); + addSetTrap("preventExtensions"); + + return new Proxy(target, handler); }; -module.exports = proxify; \ No newline at end of file +module.exports = proxify;