mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* 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
305 lines
10 KiB
TypeScript
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;
|
|
};
|