diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index d82aafa920be..a276c22764a5 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -390,7 +390,9 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global } case AsymmetricMatcherConstructorType::Array: { - if (JSC::isArray(globalObject, otherProp)) { + bool otherIsArray = JSC::isArray(globalObject, otherProp); + RETURN_IF_EXCEPTION(throwScope, AsymmetricMatcherResult::FAIL); + if (otherIsArray) { return AsymmetricMatcherResult::PASS; } break; @@ -1648,8 +1650,13 @@ bool Bun__deepMatch( // - two "simple" arrays // similar to what is done in deepEquals (canPerformFastPropertyEnumerationForIterationBun) + bool objIsArray = isArray(globalObject, objValue); + RETURN_IF_EXCEPTION(throwScope, false); + bool subsetIsArray = isArray(globalObject, subsetValue); + RETURN_IF_EXCEPTION(throwScope, false); + // arrays should match exactly - if (isArray(globalObject, objValue) && isArray(globalObject, subsetValue)) { + if (objIsArray && subsetIsArray) { if (obj->getArrayLength() != subsetObj->getArrayLength()) { return false; } @@ -4369,7 +4376,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu return JSValue::encode(currProp); } - if (isArray(globalObject, path)) { + bool pathIsArray = isArray(globalObject, path); + RETURN_IF_EXCEPTION(scope, {}); + if (pathIsArray) { // each item in array is property name, ignore dot/bracket notation JSValue currProp = value; auto* pathObject = path.toObject(globalObject); diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 92a569d6ce08..a915a4e1f111 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -677,6 +677,38 @@ describe("expect()", () => { expect(p1).toStrictEqual(p2); } }); + + // JSC::isArray() declares a throw scope on the Proxy path. expect.any(Array), + // toMatchObject, and toHaveProperty called it without checking, which aborts + // under BUN_JSC_validateExceptionChecks=1 (a no-op on release builds). + test("expect.any(Array)/toMatchObject/toHaveProperty check the isArray() exception for Proxy values", async () => { + const { bunEnv, bunExe } = require("harness"); + const src = ` + const { expect } = require("bun:test"); + try { expect(new Proxy({}, {})).toEqual(expect.any(Array)); } catch {} + try { expect(new Proxy([], {})).toEqual(expect.any(Array)); } catch {} + try { expect(new Proxy([], {})).toMatchObject([]); } catch {} + try { expect([]).toMatchObject(new Proxy([], {})); } catch {} + try { expect(new Proxy([], {})).toMatchObject(new Proxy([], {})); } catch {} + try { expect({ a: 1 }).toHaveProperty(new Proxy(["a"], {})); } catch {} + try { expect({ a: 1 }).toHaveProperty(new Proxy(new Set(["a"]), {})); } catch {} + const rp = Proxy.revocable({}, {}); rp.revoke(); + try { expect(rp.proxy).toEqual(expect.any(Array)); } catch {} + console.log("ok"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", src], + env: { ...bunEnv, BUN_JSC_validateExceptionChecks: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode, signalCode: proc.signalCode }).toMatchObject({ + stdout: "ok\n", + exitCode: 0, + signalCode: null, + }); + }); } test("deepEquals works with sets/maps/dates/strings", () => {