| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a057510 commit 42f2deb
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -299,6 +299,40 @@ If `fn` is not provided, an empty function will be used. | |||
| 299 | 299 | Returns a function that triggers an `AssertionError` if it is invoked. `msg` is | |
| 300 | 300 | used as the error message for the `AssertionError`. | |
| 301 | 301 | ||
| 302 | + ### `mustNotMutateObjectDeep([target])` | ||
| 303 | + | ||
| 304 | + * `target` [\<any>][<any>] default = `undefined` | ||
| 305 | + * return [\<any>][<any>] | ||
| 306 | + | ||
| 307 | + If `target` is an Object, returns a proxy object that triggers | ||
| 308 | + an `AssertionError` on mutation attempt, including mutation of deeply nested | ||
| 309 | + Objects. Otherwise, it returns `target` directly. | ||
| 310 | + | ||
| 311 | + Use of this function is encouraged for relevant regression tests. | ||
| 312 | + | ||
| 313 | + ```mjs | ||
| 314 | + import { open } from 'node:fs/promises'; | ||
| 315 | + import { mustNotMutateObjectDeep } from '../common/index.mjs'; | ||
| 316 | + | ||
| 317 | + const _mutableOptions = { length: 4, position: 8 }; | ||
| 318 | + const options = mustNotMutateObjectDeep(_mutableOptions); | ||
| 319 | + | ||
| 320 | + // In filehandle.read or filehandle.write, attempt to mutate options will throw | ||
| 321 | + // In the test code, options can still be mutated via _mutableOptions | ||
| 322 | + const fh = await open('/path/to/file', 'r+'); | ||
| 323 | + const { buffer } = await fh.read(options); | ||
| 324 | + _mutableOptions.position = 4; | ||
| 325 | + await fh.write(buffer, options); | ||
| 326 | + | ||
| 327 | + // Inline usage | ||
| 328 | + const stats = await fh.stat(mustNotMutateObjectDeep({ bigint: true })); | ||
| 329 | + console.log(stats.size); | ||
| 330 | + ``` | ||
| 331 | + | ||
| 332 | + Caveats: built-in objects that make use of their internal slots (for example, | ||
| 333 | + `Map`s and `Set`s) might not work with this function. It returns Functions | ||
| 334 | + directly, not preventing their mutation. | ||
| 335 | + | ||
| 302 | 336 | ### `mustSucceed([fn])` | |
| 303 | 337 | ||
| 304 | 338 | * `fn` [\<Function>][<Function>] default = () => {} | |
@@ -1024,6 +1058,7 @@ See [the WPT tests README][] for details. | |||
| 1024 | 1058 | [<Function>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function | |
| 1025 | 1059 | [<Object>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object | |
| 1026 | 1060 | [<RegExp>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp | |
| 1061 | + [<any>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Data_types | ||
| 1027 | 1062 | [<bigint>]: https://github.com/tc39/proposal-bigint | |
| 1028 | 1063 | [<boolean>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type | |
| 1029 | 1064 | [<number>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -519,6 +519,52 @@ function mustNotCall(msg) { | |||
| 519 | 519 | }; | |
| 520 | 520 | } | |
| 521 | 521 | ||
| 522 | + const _mustNotMutateObjectDeepProxies = new WeakMap(); | ||
| 523 | + | ||
| 524 | + function mustNotMutateObjectDeep(original) { | ||
| 525 | + // Return primitives and functions directly. Primitives are immutable, and | ||
| 526 | + // proxied functions are impossible to compare against originals, e.g. with | ||
| 527 | + // `assert.deepEqual()`. | ||
| 528 | + if (original === null || typeof original !== 'object') { | ||
| 529 | + return original; | ||
| 530 | + } | ||
| 531 | + | ||
| 532 | + const cachedProxy = _mustNotMutateObjectDeepProxies.get(original); | ||
| 533 | + if (cachedProxy) { | ||
| 534 | + return cachedProxy; | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + const _mustNotMutateObjectDeepHandler = { | ||
| 538 | + __proto__: null, | ||
| 539 | + defineProperty(target, property, descriptor) { | ||
| 540 | + assert.fail(`Expected no side effects, got ${inspect(property)} ` + | ||
| 541 | + 'defined'); | ||
| 542 | + }, | ||
| 543 | + deleteProperty(target, property) { | ||
| 544 | + assert.fail(`Expected no side effects, got ${inspect(property)} ` + | ||
| 545 | + 'deleted'); | ||
| 546 | + }, | ||
| 547 | + get(target, prop, receiver) { | ||
| 548 | + return mustNotMutateObjectDeep(Reflect.get(target, prop, receiver)); | ||
| 549 | + }, | ||
| 550 | + preventExtensions(target) { | ||
| 551 | + assert.fail('Expected no side effects, got extensions prevented on ' + | ||
| 552 | + inspect(target)); | ||
| 553 | + }, | ||
| 554 | + set(target, property, value, receiver) { | ||
| 555 | + assert.fail(`Expected no side effects, got ${inspect(value)} ` + | ||
| 556 | + `assigned to ${inspect(property)}`); | ||
| 557 | + }, | ||
| 558 | + setPrototypeOf(target, prototype) { | ||
| 559 | + assert.fail(`Expected no side effects, got set prototype to ${prototype}`); | ||
| 560 | + } | ||
| 561 | + }; | ||
| 562 | + | ||
| 563 | + const proxy = new Proxy(original, _mustNotMutateObjectDeepHandler); | ||
| 564 | + _mustNotMutateObjectDeepProxies.set(original, proxy); | ||
| 565 | + return proxy; | ||
| 566 | + } | ||
| 567 | + | ||
| 522 | 568 | function printSkipMessage(msg) { | |
| 523 | 569 | console.log(`1..0 # Skipped: ${msg}`); | |
| 524 | 570 | } | |
@@ -827,6 +873,7 @@ const common = { | |||
| 827 | 873 | mustCall, | |
| 828 | 874 | mustCallAtLeast, | |
| 829 | 875 | mustNotCall, | |
| 876 | + mustNotMutateObjectDeep, | ||
| 830 | 877 | mustSucceed, | |
| 831 | 878 | nodeProcessAborted, | |
| 832 | 879 | PIPE, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,6 +35,7 @@ const { | |||
| 35 | 35 | canCreateSymLink, | |
| 36 | 36 | getCallSite, | |
| 37 | 37 | mustNotCall, | |
| 38 | + mustNotMutateObjectDeep, | ||
| 38 | 39 | printSkipMessage, | |
| 39 | 40 | skip, | |
| 40 | 41 | nodeProcessAborted, | |
@@ -81,6 +82,7 @@ export { | |||
| 81 | 82 | canCreateSymLink, | |
| 82 | 83 | getCallSite, | |
| 83 | 84 | mustNotCall, | |
| 85 | + mustNotMutateObjectDeep, | ||
| 84 | 86 | printSkipMessage, | |
| 85 | 87 | skip, | |
| 86 | 88 | nodeProcessAborted, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,225 @@ | |||
| 1 | + import { mustNotMutateObjectDeep } from '../common/index.mjs'; | ||
| 2 | + import assert from 'node:assert'; | ||
| 3 | + import { promisify } from 'node:util'; | ||
| 4 | + | ||
| 5 | + // Test common.mustNotMutateObjectDeep() | ||
| 6 | + | ||
| 7 | + const original = { | ||
| 8 | + foo: { bar: 'baz' }, | ||
| 9 | + qux: null, | ||
| 10 | + quux: [ | ||
| 11 | + 'quuz', | ||
| 12 | + { corge: 'grault' }, | ||
| 13 | + ], | ||
| 14 | + }; | ||
| 15 | + | ||
| 16 | + // Make a copy to make sure original doesn't get altered by the function itself. | ||
| 17 | + const backup = structuredClone(original); | ||
| 18 | + | ||
| 19 | + // Wrapper for convenience: | ||
| 20 | + const obj = () => mustNotMutateObjectDeep(original); | ||
| 21 | + | ||
| 22 | + function testOriginal(root) { | ||
| 23 | + assert.deepStrictEqual(root, backup); | ||
| 24 | + return root.foo.bar === 'baz' && root.quux[1].corge.length === 6; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + function definePropertyOnRoot(root) { | ||
| 28 | + Object.defineProperty(root, 'xyzzy', {}); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + function definePropertyOnFoo(root) { | ||
| 32 | + Object.defineProperty(root.foo, 'xyzzy', {}); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + function deletePropertyOnRoot(root) { | ||
| 36 | + delete root.foo; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + function deletePropertyOnFoo(root) { | ||
| 40 | + delete root.foo.bar; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + function preventExtensionsOnRoot(root) { | ||
| 44 | + Object.preventExtensions(root); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + function preventExtensionsOnFoo(root) { | ||
| 48 | + Object.preventExtensions(root.foo); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + function preventExtensionsOnRootViaSeal(root) { | ||
| 52 | + Object.seal(root); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + function preventExtensionsOnFooViaSeal(root) { | ||
| 56 | + Object.seal(root.foo); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + function preventExtensionsOnRootViaFreeze(root) { | ||
| 60 | + Object.freeze(root); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + function preventExtensionsOnFooViaFreeze(root) { | ||
| 64 | + Object.freeze(root.foo); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + function setOnRoot(root) { | ||
| 68 | + root.xyzzy = 'gwak'; | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + function setOnFoo(root) { | ||
| 72 | + root.foo.xyzzy = 'gwak'; | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + function setQux(root) { | ||
| 76 | + root.qux = 'gwak'; | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + function setQuux(root) { | ||
| 80 | + root.quux.push('gwak'); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + function setQuuxItem(root) { | ||
| 84 | + root.quux[0] = 'gwak'; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + function setQuuxProperty(root) { | ||
| 88 | + root.quux[1].corge = 'gwak'; | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + function setPrototypeOfRoot(root) { | ||
| 92 | + Object.setPrototypeOf(root, Array); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + function setPrototypeOfFoo(root) { | ||
| 96 | + Object.setPrototypeOf(root.foo, Array); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + function setPrototypeOfQuux(root) { | ||
| 100 | + Object.setPrototypeOf(root.quux, Array); | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + | ||
| 104 | + { | ||
| 105 | + assert.ok(testOriginal(obj())); | ||
| 106 | + | ||
| 107 | + assert.throws( | ||
| 108 | + () => definePropertyOnRoot(obj()), | ||
| 109 | + { code: 'ERR_ASSERTION' } | ||
| 110 | + ); | ||
| 111 | + assert.throws( | ||
| 112 | + () => definePropertyOnFoo(obj()), | ||
| 113 | + { code: 'ERR_ASSERTION' } | ||
| 114 | + ); | ||
| 115 | + assert.throws( | ||
| 116 | + () => deletePropertyOnRoot(obj()), | ||
| 117 | + { code: 'ERR_ASSERTION' } | ||
| 118 | + ); | ||
| 119 | + assert.throws( | ||
| 120 | + () => deletePropertyOnFoo(obj()), | ||
| 121 | + { code: 'ERR_ASSERTION' } | ||
| 122 | + ); | ||
| 123 | + assert.throws( | ||
| 124 | + () => preventExtensionsOnRoot(obj()), | ||
| 125 | + { code: 'ERR_ASSERTION' } | ||
| 126 | + ); | ||
| 127 | + assert.throws( | ||
| 128 | + () => preventExtensionsOnFoo(obj()), | ||
| 129 | + { code: 'ERR_ASSERTION' } | ||
| 130 | + ); | ||
| 131 | + assert.throws( | ||
| 132 | + () => preventExtensionsOnRootViaSeal(obj()), | ||
| 133 | + { code: 'ERR_ASSERTION' } | ||
| 134 | + ); | ||
| 135 | + assert.throws( | ||
| 136 | + () => preventExtensionsOnFooViaSeal(obj()), | ||
| 137 | + { code: 'ERR_ASSERTION' } | ||
| 138 | + ); | ||
| 139 | + assert.throws( | ||
| 140 | + () => preventExtensionsOnRootViaFreeze(obj()), | ||
| 141 | + { code: 'ERR_ASSERTION' } | ||
| 142 | + ); | ||
| 143 | + assert.throws( | ||
| 144 | + () => preventExtensionsOnFooViaFreeze(obj()), | ||
| 145 | + { code: 'ERR_ASSERTION' } | ||
| 146 | + ); | ||
| 147 | + assert.throws( | ||
| 148 | + () => setOnRoot(obj()), | ||
| 149 | + { code: 'ERR_ASSERTION' } | ||
| 150 | + ); | ||
| 151 | + assert.throws( | ||
| 152 | + () => setOnFoo(obj()), | ||
| 153 | + { code: 'ERR_ASSERTION' } | ||
| 154 | + ); | ||
| 155 | + assert.throws( | ||
| 156 | + () => setQux(obj()), | ||
| 157 | + { code: 'ERR_ASSERTION' } | ||
| 158 | + ); | ||
| 159 | + assert.throws( | ||
| 160 | + () => setQuux(obj()), | ||
| 161 | + { code: 'ERR_ASSERTION' } | ||
| 162 | + ); | ||
| 163 | + assert.throws( | ||
| 164 | + () => setQuux(obj()), | ||
| 165 | + { code: 'ERR_ASSERTION' } | ||
| 166 | + ); | ||
| 167 | + assert.throws( | ||
| 168 | + () => setQuuxItem(obj()), | ||
| 169 | + { code: 'ERR_ASSERTION' } | ||
| 170 | + ); | ||
| 171 | + assert.throws( | ||
| 172 | + () => setQuuxProperty(obj()), | ||
| 173 | + { code: 'ERR_ASSERTION' } | ||
| 174 | + ); | ||
| 175 | + assert.throws( | ||
| 176 | + () => setPrototypeOfRoot(obj()), | ||
| 177 | + { code: 'ERR_ASSERTION' } | ||
| 178 | + ); | ||
| 179 | + assert.throws( | ||
| 180 | + () => setPrototypeOfFoo(obj()), | ||
| 181 | + { code: 'ERR_ASSERTION' } | ||
| 182 | + ); | ||
| 183 | + assert.throws( | ||
| 184 | + () => setPrototypeOfQuux(obj()), | ||
| 185 | + { code: 'ERR_ASSERTION' } | ||
| 186 | + ); | ||
| 187 | + | ||
| 188 | + // Test that no mutation happened: | ||
| 189 | + assert.ok(testOriginal(obj())); | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + // Test various supported types, directly and nested: | ||
| 193 | + [ | ||
| 194 | + undefined, null, false, true, 42, 42n, Symbol('42'), NaN, Infinity, {}, [], | ||
| 195 | + () => {}, async () => {}, Promise.resolve(), Math, Object.create(null), | ||
| 196 | + ].forEach((target) => { | ||
| 197 | + assert.deepStrictEqual(mustNotMutateObjectDeep(target), target); | ||
| 198 | + assert.deepStrictEqual(mustNotMutateObjectDeep({ target }), { target }); | ||
| 199 | + assert.deepStrictEqual(mustNotMutateObjectDeep([ target ]), [ target ]); | ||
| 200 | + }); | ||
| 201 | + | ||
| 202 | + // Test that passed functions keep working correctly: | ||
| 203 | + { | ||
| 204 | + const fn = () => 'blep'; | ||
| 205 | + fn.foo = {}; | ||
| 206 | + const fnImmutableView = mustNotMutateObjectDeep(fn); | ||
| 207 | + assert.deepStrictEqual(fnImmutableView, fn); | ||
| 208 | + | ||
| 209 | + // Test that the function still works: | ||
| 210 | + assert.strictEqual(fn(), 'blep'); | ||
| 211 | + assert.strictEqual(fnImmutableView(), 'blep'); | ||
| 212 | + | ||
| 213 | + // Test that the original function is not deeply frozen: | ||
| 214 | + fn.foo.bar = 'baz'; | ||
| 215 | + assert.strictEqual(fn.foo.bar, 'baz'); | ||
| 216 | + assert.strictEqual(fnImmutableView.foo.bar, 'baz'); | ||
| 217 | + | ||
| 218 | + // Test the original function is not frozen: | ||
| 219 | + fn.qux = 'quux'; | ||
| 220 | + assert.strictEqual(fn.qux, 'quux'); | ||
| 221 | + assert.strictEqual(fnImmutableView.qux, 'quux'); | ||
| 222 | + | ||
| 223 | + // Redefining util.promisify.custom also works: | ||
| 224 | + promisify(mustNotMutateObjectDeep(promisify(fn))); | ||
| 225 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments