Skip to content

fix(bun:test): return an object when a mock function is constructed#31620

Closed
robobun wants to merge 2 commits into
mainfrom
farm/2c93f5e1/mock-construct-non-object
Closed

fix(bun:test): return an object when a mock function is constructed#31620
robobun wants to merge 2 commits into
mainfrom
farm/2c93f5e1/mock-construct-non-object

Conversation

@robobun

@robobun robobun commented May 31, 2026

Copy link
Copy Markdown
Collaborator

What

Constructing a Bun mock function (jest.fn() / Bun.mock()) with a non-object return value crashed the process with a SIGSEGV (an ASSERTION FAILED: isCell() in debug builds).

const fn = Bun.jest(import.meta.path).fn();
Reflect.construct(fn, []); // TERMSIG 11 / isCell() assertion

JSMockFunction registered the same host function (jsMockFunctionCall) for both [[Call]] and [[Construct]]:

: Base(vm, structure, jsMockFunctionCall, jsMockFunctionCall)

Since it isn't callHostFunctionAsConstructor, InternalFunction::getConstructData exposes it as a native constructor. A native construct callback must return an object — Interpreter::executeConstruct feeds the result straight through asObject() — but jsMockFunctionCall can return undefined, a number, or any other primitive (no implementation → jsUndefined(), mockReturnValue(42)42, mockReturnThis() → the receiver). Feeding a primitive to asObject() dereferences a non-object cell → flaky crash.

(new fn() from JS bytecode happened to survive because op_construct applies the "non-object return → use this" rule itself; Reflect.construct and other native-construct paths do not.)

Fix

Split the call body into jsMockFunctionCallImpl, parameterized on the this value, and add a dedicated jsMockFunctionConstruct handler. It creates this from newTarget's prototype, runs the mock against it, and returns the mock's result only when that result is an object — otherwise the freshly created instance, matching new on an ordinary JS function. This also makes mock.contexts / mock.calls observe the constructed instance and honors newTarget.prototype (so Reflect.construct(fn, [], Base) yields a Base instance).

A construct-specific handler is used rather than checking callframe->newTarget() inside the shared handler: CallFrame::newTarget() aliases thisValue() in JSC, so that check would misfire on ordinary calls with a non-undefined receiver (e.g. obj.method() where method is a mockReturnValue(42) mock would wrongly return obj).

Test

Added mock() > are constructable with new to test/js/bun/test/mock-fn.test.js, covering: no-implementation new, Reflect.construct, this-mutating implementations, object-returning implementations, ignored primitive returns, newTarget.prototype, Reflect.construct with an explicit subclass target, and a guard that a normal call with a non-undefined this still returns the mock's value.

Found by Fuzzilli.

JSMockFunction registered jsMockFunctionCall as both the call and the
construct handler. A native construct callback must return an object —
Interpreter::executeConstruct passes the result straight through
asObject() — but jsMockFunctionCall can return undefined, a number, or
any other primitive, so constructing a mock (e.g. Reflect.construct on
jest.fn()) crashed on a non-object cell.

Split the body into jsMockFunctionCallImpl parameterized on the `this`
value and add a dedicated jsMockFunctionConstruct handler. It creates
`this` from newTarget's prototype, runs the mock against it, and returns
the mock's result only when that result is an object, otherwise the
freshly created instance — matching `new` on an ordinary function.

A construct-specific handler is used rather than checking
callframe->newTarget() inside the shared handler, because newTarget()
aliases thisValue() in JSC and would misfire on ordinary calls that
happen to have a non-undefined receiver.
@robobun

robobun commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:13 PM PT - May 30th, 2026

@robobun, your commit 6346360 has 2 failures in Build #59406 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31620

That installs a local version of the PR into your bun-31620 executable, so you can run:

bun-31620 --bun

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b6b671a7-703d-4dd4-b251-41160d0600d9

📥 Commits

Reviewing files that changed from the base of the PR and between 61e96a8 and 6346360.

📒 Files selected for processing (2)
  • src/jsc/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock-fn.test.js

Walkthrough

Mock functions now support being used as constructors. The implementation refactors the existing call logic into a shared helper that accepts a parameterized thisValue, wires a new construct handler to the InternalFunction callback, and implements constructor semantics including prototype-based this construction, mock recording, and return value precedence rules.

Changes

Mock function constructor support

Layer / File(s) Summary
Constructor implementation and refactoring
src/jsc/bindings/JSMockFunction.cpp
jsMockFunctionCallImpl extracts shared mock-call logic parameterized by thisValue. jsMockFunctionCall becomes a wrapper delegating to the helper with call-frame-derived this. jsMockFunctionConstruct constructs this from newTarget.prototype, invokes the helper, and returns object results or the constructed fallback. JSMockFunction's InternalFunction callback is wired to the new construct handler.
Constructor behavior tests
test/js/bun/test/mock-fn.test.js
New test verifies mock functions support new and Reflect.construct, including call/context recording, this preservation, prototype/newTarget respect, object return precedence, and primitive return handling.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: enabling mock functions to be constructable and properly handle non-object return values.
Description check ✅ Passed The description provides comprehensive coverage of the problem, fix, and testing approach, following the template structure with detailed context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/JSMockFunction.cpp`:
- Around line 989-1012: jsMockFunctionConstruct creates thisObject for
constructor calls but never records it in the mock's instances array; update
jsMockFunctionConstruct to push the constructed thisObject onto the mock's
instances list (fn.mock.instances) so constructor invocations are tracked.
Locate jsMockFunctionConstruct and, after thisObject is created (and before
returning), retrieve the mock function object (via the CallFrame/callee or
existing mock function value used by jsMockFunctionCallImpl) and append
thisObject to its 'mock.instances' array, handling any JS exceptions
consistently with the existing RETURN_IF_EXCEPTION usage so the original return
behavior is preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 066d6d0e-cc88-4f75-b5b4-612f2e567d81

📥 Commits

Reviewing files that changed from the base of the PR and between 61bd997 and 61e96a8.

📒 Files selected for processing (2)
  • src/jsc/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock-fn.test.js

Comment thread src/jsc/bindings/JSMockFunction.cpp
Populate fn->instances alongside fn->contexts in the shared call helper so
`new`/Reflect.construct invocations appear in mock.instances, mirroring
jest (which records each call's `this`). Verified identical output against
jest for plain construct, object-returning impl, this-mutating impl, and
regular non-construct calls.
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(bun:test): return an object when a mock function is constructed with new #31386 - Same fix: splits jsMockFunctionCall into jsMockFunctionCallImpl + jsMockFunctionConstruct to fix SIGSEGV when constructing a mock function that returns a non-object

🤖 Generated with Claude Code

Comment thread src/jsc/bindings/JSMockFunction.cpp
@robobun

robobun commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate of #31386, which fixes the same crash with the identical jsMockFunctionCall/jsMockFunctionConstruct split and predates this PR. I've ported this PR's mock.instances tracking (and the accompanying assertions) onto #31386, so the consolidated PR there now carries the complete fix plus its additional spy-construction coverage.

@robobun robobun closed this May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant