mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
Preserve missing ASAR companion occurrence identity, declared size and unknown content in graph nodes and resolution limitations. Apply URL text semantics once for ESM lookup while preserving literal CommonJS paths and original specifiers. Keep CJS wrapper var bindings and assignment history, recognizing only untouched wrapper redeclarations. Validation: real ASAR producer workflow and CLI/MCP path/coverage cases; semantic reassignment controls. Context7 Node documentation and direct Node wrapper execution confirm wrapper behavior. Fixes #3213 Fixes #3214 Fixes #3223 Related: #3278, #3254 Co-authored-by: Geronimo <varungore1@gmail.com> Co-authored-by: Paolo Valletta <paovalletta@hotmail.it>
524 lines
18 KiB
TypeScript
524 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,
|
|
isUnmodifiedCommonJsWrapperBinding,
|
|
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";
|
|
if (isUnmodifiedCommonJsWrapperBinding(node.callee, "require", state))
|
|
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")
|
|
: isUnmodifiedCommonJsWrapperBinding(value, "module", state);
|
|
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;
|
|
const requireBinding = resolveSemanticBindingState(
|
|
state,
|
|
current.callee,
|
|
"require",
|
|
);
|
|
if (
|
|
(requireBinding !== undefined &&
|
|
!isUnmodifiedCommonJsWrapperBinding(current.callee, "require", state) &&
|
|
!isWrapperRequireInitializerCall(requireBinding, current, state)) ||
|
|
(requireBinding === 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;
|
|
};
|