mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 09:55:46 +00:00
Use shared lexical bindings and recovered bundler identities for require and chunk references. Preserve uncertain writes as unresolved evidence; remove name-only loader aliases and repeated definition scans. Reviewed-PR: #2989 Co-authored-by: Hussnain Ahmad <ha7886899@gmail.com>
523 lines
18 KiB
TypeScript
523 lines
18 KiB
TypeScript
import * as t from "@babel/types";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
|
|
import { BUILTIN_RESOURCE_METHODS } from "./javascriptBuiltinResourceMethods.js";
|
|
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
semanticResolutionBlocked,
|
|
semanticStableInitializer,
|
|
type JavaScriptSemanticAnalysisState,
|
|
} from "./javascriptSemanticState.js";
|
|
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";
|
|
|
|
export interface JavaScriptRequireOrigin {
|
|
readonly origin: JavaScriptModuleOrigin;
|
|
readonly call: t.CallExpression;
|
|
}
|
|
|
|
export type JavaScriptRequireLoaderFact =
|
|
| "commonjs-loader"
|
|
| "factory-loader"
|
|
| "create-require"
|
|
| "local"
|
|
| "unknown";
|
|
|
|
/** Classify a bare `require(...)` using the shared lexical binding owner. */
|
|
export const semanticRequireLoaderAt = (
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
factoryBindingIds: ReadonlySet<string>,
|
|
): JavaScriptRequireLoaderFact => {
|
|
if (!t.isIdentifier(node.callee, { name: "require" })) return "local";
|
|
const binding = resolveSemanticBindingState(state, node.callee, "require");
|
|
if (binding === undefined)
|
|
return semanticResolutionBlocked(state, node.callee, "require")
|
|
? "unknown"
|
|
: "commonjs-loader";
|
|
if (factoryBindingIds.has(binding.bindingId))
|
|
return binding.initializers.length === 0 ? "factory-loader" : "unknown";
|
|
if (isWrapperRequireInitializerCall(binding, node, state))
|
|
return "commonjs-loader";
|
|
const scope = state.scopesById.get(binding.scopeId);
|
|
if (
|
|
scope?.kind === "program" &&
|
|
binding.declarationKinds.size > 0 &&
|
|
[...binding.declarationKinds].every((kind) => kind === "var") &&
|
|
binding.initializers.length === 0
|
|
)
|
|
return "commonjs-loader";
|
|
if (isSemanticCreateRequireBinding(binding, state)) return "create-require";
|
|
const initializer = semanticStableInitializer(binding, state)?.node;
|
|
if (
|
|
(binding.kind === "function" || binding.kind === "class") &&
|
|
initializer !== undefined
|
|
)
|
|
return "local";
|
|
if (
|
|
binding.declarationKinds.size === 1 &&
|
|
binding.declarationKinds.has("const") &&
|
|
(t.isFunctionExpression(initializer) ||
|
|
t.isArrowFunctionExpression(initializer))
|
|
)
|
|
return "local";
|
|
return "unknown";
|
|
};
|
|
|
|
/** Classify a call through an exact recovered factory-loader binding. */
|
|
export const semanticFactoryRequireLoaderAt = (
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
bindingIds: ReadonlySet<string>,
|
|
): JavaScriptRequireLoaderFact | undefined => {
|
|
const callee = unwrapJavaScriptExpression(node.callee).node;
|
|
if (t.isIdentifier(callee))
|
|
return semanticFactoryLoaderMemberAt(node, state, bindingIds, null);
|
|
if (
|
|
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
|
semanticStaticPropertyKey(callee.property, callee.computed) === "bind"
|
|
)
|
|
return semanticFactoryLoaderMemberAt(node, state, bindingIds, "bind");
|
|
return undefined;
|
|
};
|
|
|
|
/** Classify an exact loader call by binding identity and expected member. */
|
|
export const semanticFactoryLoaderMemberAt = (
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
bindingIds: ReadonlySet<string>,
|
|
member: string | null,
|
|
): JavaScriptRequireLoaderFact | undefined => {
|
|
const callee = unwrapJavaScriptExpression(node.callee).node;
|
|
let loader: t.Identifier | undefined;
|
|
if (member === null) {
|
|
if (t.isIdentifier(callee)) loader = callee;
|
|
} else if (
|
|
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
|
semanticStaticPropertyKey(callee.property, callee.computed) === member
|
|
) {
|
|
const root = unwrapJavaScriptExpression(callee.object).node;
|
|
if (t.isIdentifier(root)) loader = root;
|
|
}
|
|
if (loader === undefined) return undefined;
|
|
const binding = resolveSemanticBindingState(state, loader, loader.name);
|
|
if (binding === undefined || !bindingIds.has(binding.bindingId))
|
|
return undefined;
|
|
return binding.initializers.length === 0 ? "factory-loader" : "unknown";
|
|
};
|
|
|
|
const isSemanticCreateRequireBinding = (
|
|
binding: NonNullable<ReturnType<typeof resolveSemanticBindingState>>,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean => {
|
|
if (binding.initializers.length !== 1) return false;
|
|
const initializer = semanticStableInitializer(binding, state)?.node;
|
|
if (!t.isCallExpression(initializer)) return false;
|
|
const callee = unwrapJavaScriptExpression(initializer.callee).node;
|
|
if (t.isIdentifier(callee)) {
|
|
const origins = resolveSemanticBindingState(
|
|
state,
|
|
callee,
|
|
callee.name,
|
|
)?.directOrigins;
|
|
if (origins?.length !== 1) return false;
|
|
const [origin] = origins;
|
|
return (
|
|
origin?.importedPath.at(-1) === "createRequire" &&
|
|
["module", "node:module"].includes(origin.specifier)
|
|
);
|
|
}
|
|
const chain = semanticMemberChain(callee);
|
|
if (chain === undefined || chain.members.at(-1) !== "createRequire")
|
|
return false;
|
|
if (!t.isNode(chain.root)) return false;
|
|
const root = unwrapJavaScriptExpression(chain.root).node;
|
|
if (!t.isIdentifier(root)) return false;
|
|
const origins = resolveSemanticBindingState(
|
|
state,
|
|
root,
|
|
root.name,
|
|
)?.directOrigins;
|
|
if (origins?.length !== 1) return false;
|
|
const [origin] = origins;
|
|
return (
|
|
origin?.importedPath.length === 0 &&
|
|
["module", "node:module"].includes(origin.specifier)
|
|
);
|
|
};
|
|
|
|
/** The RHS call runs before its assignment replaces Node's wrapper loader. */
|
|
const isWrapperRequireInitializerCall = (
|
|
binding: NonNullable<ReturnType<typeof resolveSemanticBindingState>>,
|
|
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
): boolean => {
|
|
const scope = state.scopesById.get(binding.scopeId);
|
|
if (
|
|
scope?.kind !== "program" ||
|
|
binding.kind !== "variable" ||
|
|
binding.declarationKinds.size !== 1 ||
|
|
!binding.declarationKinds.has("var") ||
|
|
binding.initializers.length !== 1 ||
|
|
binding.initializers[0]?.node !== node
|
|
)
|
|
return false;
|
|
const assignment = state.parentsByNode.get(node);
|
|
if (
|
|
!t.isAssignmentExpression(assignment) ||
|
|
assignment.right !== node ||
|
|
state.conditionallySkippedExpressions.has(node)
|
|
)
|
|
return false;
|
|
const statement = state.parentsByNode.get(assignment);
|
|
if (!t.isExpressionStatement(statement)) return false;
|
|
if (state.parentsByNode.get(statement) !== state.program) return false;
|
|
const target = unwrapJavaScriptExpression(assignment.left).node;
|
|
return (
|
|
t.isIdentifier(target, { name: "require" }) &&
|
|
resolveSemanticBindingState(state, target, target.name) === binding
|
|
);
|
|
};
|
|
|
|
// 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,
|
|
): 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))
|
|
);
|
|
};
|
|
traverseJavaScriptAst(state.program, {
|
|
enter: (node) => {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
});
|
|
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) {
|
|
current = unwrapJavaScriptExpression(current).node;
|
|
if (
|
|
!t.isMemberExpression(current) &&
|
|
!t.isOptionalMemberExpression(current)
|
|
)
|
|
break;
|
|
const member = semanticStaticPropertyKey(
|
|
current.property,
|
|
current.computed,
|
|
);
|
|
if (member === null || !t.isNode(current.object)) return undefined;
|
|
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") ||
|
|
(state.ambientBindingWrites.get("require") ?? []).some(
|
|
(write) =>
|
|
!semanticTopLevelEffectFollowsCapture(
|
|
current,
|
|
write,
|
|
state.parentsByNode,
|
|
),
|
|
)
|
|
)
|
|
return undefined;
|
|
const specifier = stringValue(current.arguments[0]);
|
|
return specifier === undefined
|
|
? undefined
|
|
: {
|
|
origin: {
|
|
specifier,
|
|
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;
|
|
};
|