Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/js/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function isFunction(value) {
}

const deepEquals = Bun.deepEquals;
const isDeepStrictEqual = (a, b) => deepEquals(a, b, true);
const isDeepStrictEqual = (a, b, skipPrototype) => deepEquals(a, b, true, skipPrototype);

const parseArgs = $newRustFunction("parse_args.rs", "parseArgs", 1);

Expand Down
8 changes: 6 additions & 2 deletions src/jsc/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -698,13 +698,17 @@ JSC_DEFINE_HOST_FUNCTION(functionBunDeepEquals, (JSGlobalObject * globalObject,
JSC::JSValue arg1 = callFrame->uncheckedArgument(0);
JSC::JSValue arg2 = callFrame->uncheckedArgument(1);
JSC::JSValue strict = callFrame->argument(2);
// Node's isDeepStrictEqual third argument: skip the prototype/class comparison.
// Only meaningful in strict mode; loose mode never compares prototypes.
bool skipPrototype = callFrame->argument(3).toBoolean(globalObject);

Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer gcBuffer;

if (strict.isBoolean() && strict.asBoolean()) {

bool isEqual = Bun__deepEquals<true, false>(globalObject, arg1, arg2, gcBuffer, stack, scope, true);
bool isEqual = skipPrototype
? Bun__deepEquals<true, false, true>(globalObject, arg1, arg2, gcBuffer, stack, scope, true)
: Bun__deepEquals<true, false, false>(globalObject, arg1, arg2, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(isEqual));
} else {
Expand Down
43 changes: 25 additions & 18 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -648,10 +648,10 @@ JSValue getIndexWithoutAccessors(JSGlobalObject* globalObject, JSObject* obj, ui
return JSValue();
}

template<bool isStrict, bool enableAsymmetricMatchers>
template<bool isStrict, bool enableAsymmetricMatchers, bool skipPrototype = false>
std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope& scope, JSCell* _Nonnull c1, JSCell* _Nonnull c2);

template<bool isStrict, bool enableAsymmetricMatchers>
template<bool isStrict, bool enableAsymmetricMatchers, bool skipPrototype>
bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope& scope, bool addToStack)
{
VM& vm = globalObject->vm();
Expand Down Expand Up @@ -730,10 +730,11 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
JSCell* c2 = v2.asCell();
ASSERT(c1);
ASSERT(c2);
std::optional<bool> isSpecialEqual = specialObjectsDequal<isStrict, enableAsymmetricMatchers>(globalObject, gcBuffer, stack, scope, c1, c2);
std::optional<bool> isSpecialEqual = specialObjectsDequal<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, gcBuffer, stack, scope, c1, c2);
RETURN_IF_EXCEPTION(scope, false);
if (isSpecialEqual.has_value()) return WTF::move(*isSpecialEqual);
isSpecialEqual = specialObjectsDequal<isStrict, enableAsymmetricMatchers>(globalObject, gcBuffer, stack, scope, c2, c1);
isSpecialEqual = specialObjectsDequal<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, gcBuffer, stack, scope, c2, c1);
RETURN_IF_EXCEPTION(scope, false);
if (isSpecialEqual.has_value()) return WTF::move(*isSpecialEqual);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
JSObject* o1 = v1.getObject();
JSObject* o2 = v2.getObject();
Expand Down Expand Up @@ -780,7 +781,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
}
}

auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true);
auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, left, right, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, false);
if (!eql) return false;
}
Expand Down Expand Up @@ -835,7 +836,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}

auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true);
auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, prop1, prop2, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, false);
if (!eql) return false;
}
Expand All @@ -845,7 +846,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}

if constexpr (isStrict) {
if constexpr (isStrict && !skipPrototype) {
if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) {
return false;
}
Expand Down Expand Up @@ -883,7 +884,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
RETURN_IF_EXCEPTION(scope, false);
if (same) return true;

auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true);
auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, left, right, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, false);
if (!eql) {
result = false;
Expand Down Expand Up @@ -919,7 +920,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
RETURN_IF_EXCEPTION(scope, false);
if (same) return true;

auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true);
auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, left, right, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, false);
if (!eql) {
result = false;
Expand Down Expand Up @@ -1007,7 +1008,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}

auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true);
auto eql = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, prop1, prop2, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, false);
if (!eql) return false;
}
Expand All @@ -1028,7 +1029,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}

template<bool isStrict, bool enableAsymmetricMatchers>
template<bool isStrict, bool enableAsymmetricMatchers, bool skipPrototype>
std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope& scope, JSCell* _Nonnull c1, JSCell* _Nonnull c2)
{
VM& vm = globalObject->vm();
Expand Down Expand Up @@ -1065,7 +1066,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
JSValue key2;
bool foundMatchingKey = false;
while (iter2->next(globalObject, key2)) {
bool equal = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, key1, key2, gcBuffer, stack, scope, false);
bool equal = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, key1, key2, gcBuffer, stack, scope, false);
RETURN_IF_EXCEPTION(scope, {});
if (equal) {
foundMatchingKey = true;
Expand Down Expand Up @@ -1108,7 +1109,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
JSValue key2;
bool foundMatchingKey = false;
while (iter2->nextKeyValue(globalObject, key2, value2)) {
bool keysEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, key1, key2, gcBuffer, stack, scope, false);
bool keysEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, key1, key2, gcBuffer, stack, scope, false);
RETURN_IF_EXCEPTION(scope, {});
if (keysEqual) {
foundMatchingKey = true;
Expand All @@ -1123,7 +1124,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
// Compare both values below.
}

bool valuesEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, value1, value2, gcBuffer, stack, scope, false);
bool valuesEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, value1, value2, gcBuffer, stack, scope, false);
RETURN_IF_EXCEPTION(scope, {});
if (!valuesEqual) {
return false;
Expand Down Expand Up @@ -1255,7 +1256,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
RETURN_IF_EXCEPTION(scope, {});
auto rightCause = right->get(globalObject, cause);
RETURN_IF_EXCEPTION(scope, {});
bool causesEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true);
bool causesEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, {});
if (!causesEqual) {
return false;
Expand Down Expand Up @@ -1305,7 +1306,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return false;
}

bool propertiesEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true);
bool propertiesEqual = Bun__deepEquals<isStrict, enableAsymmetricMatchers, skipPrototype>(globalObject, prop1, prop2, gcBuffer, stack, scope, true);
RETURN_IF_EXCEPTION(scope, {});
if (!propertiesEqual) {
return false;
Expand Down Expand Up @@ -1421,8 +1422,10 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return false;
}

if (!equal(JSObject::calculatedClassName(c1->getObject()), JSObject::calculatedClassName(c2->getObject()))) {
return false;
if constexpr (!skipPrototype) {
if (!equal(JSObject::calculatedClassName(c1->getObject()), JSObject::calculatedClassName(c2->getObject()))) {
return false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

JSString* s1 = c1->toStringInline(globalObject);
Expand Down Expand Up @@ -1585,6 +1588,10 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return std::nullopt;
}

// isDeepStrictEqual's skipPrototype mode (Node's third argument) is only used from
// BunObject.cpp, so force this instantiation here where the definition is visible.
template bool Bun__deepEquals<true, false, true>(JSC::JSGlobalObject*, JSValue, JSValue, MarkedArgumentBuffer&, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>&, ThrowScope&, bool);

/**
* @brief `Bun.deepMatch(a, b)`
*
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,8 @@ extern "C" void Bun__EventLoop__runCallback2(JSC::JSGlobalObject* global, JSC::E
extern "C" void Bun__EventLoop__runCallback3(JSC::JSGlobalObject* global, JSC::EncodedJSValue callback, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue arg1, JSC::EncodedJSValue arg2, JSC::EncodedJSValue arg3);

/// @note throws a JS exception and returns false if a stack overflow occurs
template<bool isStrict, bool enableAsymmetricMatchers>
/// @note skipPrototype skips the prototype/class-name comparison (Node's isDeepStrictEqual third argument)
template<bool isStrict, bool enableAsymmetricMatchers, bool skipPrototype = false>
bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSC::JSValue v1, JSC::JSValue v2, JSC::MarkedArgumentBuffer&, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, JSC::ThrowScope& scope, bool addToStack);

/**
Expand Down
79 changes: 79 additions & 0 deletions test/js/node/util/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,82 @@ describe("util.parseEnv", () => {
expect(util.parseEnv(new String("FOO=bar"))).toEqual({ FOO: "bar" });
});
});

describe("isDeepStrictEqual skipPrototype (third argument)", () => {
class Foo {
constructor(a) {
this.a = a;
}
}
class Bar {
constructor(a) {
this.a = a;
}
}

it("has length 3 to match Node", () => {
expect(util.isDeepStrictEqual.length).toBe(3);
});

it("skips the prototype check when the third argument is truthy", () => {
// https://github.com/oven-sh/bun/issues/33074
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), true)).toBe(true);
});

it("keeps the prototype check without the third argument", () => {
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1))).toBe(false);
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), false)).toBe(false);
});

it("coerces the third argument like Node (truthiness)", () => {
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), 1)).toBe(true);
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), "x")).toBe(true);
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), {})).toBe(true);
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), 0)).toBe(false);
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), "")).toBe(false);
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), null)).toBe(false);
expect(util.isDeepStrictEqual(new Foo(1), new Bar(1), undefined)).toBe(false);
});

it("still enforces the other strict rules when skipping prototypes", () => {
expect(util.isDeepStrictEqual(new Foo(1), new Bar(2), true)).toBe(false);
const extra = new Bar(1);
extra.b = 2;
expect(util.isDeepStrictEqual(new Foo(1), extra, true)).toBe(false);
expect(util.isDeepStrictEqual([], {}, true)).toBe(false);
expect(util.isDeepStrictEqual(new Date(0), {}, true)).toBe(false);
});

it("treats plain, null-prototype and class objects as equal", () => {
expect(util.isDeepStrictEqual({ a: 1 }, new Foo(1), true)).toBe(true);
expect(util.isDeepStrictEqual(Object.create(null), {}, true)).toBe(true);
});

it("applies recursively to nested values", () => {
expect(util.isDeepStrictEqual({ x: new Foo(1) }, { x: new Bar(1) }, true)).toBe(true);
expect(util.isDeepStrictEqual({ a: { b: { c: new Foo(1) } } }, { a: { b: { c: new Bar(1) } } }, true)).toBe(true);

const g1 = {};
Object.defineProperty(g1, "x", { enumerable: true, get: () => new Foo(1) });
const g2 = {};
Object.defineProperty(g2, "x", { enumerable: true, get: () => new Bar(1) });
expect(util.isDeepStrictEqual(g1, g2, true)).toBe(true);
});

it("applies recursively through arrays, Maps and Sets", () => {
class MyMap extends Map {}
expect(util.isDeepStrictEqual([new Foo(1)], [new Bar(1)], true)).toBe(true);
expect(util.isDeepStrictEqual(new MyMap([["a", 1]]), new Map([["a", 1]]), true)).toBe(true);
expect(util.isDeepStrictEqual(new Map([["k", new Foo(1)]]), new Map([["k", new Bar(1)]]), true)).toBe(true);
expect(util.isDeepStrictEqual(new Map([[new Foo(1), 1]]), new Map([[new Bar(1), 1]]), true)).toBe(true);
expect(util.isDeepStrictEqual(new Map([[new Foo(1), 1]]), new Map([[new Bar(1), 2]]), true)).toBe(false);
expect(util.isDeepStrictEqual(new Set([new Foo(1)]), new Set([new Bar(1)]), true)).toBe(true);
});

it("skips the prototype check for boxed String objects", () => {
class MyStr extends String {}
expect(util.isDeepStrictEqual(new String("a"), new MyStr("a"), true)).toBe(true);
expect(util.isDeepStrictEqual(new String("a"), new MyStr("a"))).toBe(false);
expect(util.isDeepStrictEqual(new String("a"), new MyStr("b"), true)).toBe(false);
});
});
Loading