-
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.
Merge pull request #2 from evelynhathaway/eve-proxify
New: Proxify meta traps, mutation property tracing, new traps
- Loading branch information
Showing
2 changed files
with
171 additions
and
58 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,62 @@ | ||
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, "readOnly", { | ||
value: "Don't write to me please", | ||
}); | ||
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("Mutation assertion failed. `set` trap triggered on `target.prop`."); | ||
}); | ||
test("to not throw when accessing read-only props multiple times", async () => { | ||
expect(() => { | ||
target.readOnly; | ||
target.readOnly; | ||
Object.getOwnPropertyDescriptor(target, "readOnly"); | ||
Object.getOwnPropertyDescriptor(target, "readOnly"); | ||
Object.getOwnPropertyDescriptor(target, "accessor"); | ||
Object.getOwnPropertyDescriptor(target, "accessor"); | ||
new target.classy(); | ||
new target.classy(); | ||
}).not.toThrow(); | ||
}); | ||
}); | ||
|
||
// 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` | ||
|
||
// 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").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 | ||
// TODO: add tests to find the limitations of the proxy, dummytarget, etc. |
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,133 @@ | ||
const proxify = (object, options = {}) => { | ||
const { | ||
deep = false, prototype = false | ||
const propPath = function (path, property) { | ||
if (/^[a-zA-Z_$][\w$]*$/.test(property)) { | ||
return `${path}.${property}`; | ||
} else if (/^\d$/.test(property)) { | ||
return `${path}[${property}]`; | ||
} else { | ||
return `${path}["${property}"]`; | ||
} | ||
}; | ||
|
||
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 = propPath(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(dummyTarget, prop) { | ||
/* | ||
Early return for cached read-only properties, prevents the below invariant when adding read-only properties to the dummy: | ||
"The result of Object.getOwnPropertyDescriptor(target) can be applied to the target object using Object.defineProperty() and will not throw an exception." | ||
*/ | ||
const dummyDescriptor = Reflect.getOwnPropertyDescriptor(...arguments); | ||
if (dummyDescriptor) return dummyDescriptor; | ||
|
||
/* | ||
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; | ||
}, | ||
// Reflect using the real target, not the dummy | ||
const reflectArguments = [...arguments]; | ||
reflectArguments[0] = target; | ||
const descriptor = Reflect.getOwnPropertyDescriptor(...reflectArguments); | ||
|
||
// Early return for non-existing properties | ||
if (!descriptor) return; | ||
|
||
/* | ||
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); | ||
// If has a value instead of accessors | ||
const isValueDesc = "value" in descriptor; | ||
|
||
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 | ||
} | ||
|
||
/* | ||
Add read-only props to `dummyTarget` to meet the below invariant: | ||
"A property cannot be reported as existent, if it does not exists as an own property of the target object and the target object is not extensible." | ||
*/ | ||
const isReadOnly = descriptor.writable === false || descriptor.configurable === false; | ||
if (isReadOnly) Object.defineProperty(dummyTarget, prop, descriptor); | ||
|
||
return descriptor; | ||
}, | ||
deleteProperty() { | ||
if (triggeredByFunction) throw new Error('Darn!'); | ||
return Reflect.deleteProperty(...arguments); | ||
}; | ||
|
||
// Reflect to the real target for unused traps | ||
// This is to avoid the navtive fallback to the `dummyTarget` | ||
const addNoopReflectUsingRealTargetTrap = (trap) => { | ||
handler[trap] = function () { | ||
// Reflect using the real target, not the dummy | ||
const reflectArguments = [...arguments]; | ||
reflectArguments[0] = target; | ||
return Reflect[trap](...reflectArguments); | ||
} | ||
}); | ||
}; | ||
addNoopReflectUsingRealTargetTrap("isExtensible"); | ||
addNoopReflectUsingRealTargetTrap("has"); | ||
addNoopReflectUsingRealTargetTrap("ownKeys"); | ||
addNoopReflectUsingRealTargetTrap("apply"); | ||
addNoopReflectUsingRealTargetTrap("construct"); | ||
|
||
// Getting traps for deep mutation assertions | ||
const addDeepGetTrap = (trap) => { | ||
handler[trap] = function (dummyTarget, prop) { | ||
// Reflect using the real target, not the dummy | ||
const reflectArguments = [...arguments]; | ||
reflectArguments[0] = target; | ||
|
||
if (trap === "getPrototypeOf") prop = "__proto__"; | ||
const real = Reflect[trap](...reflectArguments); | ||
return proxify(real, {...options, path, name: prop}); | ||
}; | ||
}; | ||
deep && addDeepGetTrap("get"); // Covered by getOwnPropertyDescriptor, but is more specific | ||
prototype && addDeepGetTrap("getPrototypeOf"); | ||
|
||
// Mutation traps for erroring | ||
const addSetTrap = (trap) => { | ||
handler[trap] = function (dummyTarget, prop) { | ||
// Reflect using the real target, not the dummy | ||
const reflectArguments = [...arguments]; | ||
reflectArguments[0] = target; | ||
|
||
// 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](...reflectArguments); | ||
}; | ||
}; | ||
addSetTrap("set"); // Covered by defineProperty, but is more specific | ||
addSetTrap("defineProperty"); | ||
addSetTrap("deleteProperty"); | ||
prototype && addSetTrap("setPrototypeOf"); | ||
addSetTrap("preventExtensions"); | ||
|
||
// Don't use the true `target` as the proxy target to avoid issues with read-only types | ||
// Create `dummyTarget` based on the `target`'s constructor | ||
const dummyTarget = new (Object.getPrototypeOf(target).constructor)(); | ||
return new Proxy(dummyTarget, handler); | ||
}; | ||
|
||
module.exports = proxify; | ||
module.exports = proxify; |