fix(javascript): report fork's silent stdio as piped (#7083)

* fix(javascript): report fork's silent stdio as piped

Node's child_process.fork pipes stdin, stdout and stderr when options.silent is truthy and options.stdio is neither a string nor an array, and inherits them otherwise. REA never read silent, so fork("worker.js", { silent: true }) was reported with stdio_mode default (inherited). The unset-stdio case of fork now reads silent: truthy gives pipe, falsy or missing gives default, and an unresolved value gives dynamic.

* fix(javascript): apply fork's silent rule to other known stdio values
This commit is contained in:
Adolanium authored and GitHub committed 2026-10-25 14:23:46 +08:00
1 parent c74d302cc4
commit 80673b22a4
2 files changed
+58 -2

No files matched your search

@@ -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";
@@ -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,