2026-10-07 19:09:25 +08:00
|
|
|
import * as t from "@babel/types";
|
|
|
|
|
|
2026-10-14 08:44:52 +08:00
|
|
|
import { BUILTIN_RESOURCE_METHODS } from "./javascriptBuiltinResourceMethods.js";
|
2026-10-07 19:09:25 +08:00
|
|
|
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
|
|
|
|
import {
|
|
|
|
|
resolveSemanticBindingState,
|
|
|
|
|
semanticResolutionBlocked,
|
|
|
|
|
type JavaScriptSemanticAnalysisState,
|
|
|
|
|
} from "./javascriptSemanticState.js";
|
2026-10-10 01:39:28 +08:00
|
|
|
import {
|
|
|
|
|
semanticStaticPropertyKey,
|
|
|
|
|
unwrapJavaScriptExpression,
|
|
|
|
|
} from "./javascriptAstValues.js";
|
2026-10-14 08:44:52 +08:00
|
|
|
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
|
2026-10-07 19:09:25 +08:00
|
|
|
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
2026-10-14 08:44:52 +08:00
|
|
|
import { semanticTopLevelEffectFollowsCapture } from "./javascriptSemanticMutationInitializers.js";
|
|
|
|
|
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
2026-10-07 19:09:25 +08:00
|
|
|
|
2026-10-14 08:44:52 +08:00
|
|
|
export interface JavaScriptRequireOrigin {
|
2026-10-11 19:16:32 +08:00
|
|
|
readonly origin: JavaScriptModuleOrigin;
|
|
|
|
|
readonly call: t.CallExpression;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 08:44:52 +08:00
|
|
|
// 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 = (
|
2026-10-07 19:09:25 +08:00
|
|
|
state: JavaScriptSemanticAnalysisState,
|
2026-10-14 08:44:52 +08:00
|
|
|
): 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 => {
|
2026-10-07 19:09:25 +08:00
|
|
|
const members: string[] = [];
|
2026-10-10 01:39:28 +08:00
|
|
|
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;
|
2026-10-07 19:09:25 +08:00
|
|
|
members.push(member);
|
2026-10-10 01:39:28 +08:00
|
|
|
current = current.object;
|
2026-10-07 19:09:25 +08:00
|
|
|
}
|
2026-10-14 08:44:52 +08:00
|
|
|
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;
|
2026-10-07 19:09:25 +08:00
|
|
|
if (
|
2026-10-14 08:44:52 +08:00
|
|
|
chain === undefined ||
|
2026-10-10 01:39:28 +08:00
|
|
|
!t.isCallExpression(current) ||
|
|
|
|
|
!t.isIdentifier(current.callee, { name: "require" })
|
2026-10-07 19:09:25 +08:00
|
|
|
)
|
|
|
|
|
return undefined;
|
2026-10-14 08:44:52 +08:00
|
|
|
if (
|
|
|
|
|
writesModuleRequire(state).some(
|
|
|
|
|
(write) =>
|
|
|
|
|
!semanticTopLevelEffectFollowsCapture(
|
|
|
|
|
current,
|
|
|
|
|
write,
|
|
|
|
|
state.parentsByNode,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return undefined;
|
2026-10-07 19:09:25 +08:00
|
|
|
if (
|
2026-10-10 01:39:28 +08:00
|
|
|
resolveSemanticBindingState(state, current.callee, "require") !==
|
|
|
|
|
undefined ||
|
2026-10-14 08:44:52 +08:00
|
|
|
semanticResolutionBlocked(state, current.callee, "require") ||
|
|
|
|
|
(state.ambientBindingWrites.get("require") ?? []).some(
|
|
|
|
|
(write) =>
|
|
|
|
|
!semanticTopLevelEffectFollowsCapture(
|
|
|
|
|
current,
|
|
|
|
|
write,
|
|
|
|
|
state.parentsByNode,
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-10-07 19:09:25 +08:00
|
|
|
)
|
|
|
|
|
return undefined;
|
2026-10-10 01:39:28 +08:00
|
|
|
const specifier = stringValue(current.arguments[0]);
|
2026-10-07 19:09:25 +08:00
|
|
|
return specifier === undefined
|
|
|
|
|
? undefined
|
2026-10-11 19:16:32 +08:00
|
|
|
: {
|
2026-10-13 05:04:21 +08:00
|
|
|
origin: {
|
|
|
|
|
specifier,
|
2026-10-14 08:44:52 +08:00
|
|
|
importedPath: chain.members,
|
2026-10-13 05:04:21 +08:00
|
|
|
resolutionMode: "require",
|
|
|
|
|
},
|
2026-10-11 19:16:32 +08:00
|
|
|
call: current,
|
|
|
|
|
};
|
2026-10-07 19:09:25 +08:00
|
|
|
};
|
2026-10-14 08:44:52 +08:00
|
|
|
|
|
|
|
|
/** 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;
|
|
|
|
|
};
|