* fix(javascript): preserve effective child-process stdio options * fix(javascript): retain proven callbacks and primitive stdio keys * fix(javascript): preserve argv shapes and nullish spreads * fix(javascript): bound callable proofs and retain exposed binding uncertainty * test(javascript): keep callback acceptance within transport budget * test(javascript): inspect application coverage for callback proof limits * fix(javascript): keep spread command argument slots uncertain * fix(javascript): bound mapped argument content proofs * fix(javascript): distinguish erased declarations from runtime shadows * fix(javascript): prove runtime module context before binding isolation * fix(javascript): share erased declaration and opaque scope proofs * fix(javascript): keep lowered enum scopes opaque * fix(javascript): narrow stdio proofs and consolidate regressions * perf(javascript): reuse lexical references for child shapes * fix(javascript): retain CommonJS child argument context * fix(javascript): keep Annex B affected child arguments unknown * fix(javascript): recognize value import-equals runtime modules * fix(javascript): retain exact stdio through module and primitive syntax * fix(javascript): bound inline spread property resolution * fix(javascript): distinguish copied methods from getters
360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
import { expect } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
|
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
|
|
|
// Each row covers one ownership, overload or uncertainty boundary. Both public
|
|
// adapters read the same inert fixture; no Cartesian product of equivalent calls.
|
|
const cases = [
|
|
[
|
|
'spawn("duplicate", [], {stdio:"pipe",stdio:"inherit"})',
|
|
"duplicate",
|
|
"inherit",
|
|
0,
|
|
],
|
|
['spawn("control", [], {stdio:"inherit"})', "control", "inherit", 0],
|
|
[
|
|
'spawn("spread-after", [], {stdio:"pipe",...input})',
|
|
"spread-after",
|
|
"dynamic",
|
|
0,
|
|
],
|
|
['spawn("unknown-options", [], input)', "unknown-options", "dynamic", 0],
|
|
[
|
|
'spawn("spread-before", [], {...input,stdio:"inherit"})',
|
|
"spread-before",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("null-spreads", [], {stdio:"inherit",...null,...undefined,...void effect()})',
|
|
"null-spreads",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("computed", [], {stdio:"inherit",[key]:1})',
|
|
"computed",
|
|
"dynamic",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("coerced-keys", [], {stdio:"inherit",[true]:1,[null]:1,[-1]:1,[+(1 as number)]:1})',
|
|
"coerced-keys",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("void-key", [], {stdio:"inherit",[void effect()]:1})',
|
|
"void-key",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("primitive-spreads", [], {stdio:"inherit",...false,...42,...1n})',
|
|
"primitive-spreads",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("inline-empty", [], {stdio:"inherit",...{}})',
|
|
"inline-empty",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("inline-final", [], {stdio:"pipe",...{...{stdio:"ignore",stdio:"inherit"}}})',
|
|
"inline-final",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("inline-getter", [], {stdio:"ignore",...{get other(){this.stdio="pipe";return 0},stdio:"inherit"}})',
|
|
"inline-getter",
|
|
"dynamic",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("inline-helper", [], {stdio:"inherit",...{...{helper(){}}}})',
|
|
"inline-helper",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("inline-unknown", [], {stdio:"inherit",...{...input}})',
|
|
"inline-unknown",
|
|
"dynamic",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("getter", [], {stdio:"inherit",get stdio(){return input}})',
|
|
"getter",
|
|
"dynamic",
|
|
0,
|
|
],
|
|
['spawn("method", [], {stdio:"inherit",stdio(){}})', "method", "dynamic", 0],
|
|
[
|
|
'spawn("array-stdio", [], {stdio:["pipe",input]})',
|
|
"array-stdio",
|
|
"array",
|
|
0,
|
|
],
|
|
['spawn("missing-stdio", [])', "missing-stdio", "default", 0],
|
|
['exec("null-stdio", {stdio:null})', "null-stdio", "default", null],
|
|
[
|
|
'execFile("undefined-options", [], undefined)',
|
|
"undefined-options",
|
|
"default",
|
|
0,
|
|
],
|
|
['fork("void-options", [], void effect())', "void-options", "default", 0],
|
|
[
|
|
'spawn("options-only", {stdio:"inherit"}, {stdio:null})',
|
|
"options-only",
|
|
"inherit",
|
|
null,
|
|
],
|
|
[
|
|
'spawn("ambiguous-slot", input, {stdio:"inherit"})',
|
|
"ambiguous-slot",
|
|
"dynamic",
|
|
null,
|
|
],
|
|
[
|
|
'fork("nullish-slot", null, {stdio:"inherit"})',
|
|
"nullish-slot",
|
|
"inherit",
|
|
null,
|
|
],
|
|
[
|
|
'spawn("wrapped-argv", ([] as string[])!, {stdio:"inherit"})',
|
|
"wrapped-argv",
|
|
"inherit",
|
|
0,
|
|
],
|
|
[
|
|
'spawn("named-argv", argv, {env:{A:"1"},stdio:"inherit"})',
|
|
"named-argv",
|
|
"inherit",
|
|
null,
|
|
],
|
|
[
|
|
'spawn("captured-argv", captured, {stdio:"inherit"})',
|
|
"captured-argv",
|
|
"dynamic",
|
|
null,
|
|
],
|
|
[
|
|
'spawn("spread-argv", ["a",...input], {stdio:"inherit"})',
|
|
"spread-argv",
|
|
"inherit",
|
|
null,
|
|
],
|
|
['spawn("fixed-argv", ["a"], {stdio:"inherit"})', "fixed-argv", "inherit", 1],
|
|
[
|
|
'spawn("spread-slot", ...input, {stdio:"inherit"})',
|
|
"spread-slot",
|
|
"dynamic",
|
|
null,
|
|
],
|
|
['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("reassigned-callback", changed)',
|
|
"reassigned-callback",
|
|
"dynamic",
|
|
null,
|
|
],
|
|
['spawn("named-nullish", [], {stdio:absent})', "named-nullish", "dynamic", 0],
|
|
[
|
|
'spawn("named-null-spread", [], {stdio:"inherit",...absent})',
|
|
"named-null-spread",
|
|
"dynamic",
|
|
0,
|
|
],
|
|
] as const;
|
|
|
|
const annexBSource = `
|
|
const {execFile}=require("node:child_process");
|
|
var argv=[]; {function argv(){}}
|
|
execFile("annex-b-argv",argv,{stdio:"inherit"});
|
|
function run(){
|
|
var args=[]; switch(input){case 0:function args(){}}
|
|
execFile("annex-b-local",args,{stdio:"inherit"});
|
|
}
|
|
var stable=[]; function done(){}
|
|
execFile("cjs-var-argv",stable,{stdio:"inherit"});
|
|
execFile("cjs-hoisted-callback",done);
|
|
`;
|
|
|
|
const importEqualsSource = `
|
|
import cp = require("node:child_process");
|
|
const argv=[]; function done(){}
|
|
cp.spawn("import-equals-argv",argv,{stdio:"inherit"});
|
|
cp.exec("import-equals-callback",done);
|
|
`;
|
|
|
|
const exportAssignmentSource = `
|
|
const {spawn,execFile}=require("node:child_process");
|
|
const argv=[]; spawn("export-assignment-argv",argv,{stdio:"inherit"});
|
|
var args=[]; {function args(){}}
|
|
execFile("export-assignment-annex",args,{stdio:"inherit"});
|
|
export = argv;
|
|
`;
|
|
|
|
cliTest.for(["CLI", "stdio MCP"] as const)(
|
|
"preserves effective stdio, overloads and explicit unknowns through %s",
|
|
async (surface, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-child-stdio-");
|
|
await writeFixtureFiles(root, {
|
|
"entry.ts": `
|
|
import {spawn,exec,execFile,fork} from "node:child_process";
|
|
import {done as imported} from "./external.mjs";
|
|
throw new Error("static fixture must never execute");
|
|
function done(){}
|
|
const captured=[]; const absent=null;
|
|
function run(input,key){
|
|
const callback=done; const argv=[];
|
|
let changed=()=>{}; changed=input;
|
|
${cases.map(([source]) => `${source};`).join("\n")}
|
|
}
|
|
`,
|
|
"external.mjs": "export function done(){}",
|
|
"globals.js": `
|
|
const {spawn,exec}=require("node:child_process");
|
|
var argv=[];(()=>{}).constructor("argv={stdio:'inherit'}")();
|
|
spawn("constructed-argv",argv,{stdio:"pipe"});
|
|
let done=()=>{};(()=>{}).constructor("done={stdio:'inherit'}")();
|
|
exec("constructed-callback",done);
|
|
`,
|
|
"wrapper.cjs": `
|
|
const {spawn,exec}=require("node:child_process");
|
|
const argv=[]; const done=()=>{};
|
|
spawn("cjs-argv",argv,{stdio:"inherit"});exec("cjs-callback",done);
|
|
var module=[]; const args=arguments; args[2]={stdio:"inherit"};
|
|
spawn("cjs-wrapper-argv",module,{stdio:"pipe"});
|
|
function exports(){}; args[0]={stdio:"inherit"};
|
|
exec("cjs-wrapper-callback",exports);
|
|
`,
|
|
"wrapper.cts": `
|
|
const {spawn,exec}=require("node:child_process");
|
|
const argv: string[]=[]; const done=()=>{};
|
|
spawn("cts-argv",argv,{stdio:"inherit"});exec("cts-callback",done);
|
|
`,
|
|
"annex-b.cjs": annexBSource,
|
|
"import-equals.ts": importEqualsSource,
|
|
"export-assignment.ts": exportAssignmentSource,
|
|
"parameters.cjs": `
|
|
const {exec}=require("node:child_process");
|
|
function mapped(cb){cb=()=>{};arguments[0]={stdio:'inherit'};exec("mapped-callback",cb)}
|
|
`,
|
|
"exposed.cjs": `
|
|
const {spawn,exec}=require("node:child_process");
|
|
var mode=null; Function("mode='inherit'")();
|
|
spawn("Function-global",[],{stdio:mode});
|
|
var frameMode=null; frames.frameMode='inherit';
|
|
spawn("frames-global",[],{stdio:frameMode});
|
|
function mapped(mode){mode=null;arguments[0]='inherit';spawn("mapped-arguments",[],{stdio:mode})}
|
|
function exposed(options){let done=()=>{};eval("done=options");exec("eval-callback",done)}
|
|
function shadowed(undefined){spawn("shadowed-undefined",[],{stdio:"inherit",...undefined})}
|
|
`,
|
|
});
|
|
const graph = (await analyzeJavaScriptDirectory(surface, cli, root))
|
|
.semantic_graph;
|
|
const expected = new Map<string | null, readonly [string, number | null]>(
|
|
cases.map(([, command, mode, count]) => [command, [mode, count]]),
|
|
);
|
|
for (const extension of ["cjs", "cts"]) {
|
|
expected.set(`${extension}-argv`, ["inherit", null]);
|
|
expected.set(`${extension}-callback`, ["default", null]);
|
|
}
|
|
expected.set("cjs-var-argv", ["inherit", null]);
|
|
expected.set("cjs-hoisted-callback", ["default", null]);
|
|
expected.set("import-equals-argv", ["inherit", null]);
|
|
expected.set("import-equals-callback", ["default", null]);
|
|
expected.set("export-assignment-argv", ["inherit", 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],
|
|
] as const)
|
|
expected.set(command, ["dynamic", count]);
|
|
const children = graph.nodes.filter(
|
|
(node) => node.kind === "child-process",
|
|
);
|
|
expect(children).toHaveLength(expected.size);
|
|
expect(children.map((child) => child.properties.command).sort()).toEqual(
|
|
[...expected.keys()].sort(),
|
|
);
|
|
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
|
for (const child of children) {
|
|
const command = child.properties.command as string | null;
|
|
const entry = expected.get(command);
|
|
if (entry === undefined) throw new Error(`Unexpected child ${command}`);
|
|
const [mode, count] = entry;
|
|
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",
|
|
);
|
|
expect(child.identity.source_range).not.toBeNull();
|
|
const stdio = graph.relations.filter(
|
|
(relation) =>
|
|
relation.relation === "connects-stdio" &&
|
|
relation.source_node_id === child.node_id,
|
|
);
|
|
expect(stdio, String(command)).toHaveLength(1);
|
|
expect(stdio[0]?.resolution).toBe(
|
|
mode === "dynamic" ? "candidate" : "resolved",
|
|
);
|
|
expect(nodes.get(stdio[0]!.target_node_id)).toMatchObject({
|
|
kind: "stdio",
|
|
properties: { mode },
|
|
});
|
|
const unknowns = graph.unknowns.filter(
|
|
(unknown) =>
|
|
unknown.node_id === child.node_id &&
|
|
unknown.family === "child-process" &&
|
|
unknown.relation_kinds.includes("connects-stdio"),
|
|
);
|
|
expect(unknowns, String(command)).toHaveLength(
|
|
mode === "dynamic" ? 1 : 0,
|
|
);
|
|
if (mode === "dynamic")
|
|
expect(unknowns[0]).toMatchObject({ reason: "unknown-value" });
|
|
expect(
|
|
graph.relations.filter(
|
|
(relation) =>
|
|
relation.relation === "supplies-env" &&
|
|
relation.target_node_id === child.node_id,
|
|
),
|
|
).toHaveLength(command === "named-argv" ? 1 : 0);
|
|
expect(
|
|
graph.relations.filter(
|
|
(relation) =>
|
|
relation.relation === "supplies-argv" &&
|
|
relation.target_node_id === child.node_id,
|
|
),
|
|
).toHaveLength(count === null ? 0 : 1);
|
|
}
|
|
for (const relation of graph.relations) {
|
|
expect(nodes.has(relation.source_node_id)).toBe(true);
|
|
expect(nodes.has(relation.target_node_id)).toBe(true);
|
|
}
|
|
},
|
|
);
|