diff --git a/README.md b/README.md index c4a89bceee..05b4179598 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ Report vulnerabilities through [SECURITY.md](SECURITY.md). ## Star history -🎉 **170,000 GitHub stars — thank you!** +🎉 **180,000 GitHub stars — thank you!** Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes. diff --git a/src/domain/javascript/childProcessOptionsRegression.test.ts b/src/domain/javascript/childProcessOptionsRegression.test.ts new file mode 100644 index 0000000000..8c18fe8305 --- /dev/null +++ b/src/domain/javascript/childProcessOptionsRegression.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from "vitest"; +import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; + +describe("child-process options detection", () => { + it("detects options without an args array", () => { + const ir = analyzeJavaScriptSemantics(` + import { spawn, execFile, fork } from "node:child_process"; + + spawn("tool", { + env: { A: "1" }, + stdio: "pipe" + }); + + execFile("tool", { + env: { A: "1" } + }); + + fork("worker.js", { + env: { A: "1" }, + stdio: ["pipe", "pipe", "pipe", "ipc"] + }); + `); + + expect( + ir.childProcessSpawns.map( + ({ method, environmentSupplied, stdioMode, resolution }) => ({ + method, + environmentSupplied, + stdioMode, + resolution, + }), + ), + ).toEqual([ + { + method: "spawn", + environmentSupplied: true, + stdioMode: "pipe", + resolution: "complete", + }, + { + method: "execFile", + environmentSupplied: true, + stdioMode: "default", + resolution: "complete", + }, + { + method: "fork", + environmentSupplied: true, + stdioMode: "array", + resolution: "complete", + }, + ]); + }); + + it("preserves behavior with explicit args arrays", () => { + const ir = analyzeJavaScriptSemantics(` + import { spawn, execFile, fork } from "node:child_process"; + + spawn("tool", [], { env: { A: "1" }, stdio: "pipe" }); + execFile("tool", [], { env: { A: "1" } }); + fork("worker.js", [], { + env: { A: "1" }, + stdio: ["pipe", "pipe", "pipe", "ipc"] + }); + `); + + expect( + ir.childProcessSpawns.map(({ environmentSupplied, stdioMode }) => ({ + environmentSupplied, + stdioMode, + })), + ).toEqual([ + { environmentSupplied: true, stdioMode: "pipe" }, + { environmentSupplied: true, stdioMode: "default" }, + { environmentSupplied: true, stdioMode: "array" }, + ]); + }); + + it("does not invent options when omitted", () => { + const ir = analyzeJavaScriptSemantics(` + import { spawn, execFile, fork } from "node:child_process"; + + spawn("tool"); + execFile("tool"); + fork("worker.js"); + `); + + expect( + ir.childProcessSpawns.every( + ({ environmentSupplied, stdioMode }) => + environmentSupplied === false && stdioMode === "default", + ), + ).toBe(true); + + expect(ir.childProcessSpawns).toHaveLength(3); + }); +}); diff --git a/src/domain/javascript/javascriptSemanticChildProcesses.ts b/src/domain/javascript/javascriptSemanticChildProcesses.ts index fe7b5e8022..0fd0c9a35f 100644 --- a/src/domain/javascript/javascriptSemanticChildProcesses.ts +++ b/src/domain/javascript/javascriptSemanticChildProcesses.ts @@ -324,13 +324,18 @@ const resolveChildCandidates = ( ? [] : (spawnsByBinding.get(binding.bindingId) ?? []); }; - const spawnOptions = (candidate: SpawnCandidate): t.ObjectExpression | null => { - const index = candidate.method === "exec" ? 1 : 2; - const value = candidate.node.arguments[index]; + const args = candidate.node.arguments; + + const value = + candidate.method === "exec" + ? args[1] + : t.isObjectExpression(args[1]) + ? args[1] + : args[2]; + return t.isObjectExpression(value) ? value : null; }; - const spawnArgvCount = (candidate: SpawnCandidate): number | null => { if (candidate.method === "exec") return null; const args = candidate.node.arguments[1];