diff --git a/src/js/node/util.ts b/src/js/node/util.ts index 88b6cc4a0ceb..a7d7384614cd 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -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); diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 02a1127e00df..f13d6a152bad 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -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, 16> stack; MarkedArgumentBuffer gcBuffer; if (strict.isBoolean() && strict.asBoolean()) { - - bool isEqual = Bun__deepEquals(globalObject, arg1, arg2, gcBuffer, stack, scope, true); + bool isEqual = skipPrototype + ? Bun__deepEquals(globalObject, arg1, arg2, gcBuffer, stack, scope, true) + : Bun__deepEquals(globalObject, arg1, arg2, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(jsBoolean(isEqual)); } else { diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 3a30e116154e..68c148554f82 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -648,10 +648,10 @@ JSValue getIndexWithoutAccessors(JSGlobalObject* globalObject, JSObject* obj, ui return JSValue(); } -template +template std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, MarkedArgumentBuffer& gcBuffer, Vector, 16>& stack, ThrowScope& scope, JSCell* _Nonnull c1, JSCell* _Nonnull c2); -template +template bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, MarkedArgumentBuffer& gcBuffer, Vector, 16>& stack, ThrowScope& scope, bool addToStack) { VM& vm = globalObject->vm(); @@ -730,10 +730,11 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, JSCell* c2 = v2.asCell(); ASSERT(c1); ASSERT(c2); - std::optional isSpecialEqual = specialObjectsDequal(globalObject, gcBuffer, stack, scope, c1, c2); + std::optional isSpecialEqual = specialObjectsDequal(globalObject, gcBuffer, stack, scope, c1, c2); RETURN_IF_EXCEPTION(scope, false); if (isSpecialEqual.has_value()) return WTF::move(*isSpecialEqual); - isSpecialEqual = specialObjectsDequal(globalObject, gcBuffer, stack, scope, c2, c1); + isSpecialEqual = specialObjectsDequal(globalObject, gcBuffer, stack, scope, c2, c1); + RETURN_IF_EXCEPTION(scope, false); if (isSpecialEqual.has_value()) return WTF::move(*isSpecialEqual); JSObject* o1 = v1.getObject(); JSObject* o2 = v2.getObject(); @@ -780,7 +781,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, } } - auto eql = Bun__deepEquals(globalObject, left, right, gcBuffer, stack, scope, true); + auto eql = Bun__deepEquals(globalObject, left, right, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, false); if (!eql) return false; } @@ -835,7 +836,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, return false; } - auto eql = Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true); + auto eql = Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, false); if (!eql) return false; } @@ -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; } @@ -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(globalObject, left, right, gcBuffer, stack, scope, true); + auto eql = Bun__deepEquals(globalObject, left, right, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, false); if (!eql) { result = false; @@ -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(globalObject, left, right, gcBuffer, stack, scope, true); + auto eql = Bun__deepEquals(globalObject, left, right, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, false); if (!eql) { result = false; @@ -1007,7 +1008,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, return false; } - auto eql = Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true); + auto eql = Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, false); if (!eql) return false; } @@ -1028,7 +1029,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, return true; } -template +template std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, MarkedArgumentBuffer& gcBuffer, Vector, 16>& stack, ThrowScope& scope, JSCell* _Nonnull c1, JSCell* _Nonnull c2) { VM& vm = globalObject->vm(); @@ -1065,7 +1066,7 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark JSValue key2; bool foundMatchingKey = false; while (iter2->next(globalObject, key2)) { - bool equal = Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false); + bool equal = Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false); RETURN_IF_EXCEPTION(scope, {}); if (equal) { foundMatchingKey = true; @@ -1108,7 +1109,7 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark JSValue key2; bool foundMatchingKey = false; while (iter2->nextKeyValue(globalObject, key2, value2)) { - bool keysEqual = Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false); + bool keysEqual = Bun__deepEquals(globalObject, key1, key2, gcBuffer, stack, scope, false); RETURN_IF_EXCEPTION(scope, {}); if (keysEqual) { foundMatchingKey = true; @@ -1123,7 +1124,7 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark // Compare both values below. } - bool valuesEqual = Bun__deepEquals(globalObject, value1, value2, gcBuffer, stack, scope, false); + bool valuesEqual = Bun__deepEquals(globalObject, value1, value2, gcBuffer, stack, scope, false); RETURN_IF_EXCEPTION(scope, {}); if (!valuesEqual) { return false; @@ -1255,7 +1256,7 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark RETURN_IF_EXCEPTION(scope, {}); auto rightCause = right->get(globalObject, cause); RETURN_IF_EXCEPTION(scope, {}); - bool causesEqual = Bun__deepEquals(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true); + bool causesEqual = Bun__deepEquals(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, {}); if (!causesEqual) { return false; @@ -1305,7 +1306,7 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark return false; } - bool propertiesEqual = Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true); + bool propertiesEqual = Bun__deepEquals(globalObject, prop1, prop2, gcBuffer, stack, scope, true); RETURN_IF_EXCEPTION(scope, {}); if (!propertiesEqual) { return false; @@ -1421,8 +1422,10 @@ std::optional 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; + } } JSString* s1 = c1->toStringInline(globalObject); @@ -1585,6 +1588,10 @@ std::optional 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(JSC::JSGlobalObject*, JSValue, JSValue, MarkedArgumentBuffer&, Vector, 16>&, ThrowScope&, bool); + /** * @brief `Bun.deepMatch(a, b)` * diff --git a/src/jsc/bindings/headers-handwritten.h b/src/jsc/bindings/headers-handwritten.h index e421d79b93f7..8ddcf16e2eee 100644 --- a/src/jsc/bindings/headers-handwritten.h +++ b/src/jsc/bindings/headers-handwritten.h @@ -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 +/// @note skipPrototype skips the prototype/class-name comparison (Node's isDeepStrictEqual third argument) +template bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSC::JSValue v1, JSC::JSValue v2, JSC::MarkedArgumentBuffer&, Vector, 16>& stack, JSC::ThrowScope& scope, bool addToStack); /** diff --git a/test/js/node/util/util.test.js b/test/js/node/util/util.test.js index 8427b75c74e1..47e26493dc59 100644 --- a/test/js/node/util/util.test.js +++ b/test/js/node/util/util.test.js @@ -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); + }); +});