-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New: Proxify meta traps, mutation property tracing, new traps
WIP: getOwnPropertyDescriptor accessors, read-only props, triggeredByFunction stack tracing
- Loading branch information
1 parent
dac339c
commit 9fce41e
Showing
2 changed files
with
100 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
module.exports = proxify; |