Skip to content

Commit b8e90dd

Browse files
josephgjasnell
authored andcommitted
assert: fix deepEqual similar sets and maps bug
This fixes a bug where deepEqual and deepStrictEqual would have incorrect behaviour in sets and maps containing multiple equivalent keys. PR-URL: #13426 Fixes: #13347 Refs: #12142 Reviewed-By: Refael Ackermann <[email protected]>
1 parent 2e3b758 commit b8e90dd

File tree

2 files changed

+64
-8
lines changed

2 files changed

+64
-8
lines changed

lib/assert.js

+42-8
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,12 @@ function _deepEqual(actual, expected, strict, memos) {
285285
return areEq;
286286
}
287287

288-
function setHasSimilarElement(set, val1, strict, memo) {
289-
if (set.has(val1))
288+
function setHasSimilarElement(set, val1, usedEntries, strict, memo) {
289+
if (set.has(val1)) {
290+
if (usedEntries)
291+
usedEntries.add(val1);
290292
return true;
293+
}
291294

292295
// In strict mode the only things which can match a primitive or a function
293296
// will already be detected by set.has(val1).
@@ -296,8 +299,14 @@ function setHasSimilarElement(set, val1, strict, memo) {
296299

297300
// Otherwise go looking.
298301
for (const val2 of set) {
299-
if (_deepEqual(val1, val2, strict, memo))
302+
if (usedEntries && usedEntries.has(val2))
303+
continue;
304+
305+
if (_deepEqual(val1, val2, strict, memo)) {
306+
if (usedEntries)
307+
usedEntries.add(val2);
300308
return true;
309+
}
301310
}
302311

303312
return false;
@@ -314,21 +323,33 @@ function setEquiv(a, b, strict, memo) {
314323
if (a.size !== b.size)
315324
return false;
316325

326+
// This is a set of the entries in b which have been consumed in our pairwise
327+
// comparison.
328+
//
329+
// When the sets contain only value types (eg, lots of numbers), and we're in
330+
// strict mode, we don't need to match off the entries in a pairwise way. In
331+
// that case this initialization is done lazily to avoid the allocation &
332+
// bookkeeping cost. Unfortunately, we can't get away with that in non-strict
333+
// mode.
334+
let usedEntries = null;
335+
317336
for (const val1 of a) {
337+
if (usedEntries == null && (!strict || typeof val1 === 'object'))
338+
usedEntries = new Set();
339+
318340
// If the value doesn't exist in the second set by reference, and its an
319341
// object or an array we'll need to go hunting for something thats
320342
// deep-equal to it. Note that this is O(n^2) complexity, and will get
321343
// slower if large, very similar sets / maps are nested inside.
322344
// Unfortunately there's no real way around this.
323-
if (!setHasSimilarElement(b, val1, strict, memo)) {
345+
if (!setHasSimilarElement(b, val1, usedEntries, strict, memo))
324346
return false;
325-
}
326347
}
327348

328349
return true;
329350
}
330351

331-
function mapHasSimilarEntry(map, key1, item1, strict, memo) {
352+
function mapHasSimilarEntry(map, key1, item1, usedEntries, strict, memo) {
332353
// To be able to handle cases like:
333354
// Map([[1, 'a'], ['1', 'b']]) vs Map([['1', 'a'], [1, 'b']])
334355
// or:
@@ -338,8 +359,11 @@ function mapHasSimilarEntry(map, key1, item1, strict, memo) {
338359
// This check is not strictly necessary. The loop performs this check, but
339360
// doing it here improves performance of the common case when reference-equal
340361
// keys exist (which includes all primitive-valued keys).
341-
if (map.has(key1) && _deepEqual(item1, map.get(key1), strict, memo))
362+
if (map.has(key1) && _deepEqual(item1, map.get(key1), strict, memo)) {
363+
if (usedEntries)
364+
usedEntries.add(key1);
342365
return true;
366+
}
343367

344368
if (strict && (util.isPrimitive(key1) || util.isFunction(key1)))
345369
return false;
@@ -349,8 +373,13 @@ function mapHasSimilarEntry(map, key1, item1, strict, memo) {
349373
if (key2 === key1)
350374
continue;
351375

376+
if (usedEntries && usedEntries.has(key2))
377+
continue;
378+
352379
if (_deepEqual(key1, key2, strict, memo) &&
353380
_deepEqual(item1, item2, strict, memo)) {
381+
if (usedEntries)
382+
usedEntries.add(key2);
354383
return true;
355384
}
356385
}
@@ -366,10 +395,15 @@ function mapEquiv(a, b, strict, memo) {
366395
if (a.size !== b.size)
367396
return false;
368397

398+
let usedEntries = null;
399+
369400
for (const [key1, item1] of a) {
401+
if (usedEntries == null && (!strict || typeof key1 === 'object'))
402+
usedEntries = new Set();
403+
370404
// Just like setEquiv above, this hunt makes this function O(n^2) when
371405
// using objects and lists as keys
372-
if (!mapHasSimilarEntry(b, key1, item1, strict, memo))
406+
if (!mapHasSimilarEntry(b, key1, item1, usedEntries, strict, memo))
373407
return false;
374408
}
375409

test/parallel/test-assert-deep.js

+22
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,28 @@ assertOnlyDeepEqual(new Map([['a', '1']]), new Map([['a', 1]]));
187187

188188
assertDeepAndStrictEqual(new Set([{}]), new Set([{}]));
189189

190+
// Ref: https://github.com/nodejs/node/issues/13347
191+
assertNotDeepOrStrict(
192+
new Set([{a: 1}, {a: 1}]),
193+
new Set([{a: 1}, {a: 2}])
194+
);
195+
assertNotDeepOrStrict(
196+
new Set([{a: 1}, {a: 1}, {a: 2}]),
197+
new Set([{a: 1}, {a: 2}, {a: 2}])
198+
);
199+
assertNotDeepOrStrict(
200+
new Map([[{x: 1}, 5], [{x: 1}, 5]]),
201+
new Map([[{x: 1}, 5], [{x: 2}, 5]])
202+
);
203+
204+
assertNotDeepOrStrict(new Set([3, '3']), new Set([3, 4]));
205+
assertNotDeepOrStrict(new Map([[3, 0], ['3', 0]]), new Map([[3, 0], [4, 0]]));
206+
207+
assertNotDeepOrStrict(
208+
new Set([{a: 1}, {a: 1}, {a: 2}]),
209+
new Set([{a: 1}, {a: 2}, {a: 2}])
210+
);
211+
190212
// This is an awful case, where a map contains multiple equivalent keys:
191213
assertOnlyDeepEqual(
192214
new Map([[1, 'a'], ['1', 'b']]),

0 commit comments

Comments
 (0)