fix(javascript): retain destructuring effects in evaluation order
This commit is contained in:
parent
55ca7d8347
commit
0a66e595bb
|
|
@ -11,6 +11,48 @@ const WINDOW = `let electron = require("electron");
|
|||
electron = __toESM(electron, 1); new electron.BrowserWindow({});`;
|
||||
|
||||
describe("captured intrinsic invocation reads", () => {
|
||||
it.each(["call", "apply"])(
|
||||
"does not invent a bound receiver erased by %s sequence syntax",
|
||||
(method) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const change = Object.defineProperty.bind(null,Object,"create");
|
||||
(0, change.${method})(null, ${method === "apply" ? "[{value:replacement}]" : "{value:replacement}"});
|
||||
${ROLLDOWN_MODULE_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'[Object.defineProperty, ignored = Object.defineProperty(metadata,"x",{value:1})] = [replacement];',
|
||||
'({first: Object.defineProperty, second: ignored = Object.defineProperty(metadata,"x",{value:1})} = values);',
|
||||
'[change.call, ignored = change.call(null,metadata,"x",{value:1})] = [replacement];',
|
||||
'({first: change.call, [change.call(null,metadata,"x",{value:1})]: ignored} = values);',
|
||||
'for ([change.call, ignored = change.call(null,metadata,"x",{value:1})] of values) {}',
|
||||
])(
|
||||
"retains destructuring writes before later defaults and keys: %s",
|
||||
(mutation) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
const change = define.bind(null);
|
||||
const metadata = {}; let ignored;
|
||||
${mutation}
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("evaluates a destructuring RHS before any target writes", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
const metadata = {};
|
||||
[Object.defineProperty] = [Object.defineProperty(metadata,"x",{value:1}), replacement];
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("evaluates assignment operands before recording their writes", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import {
|
|||
semanticWriteTargets,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
javaScriptAstChildKeys,
|
||||
traverseJavaScriptAstSteps,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
|
||||
/** Match Electron's supported module entry points. */
|
||||
export const ELECTRON_MODULE =
|
||||
|
|
@ -91,6 +94,7 @@ export function* collectJavaScriptIdentityWritesSteps(
|
|||
path: readonly (string | null)[],
|
||||
kind: JavaScriptIdentityWriteKind,
|
||||
) => void,
|
||||
observeEvaluation?: (node: t.Node) => void,
|
||||
): Generator<void> {
|
||||
const objectMutators = new Set([
|
||||
"defineProperty",
|
||||
|
|
@ -163,8 +167,21 @@ export function* collectJavaScriptIdentityWritesSteps(
|
|||
}
|
||||
}
|
||||
};
|
||||
const retainTargets = (node: t.Node): void => {
|
||||
const target = t.isAssignmentExpression(node)
|
||||
const retainTarget = (leaf: t.Node, effect: t.Node): void => {
|
||||
if (t.isIdentifier(leaf)) {
|
||||
for (const identity of origin(leaf, "binding"))
|
||||
retain(effect, identity, "replace-value");
|
||||
return;
|
||||
}
|
||||
const member = unwrapJavaScriptExpression(leaf).node;
|
||||
if (!t.isMemberExpression(member) && !t.isOptionalMemberExpression(member))
|
||||
return;
|
||||
const key = semanticStaticPropertyKey(member.property, member.computed);
|
||||
for (const identity of origin(member.object, "member"))
|
||||
retain(effect, [...identity, key], "replace-value");
|
||||
};
|
||||
const targetOf = (node: t.Node): t.Node | undefined =>
|
||||
t.isAssignmentExpression(node)
|
||||
? node.left
|
||||
: t.isUpdateExpression(node)
|
||||
? node.argument
|
||||
|
|
@ -173,36 +190,59 @@ export function* collectJavaScriptIdentityWritesSteps(
|
|||
: t.isForOfStatement(node) || t.isForInStatement(node)
|
||||
? node.left
|
||||
: undefined;
|
||||
if (target === undefined) return;
|
||||
for (const leaf of semanticWriteTargets(target)) {
|
||||
if (t.isIdentifier(leaf)) {
|
||||
for (const identity of origin(leaf, "binding"))
|
||||
retain(node, identity, "replace-value");
|
||||
continue;
|
||||
}
|
||||
const member = unwrapJavaScriptExpression(leaf).node;
|
||||
if (
|
||||
!t.isMemberExpression(member) &&
|
||||
!t.isOptionalMemberExpression(member)
|
||||
)
|
||||
continue;
|
||||
const key = semanticStaticPropertyKey(member.property, member.computed);
|
||||
for (const identity of origin(member.object, "member"))
|
||||
retain(node, [...identity, key], "replace-value");
|
||||
}
|
||||
const isPattern = (node: t.Node | undefined): boolean =>
|
||||
t.isArrayPattern(node) || t.isObjectPattern(node);
|
||||
const retainTargets = (node: t.Node): void => {
|
||||
const target = targetOf(node);
|
||||
if (target !== undefined)
|
||||
for (const leaf of semanticWriteTargets(target)) retainTarget(leaf, node);
|
||||
};
|
||||
const patternLeaves = new WeakSet<t.Node>();
|
||||
const loopBodies = new WeakMap<t.Node, t.ForOfStatement | t.ForInStatement>();
|
||||
yield* traverseJavaScriptAstSteps(program, {
|
||||
childKeys: (node) => {
|
||||
// Assignment patterns are lexically before their RHS, but execute after it.
|
||||
// A simple default target evaluates its reference before its initializer;
|
||||
// a nested pattern starts destructuring only after that initializer.
|
||||
if (t.isAssignmentExpression(node) && isPattern(node.left))
|
||||
return ["right", "left"];
|
||||
if (t.isForOfStatement(node) || t.isForInStatement(node))
|
||||
return ["right", "left", "body"];
|
||||
if (t.isAssignmentPattern(node) && isPattern(node.left))
|
||||
return ["right", "left"];
|
||||
if (t.isVariableDeclarator(node) && isPattern(node.id))
|
||||
return ["init", "id"];
|
||||
return javaScriptAstChildKeys(node);
|
||||
},
|
||||
enter: (node) => {
|
||||
const target = targetOf(node);
|
||||
if (isPattern(target) && target !== undefined)
|
||||
for (const leaf of semanticWriteTargets(target))
|
||||
patternLeaves.add(leaf);
|
||||
if (t.isForOfStatement(node) || t.isForInStatement(node))
|
||||
loopBodies.set(node.body, node);
|
||||
const loop = loopBodies.get(node);
|
||||
if (loop !== undefined) retainTargets(loop);
|
||||
if (loop !== undefined && !isPattern(loop.left)) {
|
||||
observeEvaluation?.(loop);
|
||||
retainTargets(loop);
|
||||
}
|
||||
},
|
||||
exit: (node) => {
|
||||
exit: (node, parent) => {
|
||||
observeEvaluation?.(node);
|
||||
if (patternLeaves.has(node)) {
|
||||
// The default expression runs after the target reference is evaluated,
|
||||
// but before its value is written. Later leaves see this completed write.
|
||||
if (!t.isAssignmentPattern(parent) || parent.left !== node)
|
||||
retainTarget(node, node);
|
||||
} else if (t.isAssignmentPattern(node) && patternLeaves.has(node.left))
|
||||
retainTarget(node.left, node);
|
||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node))
|
||||
retainCallEffects(node);
|
||||
else if (!t.isForOfStatement(node) && !t.isForInStatement(node))
|
||||
else if (
|
||||
!t.isForOfStatement(node) &&
|
||||
!t.isForInStatement(node) &&
|
||||
!isPattern(targetOf(node))
|
||||
)
|
||||
retainTargets(node);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export const resolveJavaScriptIntrinsicInvocation = (
|
|||
);
|
||||
if (method === "call" || method === "apply") {
|
||||
const bound = boundIntrinsic(callee.object);
|
||||
if (explicitReceiver && !t.isExpression(receiver)) return undefined;
|
||||
if (
|
||||
bound !== undefined &&
|
||||
intrinsicReadChanged(callee, [...bound.path, method])
|
||||
|
|
|
|||
|
|
@ -93,6 +93,14 @@ export const createSemanticModuleInterop = (
|
|||
readonly intrinsicPath: (node: t.Node) => readonly string[] | undefined;
|
||||
},
|
||||
) => {
|
||||
// Evaluation order, unlike source offsets, preserves RHS-before-pattern and
|
||||
// each destructuring write before subsequent defaults and computed keys.
|
||||
const evaluationOrder = new WeakMap<t.Node, number>();
|
||||
let evaluationClock = 0;
|
||||
const evaluationOffset = (node: t.Node): number =>
|
||||
evaluationClock === 0
|
||||
? (node.start ?? -1)
|
||||
: (evaluationOrder.get(node) ?? Infinity);
|
||||
const helpers = new Map<string, InteropHelper | null>();
|
||||
const origins = new Map<string, InteropOrigin>();
|
||||
const calls = new WeakMap<t.Node, InteropOrigin>();
|
||||
|
|
@ -299,7 +307,7 @@ export const createSemanticModuleInterop = (
|
|||
// Reading an invocation member does not recapture the stored function.
|
||||
// Check member writes now, then check its base at the earlier capture.
|
||||
const scope = opaqueMutationPath(current).join(".");
|
||||
const offset = current.property.start ?? -1;
|
||||
const offset = evaluationOffset(current);
|
||||
if (
|
||||
writes.some(
|
||||
(write) =>
|
||||
|
|
@ -324,7 +332,7 @@ export const createSemanticModuleInterop = (
|
|||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capture.start ?? -1) &&
|
||||
write.offset < evaluationOffset(capture) &&
|
||||
(intrinsicWriteCovers(write.path, currentPath) ||
|
||||
(invocationMethod &&
|
||||
intrinsicWriteCovers(write.path, [
|
||||
|
|
@ -703,7 +711,7 @@ export const createSemanticModuleInterop = (
|
|||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capturedAt.start ?? -1) &&
|
||||
write.offset < evaluationOffset(capturedAt) &&
|
||||
intrinsicWriteCovers(write.path, path),
|
||||
);
|
||||
}) ?? false;
|
||||
|
|
@ -722,6 +730,9 @@ export const createSemanticModuleInterop = (
|
|||
staticString,
|
||||
isFreshObject,
|
||||
opaqueMutationPath,
|
||||
observeEvaluation: (node: t.Node): void => {
|
||||
evaluationOrder.set(node, ++evaluationClock);
|
||||
},
|
||||
retainWrite: (
|
||||
node: t.Node,
|
||||
path: readonly (string | null)[],
|
||||
|
|
@ -729,7 +740,10 @@ export const createSemanticModuleInterop = (
|
|||
) => {
|
||||
writes.push({
|
||||
scope: opaqueMutationPath(node).join("."),
|
||||
offset: javaScriptIdentityWriteOffset(node),
|
||||
offset:
|
||||
evaluationClock === 0
|
||||
? javaScriptIdentityWriteOffset(node)
|
||||
: evaluationOffset(node),
|
||||
path: path[0] === "opaque-intrinsic" ? ["intrinsic"] : path,
|
||||
kind,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -466,6 +466,7 @@ export function* indexJavaScriptElectronIdentitySteps(
|
|||
if (path[0] === "electron") writes.retain(path);
|
||||
else interop.retainWrite(node, path, kind);
|
||||
},
|
||||
interop.observeEvaluation,
|
||||
);
|
||||
collectedWrites = true;
|
||||
const intactExport = (path: readonly (string | null)[]): boolean =>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export const traverseJavaScriptAst = (
|
|||
export interface JavaScriptAstVisitor {
|
||||
/** Omit a child and its subtree without entering or exiting it. */
|
||||
readonly skip?: (node: t.Node) => boolean;
|
||||
/** Override child field order while retaining the shared child expansion owner. */
|
||||
readonly childKeys?: (node: t.Node) => readonly string[];
|
||||
readonly enter: (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
|
|
@ -40,7 +42,12 @@ export function* traverseJavaScriptAstSteps(
|
|||
const enter = (node: t.Node, parent: t.Node | null): TraversalFrame => {
|
||||
visitor.enter(node, parent, () => [...ancestors]);
|
||||
ancestors.push(node);
|
||||
return { node, parent, children: childNodes(node), nextIndex: 0 };
|
||||
return {
|
||||
node,
|
||||
parent,
|
||||
children: childNodes(node, visitor.childKeys?.(node)),
|
||||
nextIndex: 0,
|
||||
};
|
||||
};
|
||||
const pending = [enter(root, null)];
|
||||
let visited = 1;
|
||||
|
|
@ -84,8 +91,10 @@ interface TraversalFrame {
|
|||
* expansion; bespoke `Object.values` / duplicated `childNodes` walkers must
|
||||
* not be reintroduced (they diverge on loc/comment fields and ordering).
|
||||
*/
|
||||
export const childNodes = (node: t.Node): t.Node[] => {
|
||||
const keys = javaScriptAstChildKeys(node);
|
||||
export const childNodes = (
|
||||
node: t.Node,
|
||||
keys: readonly string[] = javaScriptAstChildKeys(node),
|
||||
): t.Node[] => {
|
||||
const children: t.Node[] = [];
|
||||
for (const key of keys) {
|
||||
const value: unknown = Reflect.get(node, key);
|
||||
|
|
|
|||
Loading…
Reference in New Issue