From 64d77e33f65b0e2bd8a76409b00bf2400993110c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 16 Aug 2024 15:42:11 -0700 Subject: [PATCH] Fixes #13331 (#13340) --- src/bun.js/bindings/BunPlugin.cpp | 90 ++++++----- src/bun.js/bindings/ErrorCode.cpp | 11 ++ src/bun.js/bindings/ErrorCode.h | 1 + src/bun.js/bindings/JSMockFunction.cpp | 200 ++++++++++++------------- src/codegen/generate-classes.ts | 9 +- test/js/bun/globals.test.js | 38 +++++ test/js/bun/test/mock-fn.test.js | 37 +++++ 7 files changed, 246 insertions(+), 140 deletions(-) diff --git a/src/bun.js/bindings/BunPlugin.cpp b/src/bun.js/bindings/BunPlugin.cpp index f0019a56d0..ad0628687d 100644 --- a/src/bun.js/bindings/BunPlugin.cpp +++ b/src/bun.js/bindings/BunPlugin.cpp @@ -1,5 +1,6 @@ #include "BunPlugin.h" +#include "JavaScriptCore/JSCast.h" #include "headers-handwritten.h" #include "helpers.h" #include "ZigGlobalObject.h" @@ -28,6 +29,8 @@ #include "CommonJSModuleRecord.h" #include "isBuiltinModule.h" +extern "C" Zig::GlobalObject* Bun__getDefaultGlobalObject(); + namespace Zig { extern "C" void Bun__onDidAppendPlugin(void* bunVM, JSGlobalObject* globalObject); @@ -133,7 +136,11 @@ static EncodedJSValue jsFunctionAppendVirtualModulePluginBody(JSC::JSGlobalObjec return JSValue::encode(jsUndefined()); } - Zig::GlobalObject* global = Zig::jsCast(globalObject); + Zig::GlobalObject* global = JSC::jsDynamicCast(globalObject); + if (!global) { + global = Bun__getDefaultGlobalObject(); + } + if (global->onLoadPlugins.virtualModules == nullptr) { global->onLoadPlugins.virtualModules = new BunPlugin::VirtualModuleMap; } @@ -200,7 +207,10 @@ static JSC::EncodedJSValue jsFunctionAppendOnResolvePluginBody(JSC::JSGlobalObje static JSC::EncodedJSValue jsFunctionAppendOnResolvePluginGlobal(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target) { - Zig::GlobalObject* global = Zig::jsCast(globalObject); + Zig::GlobalObject* global = Zig::jsDynamicCast(globalObject); + if (!global) { + global = Bun__getDefaultGlobalObject(); + } auto& plugins = global->onResolvePlugins; auto callback = Bun__onDidAppendPlugin; @@ -209,7 +219,10 @@ static JSC::EncodedJSValue jsFunctionAppendOnResolvePluginGlobal(JSC::JSGlobalOb static JSC::EncodedJSValue jsFunctionAppendOnLoadPluginGlobal(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target) { - Zig::GlobalObject* global = Zig::jsCast(globalObject); + Zig::GlobalObject* global = Zig::jsDynamicCast(globalObject); + if (!global) { + global = Bun__getDefaultGlobalObject(); + } auto& plugins = global->onLoadPlugins; auto callback = Bun__onDidAppendPlugin; @@ -263,24 +276,24 @@ static inline JSC::EncodedJSValue setupBunPlugin(JSC::JSGlobalObject* globalObje JSC::JSObject* obj = callframe->uncheckedArgument(0).getObject(); if (!obj) { JSC::throwTypeError(globalObject, throwScope, "plugin needs an object as first argument"_s); - return JSValue::encode(jsUndefined()); } + RETURN_IF_EXCEPTION(throwScope, {}); JSC::JSValue setupFunctionValue = obj->getIfPropertyExists(globalObject, Identifier::fromString(vm, "setup"_s)); if (!setupFunctionValue || setupFunctionValue.isUndefinedOrNull() || !setupFunctionValue.isCell() || !setupFunctionValue.isCallable()) { JSC::throwTypeError(globalObject, throwScope, "plugin needs a setup() function"_s); - return JSValue::encode(jsUndefined()); } + RETURN_IF_EXCEPTION(throwScope, {}); if (JSValue targetValue = obj->getIfPropertyExists(globalObject, Identifier::fromString(vm, "target"_s))) { if (auto* targetJSString = targetValue.toStringOrNull(globalObject)) { String targetString = targetJSString->value(globalObject); if (!(targetString == "node"_s || targetString == "bun"_s || targetString == "browser"_s)) { JSC::throwTypeError(globalObject, throwScope, "plugin target must be one of 'node', 'bun' or 'browser'"_s); - return JSValue::encode(jsUndefined()); } } } + RETURN_IF_EXCEPTION(throwScope, {}); JSObject* builderObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 4); @@ -364,7 +377,10 @@ JSFunction* BunPlugin::Group::find(JSC::JSGlobalObject* globalObject, String& pa void BunPlugin::OnLoad::addModuleMock(JSC::VM& vm, const String& path, JSC::JSObject* mockObject) { - Zig::GlobalObject* globalObject = Zig::jsCast(mockObject->globalObject()); + Zig::GlobalObject* globalObject = jsDynamicCast(mockObject->globalObject()); + if (!globalObject) { + globalObject = Bun__getDefaultGlobalObject(); + } if (globalObject->onLoadPlugins.virtualModules == nullptr) { globalObject->onLoadPlugins.virtualModules = new BunPlugin::VirtualModuleMap; @@ -595,44 +611,46 @@ extern "C" JSC_DEFINE_HOST_FUNCTION(JSMock__jsModuleMock, (JSC::JSGlobalObject * if (JSValue entryValue = esm->get(globalObject, specifierString)) { removeFromESM = true; - - if (entryValue.isObject()) { - JSObject* entry = entryValue.getObject(); + JSObject* entry = entryValue ? entryValue.getObject() : nullptr; + if (entry) { if (JSValue moduleValue = entry->getIfPropertyExists(globalObject, Identifier::fromString(vm, String("module"_s)))) { + RETURN_IF_EXCEPTION(scope, {}); if (auto* mod = jsDynamicCast(moduleValue)) { JSC::JSModuleNamespaceObject* moduleNamespaceObject = mod->getModuleNamespace(globalObject); - JSValue exportsValue = getJSValue(); RETURN_IF_EXCEPTION(scope, {}); - removeFromESM = false; - - if (exportsValue.isObject()) { - // TODO: use fast path for property iteration - auto* object = exportsValue.getObject(); - JSC::PropertyNameArray names(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); - JSObject::getOwnPropertyNames(object, globalObject, names, DontEnumPropertiesMode::Exclude); + if (moduleNamespaceObject) { + JSValue exportsValue = getJSValue(); RETURN_IF_EXCEPTION(scope, {}); + auto* object = exportsValue.getObject(); + removeFromESM = false; - for (auto& name : names) { - // consistent with regular esm handling code - auto catchScope = DECLARE_CATCH_SCOPE(vm); - JSValue value = object->get(globalObject, name); - if (scope.exception()) { - scope.clearException(); - value = jsUndefined(); + if (object) { + JSC::PropertyNameArray names(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + JSObject::getOwnPropertyNames(object, globalObject, names, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(scope, {}); + + for (auto& name : names) { + // consistent with regular esm handling code + auto catchScope = DECLARE_CATCH_SCOPE(vm); + JSValue value = object->get(globalObject, name); + if (scope.exception()) { + scope.clearException(); + value = jsUndefined(); + } + moduleNamespaceObject->overrideExportValue(globalObject, name, value); } - moduleNamespaceObject->overrideExportValue(globalObject, name, value); + + } else { + // if it's not an object, I guess we just set the default export? + moduleNamespaceObject->overrideExportValue(globalObject, vm.propertyNames->defaultKeyword, exportsValue); } - } else { - // if it's not an object, I guess we just set the default export? - moduleNamespaceObject->overrideExportValue(globalObject, vm.propertyNames->defaultKeyword, exportsValue); + RETURN_IF_EXCEPTION(scope, {}); + + // TODO: do we need to handle intermediate loading state here? + // entry->putDirect(vm, Identifier::fromString(vm, String("evaluated"_s)), jsBoolean(true), 0); + // entry->putDirect(vm, Identifier::fromString(vm, String("state"_s)), jsNumber(JSC::JSModuleLoader::Status::Ready), 0); } - - RETURN_IF_EXCEPTION(scope, {}); - - // TODO: do we need to handle intermediate loading state here? - // entry->putDirect(vm, Identifier::fromString(vm, String("evaluated"_s)), jsBoolean(true), 0); - // entry->putDirect(vm, Identifier::fromString(vm, String("state"_s)), jsNumber(JSC::JSModuleLoader::Status::Ready), 0); } } } @@ -640,7 +658,7 @@ extern "C" JSC_DEFINE_HOST_FUNCTION(JSMock__jsModuleMock, (JSC::JSGlobalObject * if (auto entryValue = globalObject->requireMap()->get(globalObject, specifierString)) { removeFromCJS = true; - if (auto* moduleObject = jsDynamicCast(entryValue)) { + if (auto* moduleObject = entryValue ? jsDynamicCast(entryValue) : nullptr) { JSValue exportsValue = getJSValue(); RETURN_IF_EXCEPTION(scope, {}); diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 8ed4829e7e..2a89fa0caf 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -360,3 +360,14 @@ extern "C" JSC::EncodedJSValue WebCore__CommonAbortReason__toJS(JSC::JSGlobalObj { return JSC::JSValue::encode(WebCore::toJS(globalObject, abortReason)); } + +JSC::JSObject* Bun::createInvalidThisError(JSC::JSGlobalObject* globalObject, JSC::JSValue thisValue, const ASCIILiteral typeName) +{ + if (thisValue.isEmpty() || thisValue.isUndefined()) { + return Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_THIS, makeString("Expected this to be instanceof "_s, typeName)); + } + + // Pathological case: the this value returns a string which is extremely long or causes an out of memory error. + const auto& typeString = thisValue.isString() ? String("a string"_s) : JSC::errorDescriptionForValue(globalObject, thisValue); + return Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_THIS, makeString("Expected this to be instanceof "_s, typeName, ", but received "_s, typeString)); +} \ No newline at end of file diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 07c65ead77..0f80ff639b 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -51,6 +51,7 @@ JSC::JSObject* createError(JSC::JSGlobalObject* globalObject, ErrorCode code, co JSC::JSObject* createError(Zig::GlobalObject* globalObject, ErrorCode code, JSC::JSValue message); JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options = jsUndefined()); JSC::JSValue toJS(JSC::JSGlobalObject*, ErrorCode); +JSObject* createInvalidThisError(JSGlobalObject* globalObject, JSValue thisValue, const ASCIILiteral typeName); JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_INVALID_ARG_TYPE); JSC_DECLARE_HOST_FUNCTION(jsFunction_ERR_OUT_OF_RANGE); diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 6073537f7b..888547fa4b 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -1,5 +1,7 @@ #include "root.h" +#include "ErrorCode+List.h" +#include "JavaScriptCore/Error.h" #include "JSMockFunction.h" #include #include "ZigGlobalObject.h" @@ -23,6 +25,8 @@ #include #include #include "BunPlugin.h" +#include "AsyncContextFrame.h" +#include "ErrorCode.h" BUN_DECLARE_HOST_FUNCTION(JSMock__jsUseFakeTimers); BUN_DECLARE_HOST_FUNCTION(JSMock__jsUseRealTimers); @@ -33,6 +37,12 @@ BUN_DECLARE_HOST_FUNCTION(JSMock__jsClearAllMocks); BUN_DECLARE_HOST_FUNCTION(JSMock__jsSpyOn); BUN_DECLARE_HOST_FUNCTION(JSMock__jsMockFn); +#define CHECK_IS_MOCK_FUNCTION(thisValue) \ + if (UNLIKELY(!thisObject)) { \ + scope.throwException(globalObject, createInvalidThisError(globalObject, thisValue, "Mock"_s)); \ + return {}; \ + } + namespace Bun { /** @@ -327,8 +337,9 @@ public: // Reset the spy back to the original value. if (this->spyAttributes & SpyAttributeESModuleNamespace) { - auto* moduleNamespaceObject = jsCast(target); - moduleNamespaceObject->overrideExportValue(moduleNamespaceObject->globalObject(), this->spyIdentifier, implValue); + if (auto* moduleNamespaceObject = tryJSDynamicCast(target)) { + moduleNamespaceObject->overrideExportValue(moduleNamespaceObject->globalObject(), this->spyIdentifier, implValue); + } } else { target->putDirect(this->vm(), this->spyIdentifier, implValue, this->spyAttributes); } @@ -834,14 +845,15 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje setReturnValue(createMockResult(vm, globalObject, "incomplete"_s, jsUndefined())); - WTF::NakedPtr exception; + auto catchScope = DECLARE_CATCH_SCOPE(vm); - JSValue returnValue = call(globalObject, result, callData, thisValue, args, exception); + JSValue returnValue = Bun::call(globalObject, result, callData, thisValue, args); - if (auto* exc = exception.get()) { + if (auto* exc = catchScope.exception()) { if (auto* returnValuesArray = fn->returnValues.get()) { returnValuesArray->putDirectIndex(globalObject, returnValueIndex, createMockResult(vm, globalObject, "throw"_s, exc->value())); fn->returnValues.set(vm, fn, returnValuesArray); + catchScope.clearException(); JSC::throwException(globalObject, scope, exc); return {}; } @@ -888,12 +900,12 @@ void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* g JSC_DEFINE_HOST_FUNCTION(jsMockFunctionGetMockImplementation, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - } + CHECK_IS_MOCK_FUNCTION(thisValue); if (auto* implementation = tryJSDynamicCast(thisObject->implementation.get())) { if (implementation->kind == JSMockImplementation::Kind::Call) { @@ -908,10 +920,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsMockFunctionGetter_mock, (JSC::JSGlobalObject * globa { Bun::JSMockFunction* thisObject = jsDynamicCast(JSValue::decode(thisValue)); auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(JSValue::decode(thisValue)) return JSValue::encode(thisObject->mock.getInitializedOnMainThread(thisObject)); } @@ -920,10 +929,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl, (JSC::JSGlobalObject * { Bun::JSMockFunction* thisObject = jsDynamicCast(JSValue::decode(thisValue)); auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(JSValue::decode(thisValue)) if (auto* impl = tryJSDynamicCast(thisObject->implementation.get())) { if (impl->kind == JSMockImplementation::Kind::Call) { @@ -955,12 +961,12 @@ extern "C" JSC::EncodedJSValue JSMockFunction__getReturns(EncodedJSValue encoded JSC_DEFINE_HOST_FUNCTION(jsMockFunctionGetMockName, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - } + CHECK_IS_MOCK_FUNCTION(thisValue) if (auto* impl = tryJSDynamicCast(thisObject->implementation.get())) { if (impl->kind == JSMockImplementation::Kind::Call) { @@ -979,12 +985,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionGetMockName, (JSC::JSGlobalObject * globa } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockClear, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - } + CHECK_IS_MOCK_FUNCTION(thisValue); thisObject->clear(); @@ -992,12 +998,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockClear, (JSC::JSGlobalObject * globalO } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReset, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - } + CHECK_IS_MOCK_FUNCTION(thisValue); thisObject->reset(); @@ -1005,12 +1011,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReset, (JSC::JSGlobalObject * globalO } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRestore, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - } + CHECK_IS_MOCK_FUNCTION(thisValue); thisObject->clearSpy(); @@ -1020,13 +1026,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementation, (JSC::JSGlobalObject { auto& vm = lexicalGlobalObject->vm(); auto* globalObject = jsCast(lexicalGlobalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); - if (UNLIKELY(!thisObject)) { - throwOutOfMemoryError(globalObject, scope); - return {}; - } + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + auto scope = DECLARE_THROW_SCOPE(vm); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + + CHECK_IS_MOCK_FUNCTION(thisValue); JSValue value = callframe->argument(0); @@ -1043,13 +1048,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementationOnce, (JSC::JSGlobalObj { auto& vm = lexicalGlobalObject->vm(); auto* globalObject = jsCast(lexicalGlobalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); - if (UNLIKELY(!thisObject)) { - throwOutOfMemoryError(globalObject, scope); - return {}; - } + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + auto scope = DECLARE_THROW_SCOPE(vm); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + + CHECK_IS_MOCK_FUNCTION(thisValue); JSValue value = callframe->argument(0); @@ -1064,13 +1068,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementationOnce, (JSC::JSGlobalObj } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockName, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); if (callframe->argumentCount() > 0) { auto* newName = callframe->argument(0).toStringOrNull(globalObject); @@ -1086,12 +1089,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockName, (JSC::JSGlobalObject * globalOb } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnThis, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - } + CHECK_IS_MOCK_FUNCTION(thisValue); pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnThis, jsUndefined()); @@ -1099,13 +1102,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnThis, (JSC::JSGlobalObject * gl } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, callframe->argument(0)); @@ -1113,13 +1115,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValue, (JSC::JSGlobalObject * g } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValueOnce, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, callframe->argument(0)); @@ -1127,13 +1128,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockReturnValueOnce, (JSC::JSGlobalObject } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, JSC::JSPromise::resolvedPromise(globalObject, callframe->argument(0))); @@ -1141,13 +1141,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValue, (JSC::JSGlobalObject * } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValueOnce, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, JSC::JSPromise::resolvedPromise(globalObject, callframe->argument(0))); @@ -1155,13 +1154,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockResolvedValueOnce, (JSC::JSGlobalObje } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRejectedValue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); pushImpl(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, JSC::JSPromise::rejectedPromise(globalObject, callframe->argument(0))); @@ -1169,13 +1167,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRejectedValue, (JSC::JSGlobalObject * } JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRejectedValueOnce, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); pushImplOnce(thisObject, globalObject, JSMockImplementation::Kind::ReturnValue, JSC::JSPromise::rejectedPromise(globalObject, callframe->argument(0))); @@ -1271,13 +1268,12 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionWithImplementation, (JSC::JSGlobalObject { Zig::GlobalObject* globalObject = jsCast(jsGlobalObject); - JSMockFunction* thisObject = jsDynamicCast(callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict())); + const JSValue thisValue = callframe->thisValue().toThis(globalObject, JSC::ECMAMode::strict()); + JSMockFunction* thisObject = jsDynamicCast(thisValue); + auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (UNLIKELY(!thisObject)) { - throwTypeError(globalObject, scope, "Expected Mock"_s); - return {}; - } + CHECK_IS_MOCK_FUNCTION(thisValue); JSValue tempImplValue = callframe->argument(0); JSValue callback = callframe->argument(1); @@ -1337,35 +1333,35 @@ using namespace Bun; using namespace JSC; // This is a stub. Exists so that the same code can be run in Jest -BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseFakeTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseFakeTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - return JSValue::encode(callFrame->thisValue()); + return JSValue::encode(callframe->thisValue()); } -BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseRealTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseRealTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { globalObject->overridenDateNow = -1; - return JSValue::encode(callFrame->thisValue()); + return JSValue::encode(callframe->thisValue()); } -BUN_DEFINE_HOST_FUNCTION(JSMock__jsNow, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +BUN_DEFINE_HOST_FUNCTION(JSMock__jsNow, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { return JSValue::encode(jsNumber(globalObject->jsDateNow())); } -BUN_DEFINE_HOST_FUNCTION(JSMock__jsSetSystemTime, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +BUN_DEFINE_HOST_FUNCTION(JSMock__jsSetSystemTime, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) { - JSValue argument0 = callFrame->argument(0); + JSValue argument0 = callframe->argument(0); if (auto* dateInstance = jsDynamicCast(argument0)) { if (std::isnormal(dateInstance->internalNumber())) { globalObject->overridenDateNow = dateInstance->internalNumber(); } - return JSValue::encode(callFrame->thisValue()); + return JSValue::encode(callframe->thisValue()); } // number > 0 is a valid date otherwise it's invalid and we should reset the time (set to -1) globalObject->overridenDateNow = (argument0.isNumber() && argument0.asNumber() >= 0) ? argument0.asNumber() : -1; - return JSValue::encode(callFrame->thisValue()); + return JSValue::encode(callframe->thisValue()); } BUN_DEFINE_HOST_FUNCTION(JSMock__jsRestoreAllMocks, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe)) @@ -1442,7 +1438,7 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb mock->copyNameAndLength(vm, globalObject, value); - if (JSModuleNamespaceObject* moduleNamespaceObject = jsDynamicCast(object)) { + if (JSModuleNamespaceObject* moduleNamespaceObject = tryJSDynamicCast(object)) { moduleNamespaceObject->overrideExportValue(globalObject, propertyKey, mock); mock->spyAttributes |= JSMockFunction::SpyAttributeESModuleNamespace; } else { @@ -1458,7 +1454,7 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb attributes |= PropertyAttribute::Accessor; - if (JSModuleNamespaceObject* moduleNamespaceObject = jsDynamicCast(object)) { + if (JSModuleNamespaceObject* moduleNamespaceObject = tryJSDynamicCast(object)) { moduleNamespaceObject->overrideExportValue(globalObject, propertyKey, mock); mock->spyAttributes |= JSMockFunction::SpyAttributeESModuleNamespace; } else { @@ -1538,4 +1534,6 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsMockFn, (JSC::JSGlobalObject * lexicalGlobalO activeMocks->add(vm, thisObject, thisObject); return JSValue::encode(thisObject); -} \ No newline at end of file +} + +#undef CHECK_IS_MOCK_FUNCTION \ No newline at end of file diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index a6365c3b7e..72f39f568c 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1113,13 +1113,13 @@ JSC_DEFINE_CUSTOM_SETTER(${symbolName(typeName, name)}SetterWrap, (JSGlobalObjec JSC_DEFINE_HOST_FUNCTION(${symbolName(typeName, name)}Callback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); ${className(typeName)}* thisObject = jsDynamicCast<${className(typeName)}*>(callFrame->thisValue()); if (UNLIKELY(!thisObject)) { - auto throwScope = DECLARE_THROW_SCOPE(vm); - throwVMTypeError(lexicalGlobalObject, throwScope, "Expected 'this' to be instanceof ${typeName}"_s); - return JSValue::encode({}); + scope.throwException(lexicalGlobalObject, Bun::createInvalidThisError(lexicalGlobalObject, callFrame->thisValue(), "${typeName}"_s)); + return {}; } JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); @@ -2042,6 +2042,8 @@ const GENERATED_CLASSES_IMPL_HEADER_PRE = ` #include "JSDOMConvertBufferSource.h" #include "ZigGeneratedClasses.h" +#include "ErrorCode+List.h" +#include "ErrorCode.h" #if !OS(WINDOWS) #define JSC_CALLCONV "C" @@ -2057,6 +2059,7 @@ namespace WebCore { using namespace JSC; using namespace Zig; + `; const GENERATED_CLASSES_IMPL_FOOTER = ` diff --git a/test/js/bun/globals.test.js b/test/js/bun/globals.test.js index 1a0f7b2ef9..616842c147 100644 --- a/test/js/bun/globals.test.js +++ b/test/js/bun/globals.test.js @@ -2,6 +2,44 @@ import { expect, it, describe } from "bun:test"; import { bunEnv, bunExe } from "harness"; import path from "path"; +it("ERR_INVALID_THIS", () => { + try { + Request.prototype.formData.call(undefined); + expect.unreachable(); + } catch (e) { + expect(e.code).toBe("ERR_INVALID_THIS"); + expect(e.name).toBe("TypeError"); + expect(e.message).toBe("Expected this to be instanceof Request"); + } + + try { + Request.prototype.formData.call(null); + expect.unreachable(); + } catch (e) { + expect(e.code).toBe("ERR_INVALID_THIS"); + expect(e.name).toBe("TypeError"); + expect(e.message).toBe("Expected this to be instanceof Request, but received null"); + } + + try { + Request.prototype.formData.call(new (class Boop {})()); + expect.unreachable(); + } catch (e) { + expect(e.code).toBe("ERR_INVALID_THIS"); + expect(e.name).toBe("TypeError"); + expect(e.message).toBe("Expected this to be instanceof Request, but received Boop"); + } + + try { + Request.prototype.formData.call("hellooo"); + expect.unreachable(); + } catch (e) { + expect(e.code).toBe("ERR_INVALID_THIS"); + expect(e.name).toBe("TypeError"); + expect(e.message).toBe("Expected this to be instanceof Request, but received a string"); + } +}); + it("extendable", () => { const classes = [Blob, TextDecoder, TextEncoder, Request, Response, Headers, HTMLRewriter, Bun.Transpiler, Buffer]; for (let Class of classes) { diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 7c80015533..e22d23e515 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -60,6 +60,43 @@ describe("mock()", () => { expect(onData).toHaveBeenCalled(); }); + + // https://github.com/oven-sh/bun/issues/13331 + describe("checks the this value", () => { + const fn = jest.fn(); + const proto = Object.getPrototypeOf(fn); + test.each(Object.getOwnPropertyNames(proto))("%s", fnName => { + if (fnName === "_isMockFunction") { + expect(proto[fnName]).toBe(true); + return; + } + + if (typeof Object.getOwnPropertyDescriptor(proto, fnName)?.value === "function") { + try { + const protoFn = fn[fnName]; + protoFn.call(undefined); + expect.unreachable(); + } catch (e) { + expect(e).toHaveProperty("code", "ERR_INVALID_THIS"); + expect(e.message).toContain("Mock"); + } + } else if ("value" in Object.getOwnPropertyDescriptor(proto, fnName)) { + try { + proto[fnName].value; + expect.unreachable(); + } catch (e) { + expect(e.message).not.toContain("unreachable"); + } + } else { + try { + proto[fnName]; + expect.unreachable(); + } catch (e) { + expect(e.message).not.toContain("unreachable"); + } + } + }); + }); } test("are callable", () => { const fn = jest.fn(() => 42);