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 parent
0f4c484be5
commit
04672f2b91
34 files changed
+3778
-321
No files matched your search
@@ -1,19 +1,36 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { BUILTIN_RESOURCE_METHODS } from "./javascriptBuiltinResourceMethods.js";
|
||||
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
|
||||
import {
|
||||
semanticRequireOrigin,
|
||||
semanticRequireExportsIdentity,
|
||||
semanticGlobalProcessOrigin,
|
||||
semanticMutableBuiltinOrigin,
|
||||
} from "./javascriptSemanticRequireOrigin.js";
|
||||
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
evaluateSemanticProvenance,
|
||||
evaluateSemanticProvenanceAtRead,
|
||||
} from "./javascriptSemanticValues.js";
|
||||
import {
|
||||
semanticPropertyPathKeyMatches,
|
||||
type JavaScriptSemanticPropertyPath,
|
||||
} from "./javascriptSemanticPropertyPaths.js";
|
||||
|
||||
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
|
||||
import { semanticTopLevelEffectFollowsCapture } from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
type JavaScriptSemanticMemberEffect,
|
||||
type JavaScriptSemanticRequiredModuleEffect,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
semanticStaticPropertyKey,
|
||||
semanticStaticPropertyName,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
@@ -137,59 +154,715 @@ export interface DataEffectTraversalContext {
|
||||
readonly ancestors: t.Node[];
|
||||
}
|
||||
|
||||
/** Resolve a direct or namespace-imported built-in method. */
|
||||
/** Resolve built-in methods from the shared, unambiguous binding provenance. */
|
||||
export const builtinDataEffectMethod = (
|
||||
callee: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
specifiers: readonly string[],
|
||||
): string | null => {
|
||||
const expression = unwrapJavaScriptExpression(callee).node;
|
||||
if (expression !== callee)
|
||||
return builtinDataEffectMethod(expression, state, specifiers);
|
||||
if (t.isSequenceExpression(expression)) {
|
||||
const final = expression.expressions.at(-1);
|
||||
return final === undefined
|
||||
? null
|
||||
: builtinDataEffectMethod(final, state, specifiers);
|
||||
}
|
||||
const required = semanticRequireOrigin(callee, state);
|
||||
if (required !== undefined) {
|
||||
const origin = builtinNamespaceOrigin(required.origin);
|
||||
return origin.importedPath.length === 1 &&
|
||||
specifiers.some(
|
||||
(specifier) => specifier.replace(/^node:/u, "") === origin.specifier,
|
||||
) &&
|
||||
!requiredModuleChanged(
|
||||
required.origin.specifier,
|
||||
required.origin.importedPath,
|
||||
callee,
|
||||
state,
|
||||
true,
|
||||
)
|
||||
? (origin.importedPath[0] ?? null)
|
||||
: null;
|
||||
}
|
||||
if (t.isIdentifier(callee)) {
|
||||
const binding = resolveSemanticBindingState(state, callee, callee.name);
|
||||
return moduleMethod(binding, specifiers);
|
||||
const path = builtinBindingPath(
|
||||
binding,
|
||||
state,
|
||||
specifiers,
|
||||
callee,
|
||||
[],
|
||||
true,
|
||||
);
|
||||
return path?.length === 1 ? (path[0] ?? null) : null;
|
||||
}
|
||||
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
|
||||
return null;
|
||||
if (!t.isIdentifier(callee.object)) return null;
|
||||
if (!t.isIdentifier(callee.object)) {
|
||||
const method = semanticStaticPropertyKey(callee.property, callee.computed);
|
||||
const receiver = semanticBoundMemberPath(callee.object, state);
|
||||
if (method !== null && receiver !== undefined) {
|
||||
const provenance = evaluateSemanticProvenance(receiver.binding, state);
|
||||
if (provenance.status === "module") {
|
||||
const origin = builtinNamespaceOrigin({
|
||||
...provenance.origins[0],
|
||||
importedPath: [
|
||||
...provenance.origins[0].importedPath,
|
||||
...receiver.path,
|
||||
],
|
||||
});
|
||||
if (
|
||||
specifiers.some(
|
||||
(specifier) =>
|
||||
specifier.replace(/^node:/u, "") === origin.specifier,
|
||||
)
|
||||
)
|
||||
return builtinBindingPath(
|
||||
receiver.binding,
|
||||
state,
|
||||
specifiers,
|
||||
callee,
|
||||
[...receiver.path, method],
|
||||
true,
|
||||
)?.length === 0
|
||||
? method
|
||||
: null;
|
||||
}
|
||||
}
|
||||
const source = semanticExpressionModuleOrigin(callee.object, state);
|
||||
if (method === null || source === undefined) return null;
|
||||
const origin = builtinNamespaceOrigin(source);
|
||||
return origin.importedPath.length === 0 &&
|
||||
specifiers.some(
|
||||
(specifier) => specifier.replace(/^node:/u, "") === origin.specifier,
|
||||
) &&
|
||||
!requiredModuleChanged(
|
||||
origin.specifier,
|
||||
[...origin.importedPath, method],
|
||||
callee,
|
||||
state,
|
||||
true,
|
||||
callee.object,
|
||||
)
|
||||
? method
|
||||
: null;
|
||||
}
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
callee.object,
|
||||
callee.object.name,
|
||||
);
|
||||
const namespace = binding?.directOrigins.some(
|
||||
({ specifier, importedPath }) =>
|
||||
specifiers.includes(specifier) &&
|
||||
semanticBuiltinNamespacePath(importedPath),
|
||||
const method = semanticStaticPropertyKey(callee.property, callee.computed);
|
||||
if (method === null) return null;
|
||||
const resolvedPath = builtinBindingPath(
|
||||
binding,
|
||||
state,
|
||||
specifiers,
|
||||
callee,
|
||||
[method],
|
||||
true,
|
||||
);
|
||||
return namespace
|
||||
? semanticStaticPropertyName(callee.property, callee.computed)
|
||||
if (resolvedPath?.length === 0) return method;
|
||||
const initializer = binding?.initializers[0];
|
||||
if (
|
||||
initializer === undefined ||
|
||||
binding?.initializers.length !== 1 ||
|
||||
(!t.isMemberExpression(initializer.node) &&
|
||||
!t.isOptionalMemberExpression(initializer.node))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const provenance = evaluateSemanticProvenance(binding, state);
|
||||
if (provenance.status !== "module") return null;
|
||||
const origin = builtinNamespaceOrigin(provenance.origins[0]);
|
||||
const selected = [...origin.importedPath, method];
|
||||
return specifiers.some(
|
||||
(specifier) => specifier.replace(/^node:/u, "") === origin.specifier,
|
||||
) &&
|
||||
builtinBindingPath(
|
||||
binding,
|
||||
state,
|
||||
specifiers,
|
||||
initializer.node,
|
||||
[method],
|
||||
true,
|
||||
)?.length === 0 &&
|
||||
!requiredModuleChanged(
|
||||
provenance.origins[0].specifier,
|
||||
selected,
|
||||
callee,
|
||||
state,
|
||||
true,
|
||||
initializer.node,
|
||||
)
|
||||
? method
|
||||
: null;
|
||||
};
|
||||
|
||||
const moduleMethod = (
|
||||
binding: JavaScriptSemanticBindingState | undefined,
|
||||
specifiers: readonly string[],
|
||||
): string | null => {
|
||||
for (const { specifier, importedPath } of binding?.directOrigins ?? []) {
|
||||
if (!specifiers.includes(specifier)) continue;
|
||||
return importedPath.at(-1) ?? null;
|
||||
const semanticBoundMemberPath = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
):
|
||||
| {
|
||||
readonly binding: JavaScriptSemanticBindingState;
|
||||
readonly path: string[];
|
||||
}
|
||||
| undefined => {
|
||||
const path: string[] = [];
|
||||
let current = unwrapJavaScriptExpression(node).node;
|
||||
while (
|
||||
t.isMemberExpression(current) ||
|
||||
t.isOptionalMemberExpression(current)
|
||||
) {
|
||||
const key = semanticStaticPropertyKey(current.property, current.computed);
|
||||
if (key === null || !t.isNode(current.object)) return undefined;
|
||||
path.unshift(key);
|
||||
current = unwrapJavaScriptExpression(current.object).node;
|
||||
}
|
||||
return null;
|
||||
if (!t.isIdentifier(current)) return undefined;
|
||||
const binding = resolveSemanticBindingState(state, current, current.name);
|
||||
return binding === undefined ? undefined : { binding, path };
|
||||
};
|
||||
|
||||
type BuiltinBindingMemo = Map<t.Node, Map<string, readonly string[] | null>>;
|
||||
|
||||
const builtinBindingPath = (
|
||||
binding: JavaScriptSemanticBindingState | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
specifiers: readonly string[],
|
||||
capture: t.Node,
|
||||
selectedPath: readonly string[] = [],
|
||||
callableValue = false,
|
||||
visited: ReadonlySet<string> = new Set(),
|
||||
memo: BuiltinBindingMemo = new Map(),
|
||||
provenanceCapture?: t.Identifier,
|
||||
savedObjectCapture?: t.Node,
|
||||
): readonly string[] | null => {
|
||||
if (binding === undefined) return null;
|
||||
const key = JSON.stringify([
|
||||
binding.bindingId,
|
||||
selectedPath,
|
||||
callableValue,
|
||||
provenanceCapture?.start,
|
||||
provenanceCapture?.end,
|
||||
savedObjectCapture?.start,
|
||||
savedObjectCapture?.end,
|
||||
]);
|
||||
let captured = memo.get(capture);
|
||||
if (captured?.has(key)) return captured.get(key) ?? null;
|
||||
if (visited.has(binding.bindingId)) return null;
|
||||
if (captured === undefined) {
|
||||
captured = new Map();
|
||||
memo.set(capture, captured);
|
||||
}
|
||||
const result = computeBuiltinBindingPath(
|
||||
binding,
|
||||
state,
|
||||
specifiers,
|
||||
capture,
|
||||
selectedPath,
|
||||
callableValue,
|
||||
visited,
|
||||
memo,
|
||||
provenanceCapture,
|
||||
savedObjectCapture,
|
||||
);
|
||||
captured.set(key, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const computeBuiltinBindingPath = (
|
||||
binding: JavaScriptSemanticBindingState | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
specifiers: readonly string[],
|
||||
capture: t.Node,
|
||||
selectedPath: readonly string[] = [],
|
||||
callableValue = false,
|
||||
visited: ReadonlySet<string>,
|
||||
memo: BuiltinBindingMemo,
|
||||
provenanceCapture?: t.Identifier,
|
||||
savedObjectCapture?: t.Node,
|
||||
): readonly string[] | null => {
|
||||
if (binding === undefined || visited.has(binding.bindingId)) return null;
|
||||
// Provenance identifies the module, independently of whether a selected slot
|
||||
// still refers to its original built-in after member mutation or escape.
|
||||
if (
|
||||
binding.memberWrites.some(
|
||||
({ path, node }) =>
|
||||
affectsBuiltinPath(path, selectedPath) &&
|
||||
!(
|
||||
semanticTopLevelEffectFollowsCapture(
|
||||
path.length === 0 && provenanceCapture !== undefined
|
||||
? provenanceCapture
|
||||
: capture,
|
||||
node,
|
||||
state.parentsByNode,
|
||||
) ||
|
||||
(savedObjectCapture !== undefined &&
|
||||
path.length < selectedPath.length &&
|
||||
semanticTopLevelEffectFollowsCapture(
|
||||
savedObjectCapture,
|
||||
node,
|
||||
state.parentsByNode,
|
||||
))
|
||||
),
|
||||
) ||
|
||||
builtinEscapes(binding, state).some(
|
||||
({ path, node }) =>
|
||||
affectsBuiltinPath(path, selectedPath) &&
|
||||
// A function-value escape cannot replace its native [[Call]]. An
|
||||
// escaped containing object can still replace the selected method.
|
||||
(!callableValue || path.length < selectedPath.length) &&
|
||||
!semanticTopLevelEffectFollowsCapture(
|
||||
capture,
|
||||
node,
|
||||
state.parentsByNode,
|
||||
),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const provenance =
|
||||
provenanceCapture === undefined
|
||||
? evaluateSemanticProvenance(binding, state)
|
||||
: evaluateSemanticProvenanceAtRead(binding, provenanceCapture, state);
|
||||
if (provenance.status !== "module") return null;
|
||||
const { importedPath } = provenance.origins[0];
|
||||
const namespace = builtinNamespaceOrigin(provenance.origins[0]);
|
||||
if (
|
||||
!specifiers.some((item) => {
|
||||
const name = item.replace(/^node:/u, "");
|
||||
return (
|
||||
name === namespace.specifier ||
|
||||
(namespace.specifier === "fs" &&
|
||||
selectedPath[0] === "promises" &&
|
||||
name === "fs/promises")
|
||||
);
|
||||
})
|
||||
)
|
||||
return null;
|
||||
let resolvedPath: readonly string[] = importedPath;
|
||||
for (const origin of binding.directOrigins) {
|
||||
const shared = semanticMutableBuiltinOrigin(origin, selectedPath);
|
||||
if (
|
||||
shared !== undefined &&
|
||||
requiredModuleChanged(
|
||||
shared.specifier,
|
||||
shared.path,
|
||||
capture,
|
||||
state,
|
||||
callableValue,
|
||||
)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
const ancestors = new Set([...visited, binding.bindingId]);
|
||||
for (const initializer of binding.initializers) {
|
||||
const pending = [
|
||||
{
|
||||
node: initializer.node,
|
||||
path: [...initializer.projection.map(String), ...selectedPath],
|
||||
savedObjectCapture:
|
||||
selectedPath.length > 0 &&
|
||||
(t.isMemberExpression(initializer.node) ||
|
||||
t.isOptionalMemberExpression(initializer.node))
|
||||
? initializer.node
|
||||
: savedObjectCapture,
|
||||
capture:
|
||||
callableValue &&
|
||||
selectedPath.length === 0 &&
|
||||
initializer.projection.length > 0
|
||||
? initializer.node
|
||||
: capture,
|
||||
},
|
||||
];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop()!;
|
||||
const node = unwrapJavaScriptExpression(current.node).node;
|
||||
if (t.isIdentifier(node)) {
|
||||
const source = resolveSemanticBindingState(state, node, node.name);
|
||||
const sourcePath = builtinBindingPath(
|
||||
source,
|
||||
state,
|
||||
specifiers,
|
||||
current.capture,
|
||||
current.path,
|
||||
callableValue,
|
||||
ancestors,
|
||||
memo,
|
||||
node,
|
||||
current.savedObjectCapture,
|
||||
);
|
||||
if (sourcePath === null) return null;
|
||||
// Normalize ESM default namespaces at their owner before projecting an alias.
|
||||
resolvedPath = [...sourcePath, ...current.path].slice(
|
||||
0,
|
||||
sourcePath.length + current.path.length - selectedPath.length,
|
||||
);
|
||||
} else if (
|
||||
t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node)
|
||||
) {
|
||||
const key = semanticStaticPropertyKey(node.property, node.computed);
|
||||
if (key === null) return null;
|
||||
pending.push({
|
||||
node: node.object,
|
||||
path: [key, ...current.path],
|
||||
savedObjectCapture: current.savedObjectCapture,
|
||||
// Freeze a selected function, not later member reads through a saved object.
|
||||
capture:
|
||||
callableValue && selectedPath.length === 0 ? node : current.capture,
|
||||
});
|
||||
} else if (t.isSequenceExpression(node)) {
|
||||
const final = node.expressions.at(-1);
|
||||
if (final === undefined) return null;
|
||||
pending.push({ ...current, node: final });
|
||||
} else if (t.isAssignmentExpression(node) && node.operator === "=") {
|
||||
pending.push({ ...current, node: node.right });
|
||||
} else if (t.isConditionalExpression(node)) {
|
||||
pending.push(
|
||||
{ ...current, node: node.consequent },
|
||||
{ ...current, node: node.alternate },
|
||||
);
|
||||
} else if (t.isLogicalExpression(node)) {
|
||||
pending.push(
|
||||
{ ...current, node: node.left },
|
||||
{ ...current, node: node.right },
|
||||
);
|
||||
} else {
|
||||
const required = semanticRequireOrigin(node, state);
|
||||
if (
|
||||
required !== undefined &&
|
||||
requiredModuleChanged(
|
||||
required.origin.specifier,
|
||||
[...required.origin.importedPath, ...current.path],
|
||||
current.capture,
|
||||
state,
|
||||
callableValue,
|
||||
current.savedObjectCapture,
|
||||
)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only a direct ESM default import denotes the built-in namespace;
|
||||
// require("node:fs").default is a literal property, not interop syntax.
|
||||
return binding.directOrigins.length > 0 &&
|
||||
semanticBuiltinNamespacePath(importedPath)
|
||||
? []
|
||||
: builtinNamespaceOrigin({
|
||||
...provenance.origins[0],
|
||||
importedPath: resolvedPath,
|
||||
}).importedPath;
|
||||
};
|
||||
|
||||
/** Interpret known Node namespace aliases without changing source provenance. */
|
||||
const builtinNamespaceOrigin = (
|
||||
origin: JavaScriptModuleOrigin,
|
||||
): JavaScriptModuleOrigin => {
|
||||
const specifier = origin.specifier.replace(/^node:/u, "");
|
||||
const importedPath =
|
||||
origin.resolutionMode !== "require" && origin.importedPath[0] === "default"
|
||||
? origin.importedPath.slice(1)
|
||||
: origin.importedPath;
|
||||
return specifier === "fs" && importedPath[0] === "promises"
|
||||
? {
|
||||
...origin,
|
||||
specifier: "fs/promises",
|
||||
importedPath: importedPath.slice(1),
|
||||
}
|
||||
: { ...origin, specifier, importedPath };
|
||||
};
|
||||
|
||||
const affectsBuiltinPath = (
|
||||
path: JavaScriptSemanticPropertyPath,
|
||||
selected: readonly string[],
|
||||
): boolean =>
|
||||
path.length <= selected.length &&
|
||||
path.every((key, index) =>
|
||||
semanticPropertyPathKeyMatches(key, selected[index]!),
|
||||
);
|
||||
|
||||
const BUILTIN_RECEIVER_METHODS: Readonly<Record<string, ReadonlySet<string>>> =
|
||||
{
|
||||
fs: new Set([
|
||||
"readFile",
|
||||
"readFileSync",
|
||||
"writeFile",
|
||||
"writeFileSync",
|
||||
"close",
|
||||
"closeSync",
|
||||
"existsSync",
|
||||
...BUILTIN_RESOURCE_METHODS.fs,
|
||||
]),
|
||||
"fs/promises": new Set([
|
||||
"readFile",
|
||||
"writeFile",
|
||||
...BUILTIN_RESOURCE_METHODS["fs/promises"],
|
||||
]),
|
||||
net: new Set(BUILTIN_RESOURCE_METHODS.net),
|
||||
tls: new Set(BUILTIN_RESOURCE_METHODS.tls),
|
||||
http: new Set(["request", "get"]),
|
||||
https: new Set(["request", "get"]),
|
||||
child_process: new Set(["spawn", "exec", "execFile", "fork"]),
|
||||
process: new Set(["cwd", "uptime", "hrtime", "nextTick"]),
|
||||
timers: new Set([
|
||||
"setTimeout",
|
||||
"setInterval",
|
||||
"setImmediate",
|
||||
"clearTimeout",
|
||||
"clearInterval",
|
||||
"clearImmediate",
|
||||
]),
|
||||
};
|
||||
|
||||
interface BuiltinEffectIndex {
|
||||
readonly writesByModule: Map<
|
||||
string,
|
||||
readonly JavaScriptSemanticRequiredModuleEffect[]
|
||||
>;
|
||||
readonly moduleEffects: Map<
|
||||
string,
|
||||
readonly JavaScriptSemanticRequiredModuleEffect[]
|
||||
>;
|
||||
readonly bindingEscapes: Map<
|
||||
string,
|
||||
readonly JavaScriptSemanticMemberEffect[]
|
||||
>;
|
||||
readonly receivers: WeakMap<t.Node, boolean>;
|
||||
}
|
||||
|
||||
// Derived passes run after mutation collection. These indexes share that state's
|
||||
// lifetime and never cache facts while its mutation owner is still producing them.
|
||||
const builtinEffectIndexes = new WeakMap<
|
||||
JavaScriptSemanticAnalysisState,
|
||||
BuiltinEffectIndex
|
||||
>();
|
||||
|
||||
const builtinEffectIndex = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): BuiltinEffectIndex => {
|
||||
let index = builtinEffectIndexes.get(state);
|
||||
if (index === undefined) {
|
||||
index = {
|
||||
writesByModule: new Map(
|
||||
[...state.requiredModuleEffects].map(([name, effects]) => [
|
||||
name,
|
||||
effects.filter(({ kind }) => kind === "write"),
|
||||
]),
|
||||
),
|
||||
moduleEffects: new Map(),
|
||||
bindingEscapes: new Map(),
|
||||
receivers: new WeakMap(),
|
||||
};
|
||||
builtinEffectIndexes.set(state, index);
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
const builtinEscapes = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): readonly JavaScriptSemanticMemberEffect[] => {
|
||||
const cache = builtinEffectIndex(state).bindingEscapes;
|
||||
let effects = cache.get(binding.bindingId);
|
||||
if (effects === undefined) {
|
||||
effects = binding.escapedPaths.filter(
|
||||
({ node }) => !originalBuiltinReceiver(node, state),
|
||||
);
|
||||
cache.set(binding.bindingId, effects);
|
||||
}
|
||||
return effects;
|
||||
};
|
||||
|
||||
const builtinModuleEffects = (
|
||||
specifier: string,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): readonly JavaScriptSemanticRequiredModuleEffect[] => {
|
||||
const cache = builtinEffectIndex(state).moduleEffects;
|
||||
let effects = cache.get(specifier);
|
||||
if (effects === undefined) {
|
||||
effects = (state.requiredModuleEffects.get(specifier) ?? []).filter(
|
||||
({ kind, node }) =>
|
||||
kind === "write" || !originalBuiltinReceiver(node, state),
|
||||
);
|
||||
if (specifier === "fs/promises") {
|
||||
const sharedFsEscapes = (state.requiredModuleEffects.get("fs") ?? [])
|
||||
.filter(
|
||||
({ kind, path, node }) =>
|
||||
kind === "escape" &&
|
||||
(path.length === 0 || path[0] === "promises") &&
|
||||
!originalBuiltinReceiver(node, state),
|
||||
)
|
||||
.map((effect) => ({
|
||||
...effect,
|
||||
path:
|
||||
effect.path[0] === "promises" ? effect.path.slice(1) : effect.path,
|
||||
}));
|
||||
effects = [...effects, ...sharedFsEscapes];
|
||||
}
|
||||
cache.set(specifier, effects);
|
||||
}
|
||||
return effects;
|
||||
};
|
||||
|
||||
/** Check shared exports at the use or method-capture point, not at every later call. */
|
||||
const requiredModuleChanged = (
|
||||
specifier: string,
|
||||
selected: readonly string[],
|
||||
capture: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
callableValue = false,
|
||||
savedObjectCapture?: t.Node,
|
||||
): boolean => {
|
||||
const identity = semanticRequireExportsIdentity(specifier, selected);
|
||||
return builtinModuleEffects(identity.specifier, state).some(
|
||||
({ path, node, kind }) =>
|
||||
!(
|
||||
savedObjectCapture !== undefined &&
|
||||
path.length < identity.path.length &&
|
||||
semanticTopLevelEffectFollowsCapture(
|
||||
savedObjectCapture,
|
||||
node,
|
||||
state.parentsByNode,
|
||||
)
|
||||
) &&
|
||||
affectsBuiltinPath(path, identity.path) &&
|
||||
!semanticTopLevelEffectFollowsCapture(
|
||||
capture,
|
||||
node,
|
||||
state.parentsByNode,
|
||||
) &&
|
||||
(kind === "write" ||
|
||||
!callableValue ||
|
||||
path.length < identity.path.length),
|
||||
);
|
||||
};
|
||||
|
||||
/** Calling an unchanged built-in as a member is not an escape to unknown code. */
|
||||
const originalBuiltinReceiver = (
|
||||
node: t.Node | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
if (node === undefined) return false;
|
||||
const receivers = builtinEffectIndex(state).receivers;
|
||||
const previous = receivers.get(node);
|
||||
if (previous !== undefined) return previous;
|
||||
const result = computeOriginalBuiltinReceiver(node, state);
|
||||
receivers.set(node, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const computeOriginalBuiltinReceiver = (
|
||||
node: t.Node | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
|
||||
return false;
|
||||
const callee = node.callee;
|
||||
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
|
||||
return false;
|
||||
const receiver = t.isIdentifier(callee.object)
|
||||
? resolveSemanticBindingState(state, callee.object, callee.object.name)
|
||||
: undefined;
|
||||
const provenance =
|
||||
receiver === undefined
|
||||
? undefined
|
||||
: evaluateSemanticProvenance(receiver, state);
|
||||
const sourceOrigin =
|
||||
provenance?.status === "module"
|
||||
? provenance.origins[0]
|
||||
: (semanticExpressionModuleOrigin(callee.object, state) ??
|
||||
semanticRequireOrigin(callee.object, state)?.origin ??
|
||||
semanticGlobalProcessOrigin(callee.object, state));
|
||||
if (sourceOrigin === undefined) return false;
|
||||
const origin = builtinNamespaceOrigin(sourceOrigin);
|
||||
if (origin.importedPath.length !== 0) return false;
|
||||
const method = semanticStaticPropertyKey(callee.property, callee.computed);
|
||||
const identity = semanticRequireExportsIdentity(origin.specifier, [
|
||||
method ?? "",
|
||||
]);
|
||||
if (
|
||||
method === null ||
|
||||
!BUILTIN_RECEIVER_METHODS[origin.specifier]?.has(method) ||
|
||||
receiver?.memberWrites.some(
|
||||
({ path, node: write }) =>
|
||||
affectsBuiltinPath(path, [method]) &&
|
||||
!semanticTopLevelEffectFollowsCapture(node, write, state.parentsByNode),
|
||||
) ||
|
||||
(
|
||||
builtinEffectIndex(state).writesByModule.get(identity.specifier) ?? []
|
||||
).some(
|
||||
({ path, node: write }) =>
|
||||
affectsBuiltinPath(path, identity.path) &&
|
||||
!semanticTopLevelEffectFollowsCapture(node, write, state.parentsByNode),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return !node.arguments.some((argument) => {
|
||||
if (!t.isIdentifier(argument)) return false;
|
||||
const passed = resolveSemanticBindingState(state, argument, argument.name);
|
||||
if (passed === undefined) return false;
|
||||
const passedOrigin = evaluateSemanticProvenance(passed, state);
|
||||
return (
|
||||
passedOrigin.status === "module" &&
|
||||
builtinNamespaceOrigin(passedOrigin.origins[0]).specifier ===
|
||||
origin.specifier &&
|
||||
builtinNamespaceOrigin(passedOrigin.origins[0]).importedPath.length === 0
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const semanticExpressionModuleOrigin = (
|
||||
expression: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
visited = new Set<string>(),
|
||||
): JavaScriptModuleOrigin | undefined => {
|
||||
const node = unwrapJavaScriptExpression(expression).node;
|
||||
const required = semanticRequireOrigin(node, state);
|
||||
if (required !== undefined) return required.origin;
|
||||
if (t.isIdentifier(node)) {
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
if (binding === undefined || visited.has(binding.bindingId))
|
||||
return undefined;
|
||||
const provenance = evaluateSemanticProvenance(binding, state);
|
||||
return provenance.status === "module" ? provenance.origins[0] : undefined;
|
||||
}
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const base = semanticExpressionModuleOrigin(node.object, state, visited);
|
||||
const key = semanticStaticPropertyKey(node.property, node.computed);
|
||||
return base === undefined || key === null
|
||||
? undefined
|
||||
: { ...base, importedPath: [...base.importedPath, key] };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Recognize global or node:process aliases without accepting shadowing. */
|
||||
export const isSemanticProcessObject = (
|
||||
node: t.Expression | t.Super,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
family: "env" | "argv",
|
||||
key?: string | null,
|
||||
): boolean => {
|
||||
if (!t.isIdentifier(node)) return false;
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
const selected = key === undefined || key === null ? [family] : [family, key];
|
||||
if (binding === undefined)
|
||||
return (
|
||||
node.name === "process" &&
|
||||
!semanticResolutionBlocked(state, node, node.name)
|
||||
semanticGlobalProcessOrigin(node, state) !== undefined &&
|
||||
!requiredModuleChanged("process", selected, node, state)
|
||||
);
|
||||
return binding.directOrigins.some(
|
||||
({ specifier }) => specifier === "process" || specifier === "node:process",
|
||||
return (
|
||||
builtinBindingPath(
|
||||
binding,
|
||||
state,
|
||||
["process", "node:process"],
|
||||
node,
|
||||
selected,
|
||||
)?.length === 0
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user