Skip to content

Commit cdd7206

Browse files
committed
assert: fix TypeError on deepStrictEqual with null Map key or Set member
deepStrictEqual() and util.isDeepStrictEqual() threw "Cannot read properties of null (reading 'constructor')" instead of comparing when a Map key or Set member was null/undefined (or another primitive) and lined up against object-only keys/members in the other collection with an equal count. The primitive/null handling was gated behind an optimization that is skipped when the counts match, letting such keys reach objectComparisonStart, which dereferences `.constructor`. Resolve primitive and null keys/members directly in every case. Signed-off-by: semx <7532921+semx@users.noreply.github.com>
1 parent 85d4755 commit cdd7206

2 files changed

Lines changed: 24 additions & 7 deletions

File tree

lib/internal/util/comparisons.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -686,16 +686,18 @@ function setObjectEquiv(array, a, b, mode, memo) {
686686
const comparator = mode !== kLoose ? objectComparisonStart : innerDeepEqual;
687687
const extraChecks = mode === kLoose || array.length !== a.size;
688688
for (const val1 of a) {
689-
if (extraChecks) {
690-
if (typeof val1 === 'object') {
691-
if (b.has(val1)) {
692-
continue;
693-
}
694-
} else if (b.has(val1)) {
689+
// Primitive and null members can only match by identity, and must never
690+
// reach objectComparisonStart (which throws on `val.constructor` for
691+
// null/undefined). Resolve them directly for every such member.
692+
if (typeof val1 !== 'object' || val1 === null) {
693+
if (b.has(val1)) {
695694
continue;
696-
} else if (mode !== kLoose) {
695+
}
696+
if (mode !== kLoose) {
697697
return false;
698698
}
699+
} else if (extraChecks && b.has(val1)) {
700+
continue;
699701
}
700702

701703
let innerStart = start;

test/parallel/test-assert-deep.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ test('es6 Maps and Sets', () => {
278278
assertDeepAndStrictEqual(new Set([[1, 2], [3, 4]]), new Set([[3, 4], [1, 2]]));
279279
assertNotDeepOrStrict(new Set([{ a: 0 }]), new Set([{ a: 1 }]));
280280
assertNotDeepOrStrict(new Set([Symbol()]), new Set([Symbol()]));
281+
// A null/primitive member lined up against object-only members in the other
282+
// set must report inequality, not throw on `member.constructor`.
283+
assertNotDeepOrStrict(new Set([null, {}, {}]), new Set([{}, {}, {}]));
284+
assertNotDeepOrStrict(new Set([undefined, {}, {}]), new Set([{}, {}, {}]));
281285

282286
{
283287
const a = [ 1, 2 ];
@@ -298,6 +302,17 @@ test('es6 Maps and Sets', () => {
298302
new Map([[[1], 1], [{}, 2]]),
299303
new Map([[[1], 2], [{}, 1]])
300304
);
305+
// A null/primitive key that lines up with object-only keys in the other map
306+
// must report inequality, not throw on `key.constructor`. Refs: object keys
307+
// of `b` equal in count to `a.size` used to skip the primitive-key handling.
308+
assertNotDeepOrStrict(
309+
new Map([[null, 1], [{}, 2]]),
310+
new Map([[{}, 9], [{}, 9]])
311+
);
312+
assertNotDeepOrStrict(
313+
new Map([[undefined, 1], [{}, 2]]),
314+
new Map([[{}, 9], [{}, 9]])
315+
);
301316

302317
assertNotDeepOrStrict(new Set([1]), [1]);
303318
assertNotDeepOrStrict(new Set(), []);

0 commit comments

Comments
 (0)