Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
5088b0e6f4 Fix Bun.deepMatch to properly compare Map and Set instances
Previously, Bun.deepMatch would always return true when comparing
Map and Set instances regardless of their contents or sizes, because
it treated them as regular objects and used getPropertyNames() which
doesn't enumerate Map/Set entries.

This fix adds special handling for JSMapType and JSSetType in the
Bun__deepMatch function to:
- Check that both objects are the same type (Map vs Map, Set vs Set)
- Compare their sizes first - if different, return false
- If sizes match, compare their actual contents using proper iterators

The implementation follows the same pattern used in Bun__deepEquals
but enforces strict equality rather than subset matching for Maps/Sets,
which matches the expected behavior described in the issue.

Fixes #15673

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-06 23:36:22 +00:00
3 changed files with 185 additions and 33 deletions

View File

@@ -1599,6 +1599,104 @@ bool Bun__deepMatch(
JSObject* obj = objValue.getObject();
JSObject* subsetObj = subsetValue.getObject();
// Special handling for Maps and Sets
JSCell* objCell = objValue.asCell();
JSCell* subsetCell = subsetValue.asCell();
uint8_t objType = objCell->type();
uint8_t subsetType = subsetCell->type();
if (subsetType == JSSetType) {
if (objType != JSSetType) {
return false;
}
JSSet* objSet = jsCast<JSSet*>(objCell);
JSSet* subsetSet = jsCast<JSSet*>(subsetCell);
// For Maps and Sets, require exact equality (same size and contents)
// This matches the behavior expected in the issue
if (objSet->size() != subsetSet->size()) {
return false;
}
// All elements in subset should exist in obj
auto iter = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), subsetSet, IterationKind::Keys);
RETURN_IF_EXCEPTION(throwScope, false);
JSValue key;
while (iter->next(globalObject, key)) {
bool has = objSet->has(globalObject, key);
RETURN_IF_EXCEPTION(throwScope, false);
if (has) {
continue;
}
// We couldn't find the key in the target set. This may be a false positive due to how
// JSValues are represented in JSC, so we need to fall back to a linear search to be sure.
auto objIter = JSSetIterator::create(globalObject, globalObject->setIteratorStructure(), objSet, IterationKind::Keys);
RETURN_IF_EXCEPTION(throwScope, false);
JSValue objKey;
bool foundMatchingKey = false;
while (objIter->next(globalObject, objKey)) {
bool equal = JSC::sameValue(globalObject, key, objKey);
RETURN_IF_EXCEPTION(throwScope, false);
if (equal) {
foundMatchingKey = true;
break;
}
}
if (!foundMatchingKey) {
return false;
}
}
return true;
} else if (subsetType == JSMapType) {
if (objType != JSMapType) {
return false;
}
JSMap* objMap = jsCast<JSMap*>(objCell);
JSMap* subsetMap = jsCast<JSMap*>(subsetCell);
// For Maps and Sets, require exact equality (same size and contents)
// This matches the behavior expected in the issue
if (objMap->size() != subsetMap->size()) {
return false;
}
// All key-value pairs in subset should exist in obj
auto iter = JSMapIterator::create(globalObject, globalObject->mapIteratorStructure(), subsetMap, IterationKind::Entries);
RETURN_IF_EXCEPTION(throwScope, false);
JSValue subsetKey, subsetValue;
while (iter->nextKeyValue(globalObject, subsetKey, subsetValue)) {
JSValue objValue = objMap->get(globalObject, subsetKey);
RETURN_IF_EXCEPTION(throwScope, false);
if (objValue.isUndefined()) {
// We couldn't find the key in the target map. This may be a false positive due to
// how JSValues are represented in JSC, so we need to fall back to a linear search
// to be sure.
auto objIter = JSMapIterator::create(globalObject, globalObject->mapIteratorStructure(), objMap, IterationKind::Entries);
RETURN_IF_EXCEPTION(throwScope, false);
JSValue objKey;
bool foundMatchingKey = false;
while (objIter->nextKeyValue(globalObject, objKey, objValue)) {
bool keysEqual = JSC::sameValue(globalObject, subsetKey, objKey);
RETURN_IF_EXCEPTION(throwScope, false);
if (keysEqual) {
foundMatchingKey = true;
break;
}
}
if (!foundMatchingKey) {
return false;
}
// Compare both values below.
}
bool valuesEqual = JSC::sameValue(globalObject, subsetValue, objValue);
RETURN_IF_EXCEPTION(throwScope, false);
if (!valuesEqual) {
return false;
}
}
return true;
}
PropertyNameArray subsetProps(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include);
subsetObj->getPropertyNames(globalObject, subsetProps, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(throwScope, false);

View File

@@ -66,7 +66,6 @@ describe("Bun.deepMatch", () => {
new Map([ ["foo", 1] ]),
new Map([ ["foo", 1] ]),
],
// Sets
[new Set(), new Set()],
[
@@ -100,39 +99,51 @@ describe("Bun.deepMatch", () => {
[[], [undefined]],
[["a", "b", "c"], ["a", "b", "d"]],
// Maps
// FIXME: I assume this is incorrect but I need confirmation on expected behavior.
// [
// new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
// new Map<number, number>([ [1, 2], [2, 3] ]),
// ],
// [
// new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
// new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
// ],
// [
// new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
// new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
// ],
// Maps - subset should not be contained in object
[
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
new Map<number, number>([ [1, 2], [2, 3] ]),
],
[
new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
],
[
new Map([ ["foo", 1] ]),
new Map([ ["bar", 1] ]),
],
[
new Map([ ["foo", 1] ]),
new Map([ ["foo", 2] ]),
],
// Sets
// FIXME: I assume this is incorrect but I need confirmation on expected behavior.
// [
// new Set([1, 2, 3]),
// new Set([4, 5, 6]),
// ],
// [
// new Set([1, 2, 3]),
// new Set([1, 2]),
// ],
// [
// new Set([1, 2]),
// new Set([1, 2, 3]),
// ],
// [
// new Set(["a", "b", "c"]),
// new Set(["a", "b", "d"]),
// ],
// Maps with different sizes (even if one is subset of another)
[
new Map([ ["foo", 1] ]),
new Map([ ["foo", 1], ["bar", 2] ]),
],
[
new Map<number, number>([ [1, 2], [2, 3] ]),
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
],
// Sets with different contents or sizes
[
new Set([1, 2, 3]),
new Set([4, 5, 6]),
],
[
new Set([1, 2, 3]),
new Set([1, 2]),
],
[
new Set([1, 2]),
new Set([1, 2, 3]),
],
[
new Set(["a", "b", "c"]),
new Set(["a", "b", "d"]),
],
])("Bun.deepMatch(%p, %p) === false", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(false);
});

View File

@@ -0,0 +1,43 @@
// Test for https://github.com/oven-sh/bun/issues/15673
// `Bun.deepMatch` always returns `true` when comparing `Set` and `Map` instances with different number of entries
import { test, expect } from "bun:test";
test.each([
// Maps with different number of entries should return false
[
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
new Map<number, number>([ [1, 2], [2, 3] ]),
],
[
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
],
// Sets with different number of entries should return false
[
new Set([1, 2, 3]),
new Set([1, 2]),
],
[
new Set([1, 2, 3]),
new Set([4, 5, 6]),
],
])("Bun.deepMatch should return false for Maps/Sets with different contents (%p, %p)", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(false);
});
// Additional cases that were not working in the original issue
test.each([
[new Map([ ["foo", 1] ]), new Map([ [ "bar", 1 ] ])],
[new Map([ ["foo", 1] ]), new Map([ [ "foo", 2 ] ])],
])("Bun.deepMatch should return false for Maps with different keys/values (%p, %p)", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(false);
});
// These cases should also return false (different sizes, even if subset)
test.each([
[new Map([ ["foo", 1] ]), new Map([ [ "foo", 1 ], ["bar", 2] ])],
[new Set([1, 2]), new Set([1, 2, 3])],
])("Bun.deepMatch should return false for Maps/Sets with different sizes (%p, %p)", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(false);
});