mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user