Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a66e595bb | ||
|
|
55ca7d8347 | ||
|
|
af2fe0a8cc | ||
|
|
a6de554e2a | ||
|
|
1a322ab75b |
@@ -352,10 +352,14 @@ creation. Arguments follow each actual callable invocation rather than property
|
||||
chain depth; rebinding retains the original bound receiver.
|
||||
Visibly replaced invocation intrinsics produce unknown effects instead
|
||||
of using their former call semantics; captured function aliases retain their
|
||||
earlier identity. Unexamined targets remain unresolved. `Reflect.set` examines
|
||||
its explicit receiver as well as its target, retaining unknown receiver effects.
|
||||
Creating an unused binding does not itself mutate an intrinsic. Variable
|
||||
redeclarations retain a single unconditional initializer
|
||||
earlier identity. Invocation member reads are checked separately from the earlier
|
||||
function capture. Comma-expression callees use their final value while all
|
||||
preceding expressions remain in the effect traversal. Call effects are
|
||||
classified after callee and argument effects have been collected. Unexamined
|
||||
targets remain unresolved. `Reflect.set` examines its explicit receiver as well
|
||||
as its target, retaining unknown receiver effects. Creating an unused binding
|
||||
does not itself mutate an intrinsic. Variable redeclarations retain a single
|
||||
unconditional initializer
|
||||
when they introduce no assignment or second initializer.
|
||||
Stable identifier chains to fresh object, array, function,
|
||||
undecorated class or RegExp syntax keep those targets separate from intrinsic
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ROLLDOWN_MODULE_INTEROP } from "./javascriptModuleInterop.fixture.js";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
|
||||
const CAPTURED_INTEROP = ROLLDOWN_MODULE_INTEROP.replace(
|
||||
"__defProp = Object.defineProperty",
|
||||
"__defProp = define",
|
||||
);
|
||||
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;
|
||||
const metadata = {};
|
||||
Object.defineProperty = (Object.defineProperty(metadata, "x", {value:1}), replacement);
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("retains whole-object mutations at a captured function's later member read", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
Object.assign(define, {call: replacement});
|
||||
const metadata = {}; define.call(null, metadata, "x", {value:1});
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'(0, Object.defineProperty.call)(null,Object,"create",{value:replacement});',
|
||||
'(0, Object.defineProperty.apply)(null,[Object,"create",{value:replacement}]);',
|
||||
])(
|
||||
"does not invent a receiver for a detached invocation member: %s",
|
||||
(invoke) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
${invoke}
|
||||
${ROLLDOWN_MODULE_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains callee-prefix effects before classifying the invocation", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
const metadata = {};
|
||||
(Object.defineProperty = replacement, Object.defineProperty)(metadata,"x",{value:1});
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it("retains a callee selected before an argument replaces its global property", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
const metadata = {};
|
||||
Object.defineProperty(metadata,"x",(Object.defineProperty = replacement,{value:1}));
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'define.call(null,metadata,"x",{value:1});',
|
||||
'define.apply(null,[metadata,"x",{value:1}]);',
|
||||
'const change = define.bind(null,metadata,"x"); change({value:1});',
|
||||
'const also = define; also.call(null,metadata,"x",{value:1});',
|
||||
])("retains a function captured before global replacement: %s", (invoke) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
Object.defineProperty = replacement;
|
||||
const metadata = {}; ${invoke}
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'const invoke = define.call; invoke(null,metadata,"x",{value:1});',
|
||||
'const invoke = define.call; invoke.call(define,null,metadata,"x",{value:1});',
|
||||
'(0, define.call).call(define,null,metadata,"x",{value:1});',
|
||||
'const invoke = (0, define.call); const again = invoke; again.call(define,null,metadata,"x",{value:1});',
|
||||
])("preserves the base capture through an invocation value: %s", (invoke) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
Object.defineProperty = replacement;
|
||||
const metadata = {}; ${invoke}
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each(["define", "Function.prototype"])(
|
||||
"retains an overwritten invocation member through an alias from %s",
|
||||
(owner) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
Object.defineProperty = replacement;
|
||||
${owner}.call = replacement;
|
||||
const invoke = define.call;
|
||||
const metadata = {}; invoke.call(define,null,metadata,"x",{value:1});
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["call", 'define.call(null,metadata,"x",{value:1});'],
|
||||
["apply", 'define.apply(null,[metadata,"x",{value:1}]);'],
|
||||
[
|
||||
"bind",
|
||||
'const change = define.bind(null,metadata,"x"); change({value:1});',
|
||||
],
|
||||
])(
|
||||
"checks the later %s property read on a captured function",
|
||||
(method, invoke) => {
|
||||
for (const owner of ["define", "Function.prototype"]) {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const define = Object.defineProperty;
|
||||
${owner}.${method} = replacement;
|
||||
const metadata = {}; ${invoke}
|
||||
${CAPTURED_INTEROP} ${WINDOW}
|
||||
`);
|
||||
expect(result.electron.browser_windows, owner).toEqual([]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -615,6 +615,10 @@ describe("Electron module interop bound mutators", () => {
|
||||
'Reflect.set({},"create",...valuesAndReceivers);',
|
||||
"Reflect.set({},...keysValuesAndReceivers);",
|
||||
'Reflect.set({},"create",customCreate,receiver);',
|
||||
'(0, Object.defineProperty)(Object,"create",{value:customCreate});',
|
||||
'const change = Object.defineProperty.bind(null,Object,"create"); (0, change)({value:customCreate});',
|
||||
'(0, Reflect.apply)(Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
|
||||
'Reflect.apply((0,Function.prototype.call),Object.defineProperty,[null,Object,"create",{value:customCreate}]);',
|
||||
])("rejects intrinsic writes through a bound mutator: %s", (write) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(
|
||||
@@ -641,6 +645,7 @@ describe("Electron module interop bound mutators", () => {
|
||||
'const {apply:change} = Object.defineProperty; change.call(Object.defineProperty,null,[metadata,"x",{value:1}]);',
|
||||
'Reflect.set(metadata,"x",1,{});',
|
||||
'const receiver = {}; Reflect.set({},"x",1,receiver);',
|
||||
'(0, Object.defineProperty)(metadata,"x",{value:1});',
|
||||
])("keeps a fresh bound target or unused binding separate: %s", (write) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(
|
||||
|
||||
@@ -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 =
|
||||
@@ -52,6 +55,24 @@ export interface JavaScriptIntrinsicInvocation {
|
||||
| undefined;
|
||||
}
|
||||
|
||||
/** Distinguish replacing a value from mutating members of the captured object. */
|
||||
export type JavaScriptIdentityWriteKind =
|
||||
| "replace-value"
|
||||
| "mutate-object"
|
||||
| "opaque";
|
||||
|
||||
/** The source boundary after the operands required for this effect are evaluated. */
|
||||
export const javaScriptIdentityWriteOffset = (node: t.Node): number =>
|
||||
(t.isForOfStatement(node) || t.isForInStatement(node)
|
||||
? node.right.end
|
||||
: t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isAssignmentExpression(node) ||
|
||||
t.isUpdateExpression(node) ||
|
||||
t.isUnaryExpression(node, { operator: "delete" })
|
||||
? node.end
|
||||
: node.start) ?? Infinity;
|
||||
|
||||
/**
|
||||
* Index writes through possible lexical object identities. Resolve the complete
|
||||
* object before appending the written key: self-reference properties can denote
|
||||
@@ -68,7 +89,12 @@ export function* collectJavaScriptIdentityWritesSteps(
|
||||
) => JavaScriptIntrinsicInvocation | undefined,
|
||||
staticString: (node: t.Node | undefined) => string | undefined,
|
||||
freshObject: (node: t.Node) => boolean,
|
||||
retain: (node: t.Node, path: readonly (string | null)[]) => void,
|
||||
retain: (
|
||||
node: t.Node,
|
||||
path: readonly (string | null)[],
|
||||
kind: JavaScriptIdentityWriteKind,
|
||||
) => void,
|
||||
observeEvaluation?: (node: t.Node) => void,
|
||||
): Generator<void> {
|
||||
const objectMutators = new Set([
|
||||
"defineProperty",
|
||||
@@ -83,85 +109,141 @@ export function* collectJavaScriptIdentityWritesSteps(
|
||||
"setPrototypeOf",
|
||||
]);
|
||||
const keyedMutators = new Set(["set", "deleteProperty", "defineProperty"]);
|
||||
yield* traverseJavaScriptAstSteps(program, {
|
||||
enter: (node) => {
|
||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
|
||||
const invocation = intrinsicInvocation(node);
|
||||
if (invocation?.kind === "opaque") {
|
||||
for (const scope of origin(node, "opaque-call")) retain(node, scope);
|
||||
return;
|
||||
const retainCallEffects = (
|
||||
node: t.CallExpression | t.OptionalCallExpression,
|
||||
): void => {
|
||||
const invocation = intrinsicInvocation(node);
|
||||
if (invocation?.kind === "opaque") {
|
||||
for (const scope of origin(node, "opaque-call"))
|
||||
retain(node, scope, "opaque");
|
||||
return;
|
||||
}
|
||||
const callee = invocation?.path;
|
||||
const arguments_ = invocation?.arguments;
|
||||
const method = callee?.at(-1);
|
||||
const owner = callee?.length === 3 ? callee[1] : undefined;
|
||||
if (
|
||||
(owner === "Object" && objectMutators.has(method ?? "")) ||
|
||||
(owner === "Reflect" && reflectMutators.has(method ?? ""))
|
||||
) {
|
||||
// Reflect.set can write through its explicit receiver as well as
|
||||
// dispatching through its target. Both identities need examination.
|
||||
const targets =
|
||||
owner === "Reflect" &&
|
||||
method === "set" &&
|
||||
arguments_ !== undefined &&
|
||||
(arguments_.length >= 4 || arguments_.some(t.isSpreadElement))
|
||||
? [
|
||||
arguments_[0],
|
||||
arguments_.some(t.isSpreadElement) ? undefined : arguments_[3],
|
||||
]
|
||||
: [arguments_?.[0]];
|
||||
for (const target of targets) {
|
||||
// A known mutator with an opaque argument list can overwrite any
|
||||
// captured intrinsic. Do not turn an unexamined target into proof.
|
||||
if (target === undefined || t.isSpreadElement(target)) {
|
||||
for (const scope of origin(node, "opaque-call"))
|
||||
retain(node, scope, "opaque");
|
||||
}
|
||||
const callee = invocation?.path;
|
||||
const arguments_ = invocation?.arguments;
|
||||
const method = callee?.at(-1);
|
||||
const owner = callee?.length === 3 ? callee[1] : undefined;
|
||||
if (
|
||||
(owner === "Object" && objectMutators.has(method ?? "")) ||
|
||||
(owner === "Reflect" && reflectMutators.has(method ?? ""))
|
||||
) {
|
||||
// Reflect.set can write through its explicit receiver as well as
|
||||
// dispatching through its target. Both identities need examination.
|
||||
const targets =
|
||||
owner === "Reflect" &&
|
||||
method === "set" &&
|
||||
arguments_ !== undefined &&
|
||||
(arguments_.length >= 4 || arguments_.some(t.isSpreadElement))
|
||||
? [
|
||||
arguments_[0],
|
||||
arguments_.some(t.isSpreadElement)
|
||||
? undefined
|
||||
: arguments_[3],
|
||||
]
|
||||
: [arguments_?.[0]];
|
||||
for (const target of targets) {
|
||||
// A known mutator with an opaque argument list can overwrite any
|
||||
// captured intrinsic. Do not turn an unexamined target into proof.
|
||||
if (target === undefined || t.isSpreadElement(target)) {
|
||||
for (const scope of origin(node, "opaque-call"))
|
||||
retain(node, scope);
|
||||
}
|
||||
if (t.isNode(target) && !t.isSpreadElement(target)) {
|
||||
const identities = origin(target, "member");
|
||||
if (identities.length > 0) {
|
||||
const key = keyedMutators.has(method ?? "")
|
||||
? [staticString(arguments_?.[1]) ?? null]
|
||||
: [];
|
||||
for (const identity of identities)
|
||||
retain(node, [...identity, ...key]);
|
||||
} else if (!freshObject(target)) {
|
||||
for (const scope of origin(node, "opaque-call"))
|
||||
retain(node, scope);
|
||||
}
|
||||
}
|
||||
if (t.isNode(target) && !t.isSpreadElement(target)) {
|
||||
const identities = origin(target, "member");
|
||||
if (identities.length > 0) {
|
||||
const key = keyedMutators.has(method ?? "")
|
||||
? [staticString(arguments_?.[1]) ?? null]
|
||||
: [];
|
||||
for (const identity of identities)
|
||||
retain(
|
||||
node,
|
||||
[...identity, ...key],
|
||||
keyedMutators.has(method ?? "")
|
||||
? "replace-value"
|
||||
: "mutate-object",
|
||||
);
|
||||
} else if (!freshObject(target)) {
|
||||
for (const scope of origin(node, "opaque-call"))
|
||||
retain(node, scope, "opaque");
|
||||
}
|
||||
}
|
||||
}
|
||||
const target = t.isAssignmentExpression(node)
|
||||
? node.left
|
||||
: t.isUpdateExpression(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
|
||||
: t.isUnaryExpression(node, { operator: "delete" })
|
||||
? node.argument
|
||||
: t.isUnaryExpression(node, { operator: "delete" })
|
||||
? node.argument
|
||||
: 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);
|
||||
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]);
|
||||
: t.isForOfStatement(node) || t.isForInStatement(node)
|
||||
? node.left
|
||||
: undefined;
|
||||
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 && !isPattern(loop.left)) {
|
||||
observeEvaluation?.(loop);
|
||||
retainTargets(loop);
|
||||
}
|
||||
},
|
||||
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) &&
|
||||
!isPattern(targetOf(node))
|
||||
)
|
||||
retainTargets(node);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,6 +81,15 @@ export const resolveJavaScriptIntrinsicInvocation = (
|
||||
let explicitReceiver = false;
|
||||
for (;;) {
|
||||
callee = unwrapJavaScriptExpression(callee).node;
|
||||
if (t.isSequenceExpression(callee)) {
|
||||
const finalExpression = callee.expressions.at(-1);
|
||||
if (finalExpression === undefined) return undefined;
|
||||
// Comma yields a value, not a member reference. An outer invocation
|
||||
// can still supply an explicit receiver for that value.
|
||||
explicitReceiver = true;
|
||||
callee = finalExpression;
|
||||
continue;
|
||||
}
|
||||
if (visited.has(callee)) return undefined;
|
||||
visited.add(callee);
|
||||
let path = intrinsicPath(callee);
|
||||
@@ -97,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])
|
||||
|
||||
@@ -17,13 +17,15 @@ import {
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
electronMemberPath,
|
||||
javaScriptIdentityWriteOffset,
|
||||
type JavaScriptIdentityWriteKind,
|
||||
type JavaScriptIntrinsicInvocation,
|
||||
} from "./javascriptElectronMemberWrites.js";
|
||||
import {
|
||||
expandJavaScriptInvocationArguments,
|
||||
resolveJavaScriptIntrinsicInvocation,
|
||||
} from "./javascriptIntrinsicInvocation.js";
|
||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
||||
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
unwrapJavaScriptExpression,
|
||||
@@ -74,6 +76,16 @@ interface InteropHelper {
|
||||
readonly identity: boolean;
|
||||
}
|
||||
|
||||
const intrinsicWriteCovers = (
|
||||
written: readonly (string | null)[],
|
||||
read: readonly string[],
|
||||
): boolean =>
|
||||
written.length <= read.length &&
|
||||
written.every((key, index) => {
|
||||
const name = read[index];
|
||||
return name !== undefined && semanticPropertyPathKeyMatches(key, name);
|
||||
});
|
||||
|
||||
/** Prove one inert namespace conversion without relaxing general assignment semantics. */
|
||||
export const createSemanticModuleInterop = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
@@ -81,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>();
|
||||
@@ -88,8 +108,18 @@ export const createSemanticModuleInterop = (
|
||||
const writes: {
|
||||
readonly scope: string;
|
||||
readonly offset: number;
|
||||
readonly coverage: SemanticPropertyPathCoverage;
|
||||
readonly path: readonly (string | null)[];
|
||||
readonly kind: JavaScriptIdentityWriteKind;
|
||||
}[] = [];
|
||||
const invocationMemberWriteCovers = (
|
||||
write: (typeof writes)[number],
|
||||
read: readonly string[],
|
||||
): boolean =>
|
||||
(write.kind === "opaque" ||
|
||||
(write.kind === "mutate-object" &&
|
||||
write.path.length === read.length - 1) ||
|
||||
(write.kind === "replace-value" && write.path.length === read.length)) &&
|
||||
intrinsicWriteCovers(write.path, read);
|
||||
const invalidatedBindings = new Map<string, boolean>();
|
||||
const invalidatedCalls = new WeakMap<t.Node, boolean>();
|
||||
const opaqueMutationPath = (node: t.Node): readonly string[] => {
|
||||
@@ -125,6 +155,12 @@ export const createSemanticModuleInterop = (
|
||||
const { root, members } = electronMemberPath(current);
|
||||
if (members.some((member) => member === null)) return finish(undefined);
|
||||
suffix = [...(members as readonly string[]), ...suffix];
|
||||
if (t.isSequenceExpression(root)) {
|
||||
const value = root.expressions.at(-1);
|
||||
if (value === undefined) return finish(undefined);
|
||||
current = value;
|
||||
continue;
|
||||
}
|
||||
for (const global of ["Object", "Function", "Reflect"])
|
||||
if (isUnshadowedGlobal(root, state, global))
|
||||
return finish(["intrinsic", global, ...suffix]);
|
||||
@@ -165,8 +201,20 @@ export const createSemanticModuleInterop = (
|
||||
let capturedAt = node;
|
||||
let suffixLength = 0;
|
||||
for (;;) {
|
||||
current = unwrapJavaScriptExpression(current).node;
|
||||
if (suffixLength === 0)
|
||||
capturedAt =
|
||||
t.isMemberExpression(current) || t.isOptionalMemberExpression(current)
|
||||
? current.property
|
||||
: current;
|
||||
const { root, members } = electronMemberPath(current);
|
||||
suffixLength += members.length;
|
||||
if (t.isSequenceExpression(root)) {
|
||||
const value = root.expressions.at(-1);
|
||||
if (value === undefined) return undefined;
|
||||
current = value;
|
||||
continue;
|
||||
}
|
||||
if (!t.isIdentifier(root)) return undefined;
|
||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||
if (binding === undefined) return { path, capturedAt };
|
||||
@@ -217,24 +265,84 @@ export const createSemanticModuleInterop = (
|
||||
node: t.Node,
|
||||
path: readonly string[],
|
||||
): boolean => {
|
||||
const capture = capturedIntrinsic(node)?.capturedAt ?? node;
|
||||
const scope = opaqueMutationPath(capture).join(".");
|
||||
const method = path.at(-1);
|
||||
const invocationMethod =
|
||||
method === "bind" || method === "call" || method === "apply";
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capture.start ?? -1) &&
|
||||
(write.coverage.covers(path) ||
|
||||
(invocationMethod &&
|
||||
write.coverage.covers([
|
||||
"intrinsic",
|
||||
"Function",
|
||||
"prototype",
|
||||
method,
|
||||
]))),
|
||||
);
|
||||
let current = unwrapJavaScriptExpression(node).node;
|
||||
let currentPath = path;
|
||||
const aliases = new Set<string>();
|
||||
for (;;) {
|
||||
current = unwrapJavaScriptExpression(current).node;
|
||||
if (t.isSequenceExpression(current)) {
|
||||
const value = current.expressions.at(-1);
|
||||
if (value === undefined) return true;
|
||||
current = value;
|
||||
continue;
|
||||
}
|
||||
const method = currentPath.at(-1);
|
||||
const invocationMethod =
|
||||
method === "bind" || method === "call" || method === "apply";
|
||||
// Recover the member read at an alias initializer before examining its
|
||||
// base capture. Copying a callable does not recapture its original owner.
|
||||
if (invocationMethod && 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) &&
|
||||
!aliases.has(binding.bindingId)
|
||||
) {
|
||||
aliases.add(binding.bindingId);
|
||||
current = initializer.node;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (
|
||||
invocationMethod &&
|
||||
(t.isMemberExpression(current) || t.isOptionalMemberExpression(current))
|
||||
) {
|
||||
// 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 = evaluationOffset(current);
|
||||
if (
|
||||
writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < offset &&
|
||||
(invocationMemberWriteCovers(write, currentPath) ||
|
||||
invocationMemberWriteCovers(write, [
|
||||
"intrinsic",
|
||||
"Function",
|
||||
"prototype",
|
||||
method,
|
||||
])),
|
||||
)
|
||||
)
|
||||
return true;
|
||||
current = unwrapJavaScriptExpression(current.object).node;
|
||||
currentPath = currentPath.slice(0, -1);
|
||||
continue;
|
||||
}
|
||||
const capture = capturedIntrinsic(current)?.capturedAt ?? current;
|
||||
const scope = opaqueMutationPath(capture).join(".");
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < evaluationOffset(capture) &&
|
||||
(intrinsicWriteCovers(write.path, currentPath) ||
|
||||
(invocationMethod &&
|
||||
intrinsicWriteCovers(write.path, [
|
||||
"intrinsic",
|
||||
"Function",
|
||||
"prototype",
|
||||
method,
|
||||
]))),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveBoundIntrinsic = (
|
||||
@@ -278,7 +386,7 @@ export const createSemanticModuleInterop = (
|
||||
if (members.at(-1) === "bind") {
|
||||
// A direct .bind may continue a bound callable, without discarding members.
|
||||
if (!terminal && members.length !== 1) return undefined;
|
||||
bindReads.push(captured?.capturedAt ?? current.callee);
|
||||
bindReads.push(current.callee);
|
||||
} else {
|
||||
// Reuse the invocation owner for generic call/apply/Reflect.apply of bind.
|
||||
const invocation = resolveJavaScriptIntrinsicInvocation(
|
||||
@@ -298,6 +406,8 @@ export const createSemanticModuleInterop = (
|
||||
captured = capturedIntrinsic(bindTarget);
|
||||
terminal = captured !== undefined;
|
||||
path = captured?.path;
|
||||
opaqueArguments ||=
|
||||
path !== undefined && intrinsicReadChanged(bindTarget, path);
|
||||
}
|
||||
receiver = bindingArguments?.[0];
|
||||
const ownArguments = bindingArguments?.slice(1) ?? [];
|
||||
@@ -312,17 +422,9 @@ export const createSemanticModuleInterop = (
|
||||
continue;
|
||||
}
|
||||
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)),
|
||||
);
|
||||
});
|
||||
opaqueArguments ||= bindReads.some((capture) =>
|
||||
intrinsicReadChanged(capture, boundPath),
|
||||
);
|
||||
return {
|
||||
kind: opaqueArguments ? "opaque" : "known",
|
||||
path,
|
||||
@@ -609,8 +711,8 @@ export const createSemanticModuleInterop = (
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capturedAt.start ?? -1) &&
|
||||
write.coverage.covers(path),
|
||||
write.offset < evaluationOffset(capturedAt) &&
|
||||
intrinsicWriteCovers(write.path, path),
|
||||
);
|
||||
}) ?? false;
|
||||
return {
|
||||
@@ -628,13 +730,22 @@ export const createSemanticModuleInterop = (
|
||||
staticString,
|
||||
isFreshObject,
|
||||
opaqueMutationPath,
|
||||
retainWrite: (node: t.Node, path: readonly (string | null)[]) => {
|
||||
const coverage = new SemanticPropertyPathCoverage();
|
||||
coverage.retain(path[0] === "opaque-intrinsic" ? ["intrinsic"] : path);
|
||||
observeEvaluation: (node: t.Node): void => {
|
||||
evaluationOrder.set(node, ++evaluationClock);
|
||||
},
|
||||
retainWrite: (
|
||||
node: t.Node,
|
||||
path: readonly (string | null)[],
|
||||
kind: JavaScriptIdentityWriteKind = "replace-value",
|
||||
) => {
|
||||
writes.push({
|
||||
scope: opaqueMutationPath(node).join("."),
|
||||
offset: node.start ?? Infinity,
|
||||
coverage,
|
||||
offset:
|
||||
evaluationClock === 0
|
||||
? javaScriptIdentityWriteOffset(node)
|
||||
: evaluationOffset(node),
|
||||
path: path[0] === "opaque-intrinsic" ? ["intrinsic"] : path,
|
||||
kind,
|
||||
});
|
||||
},
|
||||
invalidated: (binding: JavaScriptSemanticBindingState): boolean => {
|
||||
|
||||
@@ -462,10 +462,11 @@ export function* indexJavaScriptElectronIdentitySteps(
|
||||
interop.intrinsicInvocation,
|
||||
interop.staticString,
|
||||
interop.isFreshObject,
|
||||
(node, path) => {
|
||||
(node, path, kind) => {
|
||||
if (path[0] === "electron") writes.retain(path);
|
||||
else interop.retainWrite(node, 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);
|
||||
|
||||
@@ -37,6 +37,10 @@ const INTRINSIC_MUTATIONS = [
|
||||
'const change = Object.defineProperty.bind(null,Object,"create"); change({value:customCreate});',
|
||||
],
|
||||
["receiver.cjs", 'Reflect.set({},"create",customCreate,Object);'],
|
||||
[
|
||||
"sequence.cjs",
|
||||
'(0,Object.defineProperty)(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"deep-call.cjs",
|
||||
'Function.prototype.call.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
@@ -89,6 +93,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
function hoistedFunction() {}
|
||||
const captured = Object.defineProperty;
|
||||
Object.defineProperty = custom;
|
||||
captured.call(null,metadata,"captured",{value:1});
|
||||
${ROLLDOWN_MODULE_INTEROP.replace("__defProp = Object.defineProperty", "__defProp = captured")}
|
||||
let electron = require("electron");
|
||||
electron = __toESM(electron, 1);
|
||||
@@ -119,6 +124,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
require("./generic-bind.cjs");
|
||||
require("./replaced-apply.cjs");
|
||||
require("./receiver.cjs");
|
||||
require("./sequence.cjs");
|
||||
require("./deep-call.cjs");
|
||||
require("./deep-apply.cjs");
|
||||
throw new Error("must not execute main");
|
||||
@@ -147,7 +153,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
preload_entrypoints: 1,
|
||||
});
|
||||
expect(result.statistics.parse_failures).toBe(0);
|
||||
expect(result.statistics.parsed_javascript_files).toBe(12);
|
||||
expect(result.statistics.parsed_javascript_files).toBe(13);
|
||||
const windows = result.graph.nodes.filter(
|
||||
({ kind }) => kind === "browser-window",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user