mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(javascript): ignore async exec stdio options (#7087)
Respect Node asynchronous exec/execFile method-default stdio instead of interpreting an ignored option. Remove the superseded callback-shape query while preserving caller input, Evidence, uncertainty, argv and fork controls. Independently validated on current main through actual Node pipe capture, matching real-app CLI/MCP facts, 81 targeted tests and static checking. Closes #7217.
This commit is contained in:
1 parent
a84e0cfcfe
commit
625e32744e
4 files changed
+76
-89
No files matched your search
@@ -416,14 +416,11 @@ const stdioMode = (
|
||||
method: JavaScriptSemanticChildProcessSpawn["method"],
|
||||
context: ChildSpawnContext,
|
||||
): string => {
|
||||
// Async exec/execFile do not forward options.stdio to their underlying spawn.
|
||||
if (method === "exec" || method === "execFile") return "default";
|
||||
if (options === undefined) return "default";
|
||||
const expression = unwrapJavaScriptExpression(options).node;
|
||||
if (
|
||||
context.isNullish(expression) ||
|
||||
((method === "exec" || method === "execFile") &&
|
||||
context.isCallback(expression))
|
||||
)
|
||||
return "default";
|
||||
if (context.isNullish(expression)) return "default";
|
||||
const lookup = staticObjectProperty(expression, "stdio", context.isNullish);
|
||||
if (lookup.status === "missing")
|
||||
return unsetStdioMode(expression, method, context);
|
||||
|
||||
@@ -13,9 +13,7 @@ import {
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
|
||||
it.each([
|
||||
["function f(){read(cb);function cb(){}}", ["callback"]],
|
||||
["function f(){const cb=()=>{};const alias=cb;read(alias)}", ["callback"]],
|
||||
["function f(cb){cb=()=>{};arguments[0]={};read(cb)}", ["unknown"]],
|
||||
["function f(a){a=[];arguments[0]={};read(a)}", ["unknown"]],
|
||||
["declare let erased:any;function f(){const a=[];read(a)}", ["unknown"]],
|
||||
["namespace X{};function f(){const a=[];read(a)}", ["unknown"]],
|
||||
["var a=[];(()=>{}).constructor('a={}')();read(a);", ["unknown"]],
|
||||
@@ -60,11 +58,9 @@ it.each([
|
||||
shapes.push(
|
||||
resolver.isArray(arg)
|
||||
? "array"
|
||||
: resolver.isCallback(arg)
|
||||
? "callback"
|
||||
: resolver.isNullish(arg)
|
||||
? "nullish"
|
||||
: "unknown",
|
||||
: resolver.isNullish(arg)
|
||||
? "nullish"
|
||||
: "unknown",
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -72,10 +68,10 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("bounds long callback aliases without invoking the shared callable resolver", () => {
|
||||
const source = `export function run(){const cb=()=>{};
|
||||
${Array.from({ length: 4096 }, (_, i) => `const a${i}=${i === 0 ? "cb" : `a${i - 1}`};`).join("\n")}
|
||||
read(a4095);read(a10);read(cb);}`;
|
||||
it("bounds long array aliases without invoking the shared callable resolver", () => {
|
||||
const source = `export function run(){const argv=[];
|
||||
${Array.from({ length: 4096 }, (_, i) => `const a${i}=${i === 0 ? "argv" : `a${i - 1}`};`).join("\n")}
|
||||
read(a4095);read(a10);read(argv);}`;
|
||||
const file = parseJavaScriptSource(source, "deep.mjs");
|
||||
if (file === null) throw new Error("Expected valid deep source");
|
||||
const state = completeSemanticSteps(
|
||||
@@ -89,7 +85,7 @@ it("bounds long callback aliases without invoking the shared callable resolver",
|
||||
t.isCallExpression(node) &&
|
||||
t.isIdentifier(node.callee, { name: "read" })
|
||||
)
|
||||
results.push(resolver.isCallback(node.arguments[0]!));
|
||||
results.push(resolver.isArray(node.arguments[0]!));
|
||||
},
|
||||
});
|
||||
expect(results).toEqual([false, true, true]);
|
||||
@@ -97,55 +93,30 @@ it("bounds long callback aliases without invoking the shared callable resolver",
|
||||
});
|
||||
|
||||
it.each([
|
||||
["fixture.cjs", "const argv=[];const done=()=>{};", ["inherit", "default"]],
|
||||
["fixture.cts", "const argv=[];const done=()=>{};", ["inherit", "default"]],
|
||||
["fixture.cjs", "const argv=[];", ["inherit"]],
|
||||
["fixture.cts", "const argv=[];", ["inherit"]],
|
||||
["fixture.cjs", "var argv=[];{function argv(){}}", ["dynamic"]],
|
||||
["fixture.mjs", "var argv=[];{function argv(){}}", ["inherit"]],
|
||||
["fixture.js", "const argv=[];", ["dynamic"]],
|
||||
["fixture.cjs", "let argv=[];argv=input;", ["dynamic"]],
|
||||
["fixture.cts", "const argv=[];eval(input);", ["inherit"]],
|
||||
["fixture.cts", "namespace N{};const argv=[];", ["dynamic"]],
|
||||
[
|
||||
"fixture.cjs",
|
||||
"var argv=[];{function argv(){}}const done=()=>{};",
|
||||
["dynamic", "default"],
|
||||
],
|
||||
[
|
||||
"fixture.mjs",
|
||||
"var argv=[];{function argv(){}}const done=()=>{};",
|
||||
["inherit", "default"],
|
||||
],
|
||||
["fixture.js", "const argv=[];const done=()=>{};", ["dynamic", "dynamic"]],
|
||||
[
|
||||
"fixture.cjs",
|
||||
"let argv=[];argv=input;const done=()=>{};",
|
||||
["dynamic", "default"],
|
||||
],
|
||||
[
|
||||
"fixture.cts",
|
||||
"const argv=[];const done=()=>{};eval(input);",
|
||||
["inherit", "default"],
|
||||
],
|
||||
[
|
||||
"fixture.cts",
|
||||
"namespace N{};const argv=[];const done=()=>{};",
|
||||
["dynamic", "dynamic"],
|
||||
],
|
||||
[
|
||||
"fixture.cjs",
|
||||
"var module=[];const argv=module;const done=()=>{};const args=arguments;args[2]={};",
|
||||
["dynamic", "default"],
|
||||
],
|
||||
[
|
||||
"fixture.cts",
|
||||
"const argv=[];function exports(){};const args=arguments;args[0]={};const done=exports;",
|
||||
["inherit", "dynamic"],
|
||||
"var module=[];const argv=module;const args=arguments;args[2]={};",
|
||||
["dynamic"],
|
||||
],
|
||||
] as const)(
|
||||
"distinguishes wrapper-local child identities and uncertain bindings: %s (%s)",
|
||||
(path, declarations, expected) => {
|
||||
const loader = path.endsWith(".mjs")
|
||||
? 'import {spawn,exec} from "node:child_process";'
|
||||
: 'const {spawn,exec}=require("node:child_process");';
|
||||
? 'import {spawn} from "node:child_process";'
|
||||
: 'const {spawn}=require("node:child_process");';
|
||||
const file = parseJavaScriptSource(
|
||||
`
|
||||
${loader}
|
||||
${declarations}
|
||||
spawn("argv",argv,{stdio:"inherit"}); exec("callback",done);
|
||||
spawn("argv",argv,{stdio:"inherit"});
|
||||
`,
|
||||
path,
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import { javaScriptRuntimeContext } from "./javascriptSourceParser.js";
|
||||
|
||||
type Shape = "array" | "callback" | "unknown";
|
||||
type Shape = "array" | "unknown";
|
||||
|
||||
/** Bounded argument-shape queries shared by call-signature owners. */
|
||||
export type JavaScriptExpressionShapeResolver = ReturnType<
|
||||
@@ -41,15 +41,6 @@ const commonJsWrapperParameterNames = new Set([
|
||||
export const createSemanticExpressionShapeResolver = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
) => {
|
||||
const hoistedFunction = (node: t.Node): boolean => {
|
||||
const parent = state.parentsByNode.get(node);
|
||||
return (
|
||||
t.isFunctionDeclaration(node) &&
|
||||
(t.isProgram(parent) ||
|
||||
(t.isBlockStatement(parent) &&
|
||||
t.isFunction(state.parentsByNode.get(parent))))
|
||||
);
|
||||
};
|
||||
// Most modules have no named child arguments. Collect source-wide blockers
|
||||
// only when a query actually needs a named binding or ambient undefined.
|
||||
let blocked: boolean | undefined;
|
||||
@@ -80,8 +71,6 @@ export const createSemanticExpressionShapeResolver = (
|
||||
if (seen.has(node)) return "unknown";
|
||||
seen.add(node);
|
||||
if (t.isArrayExpression(node)) return "array";
|
||||
if (t.isFunction(node) && !("declare" in node && node.declare))
|
||||
return "callback";
|
||||
if (!t.isIdentifier(node)) return "unknown";
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
if (binding === undefined || binding.referenceInitializers.length > 0)
|
||||
@@ -106,11 +95,7 @@ export const createSemanticExpressionShapeResolver = (
|
||||
if (initializer === undefined || initializer.projection.length > 0)
|
||||
return "unknown";
|
||||
const read = semanticBindingAtRead(binding, node, state.parentsByNode);
|
||||
// Hoisted function declarations are the only accepted unordered source.
|
||||
if (
|
||||
!hoistedFunction(initializer.node) &&
|
||||
(read.status !== "ordered" || !read.hasDefiniteInitializer)
|
||||
)
|
||||
if (read.status !== "ordered" || !read.hasDefiniteInitializer)
|
||||
return "unknown";
|
||||
node = initializer.node;
|
||||
depth++;
|
||||
@@ -118,7 +103,6 @@ export const createSemanticExpressionShapeResolver = (
|
||||
};
|
||||
return {
|
||||
isArray: (node: t.Node) => shape(node) === "array",
|
||||
isCallback: (node: t.Node) => shape(node) === "callback",
|
||||
isNullish: (input: t.Node) => {
|
||||
const { node } = unwrapJavaScriptExpression(input);
|
||||
return (
|
||||
|
||||
@@ -102,6 +102,33 @@ const cases = [
|
||||
0,
|
||||
],
|
||||
['spawn("missing-stdio", [])', "missing-stdio", "default", 0],
|
||||
// Async exec/execFile ignore stdio even when the supplied value is dynamic.
|
||||
['exec("exec-inherit", {stdio:"inherit"})', "exec-inherit", "default", null],
|
||||
[
|
||||
'execFile("execfile-ignore", {stdio:"ignore"})',
|
||||
"execfile-ignore",
|
||||
"default",
|
||||
null,
|
||||
],
|
||||
[
|
||||
'execFile("execfile-array", [], {stdio:["inherit","inherit","inherit"]})',
|
||||
"execfile-array",
|
||||
"default",
|
||||
0,
|
||||
],
|
||||
[
|
||||
'execFile("execfile-dynamic", ["one"], {stdio:input,env:{A:"1"}})',
|
||||
"execfile-dynamic",
|
||||
"default",
|
||||
1,
|
||||
],
|
||||
['fork("fork-silent", [], {silent:true})', "fork-silent", "pipe", 0],
|
||||
[
|
||||
'fork("fork-explicit", [], {silent:true,stdio:"inherit"})',
|
||||
"fork-explicit",
|
||||
"inherit",
|
||||
0,
|
||||
],
|
||||
['exec("null-stdio", {stdio:null})', "null-stdio", "default", null],
|
||||
[
|
||||
'execFile("undefined-options", [], undefined)',
|
||||
@@ -162,11 +189,11 @@ const cases = [
|
||||
['spawn(...input, [], {stdio:"inherit"})', null, "dynamic", null],
|
||||
['exec("named-callback", done)', "named-callback", "default", null],
|
||||
['execFile("callback-alias", [], callback)', "callback-alias", "default", 0],
|
||||
['exec("imported-callback", imported)', "imported-callback", "dynamic", null],
|
||||
['exec("imported-callback", imported)', "imported-callback", "default", null],
|
||||
[
|
||||
'exec("reassigned-callback", changed)',
|
||||
"reassigned-callback",
|
||||
"dynamic",
|
||||
"default",
|
||||
null,
|
||||
],
|
||||
['spawn("named-nullish", [], {stdio:absent})', "named-nullish", "dynamic", 0],
|
||||
@@ -179,15 +206,15 @@ const cases = [
|
||||
] as const;
|
||||
|
||||
const annexBSource = `
|
||||
const {execFile}=require("node:child_process");
|
||||
const {spawn,execFile}=require("node:child_process");
|
||||
var argv=[]; {function argv(){}}
|
||||
execFile("annex-b-argv",argv,{stdio:"inherit"});
|
||||
spawn("annex-b-argv",argv,{stdio:"inherit"});
|
||||
function run(){
|
||||
var args=[]; switch(input){case 0:function args(){}}
|
||||
execFile("annex-b-local",args,{stdio:"inherit"});
|
||||
spawn("annex-b-local",args,{stdio:"inherit"});
|
||||
}
|
||||
var stable=[]; function done(){}
|
||||
execFile("cjs-var-argv",stable,{stdio:"inherit"});
|
||||
spawn("cjs-var-argv",stable,{stdio:"inherit"});
|
||||
execFile("cjs-hoisted-callback",done);
|
||||
`;
|
||||
|
||||
@@ -199,13 +226,21 @@ const importEqualsSource = `
|
||||
`;
|
||||
|
||||
const exportAssignmentSource = `
|
||||
const {spawn,execFile}=require("node:child_process");
|
||||
const {spawn}=require("node:child_process");
|
||||
const argv=[]; spawn("export-assignment-argv",argv,{stdio:"inherit"});
|
||||
var args=[]; {function args(){}}
|
||||
execFile("export-assignment-annex",args,{stdio:"inherit"});
|
||||
spawn("export-assignment-annex",args,{stdio:"inherit"});
|
||||
export = argv;
|
||||
`;
|
||||
|
||||
// Callback/options ambiguity cannot change exec's effective stdio.
|
||||
const opaqueExecCallbackCommands = [
|
||||
"eval-callback",
|
||||
"constructed-callback",
|
||||
"mapped-callback",
|
||||
"cjs-wrapper-callback",
|
||||
];
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"preserves effective stdio, overloads and explicit unknowns through %s",
|
||||
async (surface, { cli }) => {
|
||||
@@ -277,17 +312,15 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
expected.set("import-equals-argv", ["inherit", null]);
|
||||
expected.set("import-equals-callback", ["default", null]);
|
||||
expected.set("export-assignment-argv", ["inherit", null]);
|
||||
for (const command of opaqueExecCallbackCommands)
|
||||
expected.set(command, ["default", null]);
|
||||
for (const [command, count] of [
|
||||
["Function-global", 0],
|
||||
["frames-global", 0],
|
||||
["mapped-arguments", 0],
|
||||
["eval-callback", null],
|
||||
["shadowed-undefined", 0],
|
||||
["constructed-argv", null],
|
||||
["constructed-callback", null],
|
||||
["mapped-callback", null],
|
||||
["cjs-wrapper-argv", null],
|
||||
["cjs-wrapper-callback", null],
|
||||
["annex-b-argv", null],
|
||||
["annex-b-local", null],
|
||||
["export-assignment-annex", null],
|
||||
@@ -309,7 +342,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
expect(child.properties.stdio_mode, String(command)).toBe(mode);
|
||||
expect(child.properties.argv_count, String(command)).toBe(count);
|
||||
expect(child.properties.environment_supplied, String(command)).toBe(
|
||||
command === "named-argv",
|
||||
["named-argv", "execfile-dynamic"].includes(String(command)),
|
||||
);
|
||||
expect(child.identity.source_range).not.toBeNull();
|
||||
const stdio = graph.relations.filter(
|
||||
@@ -342,7 +375,9 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
relation.relation === "supplies-env" &&
|
||||
relation.target_node_id === child.node_id,
|
||||
),
|
||||
).toHaveLength(command === "named-argv" ? 1 : 0);
|
||||
).toHaveLength(
|
||||
["named-argv", "execfile-dynamic"].includes(String(command)) ? 1 : 0,
|
||||
);
|
||||
expect(
|
||||
graph.relations.filter(
|
||||
(relation) =>
|
||||
|
||||
Reference in new issue
Block a user