mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
256 lines
8.8 KiB
TypeScript
256 lines
8.8 KiB
TypeScript
import * as t from "@babel/types";
|
|
import { javaScriptProgramStrictness } from "./javascriptSourceParser.js";
|
|
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
|
|
import { createAmbientPathResolver } from "./javascriptSemanticAmbientWrites.js";
|
|
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
|
import {
|
|
semanticEffectTiming,
|
|
semanticBindingEffectFollowsCapture,
|
|
} from "./javascriptSemanticMutationInitializers.js";
|
|
import {
|
|
isUnshadowedGlobal,
|
|
resolveSemanticBindingState,
|
|
type JavaScriptSemanticAnalysisState,
|
|
type JavaScriptSemanticBindingState,
|
|
} from "./javascriptSemanticState.js";
|
|
|
|
/** One source owns sites and the minimal scope tree reaching them; O(AST) storage. */
|
|
export interface JavaScriptOpaqueScopeEffects {
|
|
/** Cheap source evidence collected with declarations, before alias queries. */
|
|
mayReferenceGlobalEval: boolean;
|
|
readonly sites: WeakSet<t.Node>;
|
|
readonly sloppySites: WeakSet<t.Node>;
|
|
readonly byScope: Map<string, t.CallExpression[]>;
|
|
readonly children: Map<string, Set<string>>;
|
|
readonly globalSites: (t.CallExpression | t.OptionalCallExpression)[];
|
|
}
|
|
|
|
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,
|
|
): ((node: t.Node) => void) | undefined => {
|
|
if (!state.opaqueEffects.mayReferenceGlobalEval) return undefined;
|
|
const ambientPaths = createAmbientPathResolver(state, true);
|
|
let invocationSite: t.Node | undefined;
|
|
const interop = createSemanticModuleInterop(state, {
|
|
intrinsicPath: (node) => {
|
|
const paths = ambientPaths(node, invocationSite).paths;
|
|
const path = paths.find((path) => path[1] === "eval") ?? 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"
|
|
? ["intrinsic", ...(path as readonly string[])]
|
|
: ["intrinsic", ...(path.slice(1) as readonly string[])];
|
|
},
|
|
});
|
|
return (node) => {
|
|
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
|
|
invocationSite = node;
|
|
const invocation = interop.intrinsicInvocation(node);
|
|
if (invocation?.path.join(".") === "intrinsic.global.eval")
|
|
state.opaqueEffects.globalSites.push(node);
|
|
};
|
|
};
|
|
|
|
/** Indirect eval can change 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,
|
|
))
|
|
)
|
|
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",
|
|
)
|
|
)
|
|
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;
|
|
};
|