Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticExpressionShapes.ts
T
ShuaixinHuang a2134b3a74 fix(javascript): resolve module-level Electron options in functions (#8960)
* fix(javascript): resolve module-level Electron options in functions

* fix(javascript): limit module option ancestry walk

* fix(javascript): preserve uncertain captured argument ordering

* fix(javascript): retain hoisted activation uncertainty

* fix(javascript): retain function ownership through loops

* fix(javascript): account for classic global activations

* fix(javascript): bind first activations to lexical identities

* fix(javascript): retain uncertain global activation timing

* fix(javascript): isolate receiver facts and share computed reads

* fix(javascript): retain projected and opaque global activations

* fix(javascript): refresh activation bounds as opaque effects grow

* fix(ci): format website watch directories without changing configuration

* fix(javascript): share source reads across opaque-effect updates

* fix(javascript): retain constructed-code global activation bounds

* fix(javascript): share runtime activation and receiver facts

* fix(javascript): reuse pattern reads and function positions

* fix(javascript): isolate copied paths and preserve runtime reads

* style(javascript): format migrated reference imports

* fix(javascript): normalize constructor activation and receiver effects

* fix(javascript): retain aliased receiver timing and lexical global this

* fix(javascript): preserve effect-free receiver identity across activations

* fix(javascript): retain outer activation and prototype effects

* fix(javascript): index implicit reads without deep path rescans

* fix(javascript): retain aliased prototype and parameter read uncertainty

* fix(javascript): index reflected reads through intrinsic invocation facts

* fix(javascript): retain unresolved global activation reads

* chore: refresh PR branch after review fix publication

* fix(javascript): retain projected receiver reads and cache method facts

* fix(javascript): retain inherited and implicit activation reads

* fix(javascript): retain caller-owned this uncertainty

* fix(javascript): retain reflected reads with unresolved spread arguments

* fix(javascript): preserve enclosing activation for decorators

* fix(javascript): track intrinsic literal prototype effects

* fix(javascript): retain catch activation from implicit reads

* perf(javascript): reuse effective keys for projected reads

* fix(javascript): retain possible caller-owned ambient effects
2026-11-05 16:52:19 +08:00

142 lines
5.0 KiB
TypeScript

import {
semanticOpaqueBindingReadMayHaveChanged,
semanticOpaqueNameMayHaveChanged,
} from "./javascriptSemanticOpaqueEffects.js";
import * as t from "@babel/types";
import {
isNullishJavaScriptSyntax,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
isErasedJavaScriptDeclaration,
isUnmodeledJavaScriptRuntimeScope,
} from "./javascriptModuleDeclaration.js";
import {
isUnshadowedGlobal,
semanticBindingAtRead,
} from "./javascriptSemanticMutationInitializers.js";
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
import {
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { javaScriptRuntimeContext } from "./javascriptSourceParser.js";
type Shape = "array" | "unknown";
/** Bounded argument-shape queries shared by call-signature owners. */
export type JavaScriptExpressionShapeResolver = ReturnType<
typeof createSemanticExpressionShapeResolver
>;
const commonJsWrapperParameterNames = new Set([
"exports",
"require",
"module",
"__filename",
"__dirname",
]);
/** Bounded child-argument identity only; never recover named object contents. */
export const createSemanticExpressionShapeResolver = (
state: JavaScriptSemanticAnalysisState,
) => {
// 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;
const namedProofBlocked = (): boolean => {
if (blocked !== undefined) return blocked;
blocked = false;
traverseJavaScriptAst(state.program, {
enter(node) {
blocked ||=
isErasedJavaScriptDeclaration(node) ||
isUnmodeledJavaScriptRuntimeScope(node);
},
});
return blocked;
};
const shape = (input: t.Node): Shape => {
const seen = new Set<t.Node>();
let node = input;
let depth = 0;
while (true) {
const unwrapped = unwrapJavaScriptExpression(node);
node = unwrapped.node;
depth += unwrapped.depth;
if (depth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
state.valueResourceLimits.add("expression-depth");
return "unknown";
}
if (seen.has(node)) return "unknown";
seen.add(node);
if (t.isArrayExpression(node)) return "array";
if (!t.isIdentifier(node)) return "unknown";
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined || binding.referenceInitializers.length > 0)
return "unknown";
if (state.scopesById.get(binding.scopeId)?.kind === "program") {
const context = javaScriptRuntimeContext(state.program);
// Unknown scripts expose globals; wrapper parameters can alias arguments.
// Neither environment has a proven stable local initializer here.
if (
context === "unknown" ||
(context === "commonjs" &&
commonJsWrapperParameterNames.has(binding.name))
)
return "unknown";
}
if (
namedProofBlocked() ||
semanticOpaqueBindingReadMayHaveChanged(binding, node, state)
)
return "unknown";
const initializer = semanticStableInitializer(binding, state);
if (initializer === undefined || initializer.projection.length > 0)
return "unknown";
const read = semanticBindingAtRead(binding, node, state);
if (read.status !== "ordered" || !read.hasDefiniteInitializer)
return "unknown";
node = initializer.node;
depth++;
}
};
return {
isArray: (node: t.Node) => shape(node) === "array",
isNullish: (input: t.Node) => {
const { node } = unwrapJavaScriptExpression(input);
return (
isNullishJavaScriptSyntax(node) ||
(isUnshadowedGlobal(node, state, "undefined") &&
!namedProofBlocked() &&
!semanticOpaqueNameMayHaveChanged(node, state))
);
},
};
};
/**
* Select the options argument of a `(target[, argv][, options])` signature,
* such as Node child_process spawn/execFile/fork or Electron
* utilityProcess.fork. Only a proven argv array or nullish placeholder admits
* third-slot options; any other second argument may itself be the options
* object, which makes the runtime ignore the third. Spread arguments are
* returned unchanged so callers keep their unknown positions unknown. The
* returned node is unwrapped; `undefined` means no options were supplied.
*/
export const optionsAfterOptionalArgv = (
args: readonly t.CallExpression["arguments"][number][],
shapes: JavaScriptExpressionShapeResolver,
): t.Node | undefined => {
const [first, second, third] = args;
if (t.isSpreadElement(first)) return first;
if (second === undefined) return undefined;
const options =
shapes.isArray(second) || shapes.isNullish(second) ? third : second;
return options === undefined
? undefined
: unwrapJavaScriptExpression(options).node;
};