diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 80d39756cf155a..ca79650c9d4e8f 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -686,16 +686,18 @@ function setObjectEquiv(array, a, b, mode, memo) { const comparator = mode !== kLoose ? objectComparisonStart : innerDeepEqual; const extraChecks = mode === kLoose || array.length !== a.size; for (const val1 of a) { - if (extraChecks) { - if (typeof val1 === 'object') { - if (b.has(val1)) { - continue; - } - } else if (b.has(val1)) { + // Primitive and null members can only match by identity, and must never + // reach objectComparisonStart (which throws on `val.constructor` for + // null/undefined). Resolve them directly for every such member. + if (typeof val1 !== 'object' || val1 === null) { + if (b.has(val1)) { continue; - } else if (mode !== kLoose) { + } + if (mode !== kLoose) { return false; } + } else if (extraChecks && b.has(val1)) { + continue; } let innerStart = start; @@ -837,10 +839,13 @@ function mapObjectEquiv(array, a, b, mode, memo) { let start = 0; let end = array.length - 1; const comparator = mode !== kLoose ? objectComparisonStart : innerDeepEqual; - const extraChecks = mode === kLoose || array.length !== a.size; for (const { 0: key1, 1: item1 } of a) { - if (extraChecks && (typeof key1 !== 'object' || key1 === null)) { + // Primitive and null keys can never match through the object comparator, so + // resolve them directly. This must happen for every such key (not only when + // the map sizes differ), otherwise a null/primitive key reaches + // objectComparisonStart and throws on `key.constructor`. + if (typeof key1 !== 'object' || key1 === null) { if (b.has(key1)) { if (mode !== kLoose || innerDeepEqual(item1, b.get(key1), mode, memo)) { continue; diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 5d856bf1f378b9..f7528eae79e7fd 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -279,6 +279,10 @@ test('es6 Maps and Sets', () => { assertDeepAndStrictEqual(new Set([[1, 2], [3, 4]]), new Set([[3, 4], [1, 2]])); assertNotDeepOrStrict(new Set([{ a: 0 }]), new Set([{ a: 1 }])); assertNotDeepOrStrict(new Set([Symbol()]), new Set([Symbol()])); + // A null/primitive member lined up against object-only members in the other + // set must report inequality, not throw on `member.constructor`. + assertNotDeepOrStrict(new Set([null, {}, {}]), new Set([{}, {}, {}])); + assertNotDeepOrStrict(new Set([undefined, {}, {}]), new Set([{}, {}, {}])); { const a = [ 1, 2 ]; @@ -299,6 +303,17 @@ test('es6 Maps and Sets', () => { new Map([[[1], 1], [{}, 2]]), new Map([[[1], 2], [{}, 1]]) ); + // A null/primitive key that lines up with object-only keys in the other map + // must report inequality, not throw on `key.constructor`. Refs: object keys + // of `b` equal in count to `a.size` used to skip the primitive-key handling. + assertNotDeepOrStrict( + new Map([[null, 1], [{}, 2]]), + new Map([[{}, 9], [{}, 9]]) + ); + assertNotDeepOrStrict( + new Map([[undefined, 1], [{}, 2]]), + new Map([[{}, 9], [{}, 9]]) + ); assertNotDeepOrStrict(new Set([1]), [1]); assertNotDeepOrStrict(new Set(), []);