diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index ce39ea85cff8..7c18b5f6ad31 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -2431,16 +2431,19 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb RETURN_IF_EXCEPTION(scope, {}); } - header->putDirect(vm, JSC::Identifier::fromString(vm, "commandLine"_s), JSValue::decode(Bun__Process__createExecArgv(globalObject)), 0); + JSValue commandLine = JSValue::decode(Bun__Process__createExecArgv(globalObject)); RETURN_IF_EXCEPTION(scope, {}); + header->putDirect(vm, JSC::Identifier::fromString(vm, "commandLine"_s), commandLine, 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "nodejsVersion"_s), JSC::jsString(vm, String::fromLatin1(REPORTED_NODEJS_VERSION)), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "wordSize"_s), JSC::jsNumber(64), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), constructArch(vm, header), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), constructPlatform(vm, header), 0); - header->putDirect(vm, JSC::Identifier::fromString(vm, "componentVersions"_s), constructVersions(vm, header), 0); + JSValue componentVersions = constructVersions(vm, header); RETURN_IF_EXCEPTION(scope, {}); - header->putDirect(vm, JSC::Identifier::fromString(vm, "release"_s), constructProcessReleaseObject(vm, header), 0); + header->putDirect(vm, JSC::Identifier::fromString(vm, "componentVersions"_s), componentVersions, 0); + JSValue release = constructProcessReleaseObject(vm, header); RETURN_IF_EXCEPTION(scope, {}); + header->putDirect(vm, JSC::Identifier::fromString(vm, "release"_s), release, 0); { // uname @@ -2474,10 +2477,12 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb #endif #endif - header->putDirect(vm, Identifier::fromString(vm, "cpus"_s), JSC::constructEmptyArray(globalObject, nullptr), 0); + auto* cpusArray = JSC::constructEmptyArray(globalObject, nullptr); RETURN_IF_EXCEPTION(scope, {}); - header->putDirect(vm, Identifier::fromString(vm, "networkInterfaces"_s), JSC::constructEmptyArray(globalObject, nullptr), 0); + header->putDirect(vm, Identifier::fromString(vm, "cpus"_s), cpusArray, 0); + auto* networkInterfacesArray = JSC::constructEmptyArray(globalObject, nullptr); RETURN_IF_EXCEPTION(scope, {}); + header->putDirect(vm, Identifier::fromString(vm, "networkInterfaces"_s), networkInterfacesArray, 0); return header; }; @@ -2653,32 +2658,45 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb JSC::JSObject* report = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 19); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "header"_s), constructHeader(), 0); + JSValue header = constructHeader(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "javascriptStack"_s), constructJavaScriptStack(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "header"_s), header, 0); + JSValue javascriptStack = constructJavaScriptStack(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "javascriptHeap"_s), constructJavaScriptHeap(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "javascriptStack"_s), javascriptStack, 0); + JSValue javascriptHeap = constructJavaScriptHeap(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "nativeStack"_s), constructNativeStack(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "javascriptHeap"_s), javascriptHeap, 0); + JSValue nativeStack = constructNativeStack(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "resourceUsage"_s), constructResourceUsage(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "nativeStack"_s), nativeStack, 0); + JSValue resourceUsage = constructResourceUsage(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "uvthreadResourceUsage"_s), constructUVThreadResourceUsage(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "resourceUsage"_s), resourceUsage, 0); + JSValue uvthreadResourceUsage = constructUVThreadResourceUsage(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "libuv"_s), constructLibUV(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "uvthreadResourceUsage"_s), uvthreadResourceUsage, 0); + JSValue libuv = constructLibUV(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "workers"_s), constructWorkers(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "libuv"_s), libuv, 0); + JSValue workers = constructWorkers(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "environmentVariables"_s), constructEnvironmentVariables(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "workers"_s), workers, 0); + JSValue environmentVariables = constructEnvironmentVariables(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "userLimits"_s), constructUserLimits(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "environmentVariables"_s), environmentVariables, 0); + JSValue userLimits = constructUserLimits(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "sharedObjects"_s), constructSharedObjects(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "userLimits"_s), userLimits, 0); + JSValue sharedObjects = constructSharedObjects(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "cpus"_s), constructCpus(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "sharedObjects"_s), sharedObjects, 0); + JSValue cpus = constructCpus(); RETURN_IF_EXCEPTION(scope, {}); - report->putDirect(vm, JSC::Identifier::fromString(vm, "networkInterfaces"_s), constructNetworkInterfaces(), 0); + report->putDirect(vm, JSC::Identifier::fromString(vm, "cpus"_s), cpus, 0); + JSValue networkInterfaces = constructNetworkInterfaces(); RETURN_IF_EXCEPTION(scope, {}); + report->putDirect(vm, JSC::Identifier::fromString(vm, "networkInterfaces"_s), networkInterfaces, 0); return report; } diff --git a/src/jsc/bindings/BunProcessReportObjectWindows.cpp b/src/jsc/bindings/BunProcessReportObjectWindows.cpp index 6d6e4428f3e6..e6ebc572ed75 100644 --- a/src/jsc/bindings/BunProcessReportObjectWindows.cpp +++ b/src/jsc/bindings/BunProcessReportObjectWindows.cpp @@ -82,8 +82,9 @@ JSValue constructReportObjectWindows(VM& vm, Zig::GlobalObject* globalObject, Pr } // Command line - header->putDirect(vm, Identifier::fromString(vm, "commandLine"_s), JSValue::decode(Bun__Process__createExecArgv(globalObject)), 0); + JSValue commandLine = JSValue::decode(Bun__Process__createExecArgv(globalObject)); RETURN_IF_EXCEPTION(scope, {}); + header->putDirect(vm, Identifier::fromString(vm, "commandLine"_s), commandLine, 0); // Node version header->putDirect(vm, Identifier::fromString(vm, "nodejsVersion"_s), jsString(vm, String::fromLatin1(REPORTED_NODEJS_VERSION)), 0); @@ -177,7 +178,9 @@ JSValue constructReportObjectWindows(VM& vm, Zig::GlobalObject* globalObject, Pr } header->putDirect(vm, Identifier::fromString(vm, "cpus"_s), cpuArray, 0); } else { - header->putDirect(vm, Identifier::fromString(vm, "cpus"_s), constructEmptyArray(globalObject, nullptr), 0); + JSArray* emptyCpus = constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + header->putDirect(vm, Identifier::fromString(vm, "cpus"_s), emptyCpus, 0); } RETURN_IF_EXCEPTION(scope, {}); @@ -231,7 +234,9 @@ JSValue constructReportObjectWindows(VM& vm, Zig::GlobalObject* globalObject, Pr } header->putDirect(vm, Identifier::fromString(vm, "networkInterfaces"_s), interfacesArray, 0); } else { - header->putDirect(vm, Identifier::fromString(vm, "networkInterfaces"_s), constructEmptyArray(globalObject, nullptr), 0); + JSArray* emptyInterfaces = constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + header->putDirect(vm, Identifier::fromString(vm, "networkInterfaces"_s), emptyInterfaces, 0); } report->putDirect(vm, Identifier::fromString(vm, "header"_s), header, 0); @@ -384,16 +389,19 @@ JSValue constructReportObjectWindows(VM& vm, Zig::GlobalObject* globalObject, Pr } // Native stack (empty for now) - report->putDirect(vm, Identifier::fromString(vm, "nativeStack"_s), constructEmptyArray(globalObject, nullptr), 0); + JSArray* nativeStack = constructEmptyArray(globalObject, nullptr); RETURN_IF_EXCEPTION(scope, {}); + report->putDirect(vm, Identifier::fromString(vm, "nativeStack"_s), nativeStack, 0); // libuv (empty for now) - report->putDirect(vm, Identifier::fromString(vm, "libuv"_s), constructEmptyArray(globalObject, nullptr), 0); + JSArray* libuvArray = constructEmptyArray(globalObject, nullptr); RETURN_IF_EXCEPTION(scope, {}); + report->putDirect(vm, Identifier::fromString(vm, "libuv"_s), libuvArray, 0); // Workers (empty for now) - report->putDirect(vm, Identifier::fromString(vm, "workers"_s), constructEmptyArray(globalObject, nullptr), 0); + JSArray* workersArray = constructEmptyArray(globalObject, nullptr); RETURN_IF_EXCEPTION(scope, {}); + report->putDirect(vm, Identifier::fromString(vm, "workers"_s), workersArray, 0); // Environment variables report->putDirect(vm, Identifier::fromString(vm, "environmentVariables"_s), globalObject->processEnvObject(), 0); diff --git a/src/jsc/bindings/NodeVMModule.cpp b/src/jsc/bindings/NodeVMModule.cpp index 5be76aa6e04e..16e0dedf6d25 100644 --- a/src/jsc/bindings/NodeVMModule.cpp +++ b/src/jsc/bindings/NodeVMModule.cpp @@ -578,7 +578,10 @@ JSC_DEFINE_HOST_FUNCTION(jsNodeVmModuleGetModuleRequests, (JSC::JSGlobalObject * RETURN_IF_EXCEPTION(scope, {}); for (unsigned i = 0; const NodeVMModuleRequest& request : requests) { - array->putDirectIndex(globalObject, i++, request.toJS(globalObject)); + // toJS's RETURN_IF_EXCEPTION services VM traps, so termination can surface there as a null return. + auto* requestValue = request.toJS(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + array->putDirectIndex(globalObject, i++, requestValue); RETURN_IF_EXCEPTION(scope, {}); } diff --git a/src/jsc/bindings/Path.cpp b/src/jsc/bindings/Path.cpp index 91af4a73079f..adf9149e4ceb 100644 --- a/src/jsc/bindings/Path.cpp +++ b/src/jsc/bindings/Path.cpp @@ -141,15 +141,14 @@ JSC::JSValue createNodePathBinding(Zig::GlobalObject* globalObject) auto scope = DECLARE_THROW_SCOPE(vm); auto binding = constructEmptyArray(globalObject, nullptr, 2); RETURN_IF_EXCEPTION(scope, {}); - binding->putDirectIndex( - globalObject, - (unsigned)0, - Zig::createPath(globalObject, false)); + // createPath's RETURN_IF_EXCEPTION services VM traps, so worker termination can surface there as a null return. + auto* posixPath = Zig::createPath(globalObject, false); RETURN_IF_EXCEPTION(scope, {}); - binding->putDirectIndex( - globalObject, - (unsigned)1, - Zig::createPath(globalObject, true)); + binding->putDirectIndex(globalObject, (unsigned)0, posixPath); + RETURN_IF_EXCEPTION(scope, {}); + auto* windowsPath = Zig::createPath(globalObject, true); + RETURN_IF_EXCEPTION(scope, {}); + binding->putDirectIndex(globalObject, (unsigned)1, windowsPath); RETURN_IF_EXCEPTION(scope, {}); return binding; } diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index c084a885ee86..1e4bbed24040 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5179,12 +5179,14 @@ void JSC__VM__ensureTerminationExceptionPending(JSC::VM* arg0) JSC::VM& vm = *arg0; if (vm.hasPendingTerminationException()) return; + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); if (!vm.hasTerminationRequest() && !vm.traps().needHandling(JSC::VMTraps::NeedTermination)) vm.notifyNeedTermination(); if (vm.hasTerminationRequest()) vm.throwTerminationException(); else vm.traps().handleTraps(JSC::VMTraps::NeedTermination); + scope.releaseAssertNoExceptionExceptTermination(); } // These may be called concurrently from another thread. diff --git a/test/internal/source-lints/putdirect-unchecked-helper.test.ts b/test/internal/source-lints/putdirect-unchecked-helper.test.ts new file mode 100644 index 000000000000..b9c476182594 --- /dev/null +++ b/test/internal/source-lints/putdirect-unchecked-helper.test.ts @@ -0,0 +1,263 @@ +import { file } from "bun"; +import { expect, test } from "bun:test"; +import { realpathSync } from "fs"; +import path from "path"; +import { globAllSources } from "../../../scripts/glob-sources.ts"; + +// A JSC helper that allocates under its own ThrowScope returns null with an +// exception pending when anything inside it throws. Because RETURN_IF_EXCEPTION +// services VM traps, "anything" includes a worker termination request or a vm +// watchdog timeout surfacing at one of the helper's internal checks, so the +// null return is reachable on paths that never visibly throw. Passing such a +// call inline as the value argument of putDirect/putDirectIndex consumes the +// empty JSValue before the caller's RETURN_IF_EXCEPTION runs; putDirectIndex +// dereferences the value in ASSERT(!value.isCustomGetterSetterSlow()) and +// crashes (this was a fuzzer-caught SEGV in createNodePathBinding). +// +// The fix shape, per REVIEW.md ("exception checks after every call that can +// enter JS ... before its result is used"): +// JSValue v = helper(globalObject, ...); +// RETURN_IF_EXCEPTION(scope, {}); +// obj->putDirectIndex(globalObject, i, v); +// +// The rules below name the helper families that can return an empty value: +// create*/.toJS taking a global object as the first argument (including +// underscore-qualified extern shims like Bun__Process__createExecArgv, whose +// Rust side returns JSValue::ZERO on a thrown or terminated error), +// constructEmptyArray (which has RETURN_IF_EXCEPTION(scope, nullptr) inside), +// and zero-argument construct*/create* calls (the scope-capturing local lambdas +// process.report builds its sections with). +// +// vm-first calls are deliberately not flagged: the common vm-first builders +// (jsString, jsNumber, constructArch, constructInternalProperty) are infallible, +// and the name alone cannot tell the rare fallible ones apart (constructVersions +// and constructProcessReleaseObject carried scopes and were hoisted by hand), so +// a rule would be mostly false positives. A vm-first helper that gains a throw +// scope needs its inline call sites hoisted by review. The Rust flavor of the +// same invariant is linted by empty-jsvalue-laundering.test.ts. + +const root = path.resolve(import.meta.dir, "..", "..", ".."); +const cxxSources = globAllSources().cxx; + +const tracked: Set | null = (() => { + const r = Bun.spawnSync({ + cmd: ["git", "-C", root, "ls-tree", "-r", "--name-only", "-z", "HEAD"], + stdout: "pipe", + stderr: "ignore", + }); + if (!r.success) return null; + return new Set(r.stdout.toString().split("\0").filter(Boolean)); +})(); + +// A putDirect/putDirectIndex/putDirectOffset call statement; [^;] lets the +// argument list span lines, ending at the `);` that closes the statement. +const PUT_CALL = /\bputDirect(?:Index|Offset)?\s*\(([^;]*?)\)\s*;/g; + +const GLOBALISH = String.raw`(?:globalObject|globalThis|lexicalGlobalObject)\b`; +const BANNED: { name: string; re: RegExp; hint: string }[] = [ + { + name: "create*(globalObject, ...) inline", + re: new RegExp(String.raw`(?:^|[\s(,!&*]|->|\.|::)(?:\w+__)?create[A-Z]\w*\s*\(\s*${GLOBALISH}`), + hint: "hoist into a local and RETURN_IF_EXCEPTION before the putDirect*", + }, + { + name: ".toJS(globalObject, ...) inline", + re: new RegExp(String.raw`(?:->|\.)\s*toJS\s*\(\s*${GLOBALISH}`), + hint: "hoist into a local and RETURN_IF_EXCEPTION before the putDirect*", + }, + { + name: "constructEmptyArray(...) inline", + re: /(?:^|[\s(,!&*]|::)constructEmptyArray\s*\(/, + hint: "constructEmptyArray returns null with an exception pending; hoist and RETURN_IF_EXCEPTION before the putDirect*", + }, + { + name: "construct*()/create*() inline", + re: /(?:^|[\s(,!&*])(?:construct|create)[A-Z]\w*\s*\(\s*\)/, + hint: "a zero-arg scope-capturing helper returns an empty JSValue on exception; hoist and RETURN_IF_EXCEPTION before the putDirect*", + }, +]; + +// Blank out comments and the contents of string/char literals, preserving every +// newline so reported line numbers match the original file, and preserving code +// after a literal that happens to contain "//" or "/*". +function stripForScan(content: string): string { + let out = ""; + let i = 0; + const n = content.length; + while (i < n) { + const c = content[i]!; + const next = i + 1 < n ? content[i + 1] : ""; + if (c === "/" && next === "/") { + while (i < n && content[i] !== "\n") i++; + continue; + } + if (c === "/" && next === "*") { + out += " "; + i += 2; + while (i < n) { + if (content[i] === "*" && content[i + 1] === "/") { + out += " "; + i += 2; + break; + } + out += content[i] === "\n" ? "\n" : " "; + i++; + } + continue; + } + // raw string literal: R"delim( ... )delim" + if (c === "R" && next === '"' && !/\w/.test(content[i - 1] ?? "")) { + const open = content.indexOf("(", i + 2); + const delim = open === -1 ? null : content.slice(i + 2, open); + const end = delim === null ? -1 : content.indexOf(`)${delim}"`, open + 1); + if (delim !== null && end !== -1) { + const stop = end + delim.length + 2; + out += content.slice(i, stop).replace(/[^\n]/g, " "); + i = stop; + continue; + } + } + if (c === '"' || (c === "'" && !/\d/.test(content[i - 1] ?? ""))) { + out += c; + i++; + while (i < n && content[i] !== c && content[i] !== "\n") { + out += content[i] === "\\" ? " " : " "; + i += content[i] === "\\" ? 2 : 1; + } + if (i < n && content[i] === c) { + out += c; + i++; + } + continue; + } + out += c; + i++; + } + return out; +} + +function scanSource(content: string): { line: number; name: string; hint: string }[] { + const stripped = stripForScan(content); + const found: { line: number; name: string; hint: string }[] = []; + for (const m of stripped.matchAll(PUT_CALL)) { + const argText = m[1]!; + for (const { name, re, hint } of BANNED) { + if (re.test(argText)) { + const line = stripped.slice(0, m.index).split("\n").length; + found.push({ line, name, hint }); + } + } + } + return found; +} + +const offenders: string[] = []; +let scanned = 0; +for (const abs of cxxSources) { + const source = path.relative(root, abs).replaceAll(path.sep, "/"); + if (path.relative(root, realpathSync(abs)).replaceAll(path.sep, "/") !== source) continue; + if (tracked !== null && !tracked.has(source)) continue; + scanned++; + for (const { line, name, hint } of scanSource(await file(abs).text())) { + offenders.push(`${source}:${line}: ${name} → ${hint}`); + } +} + +// Self-test fixtures: every rule must fire on its minimal offending shape (so a +// broken regex or a no-op scanner cannot silently pass) and must stay quiet on +// the hoisted fix shape and on lookalikes that cannot return an empty value. +const detects: { label: string; rule: string; line: number; cpp: string }[] = [ + { + label: "create* taking the global object first", + rule: "create*(globalObject, ...) inline", + line: 2, + cpp: `void f() {\n obj->putDirectIndex(globalObject, 0, createFoo(globalObject, false));\n}\n`, + }, + { + label: "a call spanning multiple lines", + rule: "create*(globalObject, ...) inline", + line: 2, + cpp: `void f() {\n binding->putDirectIndex(\n globalObject,\n (unsigned)0,\n Zig::createPath(globalObject, false));\n}\n`, + }, + { + label: "method .toJS taking the global object", + rule: ".toJS(globalObject, ...) inline", + line: 2, + cpp: `void f() {\n array->putDirectIndex(globalObject, i++, request.toJS(globalObject));\n}\n`, + }, + { + label: "constructEmptyArray", + rule: "constructEmptyArray(...) inline", + line: 2, + cpp: `void f() {\n header->putDirect(vm, name, constructEmptyArray(globalObject, nullptr), 0);\n}\n`, + }, + { + label: "a zero-arg section builder", + rule: "construct*()/create*() inline", + line: 2, + cpp: `void f() {\n report->putDirect(vm, name, constructHeader(), 0);\n}\n`, + }, + { + label: "an offender after a string literal containing //", + rule: "construct*()/create*() inline", + line: 2, + cpp: `void f() {\n obj->putDirect(vm, Identifier::fromString(vm, "https://bun.com"_s), constructHeader(), 0);\n}\n`, + }, + { + label: "an offender after a multi-line block comment, at its real line", + rule: "create*(globalObject, ...) inline", + line: 6, + cpp: `/*\n * license\n * header\n */\nvoid f() {\n obj->putDirectIndex(globalObject, 0, createFoo(globalObject));\n}\n`, + }, + { + label: "an underscore-qualified extern create* shim", + rule: "create*(globalObject, ...) inline", + line: 2, + cpp: `void f() {\n header->putDirect(vm, name, JSValue::decode(Bun__Process__createExecArgv(globalObject)), 0);\n}\n`, + }, +]; +const ignores: { label: string; cpp: string }[] = [ + { + label: "the hoisted and checked fix shape", + cpp: `void f() {\n auto* v = createFoo(globalObject);\n RETURN_IF_EXCEPTION(scope, {});\n obj->putDirectIndex(globalObject, 0, v);\n}\n`, + }, + { + label: "a vm-first helper, which cannot return empty", + cpp: `void f() {\n arr->putDirectIndex(globalObject, 0, createMockResult(vm, globalObject, type, value));\n}\n`, + }, + { + label: "jsString(vm, ...) inline", + cpp: `void f() {\n obj->putDirect(vm, name, jsString(vm, s), 0);\n}\n`, + }, + { + label: "a banned shape inside a comment", + cpp: `void f() {\n // obj->putDirectIndex(globalObject, 0, createFoo(globalObject));\n obj->putDirectIndex(globalObject, 0, v);\n}\n`, + }, + { + label: "a banned shape inside a string literal", + cpp: `void f() {\n obj->putDirect(vm, name, jsString(vm, "createFoo(globalObject)"_s), 0);\n}\n`, + }, + { + label: "a vm-first construct* builder, which this lint does not model", + cpp: `void f() {\n array->putDirectIndex(exec, i++, constructInternalProperty(vm, exec, name, value));\n}\n`, + }, +]; + +for (const { label, rule, line, cpp } of detects) { + test(`detects ${label}`, () => { + expect(scanSource(cpp)).toEqual([{ line, name: rule, hint: expect.any(String) }]); + }); +} +for (const { label, cpp } of ignores) { + test(`ignores ${label}`, () => { + expect(scanSource(cpp)).toEqual([]); + }); +} + +test("scans a non-empty set of tracked C++ sources", () => { + expect(scanned).toBeGreaterThan(0); +}); + +test("no throwing helper is consumed inline by putDirect* before its exception check", () => { + expect(offenders).toEqual([]); +});