diff --git a/src/jsc/bindings/NodeVMScript.cpp b/src/jsc/bindings/NodeVMScript.cpp index d7cfa6157cbd..a4cacdd7dca7 100644 --- a/src/jsc/bindings/NodeVMScript.cpp +++ b/src/jsc/bindings/NodeVMScript.cpp @@ -3,6 +3,7 @@ #include "ErrorCode.h" #include "JavaScriptCore/Completion.h" +#include "JavaScriptCore/ParserError.h" #include "JavaScriptCore/JIT.h" #include "JavaScriptCore/JSWeakMap.h" #include "JavaScriptCore/JSWeakMapInlines.h" @@ -130,6 +131,17 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT SourceCode source = makeSource(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher), JSC::SourceTaintedOrigin::Untainted, options.filename, TextPosition(options.lineOffset, options.columnOffset)); RETURN_IF_EXCEPTION(scope, {}); + { + JSC::ParserError error; + if (!JSC::checkSyntax(vm, source, error)) { + ASSERT(error.isValid()); + auto exception = error.toErrorObject(globalObject, source); + RETURN_IF_EXCEPTION(scope, {}); + JSC::throwException(globalObject, scope, exception); + return {}; + } + } + const bool produceCachedData = options.produceCachedData; auto filename = options.filename; diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index 9b630cc449b5..58df2d063421 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -697,7 +697,7 @@ test("can't use export syntax in vm.Script", () => { expect(() => { const script = new Script("export default {};"); script.createCachedData(); - }).toThrow({ message: "createCachedData failed" }); + }).toThrow({ name: "SyntaxError", message: "Unexpected keyword 'export'" }); }); test("rejects invalid bytecode", () => { diff --git a/test/regression/issue/28666.test.ts b/test/regression/issue/28666.test.ts new file mode 100644 index 000000000000..42dcceb9de74 --- /dev/null +++ b/test/regression/issue/28666.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test"; +import { Script } from "node:vm"; + +test("vm.Script throws SyntaxError for missing closing paren", () => { + expect(() => { + new Script("Math.max(a, b", { filename: "main" }); + }).toThrow(SyntaxError); +}); + +test("vm.Script throws SyntaxError for unterminated string", () => { + expect(() => { + new Script('"hello', { filename: "main" }); + }).toThrow(SyntaxError); +}); + +test("vm.Script throws SyntaxError for invalid token", () => { + expect(() => { + new Script("let @x = 1;", { filename: "main" }); + }).toThrow(SyntaxError); +}); + +test("vm.Script throws SyntaxError at construction, not at run time", () => { + let reachedRun = false; + try { + const script = new Script("Math.max(a, b", { filename: "main" }); + reachedRun = true; + script.runInThisContext(); + } catch (error: unknown) { + expect(error).toBeInstanceOf(SyntaxError); + } + expect(reachedRun).toBe(false); +}); + +test("vm.Script does not throw for valid syntax", () => { + expect(() => { + new Script("Math.max(1, 2)", { filename: "main" }); + }).not.toThrow(); +});