fix(javascript): preserve effective child-process stdio options (#4336)

* 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
This commit is contained in:
星闌
2026-10-17 15:51:55 +08:00
committed by GitHub
parent 0cd4eafd61
commit de9371b85d
18 changed files with 1114 additions and 93 deletions
@@ -4,10 +4,13 @@ import type {
JavaScriptSemanticCallable,
JavaScriptSemanticChildProcessInteraction,
JavaScriptSemanticChildProcessSpawn,
JavaScriptSemanticReference,
} from "./javascriptSemanticIr.js";
import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js";
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
import {
semanticStaticPropertyName,
staticObjectProperty,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
@@ -38,6 +41,10 @@ interface ChildProcessAnalysis {
readonly childProcessInteractions: readonly JavaScriptSemanticChildProcessInteraction[];
}
type ChildSpawnContext = ReturnType<
typeof createSemanticExpressionShapeResolver
>;
interface SpawnCandidate {
readonly node: t.CallExpression | t.OptionalCallExpression;
readonly method: JavaScriptSemanticChildProcessSpawn["method"];
@@ -61,10 +68,12 @@ export const collectJavaScriptSemanticChildProcesses = (
program: t.Program,
state: JavaScriptSemanticAnalysisState,
callables: readonly JavaScriptSemanticCallable[],
references: readonly JavaScriptSemanticReference[],
): ChildProcessAnalysis => {
const candidates = collectSpawns(program, state, callables);
const context = createSemanticExpressionShapeResolver(state, references);
const childProcessSpawns = candidates.map((candidate) =>
immutableSpawn(candidate),
immutableSpawn(candidate, context),
);
return {
childProcessSpawns,
@@ -126,8 +135,9 @@ const collectSpawns = (
const immutableSpawn = (
candidate: SpawnCandidate,
context: ChildSpawnContext,
): JavaScriptSemanticChildProcessSpawn => {
const options = spawnOptions(candidate);
const { options, argvCount } = spawnArguments(candidate, context);
const command = dataEffectLiteralString(candidate.node.arguments[0]);
return {
processId: spawnId(candidate),
@@ -137,9 +147,9 @@ const immutableSpawn = (
resultBindingId: candidate.resultBindingId,
projectedResultBindings: candidate.projectedResultBindings,
command,
argvCount: spawnArgvCount(candidate),
argvCount,
environmentSupplied: objectHasProperty(options, "env"),
stdioMode: stdioMode(options),
stdioMode: stdioMode(options, candidate.method, context),
resolution: command === null ? "partial" : "complete",
};
};
@@ -366,52 +376,73 @@ const resolveChildCandidates = (
complete: complete && candidates.size === 1,
};
};
const spawnOptions = (candidate: SpawnCandidate): t.ObjectExpression | null => {
const spawnArguments = (
candidate: SpawnCandidate,
context: ChildSpawnContext,
): {
readonly options: t.Node | undefined;
readonly argvCount: number | null;
} => {
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];
return t.isArrayExpression(args) &&
!args.elements.some((element) => t.isSpreadElement(element))
? args.elements.length
: null;
if (t.isSpreadElement(args[0])) return { options: args[0], argvCount: null };
const second = args[1] && unwrapJavaScriptExpression(args[1]).node;
const third = args[2] && unwrapJavaScriptExpression(args[2]).node;
const expressionShape = context;
const argv =
candidate.method !== "exec" && t.isArrayExpression(second)
? second
: undefined;
// Only a proven argv array or nullish placeholder admits third-slot options.
// Otherwise the second argument may itself be options, ignoring the third.
const options =
candidate.method === "exec" || second === undefined
? second
: argv !== undefined ||
expressionShape.isArray(second) ||
expressionShape.isNullish(second)
? third
: second;
return {
options,
argvCount:
argv !== undefined &&
!argv.elements.some((element) => t.isSpreadElement(element))
? argv.elements.length
: null,
};
};
const objectHasProperty = (
object: t.ObjectExpression | null,
name: string,
): boolean => objectProperty(object, name) !== null;
const objectHasProperty = (object: t.Node | undefined, name: string): boolean =>
t.isObjectExpression(object) &&
object.properties.some(
(property) =>
!t.isSpreadElement(property) &&
semanticStaticPropertyName(property.key, property.computed) === name,
);
const stdioMode = (options: t.ObjectExpression | null): string => {
const property = objectProperty(options, "stdio");
if (property === null || !t.isObjectProperty(property)) return "default";
if (t.isStringLiteral(property.value)) return property.value.value;
if (t.isArrayExpression(property.value)) return "array";
const stdioMode = (
options: t.Node | undefined,
method: JavaScriptSemanticChildProcessSpawn["method"],
context: ChildSpawnContext,
): string => {
if (options === undefined) return "default";
const expression = unwrapJavaScriptExpression(options).node;
if (
context.isNullish(expression) ||
((method === "exec" || method === "execFile") &&
context.isCallback(expression))
)
return "default";
const lookup = staticObjectProperty(expression, "stdio", context.isNullish);
if (lookup.status === "missing") return "default";
if (lookup.status !== "explicit") return "dynamic";
const value = unwrapJavaScriptExpression(lookup.property.value).node;
if (context.isNullish(value)) return "default";
if (t.isStringLiteral(value)) return value.value;
if (t.isArrayExpression(value)) return "array";
return "dynamic";
};
const objectProperty = (
object: t.ObjectExpression | null,
name: string,
): t.ObjectMethod | t.ObjectProperty | null =>
object?.properties.find(
(property): property is t.ObjectMethod | t.ObjectProperty =>
(t.isObjectMethod(property) || t.isObjectProperty(property)) &&
// `{[env]: vars}` has a dynamic key; treating it as `env` would claim
// the caller supplied an environment when the real key is unknown.
semanticStaticPropertyName(property.key, property.computed) === name,
) ?? null;
const objectBinding = (
node: t.Expression | t.Super,
state: JavaScriptSemanticAnalysisState,