-
Notifications
You must be signed in to change notification settings - Fork 1
/
proxify.js
192 lines (167 loc) · 6.16 KB
/
proxify.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// Create property paths that mostly match the limitations of dot notation
const propertyPath = function (path, property) {
if (/^[$A-Z_a-z][\w$]*$/.test(property)) {
return `${path}.${property}`;
} else if (/^\d$/.test(property)) {
return `${path}[${property}]`;
} else {
return `${path}["${property}"]`;
}
};
// DO NOT CHANGE THE VARIABLE NAME WITHOUT UPDATING THE PLUGIN CODE
const _will_mutate_check_proxify = (target, options = {}) => {
// Early return for non-objects
if (!(target instanceof Object)) return target;
// Options
const {deep = false, prototype = false, _isSetter = false, _isGetter = false} = options;
const isShallowSetter = _isSetter && !deep;
// Naming properties for mutation tracing in errors
const {
name = (typeof target.name === "string" && target.name),
} = options;
const hasName = name !== undefined && name !== false;
let pathIsName = false;
let {path} = options;
if (!path) {
if (hasName) {
path = name;
pathIsName = true;
} else {
path = "target";
}
}
if (hasName && !pathIsName) path = propertyPath(path, name);
// Options for recursive function
const recursiveOptions = {
// Extend existing options
...options,
// Add new path
path,
// Reset temporary internal flags
_isSetter: false,
_isGetter: false,
};
// Proxy handler
const handler = {};
// Get traps for deep mutation assertions
// Accessor edge case traps
handler.getOwnPropertyDescriptor = function (dummyTarget, property) {
/*
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;
// 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;
// If has a value instead of accessors
const isValueDesc = "value" in descriptor;
if (deep) {
if (isValueDesc) {
descriptor.value = _will_mutate_check_proxify(
descriptor.value,
{
...recursiveOptions,
name: false, // Hide name, use custom path logic instead
path: `${propertyPath(path, property)}.descriptor.value`,
}
);
} else {
descriptor.get = _will_mutate_check_proxify(
descriptor.get,
{
...recursiveOptions,
name: false, // Hide name, use custom path logic instead
path: `${propertyPath(path, property)}.descriptor.get`,
_isGetter: true,
}
);
}
}
if (!isValueDesc) {
descriptor.set = _will_mutate_check_proxify(
descriptor.set,
{
...recursiveOptions,
name: false, // Hide name, use custom path logic instead
path: `${propertyPath(path, property)}.descriptor.set`,
_isSetter: true,
}
);
}
/*
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, property, descriptor);
return descriptor;
};
const addGetTrap = (trap) => {
handler[trap] = function (dummyTarget, property) {
// Reflect using the real target, not the dummy
const reflectArguments = [...arguments];
reflectArguments[0] = target;
if (trap === "getPrototypeOf") property = "__proto__";
if (trap === "apply") {
path += "()";
property = false; // Get apply trap doesn't need a prop
}
const real = Reflect[trap](...reflectArguments);
return deep || _isGetter ? _will_mutate_check_proxify(real, {...recursiveOptions, path, name: property}) : real; // Will revert to the actual target if not deep
};
};
addGetTrap("get"); // Covered by getOwnPropertyDescriptor, but is more specific
prototype && addGetTrap("getPrototypeOf");
_isGetter && addGetTrap("apply");
// Mutation traps for erroring
const addSetTrap = (trap) => {
handler[trap] = function (dummyTarget, property) {
// Naming properties for mutation tracing in errors
// Keep path mutuations inside this scope, the `path` available in the closure will not reset if the exception is caught
let internalPath = path;
if (trap === "apply") {
internalPath += "()";
property = false; // Set apply trap doesn't need a prop
} else if (trap !== "preventExtensions") {
if (trap === "setPrototypeOf") property = "__proto__";
internalPath = propertyPath(internalPath, property);
}
throw new Error(`Mutation assertion failed. \`${trap}\` trap triggered on \`${internalPath}\`.`);
};
};
if (!isShallowSetter) {
addSetTrap("set"); // Covered by defineProperty, but is more specific
addSetTrap("defineProperty");
addSetTrap("deleteProperty");
addSetTrap("preventExtensions");
prototype && addSetTrap("setPrototypeOf");
}
_isSetter && addSetTrap("apply");
// Reflect to the real target for unused traps
// This is to avoid the navtive fallback to the `dummyTarget`
const addNoopReflectUsingRealTargetTrap = (trap) => {
// Early return for existing traps
if (handler[trap]) return;
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");
// 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 = _will_mutate_check_proxify;