fix(javascript): retain bound intrinsic mutation effects

This commit is contained in:
morluto committed 2026-10-12 00:34:35 +08:00
1 parent b60248c462
commit 1e1a611012
5 files changed
+171 -22

No files matched your search

@@ -15,7 +15,10 @@ import {
childNodes,
javaScriptAstChildKeys,
} from "./javascriptSemanticTraversal.js";
import { electronMemberPath } from "./javascriptElectronMemberWrites.js";
import {
electronMemberPath,
type JavaScriptIntrinsicInvocation,
} from "./javascriptElectronMemberWrites.js";
import { resolveJavaScriptIntrinsicInvocation } from "./javascriptIntrinsicInvocation.js";
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
import {
@@ -173,6 +176,109 @@ export const createSemanticModuleInterop = (
}
};
const staticString = (node: t.Node | undefined): string | undefined => {
if (node === undefined) return undefined;
const visited = new Set<string>();
let current = node;
for (;;) {
const literal = readExactJavaScriptLiteral(current);
if (literal.found)
return typeof literal.value === "string" ? literal.value : undefined;
const expression = unwrapJavaScriptExpression(current).node;
if (!t.isIdentifier(expression)) return undefined;
const binding = resolveSemanticBindingState(
state,
expression,
expression.name,
);
const initializer = semanticStableInitializer(binding, state);
if (
binding === undefined ||
initializer === undefined ||
initializer.projection.length !== 0 ||
(initializer.node.end ?? Infinity) >= (expression.start ?? -1) ||
visited.has(binding.bindingId)
)
return undefined;
visited.add(binding.bindingId);
current = initializer.node;
}
};
const boundIntrinsic = (
node: t.Node,
): JavaScriptIntrinsicInvocation | undefined => {
const visited = new Set<string>();
const argumentGroups: t.CallExpression["arguments"][] = [];
const bindReads: t.Node[] = [];
let opaqueArguments = false;
let current = node;
for (;;) {
current = unwrapJavaScriptExpression(current).node;
if (t.isIdentifier(current)) {
const binding = resolveSemanticBindingState(
state,
current,
current.name,
);
const initializer = semanticStableInitializer(binding, state);
if (
binding === undefined ||
initializer === undefined ||
initializer.projection.length !== 0 ||
(initializer.node.end ?? Infinity) >= (current.start ?? -1) ||
visited.has(binding.bindingId)
)
return undefined;
visited.add(binding.bindingId);
current = initializer.node;
continue;
}
if (!t.isCallExpression(current)) return undefined;
const { root: bindTarget, members } = electronMemberPath(current.callee);
if (members.at(-1) !== "bind") return undefined;
const captured = capturedIntrinsic(current.callee);
const terminal = captured?.path.at(-1) === "bind";
// Only a direct .bind can continue a previously bound callable. Keeping
// intervening members would require a separate proof of their identity.
if (!terminal && members.length !== 1) return undefined;
bindReads.push(captured?.capturedAt ?? current.callee);
const thisArgument = current.arguments[0];
const ownArguments = current.arguments.slice(1);
opaqueArguments ||=
thisArgument !== undefined &&
!readExactJavaScriptLiteral(thisArgument).found;
opaqueArguments ||= ownArguments.some((argument) =>
t.isSpreadElement(argument),
);
argumentGroups.push(ownArguments);
if (!terminal || captured === undefined) {
current = bindTarget;
continue;
}
const path = captured.path.slice(0, -1);
const boundPath = [...path, "bind"];
const functionBind = ["intrinsic", "Function", "prototype", "bind"];
opaqueArguments ||= bindReads.some((capture) => {
const scope = opaqueMutationPath(capture).join(".");
return writes.some(
(write) =>
(write.scope === scope || write.scope === "intrinsic") &&
write.offset < (capture.start ?? -1) &&
(write.coverage.covers(boundPath) ||
write.coverage.covers(functionBind)),
);
});
return {
path,
// Rebinding appends arguments to the earlier bound argument list.
arguments: opaqueArguments
? undefined
: argumentGroups.toReversed().flat(),
};
}
};
const isFreshObject = (node: t.Node): boolean => {
const visited = new Set<string>();
let current = node;
@@ -443,7 +549,14 @@ export const createSemanticModuleInterop = (
callOrigin,
intrinsicPath,
intrinsicInvocation: (node: t.CallExpression | t.OptionalCallExpression) =>
resolveJavaScriptIntrinsicInvocation(node, state, intrinsicPath),
resolveJavaScriptIntrinsicInvocation(
node,
state,
intrinsicPath,
boundIntrinsic,
),
boundIntrinsic,
staticString,
isFreshObject,
opaqueMutationPath,
retainWrite: (node: t.Node, path: readonly (string | null)[]) => {