Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a66e595bb | ||
|
|
55ca7d8347 | ||
|
|
af2fe0a8cc | ||
|
|
a6de554e2a | ||
|
|
1a322ab75b |
No files matched your search
@@ -352,10 +352,14 @@ creation. Arguments follow each actual callable invocation rather than property
|
|||||||
chain depth; rebinding retains the original bound receiver.
|
chain depth; rebinding retains the original bound receiver.
|
||||||
Visibly replaced invocation intrinsics produce unknown effects instead
|
Visibly replaced invocation intrinsics produce unknown effects instead
|
||||||
of using their former call semantics; captured function aliases retain their
|
of using their former call semantics; captured function aliases retain their
|
||||||
earlier identity. Unexamined targets remain unresolved. `Reflect.set` examines
|
earlier identity. Invocation member reads are checked separately from the earlier
|
||||||
its explicit receiver as well as its target, retaining unknown receiver effects.
|
function capture. Comma-expression callees use their final value while all
|
||||||
Creating an unused binding does not itself mutate an intrinsic. Variable
|
preceding expressions remain in the effect traversal. Call effects are
|
||||||
redeclarations retain a single unconditional initializer
|
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.
|
when they introduce no assignment or second initializer.
|
||||||
Stable identifier chains to fresh object, array, function,
|
Stable identifier chains to fresh object, array, function,
|
||||||
undecorated class or RegExp syntax keep those targets separate from intrinsic
|
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({},"create",...valuesAndReceivers);',
|
||||||
"Reflect.set({},...keysValuesAndReceivers);",
|
"Reflect.set({},...keysValuesAndReceivers);",
|
||||||
'Reflect.set({},"create",customCreate,receiver);',
|
'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) => {
|
])("rejects intrinsic writes through a bound mutator: %s", (write) => {
|
||||||
expect(
|
expect(
|
||||||
analyzeJavaScriptStaticSource(
|
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}]);',
|
'const {apply:change} = Object.defineProperty; change.call(Object.defineProperty,null,[metadata,"x",{value:1}]);',
|
||||||
'Reflect.set(metadata,"x",1,{});',
|
'Reflect.set(metadata,"x",1,{});',
|
||||||
'const receiver = {}; Reflect.set({},"x",1,receiver);',
|
'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) => {
|
])("keeps a fresh bound target or unused binding separate: %s", (write) => {
|
||||||
expect(
|
expect(
|
||||||
analyzeJavaScriptStaticSource(
|
analyzeJavaScriptStaticSource(
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import {
|
|||||||
semanticWriteTargets,
|
semanticWriteTargets,
|
||||||
unwrapJavaScriptExpression,
|
unwrapJavaScriptExpression,
|
||||||
} from "./javascriptAstValues.js";
|
} from "./javascriptAstValues.js";
|
||||||
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
|
import {
|
||||||
|
javaScriptAstChildKeys,
|
||||||
|
traverseJavaScriptAstSteps,
|
||||||
|
} from "./javascriptSemanticTraversal.js";
|
||||||
|
|
||||||
/** Match Electron's supported module entry points. */
|
/** Match Electron's supported module entry points. */
|
||||||
export const ELECTRON_MODULE =
|
export const ELECTRON_MODULE =
|
||||||
@@ -52,6 +55,24 @@ export interface JavaScriptIntrinsicInvocation {
|
|||||||
| undefined;
|
| 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
|
* Index writes through possible lexical object identities. Resolve the complete
|
||||||
* object before appending the written key: self-reference properties can denote
|
* object before appending the written key: self-reference properties can denote
|
||||||
@@ -68,7 +89,12 @@ export function* collectJavaScriptIdentityWritesSteps(
|
|||||||
) => JavaScriptIntrinsicInvocation | undefined,
|
) => JavaScriptIntrinsicInvocation | undefined,
|
||||||
staticString: (node: t.Node | undefined) => string | undefined,
|
staticString: (node: t.Node | undefined) => string | undefined,
|
||||||
freshObject: (node: t.Node) => boolean,
|
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> {
|
): Generator<void> {
|
||||||
const objectMutators = new Set([
|
const objectMutators = new Set([
|
||||||
"defineProperty",
|
"defineProperty",
|
||||||
@@ -83,85 +109,141 @@ export function* collectJavaScriptIdentityWritesSteps(
|
|||||||
"setPrototypeOf",
|
"setPrototypeOf",
|
||||||
]);
|
]);
|
||||||
const keyedMutators = new Set(["set", "deleteProperty", "defineProperty"]);
|
const keyedMutators = new Set(["set", "deleteProperty", "defineProperty"]);
|
||||||
yield* traverseJavaScriptAstSteps(program, {
|
const retainCallEffects = (
|
||||||
enter: (node) => {
|
node: t.CallExpression | t.OptionalCallExpression,
|
||||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
|
): void => {
|
||||||
const invocation = intrinsicInvocation(node);
|
const invocation = intrinsicInvocation(node);
|
||||||
if (invocation?.kind === "opaque") {
|
if (invocation?.kind === "opaque") {
|
||||||
for (const scope of origin(node, "opaque-call")) retain(node, scope);
|
for (const scope of origin(node, "opaque-call"))
|
||||||
return;
|
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;
|
if (t.isNode(target) && !t.isSpreadElement(target)) {
|
||||||
const arguments_ = invocation?.arguments;
|
const identities = origin(target, "member");
|
||||||
const method = callee?.at(-1);
|
if (identities.length > 0) {
|
||||||
const owner = callee?.length === 3 ? callee[1] : undefined;
|
const key = keyedMutators.has(method ?? "")
|
||||||
if (
|
? [staticString(arguments_?.[1]) ?? null]
|
||||||
(owner === "Object" && objectMutators.has(method ?? "")) ||
|
: [];
|
||||||
(owner === "Reflect" && reflectMutators.has(method ?? ""))
|
for (const identity of identities)
|
||||||
) {
|
retain(
|
||||||
// Reflect.set can write through its explicit receiver as well as
|
node,
|
||||||
// dispatching through its target. Both identities need examination.
|
[...identity, ...key],
|
||||||
const targets =
|
keyedMutators.has(method ?? "")
|
||||||
owner === "Reflect" &&
|
? "replace-value"
|
||||||
method === "set" &&
|
: "mutate-object",
|
||||||
arguments_ !== undefined &&
|
);
|
||||||
(arguments_.length >= 4 || arguments_.some(t.isSpreadElement))
|
} else if (!freshObject(target)) {
|
||||||
? [
|
for (const scope of origin(node, "opaque-call"))
|
||||||
arguments_[0],
|
retain(node, scope, "opaque");
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
? node.argument
|
||||||
: t.isUnaryExpression(node, { operator: "delete" })
|
: t.isForOfStatement(node) || t.isForInStatement(node)
|
||||||
? node.argument
|
? node.left
|
||||||
: t.isForOfStatement(node) || t.isForInStatement(node)
|
: undefined;
|
||||||
? node.left
|
const isPattern = (node: t.Node | undefined): boolean =>
|
||||||
: undefined;
|
t.isArrayPattern(node) || t.isObjectPattern(node);
|
||||||
if (target === undefined) return;
|
const retainTargets = (node: t.Node): void => {
|
||||||
for (const leaf of semanticWriteTargets(target)) {
|
const target = targetOf(node);
|
||||||
if (t.isIdentifier(leaf)) {
|
if (target !== undefined)
|
||||||
for (const identity of origin(leaf, "binding"))
|
for (const leaf of semanticWriteTargets(target)) retainTarget(leaf, node);
|
||||||
retain(node, identity);
|
};
|
||||||
continue;
|
const patternLeaves = new WeakSet<t.Node>();
|
||||||
}
|
const loopBodies = new WeakMap<t.Node, t.ForOfStatement | t.ForInStatement>();
|
||||||
const member = unwrapJavaScriptExpression(leaf).node;
|
yield* traverseJavaScriptAstSteps(program, {
|
||||||
if (
|
childKeys: (node) => {
|
||||||
!t.isMemberExpression(member) &&
|
// Assignment patterns are lexically before their RHS, but execute after it.
|
||||||
!t.isOptionalMemberExpression(member)
|
// A simple default target evaluates its reference before its initializer;
|
||||||
)
|
// a nested pattern starts destructuring only after that initializer.
|
||||||
continue;
|
if (t.isAssignmentExpression(node) && isPattern(node.left))
|
||||||
const key = semanticStaticPropertyKey(member.property, member.computed);
|
return ["right", "left"];
|
||||||
for (const identity of origin(member.object, "member"))
|
if (t.isForOfStatement(node) || t.isForInStatement(node))
|
||||||
retain(node, [...identity, key]);
|
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;
|
let explicitReceiver = false;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
callee = unwrapJavaScriptExpression(callee).node;
|
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;
|
if (visited.has(callee)) return undefined;
|
||||||
visited.add(callee);
|
visited.add(callee);
|
||||||
let path = intrinsicPath(callee);
|
let path = intrinsicPath(callee);
|
||||||
@@ -97,6 +106,7 @@ export const resolveJavaScriptIntrinsicInvocation = (
|
|||||||
);
|
);
|
||||||
if (method === "call" || method === "apply") {
|
if (method === "call" || method === "apply") {
|
||||||
const bound = boundIntrinsic(callee.object);
|
const bound = boundIntrinsic(callee.object);
|
||||||
|
if (explicitReceiver && !t.isExpression(receiver)) return undefined;
|
||||||
if (
|
if (
|
||||||
bound !== undefined &&
|
bound !== undefined &&
|
||||||
intrinsicReadChanged(callee, [...bound.path, method])
|
intrinsicReadChanged(callee, [...bound.path, method])
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ import {
|
|||||||
} from "./javascriptSemanticTraversal.js";
|
} from "./javascriptSemanticTraversal.js";
|
||||||
import {
|
import {
|
||||||
electronMemberPath,
|
electronMemberPath,
|
||||||
|
javaScriptIdentityWriteOffset,
|
||||||
|
type JavaScriptIdentityWriteKind,
|
||||||
type JavaScriptIntrinsicInvocation,
|
type JavaScriptIntrinsicInvocation,
|
||||||
} from "./javascriptElectronMemberWrites.js";
|
} from "./javascriptElectronMemberWrites.js";
|
||||||
import {
|
import {
|
||||||
expandJavaScriptInvocationArguments,
|
expandJavaScriptInvocationArguments,
|
||||||
resolveJavaScriptIntrinsicInvocation,
|
resolveJavaScriptIntrinsicInvocation,
|
||||||
} from "./javascriptIntrinsicInvocation.js";
|
} from "./javascriptIntrinsicInvocation.js";
|
||||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
|
||||||
import {
|
import {
|
||||||
readExactJavaScriptLiteral,
|
readExactJavaScriptLiteral,
|
||||||
unwrapJavaScriptExpression,
|
unwrapJavaScriptExpression,
|
||||||
@@ -74,6 +76,16 @@ interface InteropHelper {
|
|||||||
readonly identity: boolean;
|
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. */
|
/** Prove one inert namespace conversion without relaxing general assignment semantics. */
|
||||||
export const createSemanticModuleInterop = (
|
export const createSemanticModuleInterop = (
|
||||||
state: JavaScriptSemanticAnalysisState,
|
state: JavaScriptSemanticAnalysisState,
|
||||||
@@ -81,6 +93,14 @@ export const createSemanticModuleInterop = (
|
|||||||
readonly intrinsicPath: (node: t.Node) => readonly string[] | undefined;
|
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 helpers = new Map<string, InteropHelper | null>();
|
||||||
const origins = new Map<string, InteropOrigin>();
|
const origins = new Map<string, InteropOrigin>();
|
||||||
const calls = new WeakMap<t.Node, InteropOrigin>();
|
const calls = new WeakMap<t.Node, InteropOrigin>();
|
||||||
@@ -88,8 +108,18 @@ export const createSemanticModuleInterop = (
|
|||||||
const writes: {
|
const writes: {
|
||||||
readonly scope: string;
|
readonly scope: string;
|
||||||
readonly offset: number;
|
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 invalidatedBindings = new Map<string, boolean>();
|
||||||
const invalidatedCalls = new WeakMap<t.Node, boolean>();
|
const invalidatedCalls = new WeakMap<t.Node, boolean>();
|
||||||
const opaqueMutationPath = (node: t.Node): readonly string[] => {
|
const opaqueMutationPath = (node: t.Node): readonly string[] => {
|
||||||
@@ -125,6 +155,12 @@ export const createSemanticModuleInterop = (
|
|||||||
const { root, members } = electronMemberPath(current);
|
const { root, members } = electronMemberPath(current);
|
||||||
if (members.some((member) => member === null)) return finish(undefined);
|
if (members.some((member) => member === null)) return finish(undefined);
|
||||||
suffix = [...(members as readonly string[]), ...suffix];
|
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"])
|
for (const global of ["Object", "Function", "Reflect"])
|
||||||
if (isUnshadowedGlobal(root, state, global))
|
if (isUnshadowedGlobal(root, state, global))
|
||||||
return finish(["intrinsic", global, ...suffix]);
|
return finish(["intrinsic", global, ...suffix]);
|
||||||
@@ -165,8 +201,20 @@ export const createSemanticModuleInterop = (
|
|||||||
let capturedAt = node;
|
let capturedAt = node;
|
||||||
let suffixLength = 0;
|
let suffixLength = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
current = unwrapJavaScriptExpression(current).node;
|
||||||
|
if (suffixLength === 0)
|
||||||
|
capturedAt =
|
||||||
|
t.isMemberExpression(current) || t.isOptionalMemberExpression(current)
|
||||||
|
? current.property
|
||||||
|
: current;
|
||||||
const { root, members } = electronMemberPath(current);
|
const { root, members } = electronMemberPath(current);
|
||||||
suffixLength += members.length;
|
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;
|
if (!t.isIdentifier(root)) return undefined;
|
||||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||||
if (binding === undefined) return { path, capturedAt };
|
if (binding === undefined) return { path, capturedAt };
|
||||||
@@ -217,24 +265,84 @@ export const createSemanticModuleInterop = (
|
|||||||
node: t.Node,
|
node: t.Node,
|
||||||
path: readonly string[],
|
path: readonly string[],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const capture = capturedIntrinsic(node)?.capturedAt ?? node;
|
let current = unwrapJavaScriptExpression(node).node;
|
||||||
const scope = opaqueMutationPath(capture).join(".");
|
let currentPath = path;
|
||||||
const method = path.at(-1);
|
const aliases = new Set<string>();
|
||||||
const invocationMethod =
|
for (;;) {
|
||||||
method === "bind" || method === "call" || method === "apply";
|
current = unwrapJavaScriptExpression(current).node;
|
||||||
return writes.some(
|
if (t.isSequenceExpression(current)) {
|
||||||
(write) =>
|
const value = current.expressions.at(-1);
|
||||||
(write.scope === scope || write.scope === "intrinsic") &&
|
if (value === undefined) return true;
|
||||||
write.offset < (capture.start ?? -1) &&
|
current = value;
|
||||||
(write.coverage.covers(path) ||
|
continue;
|
||||||
(invocationMethod &&
|
}
|
||||||
write.coverage.covers([
|
const method = currentPath.at(-1);
|
||||||
"intrinsic",
|
const invocationMethod =
|
||||||
"Function",
|
method === "bind" || method === "call" || method === "apply";
|
||||||
"prototype",
|
// Recover the member read at an alias initializer before examining its
|
||||||
method,
|
// 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 = (
|
const resolveBoundIntrinsic = (
|
||||||
@@ -278,7 +386,7 @@ export const createSemanticModuleInterop = (
|
|||||||
if (members.at(-1) === "bind") {
|
if (members.at(-1) === "bind") {
|
||||||
// A direct .bind may continue a bound callable, without discarding members.
|
// A direct .bind may continue a bound callable, without discarding members.
|
||||||
if (!terminal && members.length !== 1) return undefined;
|
if (!terminal && members.length !== 1) return undefined;
|
||||||
bindReads.push(captured?.capturedAt ?? current.callee);
|
bindReads.push(current.callee);
|
||||||
} else {
|
} else {
|
||||||
// Reuse the invocation owner for generic call/apply/Reflect.apply of bind.
|
// Reuse the invocation owner for generic call/apply/Reflect.apply of bind.
|
||||||
const invocation = resolveJavaScriptIntrinsicInvocation(
|
const invocation = resolveJavaScriptIntrinsicInvocation(
|
||||||
@@ -298,6 +406,8 @@ export const createSemanticModuleInterop = (
|
|||||||
captured = capturedIntrinsic(bindTarget);
|
captured = capturedIntrinsic(bindTarget);
|
||||||
terminal = captured !== undefined;
|
terminal = captured !== undefined;
|
||||||
path = captured?.path;
|
path = captured?.path;
|
||||||
|
opaqueArguments ||=
|
||||||
|
path !== undefined && intrinsicReadChanged(bindTarget, path);
|
||||||
}
|
}
|
||||||
receiver = bindingArguments?.[0];
|
receiver = bindingArguments?.[0];
|
||||||
const ownArguments = bindingArguments?.slice(1) ?? [];
|
const ownArguments = bindingArguments?.slice(1) ?? [];
|
||||||
@@ -312,17 +422,9 @@ export const createSemanticModuleInterop = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const boundPath = [...path, "bind"];
|
const boundPath = [...path, "bind"];
|
||||||
const functionBind = ["intrinsic", "Function", "prototype", "bind"];
|
opaqueArguments ||= bindReads.some((capture) =>
|
||||||
opaqueArguments ||= bindReads.some((capture) => {
|
intrinsicReadChanged(capture, boundPath),
|
||||||
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 {
|
return {
|
||||||
kind: opaqueArguments ? "opaque" : "known",
|
kind: opaqueArguments ? "opaque" : "known",
|
||||||
path,
|
path,
|
||||||
@@ -609,8 +711,8 @@ export const createSemanticModuleInterop = (
|
|||||||
return writes.some(
|
return writes.some(
|
||||||
(write) =>
|
(write) =>
|
||||||
(write.scope === scope || write.scope === "intrinsic") &&
|
(write.scope === scope || write.scope === "intrinsic") &&
|
||||||
write.offset < (capturedAt.start ?? -1) &&
|
write.offset < evaluationOffset(capturedAt) &&
|
||||||
write.coverage.covers(path),
|
intrinsicWriteCovers(write.path, path),
|
||||||
);
|
);
|
||||||
}) ?? false;
|
}) ?? false;
|
||||||
return {
|
return {
|
||||||
@@ -628,13 +730,22 @@ export const createSemanticModuleInterop = (
|
|||||||
staticString,
|
staticString,
|
||||||
isFreshObject,
|
isFreshObject,
|
||||||
opaqueMutationPath,
|
opaqueMutationPath,
|
||||||
retainWrite: (node: t.Node, path: readonly (string | null)[]) => {
|
observeEvaluation: (node: t.Node): void => {
|
||||||
const coverage = new SemanticPropertyPathCoverage();
|
evaluationOrder.set(node, ++evaluationClock);
|
||||||
coverage.retain(path[0] === "opaque-intrinsic" ? ["intrinsic"] : path);
|
},
|
||||||
|
retainWrite: (
|
||||||
|
node: t.Node,
|
||||||
|
path: readonly (string | null)[],
|
||||||
|
kind: JavaScriptIdentityWriteKind = "replace-value",
|
||||||
|
) => {
|
||||||
writes.push({
|
writes.push({
|
||||||
scope: opaqueMutationPath(node).join("."),
|
scope: opaqueMutationPath(node).join("."),
|
||||||
offset: node.start ?? Infinity,
|
offset:
|
||||||
coverage,
|
evaluationClock === 0
|
||||||
|
? javaScriptIdentityWriteOffset(node)
|
||||||
|
: evaluationOffset(node),
|
||||||
|
path: path[0] === "opaque-intrinsic" ? ["intrinsic"] : path,
|
||||||
|
kind,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
invalidated: (binding: JavaScriptSemanticBindingState): boolean => {
|
invalidated: (binding: JavaScriptSemanticBindingState): boolean => {
|
||||||
|
|||||||
@@ -462,10 +462,11 @@ export function* indexJavaScriptElectronIdentitySteps(
|
|||||||
interop.intrinsicInvocation,
|
interop.intrinsicInvocation,
|
||||||
interop.staticString,
|
interop.staticString,
|
||||||
interop.isFreshObject,
|
interop.isFreshObject,
|
||||||
(node, path) => {
|
(node, path, kind) => {
|
||||||
if (path[0] === "electron") writes.retain(path);
|
if (path[0] === "electron") writes.retain(path);
|
||||||
else interop.retainWrite(node, path);
|
else interop.retainWrite(node, path, kind);
|
||||||
},
|
},
|
||||||
|
interop.observeEvaluation,
|
||||||
);
|
);
|
||||||
collectedWrites = true;
|
collectedWrites = true;
|
||||||
const intactExport = (path: readonly (string | null)[]): boolean =>
|
const intactExport = (path: readonly (string | null)[]): boolean =>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const traverseJavaScriptAst = (
|
|||||||
export interface JavaScriptAstVisitor {
|
export interface JavaScriptAstVisitor {
|
||||||
/** Omit a child and its subtree without entering or exiting it. */
|
/** Omit a child and its subtree without entering or exiting it. */
|
||||||
readonly skip?: (node: t.Node) => boolean;
|
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: (
|
readonly enter: (
|
||||||
node: t.Node,
|
node: t.Node,
|
||||||
parent: t.Node | null,
|
parent: t.Node | null,
|
||||||
@@ -40,7 +42,12 @@ export function* traverseJavaScriptAstSteps(
|
|||||||
const enter = (node: t.Node, parent: t.Node | null): TraversalFrame => {
|
const enter = (node: t.Node, parent: t.Node | null): TraversalFrame => {
|
||||||
visitor.enter(node, parent, () => [...ancestors]);
|
visitor.enter(node, parent, () => [...ancestors]);
|
||||||
ancestors.push(node);
|
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)];
|
const pending = [enter(root, null)];
|
||||||
let visited = 1;
|
let visited = 1;
|
||||||
@@ -84,8 +91,10 @@ interface TraversalFrame {
|
|||||||
* expansion; bespoke `Object.values` / duplicated `childNodes` walkers must
|
* expansion; bespoke `Object.values` / duplicated `childNodes` walkers must
|
||||||
* not be reintroduced (they diverge on loc/comment fields and ordering).
|
* not be reintroduced (they diverge on loc/comment fields and ordering).
|
||||||
*/
|
*/
|
||||||
export const childNodes = (node: t.Node): t.Node[] => {
|
export const childNodes = (
|
||||||
const keys = javaScriptAstChildKeys(node);
|
node: t.Node,
|
||||||
|
keys: readonly string[] = javaScriptAstChildKeys(node),
|
||||||
|
): t.Node[] => {
|
||||||
const children: t.Node[] = [];
|
const children: t.Node[] = [];
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value: unknown = Reflect.get(node, key);
|
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});',
|
'const change = Object.defineProperty.bind(null,Object,"create"); change({value:customCreate});',
|
||||||
],
|
],
|
||||||
["receiver.cjs", 'Reflect.set({},"create",customCreate,Object);'],
|
["receiver.cjs", 'Reflect.set({},"create",customCreate,Object);'],
|
||||||
|
[
|
||||||
|
"sequence.cjs",
|
||||||
|
'(0,Object.defineProperty)(Object,"create",{value:customCreate});',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"deep-call.cjs",
|
"deep-call.cjs",
|
||||||
'Function.prototype.call.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
'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() {}
|
function hoistedFunction() {}
|
||||||
const captured = Object.defineProperty;
|
const captured = Object.defineProperty;
|
||||||
Object.defineProperty = custom;
|
Object.defineProperty = custom;
|
||||||
|
captured.call(null,metadata,"captured",{value:1});
|
||||||
${ROLLDOWN_MODULE_INTEROP.replace("__defProp = Object.defineProperty", "__defProp = captured")}
|
${ROLLDOWN_MODULE_INTEROP.replace("__defProp = Object.defineProperty", "__defProp = captured")}
|
||||||
let electron = require("electron");
|
let electron = require("electron");
|
||||||
electron = __toESM(electron, 1);
|
electron = __toESM(electron, 1);
|
||||||
@@ -119,6 +124,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
|||||||
require("./generic-bind.cjs");
|
require("./generic-bind.cjs");
|
||||||
require("./replaced-apply.cjs");
|
require("./replaced-apply.cjs");
|
||||||
require("./receiver.cjs");
|
require("./receiver.cjs");
|
||||||
|
require("./sequence.cjs");
|
||||||
require("./deep-call.cjs");
|
require("./deep-call.cjs");
|
||||||
require("./deep-apply.cjs");
|
require("./deep-apply.cjs");
|
||||||
throw new Error("must not execute main");
|
throw new Error("must not execute main");
|
||||||
@@ -147,7 +153,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
|||||||
preload_entrypoints: 1,
|
preload_entrypoints: 1,
|
||||||
});
|
});
|
||||||
expect(result.statistics.parse_failures).toBe(0);
|
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(
|
const windows = result.graph.nodes.filter(
|
||||||
({ kind }) => kind === "browser-window",
|
({ kind }) => kind === "browser-window",
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user