mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
fix(javascript): preserve builtin identity and share alias traversal
Track module writes and escapes at their owner, preserve capture and reassignment ordering, and share CommonJS/default namespace identity without erasing parent-slot changes. Carry write sites into ambient alias resolution and use parsed write targets across consumers. Deduplicate returned-reference work with cycle context; retain unchanged temporal selection identity so provenance memoization stays effective. Reviewed fixes include #2041, #2465, #2830, #2881, and #2948. Remove superseded mutation representations and the ineffective per-markValue dedup layer. Validation: 380 focused JavaScript tests passed under a 384 MiB heap cap, including the previously aborting 30-level/128-call alias regression. Six public CLI/MCP builtin tests passed. Exact pdf.js completed-workflow performance remains unmeasured.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { BUILTIN_RESOURCE_METHODS } from "./javascriptBuiltinResourceMethods.js";
|
||||
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
@@ -10,18 +11,243 @@ import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
|
||||
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { semanticTopLevelEffectFollowsCapture } from "./javascriptSemanticMutationInitializers.js";
|
||||
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
||||
|
||||
interface JavaScriptRequireOrigin {
|
||||
export interface JavaScriptRequireOrigin {
|
||||
readonly origin: JavaScriptModuleOrigin;
|
||||
readonly call: t.CallExpression;
|
||||
}
|
||||
|
||||
/** Recover an unshadowed literal require origin and its exact member path. */
|
||||
export const semanticRequireOrigin = (
|
||||
node: t.Node | null | undefined,
|
||||
// Ambient indexing finishes before these derived facts are consumed. Keep only
|
||||
// replacement sites, rather than rescanning unrelated global member writes.
|
||||
const globalProcessReplacements = new WeakMap<
|
||||
JavaScriptSemanticAnalysisState,
|
||||
readonly (t.Node | undefined)[]
|
||||
>();
|
||||
const moduleRequireWrites = new WeakMap<
|
||||
JavaScriptSemanticAnalysisState,
|
||||
readonly t.Node[]
|
||||
>();
|
||||
|
||||
const writesModuleRequire = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptRequireOrigin | undefined => {
|
||||
): readonly t.Node[] => {
|
||||
const cached = moduleRequireWrites.get(state);
|
||||
if (cached !== undefined) return cached;
|
||||
// Interop invocation resolution may ask for require provenance while this
|
||||
// index is being built. Publish a temporary empty result to break that cycle.
|
||||
moduleRequireWrites.set(state, []);
|
||||
const writes: t.Node[] = [];
|
||||
const interop = createSemanticModuleInterop(state);
|
||||
const isModuleObject = (
|
||||
node: t.Node,
|
||||
visited = new Set<string>(),
|
||||
): boolean => {
|
||||
const value = unwrapJavaScriptExpression(node).node;
|
||||
if (!t.isIdentifier(value)) return false;
|
||||
if (value.name === "module")
|
||||
return (
|
||||
resolveSemanticBindingState(state, value, "module") === undefined &&
|
||||
!semanticResolutionBlocked(state, value, "module")
|
||||
);
|
||||
const binding = resolveSemanticBindingState(state, value, value.name);
|
||||
if (binding === undefined || visited.has(binding.bindingId)) return false;
|
||||
const next = new Set(visited).add(binding.bindingId);
|
||||
return (
|
||||
binding.initializers.length > 0 &&
|
||||
binding.initializers.every(({ node: init }) => isModuleObject(init, next))
|
||||
);
|
||||
};
|
||||
const pending: t.Node[] = [state.program];
|
||||
while (pending.length > 0) {
|
||||
const node = pending.pop();
|
||||
if (node === undefined) break;
|
||||
const target = t.isAssignmentExpression(node)
|
||||
? node.left
|
||||
: t.isUpdateExpression(node) ||
|
||||
t.isUnaryExpression(node, { operator: "delete" })
|
||||
? node.argument
|
||||
: undefined;
|
||||
if (target !== undefined) {
|
||||
const members: string[] = [];
|
||||
let root: t.Node = target;
|
||||
while (t.isMemberExpression(root) || t.isOptionalMemberExpression(root)) {
|
||||
const key = semanticStaticPropertyKey(root.property, root.computed);
|
||||
if (key === null) break;
|
||||
members.unshift(key);
|
||||
root = root.object;
|
||||
}
|
||||
if (
|
||||
members.length === 1 &&
|
||||
members[0] === "require" &&
|
||||
isModuleObject(root)
|
||||
)
|
||||
writes.push(node);
|
||||
else if (
|
||||
members.length === 0 &&
|
||||
(t.isMemberExpression(target) ||
|
||||
t.isOptionalMemberExpression(target)) &&
|
||||
target.computed &&
|
||||
semanticStaticPropertyKey(target.property, true) === null &&
|
||||
isModuleObject(target.object)
|
||||
)
|
||||
// An unknown computed key may be "require"; retain uncertainty rather
|
||||
// than treating this direct delegate write as a proven unrelated slot.
|
||||
writes.push(node);
|
||||
}
|
||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
|
||||
const invocation = interop.intrinsicInvocation(node);
|
||||
const owner =
|
||||
invocation?.path?.length === 3 ? invocation.path[1] : undefined;
|
||||
const method = invocation?.path?.at(-1);
|
||||
const args = invocation?.arguments;
|
||||
if (
|
||||
(owner === "Object" || owner === "Reflect") &&
|
||||
method === "defineProperty" &&
|
||||
isModuleObject(args?.[0] ?? t.nullLiteral())
|
||||
) {
|
||||
const key = interop.staticString(args?.[1]);
|
||||
if (key === "require" || key === undefined) writes.push(node);
|
||||
} else if (
|
||||
owner === "Object" &&
|
||||
method === "defineProperties" &&
|
||||
isModuleObject(args?.[0] ?? t.nullLiteral())
|
||||
) {
|
||||
const descriptors = args?.[1];
|
||||
if (
|
||||
!t.isObjectExpression(descriptors) ||
|
||||
descriptors.properties.some(
|
||||
(property) =>
|
||||
t.isSpreadElement(property) ||
|
||||
(t.isObjectProperty(property) &&
|
||||
semanticStaticPropertyKey(property.key, property.computed) ===
|
||||
"require"),
|
||||
)
|
||||
)
|
||||
writes.push(node);
|
||||
} else if (
|
||||
owner === "Object" &&
|
||||
method === "assign" &&
|
||||
args?.some((arg) => isModuleObject(arg))
|
||||
) {
|
||||
const target = args?.[0];
|
||||
if (target !== undefined && isModuleObject(target)) {
|
||||
const sourceHasRequire = args
|
||||
?.slice(1)
|
||||
.some(
|
||||
(arg) =>
|
||||
t.isObjectExpression(arg) &&
|
||||
arg.properties.some(
|
||||
(property) =>
|
||||
t.isObjectProperty(property) &&
|
||||
semanticStaticPropertyKey(
|
||||
property.key,
|
||||
property.computed,
|
||||
) === "require",
|
||||
),
|
||||
);
|
||||
if (
|
||||
sourceHasRequire ||
|
||||
args?.slice(1).some((arg) => !t.isObjectExpression(arg))
|
||||
)
|
||||
writes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of t.VISITOR_KEYS[node.type] ?? []) {
|
||||
const value = (node as unknown as Record<string, unknown>)[key];
|
||||
if (Array.isArray(value)) {
|
||||
for (const child of value) if (t.isNode(child)) pending.push(child);
|
||||
} else if (t.isNode(value)) pending.push(value);
|
||||
}
|
||||
}
|
||||
moduleRequireWrites.set(state, writes);
|
||||
return writes;
|
||||
};
|
||||
|
||||
/** Whether the original global process object can still own effects at this source site. */
|
||||
export const semanticOriginalGlobalProcessAt = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
let replacements = globalProcessReplacements.get(state);
|
||||
if (replacements === undefined) {
|
||||
replacements = [
|
||||
...state.ambientWriteEffects
|
||||
.filter(
|
||||
({ path }) =>
|
||||
path.length <= 2 &&
|
||||
path.every((key, index) =>
|
||||
semanticPropertyPathKeyMatches(
|
||||
key,
|
||||
["global", "process"][index]!,
|
||||
),
|
||||
),
|
||||
)
|
||||
.map(({ node: write }) => write),
|
||||
...(state.ambientBindingWrites.get("process") ?? []),
|
||||
];
|
||||
globalProcessReplacements.set(state, replacements);
|
||||
}
|
||||
return !replacements.some(
|
||||
(write) =>
|
||||
!semanticTopLevelEffectFollowsCapture(node, write, state.parentsByNode),
|
||||
);
|
||||
};
|
||||
|
||||
/** Node's unbound process reference shares the cached process exports object. */
|
||||
export const semanticGlobalProcessOrigin = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptModuleOrigin | undefined =>
|
||||
t.isIdentifier(node, { name: "process" }) &&
|
||||
resolveSemanticBindingState(state, node, "process") === undefined &&
|
||||
!semanticResolutionBlocked(state, node, "process") &&
|
||||
semanticOriginalGlobalProcessAt(node, state)
|
||||
? { specifier: "node:process", importedPath: [], resolutionMode: "require" }
|
||||
: undefined;
|
||||
|
||||
/** Resolve qualified global-object process references, preserving their selected path. */
|
||||
export const semanticGlobalProcessPathOrigin = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptModuleOrigin | undefined => {
|
||||
const members: string[] = [];
|
||||
let current = unwrapJavaScriptExpression(node).node;
|
||||
while (
|
||||
t.isMemberExpression(current) ||
|
||||
t.isOptionalMemberExpression(current)
|
||||
) {
|
||||
const key = semanticStaticPropertyKey(current.property, current.computed);
|
||||
if (key === null) return undefined;
|
||||
members.unshift(key);
|
||||
current = unwrapJavaScriptExpression(current.object).node;
|
||||
}
|
||||
if (
|
||||
!t.isIdentifier(current) ||
|
||||
!["globalThis", "global", "window", "self"].includes(current.name) ||
|
||||
resolveSemanticBindingState(state, current, current.name) !== undefined ||
|
||||
semanticResolutionBlocked(state, current, current.name) ||
|
||||
members[0] !== "process" ||
|
||||
!semanticOriginalGlobalProcessAt(node, state)
|
||||
)
|
||||
return undefined;
|
||||
return {
|
||||
specifier: "node:process",
|
||||
importedPath: members.slice(1),
|
||||
resolutionMode: "require",
|
||||
};
|
||||
};
|
||||
|
||||
/** Strip a static member chain, returning its root expression and member path. */
|
||||
export const semanticMemberChain = (
|
||||
node: t.Node | null | undefined,
|
||||
):
|
||||
| { readonly root: t.Node | null | undefined; readonly members: string[] }
|
||||
| undefined => {
|
||||
const members: string[] = [];
|
||||
let current = node;
|
||||
while (current !== null && current !== undefined) {
|
||||
@@ -39,15 +265,45 @@ export const semanticRequireOrigin = (
|
||||
members.push(member);
|
||||
current = current.object;
|
||||
}
|
||||
return { root: current, members: members.reverse() };
|
||||
};
|
||||
|
||||
/** Recover an unshadowed literal require origin and its exact member path. */
|
||||
export const semanticRequireOrigin = (
|
||||
node: t.Node | null | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptRequireOrigin | undefined => {
|
||||
const chain = semanticMemberChain(node);
|
||||
const current = chain?.root;
|
||||
if (
|
||||
chain === undefined ||
|
||||
!t.isCallExpression(current) ||
|
||||
!t.isIdentifier(current.callee, { name: "require" })
|
||||
)
|
||||
return undefined;
|
||||
if (
|
||||
writesModuleRequire(state).some(
|
||||
(write) =>
|
||||
!semanticTopLevelEffectFollowsCapture(
|
||||
current,
|
||||
write,
|
||||
state.parentsByNode,
|
||||
),
|
||||
)
|
||||
)
|
||||
return undefined;
|
||||
if (
|
||||
resolveSemanticBindingState(state, current.callee, "require") !==
|
||||
undefined ||
|
||||
semanticResolutionBlocked(state, current.callee, "require")
|
||||
semanticResolutionBlocked(state, current.callee, "require") ||
|
||||
(state.ambientBindingWrites.get("require") ?? []).some(
|
||||
(write) =>
|
||||
!semanticTopLevelEffectFollowsCapture(
|
||||
current,
|
||||
write,
|
||||
state.parentsByNode,
|
||||
),
|
||||
)
|
||||
)
|
||||
return undefined;
|
||||
const specifier = stringValue(current.arguments[0]);
|
||||
@@ -56,9 +312,50 @@ export const semanticRequireOrigin = (
|
||||
: {
|
||||
origin: {
|
||||
specifier,
|
||||
importedPath: members.reverse(),
|
||||
importedPath: chain.members,
|
||||
resolutionMode: "require",
|
||||
},
|
||||
call: current,
|
||||
};
|
||||
};
|
||||
|
||||
/** Node's fs.promises export and fs/promises require share one object. */
|
||||
export const semanticRequireExportsIdentity = <Key>(
|
||||
specifier: string,
|
||||
path: readonly Key[],
|
||||
): { readonly specifier: string; readonly path: readonly (Key | string)[] } => {
|
||||
const name = specifier.replace(/^node:/u, "");
|
||||
if (name === "fs/promises") return { specifier: name, path };
|
||||
return name === "fs" && path[0] === "promises" && path.length > 1
|
||||
? { specifier: "fs/promises", path: path.slice(1) }
|
||||
: { specifier: name, path };
|
||||
};
|
||||
|
||||
const MUTABLE_BUILTIN_DEFAULTS = new Set([
|
||||
...Object.keys(BUILTIN_RESOURCE_METHODS),
|
||||
"http",
|
||||
"https",
|
||||
"child_process",
|
||||
"timers",
|
||||
"process",
|
||||
]);
|
||||
|
||||
/** Mutable default/object selections share CommonJS identity; namespace slots do not. */
|
||||
export const semanticMutableBuiltinOrigin = <Key = never>(
|
||||
origin: JavaScriptModuleOrigin,
|
||||
selected: readonly Key[] = [],
|
||||
):
|
||||
| { readonly specifier: string; readonly path: readonly (Key | string)[] }
|
||||
| undefined => {
|
||||
const path = [...origin.importedPath, ...selected];
|
||||
if (origin.resolutionMode === "require")
|
||||
return { specifier: origin.specifier, path };
|
||||
const specifier = origin.specifier.replace(/^node:/u, "");
|
||||
if (!MUTABLE_BUILTIN_DEFAULTS.has(specifier)) return undefined;
|
||||
if (path[0] === "default") return { specifier, path: path.slice(1) };
|
||||
if (specifier === "process" && (path[0] === "env" || path[0] === "argv"))
|
||||
return { specifier, path };
|
||||
return specifier === "fs" && path[0] === "promises"
|
||||
? { specifier, path }
|
||||
: undefined;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user