Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticOpaqueEffects.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

305 lines
10 KiB
TypeScript

import * as t from "@babel/types";
import { javaScriptProgramStrictness } from "./javascriptSourceParser.js";
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
import {
isUnshadowedGlobal,
semanticEffectTiming,
semanticBindingEffectFollowsCapture,
semanticBindingEffectFollowsRead,
semanticReadPrecedesEffectWithinActivation,
} from "./javascriptSemanticMutationInitializers.js";
import {
resolveSemanticBindingState,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
export const indexSemanticOpaqueEffects = (
candidates: readonly {
node: t.CallExpression;
scopeId: string;
}[],
state: JavaScriptSemanticAnalysisState,
): void => {
for (const { node, scopeId } of candidates) {
const callee = unwrapJavaScriptExpression(node.callee).node;
if (!isUnshadowedGlobal(callee, state, "eval")) continue;
state.opaqueEffects.sites.add(node);
if (!strictEval(node, state)) state.opaqueEffects.sloppySites.add(node);
const sites = state.opaqueEffects.byScope.get(scopeId) ?? [];
sites.push(node);
state.opaqueEffects.byScope.set(scopeId, sites);
let scope = state.scopesById.get(scopeId);
while (scope?.parentScopeId != null) {
const children =
state.opaqueEffects.children.get(scope.parentScopeId) ??
new Set<string>();
if (children.has(scope.scopeId)) break;
children.add(scope.scopeId);
state.opaqueEffects.children.set(scope.parentScopeId, children);
scope = state.scopesById.get(scope.parentScopeId);
}
}
};
/** The existing ambient invocation walk owns global effects; ordinary sources
* neither retain a second call list nor create another alias resolver. */
export const createSemanticOpaqueGlobalObserver = (
state: JavaScriptSemanticAnalysisState,
dependencies: {
createAmbientPathResolver: (
state: JavaScriptSemanticAnalysisState,
observeGlobals: boolean,
) => (
node: t.Node,
site: t.Node | undefined,
) => {
paths: readonly (readonly (string | null)[])[];
callerPaths: readonly (readonly (string | null)[])[];
};
createSemanticModuleInterop: (
state: JavaScriptSemanticAnalysisState,
options?: {
intrinsicPath: (node: t.Node) => readonly string[] | undefined;
},
) => {
intrinsicPath: (node: t.Node) => readonly string[] | undefined;
intrinsicInvocation: (
node:
| t.CallExpression
| t.OptionalCallExpression
| t.NewExpression
| t.TaggedTemplateExpression,
) => { readonly path: readonly string[] } | undefined;
};
},
): ((node: t.Node) => void) | undefined => {
if (!state.opaqueEffects.mayReferenceGlobalCode) return undefined;
const ambientPaths = dependencies.createAmbientPathResolver(state, true);
let invocationSite: t.Node | undefined;
const interop = dependencies.createSemanticModuleInterop(state, {
intrinsicPath: (node) => {
const { paths, callerPaths } = ambientPaths(node, invocationSite);
const path =
paths.find((path) => path[1] === "eval" || path[1] === "Function") ??
callerPaths.find(
(path) => path[1] === "eval" || path[1] === "Function",
) ??
paths[0];
if (path?.[0] !== "global" || path.some((key) => key === null))
return undefined;
// Keep the global callable owner for generic call/apply/bind resolution.
return path[1] === "eval" || path[1] === "Function"
? ["intrinsic", ...(path as readonly string[])]
: ["intrinsic", ...(path.slice(1) as readonly string[])];
},
});
return (node) => {
if (
!t.isCallExpression(node) &&
!t.isOptionalCallExpression(node) &&
!t.isNewExpression(node) &&
!t.isTaggedTemplateExpression(node)
)
return;
invocationSite = node;
const path = interop.intrinsicInvocation(node)?.path;
// Constructed code may escape and activate a classic-script global. Keep
// its creation as a conservative timing bound, not proof of execution.
if (
path?.join(".") === "intrinsic.global.eval" ||
path?.join(".") === "intrinsic.global.Function"
)
state.opaqueEffects.globalSites.push(node);
};
};
/** Global code can affect globals, but cannot rebind its caller's locals. */
export const semanticOpaqueGlobalMayHaveChanged = (
read: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean =>
state.opaqueEffects.globalSites.some(
(effect) => semanticEffectTiming(read, effect, state) !== "after",
);
/** Rebinding requires lexical visibility. Object references can remain reachable
* through aliases even where the original name is shadowed or out of block scope.
* Without a disjointness proof, consider their enclosing activation and closures.
* Queries retain no binding-by-eval table. */
export function* semanticOpaqueBindingEffects(
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
kind: "binding" | "reference" = "binding",
): Generator<t.CallExpression> {
if (state.opaqueEffects.byScope.size === 0) return;
const pending = [
kind === "reference" ? activation(binding.scopeId, state) : binding.scopeId,
];
while (pending.length > 0) {
const scope = pending.pop()!;
for (const node of state.opaqueEffects.byScope.get(scope) ?? [])
if (
kind === "reference" ||
resolveSemanticBindingState(state, node, binding.name)?.bindingId ===
binding.bindingId
)
yield node;
for (const child of state.opaqueEffects.children.get(scope) ?? [])
pending.push(child);
}
}
export const semanticBindingMayHaveOpaqueEffect = (
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
read?: t.Node,
kind: "binding" | "reference" = "binding",
): boolean => {
if (state.opaqueEffects.byScope.size === 0) return false;
for (const effect of semanticOpaqueBindingEffects(binding, state, kind))
if (
read === undefined ||
(semanticEffectTiming(read, effect, state) !== "after" &&
!semanticBindingEffectFollowsCapture(
binding,
read,
effect,
state.parentsByNode,
) &&
// Rebinding a freshly initialized local is activation-specific;
// mutations to the referenced object may persist across calls.
!(
kind === "binding" &&
semanticBindingEffectFollowsRead(
binding,
read,
effect,
state.parentsByNode,
)
))
)
return true;
return false;
};
/** Opaque eval may introduce an otherwise unbound name in a visible activation. */
export const semanticOpaqueNameMayHaveChanged = (
read: t.Node,
state: JavaScriptSemanticAnalysisState,
binding?: JavaScriptSemanticBindingState,
): boolean => {
if (state.opaqueEffects.byScope.size === 0) return false;
const visible = new Set<string>();
let node: t.Node | undefined = read;
while (node !== undefined) {
const scope = state.scopeByNode.get(node);
if (scope !== undefined) {
let current: typeof scope | undefined = scope;
while (current !== undefined) {
visible.add(current.scopeId);
current =
current.parentScopeId === null
? undefined
: state.scopesById.get(current.parentScopeId);
}
break;
}
node = state.parentsByNode.get(node);
}
for (const [scopeId, sites] of state.opaqueEffects.byScope) {
let scope = state.scopesById.get(scopeId);
while (
scope !== undefined &&
scope.kind !== "function" &&
scope.kind !== "program"
)
scope =
scope.parentScopeId === null
? undefined
: state.scopesById.get(scope.parentScopeId);
if (
scope !== undefined &&
visible.has(scope.scopeId) &&
(binding === undefined ||
activation(binding.scopeId, state) !== scope.scopeId) &&
sites.some(
(effect) =>
state.opaqueEffects.sloppySites.has(effect) &&
(binding === undefined ||
resolveSemanticBindingState(state, effect, binding.name)
?.bindingId === binding.bindingId) &&
semanticEffectTiming(read, effect, state) !== "after" &&
// A sloppy-eval name belongs to this activation, not a prior call.
!semanticReadPrecedesEffectWithinActivation(
read,
effect,
state.parentsByNode,
),
)
)
return true;
}
return false;
};
/** Declaration assignability is independent of observed explicit writes. */
export const semanticBindingCanBeRebound = (
binding: JavaScriptSemanticBindingState,
): boolean =>
binding.mutable ||
(binding.definitions.some(
({ kind }) => kind === "function" || kind === "class",
) &&
binding.initializers.some(
({ node }) => t.isFunctionDeclaration(node) || t.isClassDeclaration(node),
));
export const semanticOpaqueBindingReadMayHaveChanged = (
binding: JavaScriptSemanticBindingState,
read: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean =>
(semanticBindingCanBeRebound(binding) &&
semanticBindingMayHaveOpaqueEffect(binding, state, read)) ||
semanticOpaqueNameMayHaveChanged(read, state, binding);
const activation = (
scopeId: string,
state: JavaScriptSemanticAnalysisState,
): string => {
let scope = state.scopesById.get(scopeId);
while (
scope !== undefined &&
scope.kind !== "function" &&
scope.kind !== "program" &&
scope.parentScopeId !== null
)
scope = state.scopesById.get(scope.parentScopeId);
return scope?.scopeId ?? scopeId;
};
const strictEval = (
site: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean => {
for (
let node: t.Node | undefined = site;
node !== undefined;
node = state.parentsByNode.get(node)
) {
if (
t.isClass(node) ||
(t.isProgram(node) && javaScriptProgramStrictness(node) === "strict")
)
return true;
const directives =
t.isFunction(node) && t.isBlockStatement(node.body)
? node.body.directives
: [];
if (directives.some(({ value }) => value.value === "use strict"))
return true;
}
return false;
};