Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions lib/internal/util/comparisons.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
Array,
ArrayBuffer,
ArrayIsArray,
ArrayPrototypeFill,
ArrayPrototypePush,
BigInt,
BigInt64Array,
Expand Down Expand Up @@ -638,6 +639,82 @@ function mapMightHaveLoosePrim(a, b, prim, item2, memo) {
return !b.has(altValue) && innerDeepEqual(item1, item2, kLoose, memo);
}

function partialCollectionEquiv(a, b, memo, iterationType) {
const valuesA = [];
const valuesB = [];
// Primitive keys and members were handled by the caller. Shared Set members
// are already matched, but shared Map keys may have different values.
for (const value of a) {
const key = iterationType === kIsMap ? value[0] : value;
if (typeof key === 'object' && key !== null &&
(iterationType === kIsMap || !b.has(key))) {
ArrayPrototypePush(valuesA, value);
}
}
for (const value of iterationType === kIsMap ? b : b.values()) {
const key = iterationType === kIsMap ? value[0] : value;
if (typeof key === 'object' && key !== null &&
(iterationType === kIsMap || !a.has(key))) {
ArrayPrototypePush(valuesB, value);
}
}
if (valuesA.length < valuesB.length) {
return false;
}
const matched = new Uint32Array(valuesA.length);
const visited = new Uint32Array(valuesA.length);
const parents = new Uint32Array(valuesB.length);
const paths = new Uint32Array(valuesB.length);
const comparisons = ArrayPrototypeFill(new Array(valuesB.length), undefined);

// Partial matches can overlap. If greedy matching fails, find augmenting
// paths to reassign earlier matches, comparing each pair at most once.
for (let i = 0; i < valuesB.length; i++) {
const queue = [i];
let found = false;
for (let j = 0; j < queue.length && !found; j++) {
const current = queue[j];
const val2 = valuesB[current];
const results = comparisons[current] ??= new Uint8Array(valuesA.length);
for (let k = 0; k < valuesA.length; k++) {
if (visited[k] === i + 1) {
continue;
}
if (results[k] === 0) {
const val1 = valuesA[k];
const equal = iterationType === kIsMap ?
innerDeepEqual(val1[0], val2[0], kPartial, memo) &&
innerDeepEqual(val1[1], val2[1], kPartial, memo) :
innerDeepEqual(val1, val2, kPartial, memo);
results[k] = equal ? 2 : 1;
}
if (results[k] === 1) {
continue;
}
visited[k] = i + 1;
if (matched[k] === 0) {
matched[k] = current + 1;
let previous = current;
while (previous !== i) {
matched[paths[previous]] = parents[previous] + 1;
previous = parents[previous];
}
found = true;
break;
}
const next = matched[k] - 1;
parents[next] = current;
paths[next] = k;
ArrayPrototypePush(queue, next);
}
}
if (!found) {
return false;
}
}
return true;
}

function partialObjectSetEquiv(array, a, b, mode, memo) {
let aPos = 0;
let direction = 1;
Expand Down Expand Up @@ -745,7 +822,13 @@ function compareSmallSets(a, b, val, iteratorB, mode, memo) {
return true;
}
const secondA = iteratorA.next().value;
return b.has(secondA) || innerDeepEqual(secondA, iteratorB.next().value, mode, memo);
if (b.has(secondA)) {
return true;
}
const secondB = iteratorB.next().value;
return innerDeepEqual(secondA, secondB, mode, memo) ||
(mode === kPartial && innerDeepEqual(secondA, val, mode, memo) &&
(b.has(firstA) || innerDeepEqual(firstA, secondB, mode, memo)));
}
return a.size !== 1 && innerDeepEqual(iteratorA.next().value, val, mode, memo) && (
b.size === 1 || // Partial mode
Expand Down Expand Up @@ -784,7 +867,8 @@ function setEquiv(a, b, mode, memo) {
return true;
}
if (mode === kPartial) {
return partialObjectSetEquiv(array, a, b, mode, memo);
return partialObjectSetEquiv(array, a, b, mode, memo) ||
partialCollectionEquiv(a, b, memo, kIsSet);
}
return setObjectEquiv(array, a, b, mode, memo);
}
Expand Down Expand Up @@ -932,7 +1016,8 @@ function mapEquiv(a, b, mode, memo) {
}

if (mode === kPartial) {
return partialObjectMapEquiv(array, a, b, mode, memo);
return partialObjectMapEquiv(array, a, b, mode, memo) ||
partialCollectionEquiv(a, b, memo, kIsMap);
}

return mapObjectEquiv(array, a, b, mode, memo);
Expand Down
31 changes: 31 additions & 0 deletions test/parallel/test-assert-deep.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,37 @@ test('es6 Maps and Sets', () => {
}
});

test('partial comparison of unordered Sets and Maps with overlapping entries', () => {
assertDeepAndStrictEqual(
new Set([[1, 2], [1]]),
new Set([[1], [1, 2]])
);
assertDeepAndStrictEqual(
new Set([[1, 2], [1], [3]]),
new Set([[1], [1, 2], [3]])
);
assertDeepAndStrictEqual(
new Set([{ a: 1, b: 1 }, { a: 1 }]),
new Set([{ a: 1 }, { a: 1, b: 1 }])
);
assertDeepAndStrictEqual(
new Map([[{ a: 1, b: 1 }, 1], [{ a: 1 }, 1]]),
new Map([[{ a: 1 }, 1], [{ a: 1, b: 1 }, 1]])
);
});

test('partial comparison of unordered Sets with circular entries', () => {
const actual = new Set([{ a: 1, b: 1 }, { a: 1 }, { a: 2 }]);
const expected = new Set([{ a: 1 }, { a: 1, b: 1 }, { a: 2 }]);
for (const value of actual) {
value.self = actual;
}
for (const value of expected) {
value.self = expected;
}
assertDeepAndStrictEqual(actual, expected);
});

test('GH-6416. Make sure circular refs do not throw', () => {
const b = {};
b.b = b;
Expand Down
18 changes: 18 additions & 0 deletions test/parallel/test-util-isPartialDeepStrictEqual.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ test('util.isPartialDeepStrictEqual returns true with expected properties', () =
);
});

test('util.isPartialDeepStrictEqual matches unordered collections with overlapping entries', () => {
const shared = { c: 3 };
assert.strictEqual(
util.isPartialDeepStrictEqual(
new Set([null, shared, 1, [1, 2, 3], [1, 3], [4]]),
new Set([null, shared, 1, [1], [1, 2]]),
),
true,
);
assert.strictEqual(
util.isPartialDeepStrictEqual(
new Map([[null, 0], [1, 1], [shared, [1, 2, 3]], [{}, [1, 3]]]),
new Map([[null, 0], [1, 1], [{}, [1]], [shared, [1, 2]]]),
),
true,
);
});

test('util.isPartialDeepStrictEqual returns false with unexpected properties', () => {
assert.strictEqual(
util.isPartialDeepStrictEqual({ a: 1 }, { a: 2 }),
Expand Down