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
45 changes: 44 additions & 1 deletion src/jsc/bindings/JSMockFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ inline To tryJSDynamicCast(JSC::WriteBarrier<WriteBarrierT>& from)
}

JSC_DECLARE_HOST_FUNCTION(jsMockFunctionCall);
JSC_DECLARE_HOST_FUNCTION(jsMockFunctionConstruct);
JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl);
JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_mock);
JSC_DECLARE_HOST_FUNCTION(jsMockFunctionGetter_mockGetLastCall);
Expand Down Expand Up @@ -463,7 +464,7 @@ class JSMockFunction : public JSC::InternalFunction {
}

JSMockFunction(JSC::VM& vm, JSC::Structure* structure, CallbackKind wrapKind)
: Base(vm, structure, jsMockFunctionCall, jsMockFunctionCall)
: Base(vm, structure, jsMockFunctionCall, jsMockFunctionConstruct)
{
initMock();
}
Expand Down Expand Up @@ -976,6 +977,48 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje
return JSValue::encode(jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
{
Zig::GlobalObject* globalObject = uncheckedDowncast<Zig::GlobalObject>(lexicalGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSMockFunction* fn = dynamicDowncast<JSMockFunction>(callframe->jsCallee());
if (!fn) [[unlikely]] {
throwTypeError(globalObject, scope, "Expected callee to be mock function"_s);
return {};
}

JSValue newTarget = callframe->newTarget();
JSObject* newTargetObject = newTarget.isObject() ? asObject(newTarget) : fn;
JSValue prototype = newTargetObject->get(globalObject, vm.propertyNames->prototype);
RETURN_IF_EXCEPTION(scope, {});
JSObject* instance = prototype.isObject()
? JSC::constructEmptyObject(globalObject, asObject(prototype))
: JSC::constructEmptyObject(globalObject);
RETURN_IF_EXCEPTION(scope, {});

if (JSC::JSArray* instances = fn->instances.get()) {
instances->push(globalObject, instance);
RETURN_IF_EXCEPTION(scope, {});
} else {
JSC::ObjectInitializationScope object(vm);
instances = JSC::JSArray::tryCreateUninitializedRestricted(
object,
globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
1);
instances->initializeIndex(object, 0, instance);
fn->instances.set(vm, fn, instances);
}

callframe->setThisValue(instance);
EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe);
RETURN_IF_EXCEPTION(scope, {});
JSValue result = JSValue::decode(encodedResult);
if (result.isObject())
return encodedResult;
return JSValue::encode(instance);
}

Comment on lines +980 to +1021

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the vendored JSC CallFrame this-slot/newTarget behavior used by this construct callback.
fd -a CallFrame.h | xargs -I{} rg -n -B2 -A2 'setThisValue|newTarget\(\)' {}

Repository: oven-sh/bun

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- CallFrame.h candidates ---'
fd -a -i 'CallFrame\.h$' . | head -20

printf '%s\n' '--- construct callback and callframe access ---'
rg -n -B4 -A8 'jsMockFunctionConstruct|setThisValue|newTarget\(\)|thisValue\(\)' \
  src/jsc/bindings src 2>/dev/null | head -240

Repository: oven-sh/bun

Length of output: 19972


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- tracked CallFrame files and JSC source roots ---'
git ls-files | rg -i '(^|/)(CallFrame\.h|JavaScriptCore|jsc|webkit)(/|$)' | head -160

printf '%s\n' '--- CallFrame API references ---'
rg -n -S 'class CallFrame|newTarget\(\)|setThisValue\(|thisValue\(\) const|thisValue\(\)' \
  --glob '*.{h,hpp,cpp,cc,mm,inc}' . 2>/dev/null | head -240

printf '%s\n' '--- exact mock constructor context ---'
sed -n '960,1030p' src/jsc/bindings/JSMockFunction.cpp

Repository: oven-sh/bun

Length of output: 38257


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- CallFrame.rs slot definitions ---'
rg -n -B8 -A14 'pub fn (new_target|this_value|set_this_value)|newTarget|setThisValue|thisValue' \
  src/jsc/CallFrame.rs | head -180

printf '%s\n' '--- analogous host-constructor slot reuse ---'
sed -n '1,95p' src/jsc/bindings/NapiClass.cpp
sed -n '1025,1065p' src/jsc/bindings/napi.h

printf '%s\n' '--- constructor implementations using both slots ---'
sed -n '320,350p' src/runtime/webview/JSWebViewConstructor.cpp
sed -n '165,195p' src/jsc/bindings/NodeDirent.cpp

Repository: oven-sh/bun

Length of output: 8808


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- generated CallFrame.rs contents ---'
wc -l src/jsc/CallFrame.rs
cat -n src/jsc/CallFrame.rs | head -220

printf '%s\n' '--- CallFrame generation and WebKit source references ---'
rg -n -S 'CallFrame\.rs|CallFrame|WebKit|oven-sh/WebKit|397dafc9721b8f8046f9448abb6dbc14efe096d3' \
  build.zig build.zig.zon src package.json README.md 2>/dev/null | head -220

Repository: oven-sh/bun

Length of output: 31334


Document the intentional CallFrame slot reuse.

At Line 1013, add a short comment that callframe->newTarget() was captured before setThisValue(instance) repurposes the constructor frame’s this slot for jsMockFunctionCall. src/jsc/CallFrame.rs documents this slot-sharing behavior.

🤖 Prompt for 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.

In `@src/jsc/bindings/JSMockFunction.cpp` around lines 980 - 1021, Add a brief
comment in jsMockFunctionConstruct immediately before
callframe->setThisValue(instance), documenting that newTarget was captured
earlier because setThisValue repurposes the constructor CallFrame’s this slot
for jsMockFunctionCall, consistent with CallFrame.rs.

Source: Coding guidelines

void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
Expand Down
34 changes: 34 additions & 0 deletions test/js/bun/test/mock-fn.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,40 @@ describe("mock()", () => {

expect(bar()()).toBe(true);
});

test("Reflect.construct on a mock returns an object and records the instance", () => {
const fn = jest.fn();
const instance = Reflect.construct(fn, [1, 2]);
expect(typeof instance).toBe("object");
expect(instance).not.toBeNull();
expect(fn.mock.instances).toEqual([instance]);
expect(fn.mock.contexts).toEqual([instance]);
expect(fn.mock.calls).toEqual([[1, 2]]);

const fn2 = jest.fn(() => ({ tag: "impl" }));
const result2 = Reflect.construct(fn2, []);
expect(result2).toEqual({ tag: "impl" });
expect(fn2.mock.instances).toHaveLength(1);
expect(fn2.mock.instances[0]).not.toBe(result2);

const fn3 = jest.fn(() => 42);
const result3 = new fn3();
expect(typeof result3).toBe("object");
expect(fn3.mock.instances).toEqual([result3]);

class NewTarget {}
const fn4 = jest.fn();
const result4 = Reflect.construct(fn4, [], NewTarget);
expect(Object.getPrototypeOf(result4)).toBe(NewTarget.prototype);
expect(result4).toBeInstanceOf(NewTarget);
expect(fn4.mock.instances).toEqual([result4]);

const fn5 = jest.fn(() => {
throw new Error("boom");
});
expect(() => new fn5()).toThrow("boom");
expect(() => Reflect.construct(fn5, [])).toThrow("boom");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
});

describe("resetAllMocks", () => {
Expand Down
Loading