diff --git a/src/domain/javascript/childProcessOptionsRegression.test.ts b/src/domain/javascript/childProcessOptionsRegression.test.ts index 8c18fe8305..9b35a8f7ac 100644 --- a/src/domain/javascript/childProcessOptionsRegression.test.ts +++ b/src/domain/javascript/childProcessOptionsRegression.test.ts @@ -52,6 +52,36 @@ describe("child-process options detection", () => { ]); }); + it("reads fork's silent option when stdio is not a string or array", () => { + const ir = analyzeJavaScriptSemantics(` + import { fork, spawn } from "node:child_process"; + + fork("piped.js", { silent: true }); + fork("inherited.js", [], { silent: false }); + fork("nullish-stdio.js", { silent: 1, stdio: null }); + fork("explicit-stdio.js", { silent: true, stdio: "inherit" }); + fork("dynamic.js", { silent: process.env.QUIET }); + spawn("tool", { silent: true }); + fork("boolean-stdio.js", { stdio: false, silent: true }); + fork("object-stdio.js", { stdio: {}, silent: false }); + fork("unresolved-stdio.js", { stdio: streams, silent: true }); + spawn("tool", { stdio: false }); + `); + + expect(ir.childProcessSpawns.map(({ stdioMode }) => stdioMode)).toEqual([ + "pipe", + "default", + "pipe", + "inherit", + "dynamic", + "default", + "pipe", + "default", + "dynamic", + "dynamic", + ]); + }); + it("preserves behavior with explicit args arrays", () => { const ir = analyzeJavaScriptSemantics(` import { spawn, execFile, fork } from "node:child_process"; diff --git a/src/domain/javascript/javascriptSemanticChildProcesses.ts b/src/domain/javascript/javascriptSemanticChildProcesses.ts index 7d80c29297..61feb46e24 100644 --- a/src/domain/javascript/javascriptSemanticChildProcesses.ts +++ b/src/domain/javascript/javascriptSemanticChildProcesses.ts @@ -14,6 +14,7 @@ import { } from "./javascriptSemanticExpressionShapes.js"; import { semanticStaticPropertyName, + staticJavaScriptTruthiness, staticObjectProperty, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; @@ -424,15 +425,40 @@ const stdioMode = ( ) return "default"; const lookup = staticObjectProperty(expression, "stdio", context.isNullish); - if (lookup.status === "missing") return "default"; + if (lookup.status === "missing") + return unsetStdioMode(expression, method, context); if (lookup.status !== "explicit") return "dynamic"; const value = unwrapJavaScriptExpression(lookup.property.value).node; - if (context.isNullish(value)) return "default"; + if (context.isNullish(value)) + return unsetStdioMode(expression, method, context); if (t.isStringLiteral(value)) return value.value; if (t.isArrayExpression(value)) return "array"; + // fork ignores any other known stdio value and falls back to `silent`. + if (method === "fork" && staticJavaScriptTruthiness(value) !== undefined) + return unsetStdioMode(expression, method, context); return "dynamic"; }; +/** + * Without a string or array `stdio`, Node's `fork` pipes the standard streams + * when `silent` is truthy and inherits them otherwise. + */ +const unsetStdioMode = ( + options: t.Node, + method: JavaScriptSemanticChildProcessSpawn["method"], + context: ChildSpawnContext, +): string => { + if (method !== "fork") return "default"; + const silent = staticObjectProperty(options, "silent", context.isNullish); + if (silent.status === "missing") return "default"; + if (silent.status !== "explicit") return "dynamic"; + const value = unwrapJavaScriptExpression(silent.property.value).node; + if (context.isNullish(value)) return "default"; + const truthy = staticJavaScriptTruthiness(value); + if (truthy === undefined) return "dynamic"; + return truthy ? "pipe" : "default"; +}; + const objectBinding = ( node: t.Expression | t.Super, state: JavaScriptSemanticAnalysisState,