Skip to content

Commit 4ed9d21

Browse files
lib: update isDeepStrictEqual to support options
PR-URL: #59762 Reviewed-By: Jordan Harband <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]>
1 parent 5ed1a47 commit 4ed9d21

File tree

7 files changed

+315
-13
lines changed

7 files changed

+315
-13
lines changed

doc/api/assert.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,20 @@ The `Assert` class allows creating independent assertion instances with custom o
229229

230230
### `new assert.Assert([options])`
231231

232+
<!-- YAML
233+
changes:
234+
- version: REPLACEME
235+
pr-url: https://github.com/nodejs/node/pull/59762
236+
description: Added `skipPrototype` option.
237+
-->
238+
232239
* `options` {Object}
233240
* `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`.
234241
Accepted values: `'simple'`, `'full'`.
235242
* `strict` {boolean} If set to `true`, non-strict methods behave like their
236243
corresponding strict methods. Defaults to `true`.
244+
* `skipPrototype` {boolean} If set to `true`, skips prototype and constructor
245+
comparison in deep equality checks. Defaults to `false`.
237246

238247
Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages.
239248

@@ -245,7 +254,8 @@ assertInstance.deepStrictEqual({ a: 1 }, { a: 2 });
245254
```
246255

247256
**Important**: When destructuring assertion methods from an `Assert` instance,
248-
the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings).
257+
the methods lose their connection to the instance's configuration options (such
258+
as `diff`, `strict`, and `skipPrototype` settings).
249259
The destructured methods will fall back to default behavior instead.
250260

251261
```js
@@ -259,6 +269,33 @@ const { strictEqual } = myAssert;
259269
strictEqual({ a: 1 }, { b: { c: 1 } });
260270
```
261271

272+
The `skipPrototype` option affects all deep equality methods:
273+
274+
```js
275+
class Foo {
276+
constructor(a) {
277+
this.a = a;
278+
}
279+
}
280+
281+
class Bar {
282+
constructor(a) {
283+
this.a = a;
284+
}
285+
}
286+
287+
const foo = new Foo(1);
288+
const bar = new Bar(1);
289+
290+
// Default behavior - fails due to different constructors
291+
const assert1 = new Assert();
292+
assert1.deepStrictEqual(foo, bar); // AssertionError
293+
294+
// Skip prototype comparison - passes if properties are equal
295+
const assert2 = new Assert({ skipPrototype: true });
296+
assert2.deepStrictEqual(foo, bar); // OK
297+
```
298+
262299
When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior
263300
(diff: 'simple', non-strict mode).
264301
To maintain custom options when using destructured methods, avoid

doc/api/util.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1574,19 +1574,56 @@ inspect.defaultOptions.maxArrayLength = null;
15741574
console.log(arr); // logs the full array
15751575
```
15761576

1577-
## `util.isDeepStrictEqual(val1, val2)`
1577+
## `util.isDeepStrictEqual(val1, val2[, options])`
15781578

15791579
<!-- YAML
15801580
added: v9.0.0
1581+
changes:
1582+
- version: REPLACEME
1583+
pr-url: https://github.com/nodejs/node/pull/59762
1584+
description: Added `options` parameter to allow skipping prototype comparison.
15811585
-->
15821586

15831587
* `val1` {any}
15841588
* `val2` {any}
1589+
* `skipPrototype` {boolean} If `true`, prototype and constructor
1590+
comparison is skipped during deep strict equality check. **Default:** `false`.
15851591
* Returns: {boolean}
15861592

15871593
Returns `true` if there is deep strict equality between `val1` and `val2`.
15881594
Otherwise, returns `false`.
15891595

1596+
By default, deep strict equality includes comparison of object prototypes and
1597+
constructors. When `skipPrototype` is `true`, objects with
1598+
different prototypes or constructors can still be considered equal if their
1599+
enumerable properties are deeply strictly equal.
1600+
1601+
```js
1602+
const util = require('node:util');
1603+
1604+
class Foo {
1605+
constructor(a) {
1606+
this.a = a;
1607+
}
1608+
}
1609+
1610+
class Bar {
1611+
constructor(a) {
1612+
this.a = a;
1613+
}
1614+
}
1615+
1616+
const foo = new Foo(1);
1617+
const bar = new Bar(1);
1618+
1619+
// Different constructors, same properties
1620+
console.log(util.isDeepStrictEqual(foo, bar));
1621+
// false
1622+
1623+
console.log(util.isDeepStrictEqual(foo, bar, true));
1624+
// true
1625+
```
1626+
15901627
See [`assert.deepStrictEqual()`][] for more information about deep strict
15911628
equality.
15921629

lib/assert.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ const NO_EXCEPTION_SENTINEL = {};
9393
* @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors.
9494
* @property {boolean} [strict=true] - If set to true, non-strict methods behave like their corresponding
9595
* strict methods.
96+
* @property {boolean} [skipPrototype=false] - If set to true, skips comparing prototypes
97+
* in deep equality checks.
9698
*/
9799

98100
/**
@@ -105,7 +107,7 @@ function Assert(options) {
105107
throw new ERR_CONSTRUCT_CALL_REQUIRED('Assert');
106108
}
107109

108-
options = ObjectAssign({ __proto__: null, strict: true }, options);
110+
options = ObjectAssign({ __proto__: null, strict: true, skipPrototype: false }, options);
109111

110112
const allowedDiffs = ['simple', 'full'];
111113
if (options.diff !== undefined) {
@@ -311,7 +313,7 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, me
311313
throw new ERR_MISSING_ARGS('actual', 'expected');
312314
}
313315
if (isDeepEqual === undefined) lazyLoadComparison();
314-
if (!isDeepStrictEqual(actual, expected)) {
316+
if (!isDeepStrictEqual(actual, expected, this?.[kOptions]?.skipPrototype)) {
315317
innerFail({
316318
actual,
317319
expected,
@@ -337,7 +339,7 @@ function notDeepStrictEqual(actual, expected, message) {
337339
throw new ERR_MISSING_ARGS('actual', 'expected');
338340
}
339341
if (isDeepEqual === undefined) lazyLoadComparison();
340-
if (isDeepStrictEqual(actual, expected)) {
342+
if (isDeepStrictEqual(actual, expected, this?.[kOptions]?.skipPrototype)) {
341343
innerFail({
342344
actual,
343345
expected,

lib/internal/util/comparisons.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,10 @@ const {
127127
getOwnNonIndexProperties,
128128
} = internalBinding('util');
129129

130-
const kStrict = 1;
130+
const kStrict = 2;
131+
const kStrictWithoutPrototypes = 3;
131132
const kLoose = 0;
132-
const kPartial = 2;
133+
const kPartial = 1;
133134

134135
const kNoIterator = 0;
135136
const kIsArray = 1;
@@ -458,7 +459,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
458459
}
459460
} else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) {
460461
return false;
461-
} else if (mode === kStrict) {
462+
} else if (mode === kStrict || mode === kStrictWithoutPrototypes) {
462463
const symbolKeysA = getOwnSymbols(val1);
463464
if (symbolKeysA.length !== 0) {
464465
let count = 0;
@@ -1027,7 +1028,10 @@ module.exports = {
10271028
isDeepEqual(val1, val2) {
10281029
return detectCycles(val1, val2, kLoose);
10291030
},
1030-
isDeepStrictEqual(val1, val2) {
1031+
isDeepStrictEqual(val1, val2, skipPrototype) {
1032+
if (skipPrototype) {
1033+
return detectCycles(val1, val2, kStrictWithoutPrototypes);
1034+
}
10311035
return detectCycles(val1, val2, kStrict);
10321036
},
10331037
isPartialStrictEqual(val1, val2) {

lib/util.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,12 +487,11 @@ module.exports = {
487487
isArray: deprecate(ArrayIsArray,
488488
'The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.',
489489
'DEP0044'),
490-
isDeepStrictEqual(a, b) {
490+
isDeepStrictEqual(a, b, skipPrototype) {
491491
if (internalDeepEqual === undefined) {
492-
internalDeepEqual = require('internal/util/comparisons')
493-
.isDeepStrictEqual;
492+
internalDeepEqual = require('internal/util/comparisons').isDeepStrictEqual;
494493
}
495-
return internalDeepEqual(a, b);
494+
return internalDeepEqual(a, b, skipPrototype);
496495
},
497496
promisify,
498497
stripVTControlCharacters,

test/parallel/test-assert-class.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,163 @@ test('Assert class non strict with simple diff', () => {
478478
);
479479
}
480480
});
481+
482+
// Shared setup for skipPrototype tests
483+
{
484+
const message = 'Expected values to be strictly deep-equal:\n' +
485+
'+ actual - expected\n' +
486+
'\n' +
487+
' [\n' +
488+
' 1,\n' +
489+
' 2,\n' +
490+
' 3,\n' +
491+
' 4,\n' +
492+
' 5,\n' +
493+
'+ 6,\n' +
494+
'- 9,\n' +
495+
' 7\n' +
496+
' ]\n';
497+
498+
function CoolClass(name) { this.name = name; }
499+
500+
function AwesomeClass(name) { this.name = name; }
501+
502+
class Modern { constructor(value) { this.value = value; } }
503+
class Legacy { constructor(value) { this.value = value; } }
504+
505+
const cool = new CoolClass('Assert is inspiring');
506+
const awesome = new AwesomeClass('Assert is inspiring');
507+
const modern = new Modern(42);
508+
const legacy = new Legacy(42);
509+
510+
test('Assert class strict with skipPrototype', () => {
511+
const assertInstance = new Assert({ skipPrototype: true });
512+
513+
assert.throws(
514+
() => assertInstance.deepEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]),
515+
{ message }
516+
);
517+
518+
assertInstance.deepEqual(cool, awesome);
519+
assertInstance.deepStrictEqual(cool, awesome);
520+
assertInstance.deepEqual(modern, legacy);
521+
assertInstance.deepStrictEqual(modern, legacy);
522+
523+
const cool2 = new CoolClass('Soooo coooool');
524+
assert.throws(
525+
() => assertInstance.deepStrictEqual(cool, cool2),
526+
{ code: 'ERR_ASSERTION' }
527+
);
528+
529+
const nested1 = { obj: new CoolClass('test'), arr: [1, 2, 3] };
530+
const nested2 = { obj: new AwesomeClass('test'), arr: [1, 2, 3] };
531+
assertInstance.deepStrictEqual(nested1, nested2);
532+
533+
const arr = new Uint8Array([1, 2, 3]);
534+
const buf = Buffer.from([1, 2, 3]);
535+
assertInstance.deepStrictEqual(arr, buf);
536+
});
537+
538+
test('Assert class non strict with skipPrototype', () => {
539+
const assertInstance = new Assert({ strict: false, skipPrototype: true });
540+
541+
assert.throws(
542+
() => assertInstance.deepStrictEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]),
543+
{ message }
544+
);
545+
546+
assertInstance.deepStrictEqual(cool, awesome);
547+
assertInstance.deepStrictEqual(modern, legacy);
548+
});
549+
550+
test('Assert class skipPrototype with complex objects', () => {
551+
const assertInstance = new Assert({ skipPrototype: true });
552+
553+
function ComplexAwesomeClass(name, age) {
554+
this.name = name;
555+
this.age = age;
556+
this.settings = {
557+
theme: 'dark',
558+
lang: 'en'
559+
};
560+
}
561+
562+
function ComplexCoolClass(name, age) {
563+
this.name = name;
564+
this.age = age;
565+
this.settings = {
566+
theme: 'dark',
567+
lang: 'en'
568+
};
569+
}
570+
571+
const awesome1 = new ComplexAwesomeClass('Foo', 30);
572+
const cool1 = new ComplexCoolClass('Foo', 30);
573+
574+
assertInstance.deepStrictEqual(awesome1, cool1);
575+
576+
const cool2 = new ComplexCoolClass('Foo', 30);
577+
cool2.settings.theme = 'light';
578+
579+
assert.throws(
580+
() => assertInstance.deepStrictEqual(awesome1, cool2),
581+
{ code: 'ERR_ASSERTION' }
582+
);
583+
});
584+
585+
test('Assert class skipPrototype with arrays and special objects', () => {
586+
const assertInstance = new Assert({ skipPrototype: true });
587+
588+
const arr1 = [1, 2, 3];
589+
const arr2 = new Array(1, 2, 3);
590+
assertInstance.deepStrictEqual(arr1, arr2);
591+
592+
const date1 = new Date('2023-01-01');
593+
const date2 = new Date('2023-01-01');
594+
assertInstance.deepStrictEqual(date1, date2);
595+
596+
const regex1 = /test/g;
597+
const regex2 = new RegExp('test', 'g');
598+
assertInstance.deepStrictEqual(regex1, regex2);
599+
600+
const date3 = new Date('2023-01-02');
601+
assert.throws(
602+
() => assertInstance.deepStrictEqual(date1, date3),
603+
{ code: 'ERR_ASSERTION' }
604+
);
605+
});
606+
607+
test('Assert class skipPrototype with notDeepStrictEqual', () => {
608+
const assertInstance = new Assert({ skipPrototype: true });
609+
610+
assert.throws(
611+
() => assertInstance.notDeepStrictEqual(cool, awesome),
612+
{ code: 'ERR_ASSERTION' }
613+
);
614+
615+
const notAwesome = new AwesomeClass('Not so awesome');
616+
assertInstance.notDeepStrictEqual(cool, notAwesome);
617+
618+
const defaultAssertInstance = new Assert({ skipPrototype: false });
619+
defaultAssertInstance.notDeepStrictEqual(cool, awesome);
620+
});
621+
622+
test('Assert class skipPrototype with mixed types', () => {
623+
const assertInstance = new Assert({ skipPrototype: true });
624+
625+
const obj1 = { value: 42, nested: { prop: 'test' } };
626+
627+
function CustomObj(value, nested) {
628+
this.value = value;
629+
this.nested = nested;
630+
}
631+
632+
const obj2 = new CustomObj(42, { prop: 'test' });
633+
assertInstance.deepStrictEqual(obj1, obj2);
634+
635+
assert.throws(
636+
() => assertInstance.deepStrictEqual({ num: 42 }, { num: '42' }),
637+
{ code: 'ERR_ASSERTION' }
638+
);
639+
});
640+
}

0 commit comments

Comments
 (0)