Skip to content

Commit bdd3afb

Browse files
BridgeARtargos
authored andcommitted
assert: fix loose set and map comparison
The fast path did not anticipate different ways to express a loose equal string value (e.g., 1n == '+0001'). This is now fixed with the downside that all primitives that could theoretically have equal entries must go through a full comparison. Only some strings, symbols, undefined, null and NaN can be detected in a fast path as those entries have a strictly limited set of possible equal entries. PR-URL: #22495 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: John-David Dalton <[email protected]>
1 parent ff8fadc commit bdd3afb

File tree

2 files changed

+81
-108
lines changed

2 files changed

+81
-108
lines changed

lib/internal/util/comparisons.js

+77-104
Original file line numberDiff line numberDiff line change
@@ -354,23 +354,52 @@ function setHasEqualElement(set, val1, strict, memo) {
354354
return false;
355355
}
356356

357-
// Note: we currently run this multiple times for each loose key!
358-
// This is done to prevent slowing down the average case.
359-
function setHasLoosePrim(a, b, val) {
360-
const altValues = findLooseMatchingPrimitives(val);
361-
if (altValues === undefined)
362-
return false;
357+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#Loose_equality_using
358+
// Sadly it is not possible to detect corresponding values properly in case the
359+
// type is a string, number, bigint or boolean. The reason is that those values
360+
// can match lots of different string values (e.g., 1n == '+00001').
361+
function findLooseMatchingPrimitives(prim) {
362+
switch (typeof prim) {
363+
case 'undefined':
364+
return null;
365+
case 'object': // Only pass in null as object!
366+
return undefined;
367+
case 'symbol':
368+
return false;
369+
case 'string':
370+
prim = +prim;
371+
// Loose equal entries exist only if the string is possible to convert to
372+
// a regular number and not NaN.
373+
// Fall through
374+
case 'number':
375+
if (Number.isNaN(prim)) {
376+
return false;
377+
}
378+
}
379+
return true;
380+
}
363381

364-
let matches = 1;
365-
for (var i = 0; i < altValues.length; i++) {
366-
if (b.has(altValues[i])) {
367-
matches--;
368-
}
369-
if (a.has(altValues[i])) {
370-
matches++;
371-
}
382+
function setMightHaveLoosePrim(a, b, prim) {
383+
const altValue = findLooseMatchingPrimitives(prim);
384+
if (altValue != null)
385+
return altValue;
386+
387+
return b.has(altValue) && !a.has(altValue);
388+
}
389+
390+
function mapMightHaveLoosePrim(a, b, prim, item, memo) {
391+
const altValue = findLooseMatchingPrimitives(prim);
392+
if (altValue != null) {
393+
return altValue;
394+
}
395+
const curB = b.get(altValue);
396+
if (curB === undefined && !b.has(altValue) ||
397+
!innerDeepEqual(item, curB, false, memo)) {
398+
return false;
372399
}
373-
return matches === 0;
400+
const curA = a.get(altValue);
401+
return curA === undefined && a.has(altValue) ||
402+
innerDeepEqual(item, curA, false, memo);
374403
}
375404

376405
function setEquiv(a, b, strict, memo) {
@@ -390,8 +419,19 @@ function setEquiv(a, b, strict, memo) {
390419
// hunting for something thats deep-(strict-)equal to it. To make this
391420
// O(n log n) complexity we have to copy these values in a new set first.
392421
set.add(val);
393-
} else if (!b.has(val) && (strict || !setHasLoosePrim(a, b, val))) {
394-
return false;
422+
} else if (!b.has(val)) {
423+
if (strict)
424+
return false;
425+
426+
// Fast path to detect missing string, symbol, undefined and null values.
427+
if (!setMightHaveLoosePrim(a, b, val)) {
428+
return false;
429+
}
430+
431+
if (set === null) {
432+
set = new Set();
433+
}
434+
set.add(val);
395435
}
396436
}
397437

@@ -402,96 +442,18 @@ function setEquiv(a, b, strict, memo) {
402442
if (typeof val === 'object' && val !== null) {
403443
if (!setHasEqualElement(set, val, strict, memo))
404444
return false;
405-
} else if (!a.has(val) && (strict || !setHasLoosePrim(b, a, val))) {
445+
} else if (!strict &&
446+
!a.has(val) &&
447+
!setHasEqualElement(set, val, strict, memo)) {
406448
return false;
407449
}
408450
}
451+
return set.size === 0;
409452
}
410453

411454
return true;
412455
}
413456

414-
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#Loose_equality_using
415-
function findLooseMatchingPrimitives(prim) {
416-
switch (typeof prim) {
417-
case 'number':
418-
if (prim === 0) {
419-
return ['', '0', false];
420-
}
421-
if (prim === 1) {
422-
return ['1', true];
423-
}
424-
return ['' + prim];
425-
case 'string':
426-
if (prim === '' || prim === '0') {
427-
return [0, false];
428-
}
429-
if (prim === '1') {
430-
return [1, true];
431-
}
432-
const number = +prim;
433-
if ('' + number === prim) {
434-
return [number];
435-
}
436-
return;
437-
case 'undefined':
438-
return [null];
439-
case 'object': // Only pass in null as object!
440-
return [undefined];
441-
case 'boolean':
442-
if (prim === false) {
443-
return ['', '0', 0];
444-
}
445-
return ['1', 1];
446-
}
447-
}
448-
449-
// This is a ugly but relatively fast way to determine if a loose equal entry
450-
// currently has a correspondent matching entry. Otherwise checking for such
451-
// values would be way more expensive (O(n^2)).
452-
// Note: we currently run this multiple times for each loose key!
453-
// This is done to prevent slowing down the average case.
454-
function mapHasLoosePrim(a, b, key1, memo, item1, item2) {
455-
const altKeys = findLooseMatchingPrimitives(key1);
456-
if (altKeys === undefined)
457-
return false;
458-
459-
const setA = new Set();
460-
const setB = new Set();
461-
462-
let keyCount = 1;
463-
464-
setA.add(item1);
465-
if (b.has(key1)) {
466-
keyCount--;
467-
setB.add(item2);
468-
}
469-
470-
for (var i = 0; i < altKeys.length; i++) {
471-
const key2 = altKeys[i];
472-
if (a.has(key2)) {
473-
keyCount++;
474-
setA.add(a.get(key2));
475-
}
476-
if (b.has(key2)) {
477-
keyCount--;
478-
setB.add(b.get(key2));
479-
}
480-
}
481-
if (keyCount !== 0 || setA.size !== setB.size)
482-
return false;
483-
484-
for (const val of setA) {
485-
if (typeof val === 'object' && val !== null) {
486-
if (!setHasEqualElement(setB, val, false, memo))
487-
return false;
488-
} else if (!setB.has(val) && !setHasLoosePrim(setA, setB, val)) {
489-
return false;
490-
}
491-
}
492-
return true;
493-
}
494-
495457
function mapHasEqualEntry(set, map, key1, item1, strict, memo) {
496458
// To be able to handle cases like:
497459
// Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']])
@@ -521,9 +483,17 @@ function mapEquiv(a, b, strict, memo) {
521483
// almost all possible cases.
522484
const item2 = b.get(key);
523485
if ((item2 === undefined && !b.has(key) ||
524-
!innerDeepEqual(item1, item2, strict, memo)) &&
525-
(strict || !mapHasLoosePrim(a, b, key, memo, item1, item2))) {
526-
return false;
486+
!innerDeepEqual(item1, item2, strict, memo))) {
487+
if (strict)
488+
return false;
489+
// Fast path to detect missing string, symbol, undefined and null
490+
// keys.
491+
if (!mapMightHaveLoosePrim(a, b, key, item1, memo))
492+
return false;
493+
if (set === null) {
494+
set = new Set();
495+
}
496+
set.add(key);
527497
}
528498
}
529499
}
@@ -533,11 +503,14 @@ function mapEquiv(a, b, strict, memo) {
533503
if (typeof key === 'object' && key !== null) {
534504
if (!mapHasEqualEntry(set, a, key, item, strict, memo))
535505
return false;
536-
} else if (!a.has(key) &&
537-
(strict || !mapHasLoosePrim(b, a, key, memo, item))) {
506+
} else if (!strict &&
507+
(!a.has(key) ||
508+
!innerDeepEqual(a.get(key), item, false, memo)) &&
509+
!mapHasEqualEntry(set, a, key, item, false, memo)) {
538510
return false;
539511
}
540512
}
513+
return set.size === 0;
541514
}
542515

543516
return true;

test/parallel/test-assert-deep.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -364,13 +364,13 @@ assertDeepAndStrictEqual(
364364
new Map([[null, 3]])
365365
);
366366
assertOnlyDeepEqual(
367-
new Map([[null, undefined]]),
368-
new Map([[undefined, null]])
367+
new Map([[undefined, null], ['+000', 2n]]),
368+
new Map([[null, undefined], [false, '2']]),
369369
);
370370

371371
assertOnlyDeepEqual(
372-
new Set([null, '']),
373-
new Set([undefined, 0])
372+
new Set([null, '', 1n, 5, 2n, false]),
373+
new Set([undefined, 0, 5n, true, '2', '-000'])
374374
);
375375
assertNotDeepOrStrict(
376376
new Set(['']),

0 commit comments

Comments
 (0)