Skip to content

Commit

Permalink
Merge pull request #2 from evelynhathaway/eve-proxify
Browse files Browse the repository at this point in the history
New: Proxify meta traps, mutation property tracing, new traps
  • Loading branch information
crutchcorn authored May 5, 2020
2 parents 08345b1 + 594395f commit 653494c
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 58 deletions.
67 changes: 48 additions & 19 deletions plugin/__tests__/proxy.js
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.
162 changes: 123 additions & 39 deletions plugin/proxy.js
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;

0 comments on commit 653494c

Please sign in to comment.