Skip to content

Commit fed1dac

Browse files
miguelmarcondesftargos
authored andcommitted
lib: update isDeepStrictEqual to support options
PR-URL: #59762 Reviewed-By: Jordan Harband <ljharb@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
1 parent cbec4fd commit fed1dac

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
@@ -227,11 +227,20 @@ The `Assert` class allows creating independent assertion instances with custom o
227227

228228
### `new assert.Assert([options])`
229229

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

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

@@ -243,7 +252,8 @@ assertInstance.deepStrictEqual({ a: 1 }, { a: 2 });
243252
```
244253

245254
**Important**: When destructuring assertion methods from an `Assert` instance,
246-
the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings).
255+
the methods lose their connection to the instance's configuration options (such
256+
as `diff`, `strict`, and `skipPrototype` settings).
247257
The destructured methods will fall back to default behavior instead.
248258

249259
```js
@@ -257,6 +267,33 @@ const { strictEqual } = myAssert;
257267
strictEqual({ a: 1 }, { b: { c: 1 } });
258268
```
259269

270+
The `skipPrototype` option affects all deep equality methods:
271+
272+
```js
273+
class Foo {
274+
constructor(a) {
275+
this.a = a;
276+
}
277+
}
278+
279+
class Bar {
280+
constructor(a) {
281+
this.a = a;
282+
}
283+
}
284+
285+
const foo = new Foo(1);
286+
const bar = new Bar(1);
287+
288+
// Default behavior - fails due to different constructors
289+
const assert1 = new Assert();
290+
assert1.deepStrictEqual(foo, bar); // AssertionError
291+
292+
// Skip prototype comparison - passes if properties are equal
293+
const assert2 = new Assert({ skipPrototype: true });
294+
assert2.deepStrictEqual(foo, bar); // OK
295+
```
296+
260297
When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior
261298
(diff: 'simple', non-strict mode).
262299
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
@@ -1551,19 +1551,56 @@ inspect.defaultOptions.maxArrayLength = null;
15511551
console.log(arr); // logs the full array
15521552
```
15531553

1554-
## `util.isDeepStrictEqual(val1, val2)`
1554+
## `util.isDeepStrictEqual(val1, val2[, options])`
15551555

15561556
<!-- YAML
15571557
added: v9.0.0
1558+
changes:
1559+
- version: REPLACEME
1560+
pr-url: https://github.com/nodejs/node/pull/59762
1561+
description: Added `options` parameter to allow skipping prototype comparison.
15581562
-->
15591563

15601564
* `val1` {any}
15611565
* `val2` {any}
1566+
* `skipPrototype` {boolean} If `true`, prototype and constructor
1567+
comparison is skipped during deep strict equality check. **Default:** `false`.
15621568
* Returns: {boolean}
15631569

15641570
Returns `true` if there is deep strict equality between `val1` and `val2`.
15651571
Otherwise, returns `false`.
15661572

1573+
By default, deep strict equality includes comparison of object prototypes and
1574+
constructors. When `skipPrototype` is `true`, objects with
1575+
different prototypes or constructors can still be considered equal if their
1576+
enumerable properties are deeply strictly equal.
1577+
1578+
```js
1579+
const util = require('node:util');
1580+
1581+
class Foo {
1582+
constructor(a) {
1583+
this.a = a;
1584+
}
1585+
}
1586+
1587+
class Bar {
1588+
constructor(a) {
1589+
this.a = a;
1590+
}
1591+
}
1592+
1593+
const foo = new Foo(1);
1594+
const bar = new Bar(1);
1595+
1596+
// Different constructors, same properties
1597+
console.log(util.isDeepStrictEqual(foo, bar));
1598+
// false
1599+
1600+
console.log(util.isDeepStrictEqual(foo, bar, true));
1601+
// true
1602+
```
1603+
15671604
See [`assert.deepStrictEqual()`][] for more information about deep strict
15681605
equality.
15691606

lib/assert.js

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

101103
/**
@@ -108,7 +110,7 @@ function Assert(options) {
108110
throw new ERR_CONSTRUCT_CALL_REQUIRED('Assert');
109111
}
110112

111-
options = ObjectAssign({ __proto__: null, strict: true }, options);
113+
options = ObjectAssign({ __proto__: null, strict: true, skipPrototype: false }, options);
112114

113115
const allowedDiffs = ['simple', 'full'];
114116
if (options.diff !== undefined) {
@@ -336,7 +338,7 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, me
336338
throw new ERR_MISSING_ARGS('actual', 'expected');
337339
}
338340
if (isDeepEqual === undefined) lazyLoadComparison();
339-
if (!isDeepStrictEqual(actual, expected)) {
341+
if (!isDeepStrictEqual(actual, expected, this?.[kOptions]?.skipPrototype)) {
340342
innerFail({
341343
actual,
342344
expected,
@@ -362,7 +364,7 @@ function notDeepStrictEqual(actual, expected, message) {
362364
throw new ERR_MISSING_ARGS('actual', 'expected');
363365
}
364366
if (isDeepEqual === undefined) lazyLoadComparison();
365-
if (isDeepStrictEqual(actual, expected)) {
367+
if (isDeepStrictEqual(actual, expected, this?.[kOptions]?.skipPrototype)) {
366368
innerFail({
367369
actual,
368370
expected,

lib/internal/util/comparisons.js

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

129-
const kStrict = 1;
129+
const kStrict = 2;
130+
const kStrictWithoutPrototypes = 3;
130131
const kLoose = 0;
131-
const kPartial = 2;
132+
const kPartial = 1;
132133

133134
const kNoIterator = 0;
134135
const kIsArray = 1;
@@ -452,7 +453,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
452453
}
453454
} else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) {
454455
return false;
455-
} else if (mode === kStrict) {
456+
} else if (mode === kStrict || mode === kStrictWithoutPrototypes) {
456457
const symbolKeysA = getOwnSymbols(val1);
457458
if (symbolKeysA.length !== 0) {
458459
let count = 0;
@@ -1021,7 +1022,10 @@ module.exports = {
10211022
isDeepEqual(val1, val2) {
10221023
return detectCycles(val1, val2, kLoose);
10231024
},
1024-
isDeepStrictEqual(val1, val2) {
1025+
isDeepStrictEqual(val1, val2, skipPrototype) {
1026+
if (skipPrototype) {
1027+
return detectCycles(val1, val2, kStrictWithoutPrototypes);
1028+
}
10251029
return detectCycles(val1, val2, kStrict);
10261030
},
10271031
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)