fix(javascript): resolve module-level Electron options in functions (#8960)

* fix(javascript): resolve module-level Electron options in functions

* fix(javascript): limit module option ancestry walk

* fix(javascript): preserve uncertain captured argument ordering

* fix(javascript): retain hoisted activation uncertainty

* fix(javascript): retain function ownership through loops

* fix(javascript): account for classic global activations

* fix(javascript): bind first activations to lexical identities

* fix(javascript): retain uncertain global activation timing

* fix(javascript): isolate receiver facts and share computed reads

* fix(javascript): retain projected and opaque global activations

* fix(javascript): refresh activation bounds as opaque effects grow

* fix(ci): format website watch directories without changing configuration

* fix(javascript): share source reads across opaque-effect updates

* fix(javascript): retain constructed-code global activation bounds

* fix(javascript): share runtime activation and receiver facts

* fix(javascript): reuse pattern reads and function positions

* fix(javascript): isolate copied paths and preserve runtime reads

* style(javascript): format migrated reference imports

* fix(javascript): normalize constructor activation and receiver effects

* fix(javascript): retain aliased receiver timing and lexical global this

* fix(javascript): preserve effect-free receiver identity across activations

* fix(javascript): retain outer activation and prototype effects

* fix(javascript): index implicit reads without deep path rescans

* fix(javascript): retain aliased prototype and parameter read uncertainty

* fix(javascript): index reflected reads through intrinsic invocation facts

* fix(javascript): retain unresolved global activation reads

* chore: refresh PR branch after review fix publication

* fix(javascript): retain projected receiver reads and cache method facts

* fix(javascript): retain inherited and implicit activation reads

* fix(javascript): retain caller-owned this uncertainty

* fix(javascript): retain reflected reads with unresolved spread arguments

* fix(javascript): preserve enclosing activation for decorators

* fix(javascript): track intrinsic literal prototype effects

* fix(javascript): retain catch activation from implicit reads

* perf(javascript): reuse effective keys for projected reads

* fix(javascript): retain possible caller-owned ambient effects
This commit is contained in:
ShuaixinHuang authored and GitHub committed 2026-11-05 16:52:19 +08:00
1 parent ce07d358cd
commit a2134b3a74
40 files changed
+2585 -191

No files matched your search

@@ -541,6 +541,14 @@ the same artifact. Inline option syntax keeps its observed Evidence. This
does not execute code or establish runtime configuration; popup overrides from
`setWindowOpenHandler` are not covered by this constant-options resolution.
A hoisted function does not establish initialization timing by declaration order.
For classic scripts, the shared index also accounts for matching literal global
property accesses through `globalThis`, `window`, `self`, and `this`, including
optional and computed literal access. An early read or an indirect activation
keeps captured options unknown even when a later bare call exists. ES module
function declarations are not inferred to be global properties. This remains a
bounded source-order check, not a proof that runtime activation succeeds.
Recognized literal module-table bootstraps also support bounded lexical
substitution through module exports, import helper bodies, local wrapper calls,
returned closures, object spreads, and callback parameters. Recovered Electron
@@ -1,7 +1,14 @@
import { runInNewContext } from "node:vm";
import { describe, expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import * as t from "@babel/types";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { semanticPropertyReadSources } from "./javascriptSemanticIterationSources.js";
import {
analyzeParsedJavaScriptStaticSource,
analyzeJavaScriptStaticSource,
} from "./javascriptStaticAnalysis.js";
describe("Electron constant option evidence", () => {
it("keeps mixed constant, inline and unresolved windows in construction order", () => {
@@ -200,6 +207,714 @@ describe("Electron option captures and initialization", () => {
});
});
it("resolves module-level webPreferences used inside a function", () => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron");
const webPreferences = { nodeIntegration: true, contextIsolation: false };
function createWindow() {
return new BrowserWindow({ width: 800, webPreferences });
}
createWindow();
`);
const window = analysis.electron.browser_windows[0];
expect(window?.web_preferences_status).toBe("object-literal");
expect(window?.web_preferences).toEqual(
expect.arrayContaining([
{
name: "nodeIntegration",
value: expect.objectContaining({ status: "literal", value: true }),
},
{
name: "contextIsolation",
value: expect.objectContaining({ status: "literal", value: false }),
},
]),
);
});
it("keeps a module-level option unresolved when called before initialization", () => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron");
function createWindow() {
return new BrowserWindow({ webPreferences });
}
createWindow();
const webPreferences = { nodeIntegration: true };
`);
expect(analysis.electron.browser_windows[0]).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
});
it.each([
{ label: "a direct call", before: "createWindow();", after: "" },
{
label: "an aliased call",
before: "const early = createWindow; early();",
after: "",
},
{
label: "a forwarding function",
before: "start();",
after: "function start() { createWindow(); }",
},
])(
"does not initialize a hoisted window function reached by $label",
({ before, after }) => {
const source = `
const { BrowserWindow } = require("electron");
${before}
const webPreferences = { nodeIntegration: true };
function createWindow() {
return new BrowserWindow({ webPreferences });
}
${after}
`;
expect(() =>
runInNewContext(source, {
require: () => ({ BrowserWindow: class {} }),
}),
).toThrow(/before initialization/);
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
},
);
it("resolves a hoisted window function called after module initialization", () => {
const source = `
const { BrowserWindow } = require("electron");
function createWindow() {
return new BrowserWindow({ webPreferences });
}
const webPreferences = { nodeIntegration: true };
createWindow();
`;
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it.each([
"for (const item of [0])",
"for (let index = 0; index < 1; index++)",
"while (true)",
])(
"keeps function initialization coverage separate from %s ordering",
(loop) => {
const source = (early: boolean) => `
const { BrowserWindow } = require("electron");
${early ? "createWindow();" : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() {
${loop} {
return new BrowserWindow({ webPreferences });
}
}
${early ? "" : "createWindow();"}
`;
const runtime = () => {
const context = { require: () => ({ BrowserWindow: class {} }) };
return { ...context, window: undefined, self: undefined };
};
expect(() => runInNewContext(source(false), runtime())).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source(false)).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
expect(() => runInNewContext(source(true), runtime())).toThrow(
/before initialization/,
);
expect(
analyzeJavaScriptStaticSource(source(true)).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
},
);
it("keeps class-owned this separate from a global activation", () => {
const source = `const { BrowserWindow } = require("electron");
class Other { static createWindow() {} static value = this.createWindow(); }
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({webPreferences}); }
createWindow();`;
expect(() =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) }),
).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it.each([32, 64])(
"shares guarded subtree summaries across %i nested catches",
(size) => {
const source =
"try {".repeat(size) +
"invoke();" +
"} catch ({createWindow: start}) {}".repeat(size);
const parsed = parseJavaScriptSource(source);
expect(parsed).not.toBeNull();
if (parsed === null) return;
const catches: { node: t.CatchClause; parent: t.TryStatement }[] = [];
const blocks: t.BlockStatement[] = [];
traverseJavaScriptAst(parsed.program, {
enter(node, parent) {
if (t.isBlockStatement(node)) blocks.push(node);
if (t.isCatchClause(node) && t.isTryStatement(parent))
catches.push({ node, parent });
},
});
let reads = 0;
for (const block of blocks) {
const body = block.body;
Object.defineProperty(block, "body", {
configurable: true,
get() {
reads++;
return body;
},
});
}
for (const { node, parent } of catches)
expect(semanticPropertyReadSources(node, parent).length).toBeGreaterThan(
0,
);
expect(reads).toBeLessThanOrEqual(size * 8);
},
);
it.each([
"function boot() { this.createWindow(); } boot();",
"function boot() { this.createWindow(); } boot.call(globalThis);",
"function boot() { const run = () => this.createWindow(); run(); } boot();",
])("retains caller-owned this uncertainty: %s", (activation) => {
for (const early of [true, false]) {
const source = `const { BrowserWindow } = require("electron");
${early ? activation : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({webPreferences}); }
${early ? "" : activation}
createWindow();`;
const run = () =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) });
if (early) expect(run).toThrow(/before initialization/);
else expect(run).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
}
});
const CLASSIC_GLOBAL_ACTIVATIONS = [
"function boot() { Object.setPrototypeOf(this.Array.prototype, globalThis); } boot(); [].createWindow();",
"const box = { get value() { throw globalThis; } }; try { const { value } = box; } catch ({ createWindow: start }) { start(); }",
"function fail() { throw globalThis; } try { const box = { [fail()]() {} }; } catch ({ createWindow: start }) { start(); }",
"Object.setPrototypeOf(Number.prototype, globalThis); (0).createWindow();",
"Object.setPrototypeOf(Boolean.prototype, globalThis); true.createWindow();",
"Object.setPrototypeOf(RegExp.prototype, globalThis); /x/.createWindow();",
"Object.setPrototypeOf(BigInt.prototype, globalThis); (0n).createWindow();",
"Object.assign(Object.prototype, globalThis); (0).createWindow();",
"Object.assign(Object.prototype, globalThis); true.createWindow();",
"Object.assign(Object.prototype, globalThis); /x/.createWindow();",
"Object.setPrototypeOf(globalThis.window.Array.prototype, globalThis); [].createWindow();",
"function fail() { throw globalThis; } try { fail``; } catch ({ createWindow: start }) { start(); }",
"const box = { get value() { throw globalThis; } }; try { box.value; } catch ({ createWindow: start }) { start(); }",
"const { root } = { root: globalThis, createWindow() {} }; const early = root.createWindow; early();",
"let root; ({ root } = { root: globalThis, createWindow() {} }); const early = root.createWindow; early();",
"const { nested: { root } } = { nested: { root: globalThis }, createWindow() {} }; root.createWindow();",
"function fail() { throw globalThis; } try { fail(); } catch ({ createWindow: start }) { start(); }",
"function fail() { throw globalThis; } try { if (true) fail(); } catch ({ createWindow: start }) { start(); }",
"const args = [globalThis, 'createWindow']; Reflect.get.apply(Reflect, args)();",
"const args = [globalThis, 'createWindow']; Reflect.get(...args)();",
"Object.assign(Object.prototype, globalThis); ({}).createWindow();",
"Object.setPrototypeOf(String.prototype, globalThis); ''.createWindow();",
"Object.setPrototypeOf(this.Array.prototype, globalThis); [].createWindow();",
"globalThis[`Reflect`].get(globalThis, 'createWindow')();",
"Object.extra = 1; function unused() { [].createWindow; } Reflect.get?.(globalThis, 'createWindow')();",
"const R = Reflect; R.get(globalThis, 'createWindow')();",
"const read = Reflect.get; read(globalThis, 'createWindow')();",
"const read = Reflect.get.bind(Reflect); read(globalThis, 'createWindow')();",
"const root = []; Object.setPrototypeOf(Array.prototype, globalThis); root.createWindow();",
"const root = []; const alias = root; Object.setPrototypeOf(Array.prototype, globalThis); alias.createWindow();",
"const root = function(){}; const alias = root; Object.setPrototypeOf(Function.prototype, globalThis); alias.createWindow();",
"globalThis.createWindow();",
'globalThis["createWindow"]();',
"const early = globalThis.createWindow; early();",
"globalThis?.createWindow?.();",
"window.createWindow();",
'self["createWindow"]();',
"this.createWindow();",
"const root = globalThis; root.createWindow();",
"const root = window; const alias = root; alias.createWindow();",
"const root = globalThis.window; root.createWindow();",
"const { root } = { root: globalThis }; root.createWindow();",
"const key = 'createWindow'; globalThis[key]();",
"with ({}) { createWindow(); }",
"const { createWindow: start } = globalThis; start();",
"let start; ({ createWindow: start } = globalThis); start();",
"let start; for ({ createWindow: start } of [globalThis]) start();",
"for (const { createWindow: start } of [globalThis]) start();",
"let start; ({ root: { createWindow: start } } = { root: globalThis }); start();",
"const start = (createWindow ||= null); start();",
"const root = { __proto__: globalThis }; root.createWindow();",
"const root = { ...globalThis }; root.createWindow();",
"(0, eval)('createWindow()');",
"Function('createWindow()')();",
"class C extends (this.Function('createWindow()')(), Object) {}",
"({ [this.eval('createWindow()')]() {} });",
"Function`createWindow()`();",
"Object.setPrototypeOf(Array.prototype, globalThis); [].createWindow();",
"Object.extra=1; function unusedArrayRead(){[].createWindow;} Reflect.get(globalThis, 'createWindow')();",
"Object.extra=1; function unusedArrayRead(){[].createWindow;} try { throw globalThis; } catch ({ createWindow: start }) { start(); }",
"Object.setPrototypeOf(Function.prototype, globalThis); (function(){}).createWindow();",
"new Function('createWindow()')();",
"this['Function']('createWindow()')();",
"const root = {}; const alias = root; Object.setPrototypeOf(root, globalThis); alias.createWindow();",
"const root = {}; const alias = root; Object.assign(root, globalThis); alias.createWindow();",
"const F = Function.bind(null); new F('createWindow()')();",
"Reflect.construct(Function, ['createWindow()'])();",
"const root = {}; Object.setPrototypeOf(root, globalThis); root.createWindow();",
"const root = {}; Object.assign(root, globalThis); root.createWindow();",
"const F = Function; F('createWindow()')();",
"Function.call(null, 'createWindow()')();",
"Function.apply(null, ['createWindow()'])();",
"globalThis.Function('createWindow()')();",
"const F = Function.bind(null); F('createWindow()')();",
"const noop = () => {}; function unused() { noop(); } (0, eval)('createWindow()');",
"const noop = () => {}; (0, eval)(''); function unused() { noop(); } (0, eval)('createWindow()');",
];
it.each([
"",
"const root = globalThis;",
"const unrelated = { createWindow() {} }; function unusedReceiverRead() { unrelated.createWindow; }",
"for (const ignored of [1, 2]) { ({ createWindow() {} }).createWindow; }",
])("keeps class heritage activation after initialization with %s", (extra) => {
const source = `
const { BrowserWindow } = require("electron");
${extra}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
class C extends (this.Function('createWindow()')(), Object) {}
createWindow();
`;
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it.each([64, 128])(
"bounds own-method key visits for %i repeated reads",
(size) => {
const source = `const { BrowserWindow } = require('electron');
const box = { createWindow() {}, ${Array.from({ length: size }, (_, i) => `p${i}: 0`).join(",")} };
${"box.createWindow; box.unrelated;".repeat(size)}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();`;
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("fixture did not parse");
const statement = parsed.program.body[1];
if (statement?.type !== "VariableDeclaration")
throw new Error("fixture changed");
const object = statement.declarations[0]?.init;
if (object?.type !== "ObjectExpression") throw new Error("fixture changed");
let keyVisits = 0;
for (const property of object.properties) {
if (property.type === "SpreadElement") continue;
const key = property.key;
Object.defineProperty(property, "key", {
configurable: true,
get: () => {
keyVisits += 1;
return key;
},
});
}
expect(
analyzeParsedJavaScriptStaticSource(source, parsed).electron
.browser_windows[0],
).toMatchObject({ web_preferences_status: "object-literal" });
expect(keyVisits).toBeLessThanOrEqual(32 * (size + 1));
},
);
it.each([64, 128])(
"bounds projected property key visits for %i distinct reads",
(size) => {
const names = Array.from({ length: size }, (_, i) => `p${i}`);
const source = `const { BrowserWindow } = require('electron');
const { ${names.map((name) => `${name}: { unrelated: ${name} }`).join(",")} } =
{ ${names.map((name) => `${name}: {}`).join(",")} };
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();`;
const parsed = parseJavaScriptSource(source);
const statement = parsed?.program.body[1];
if (parsed === null || statement?.type !== "VariableDeclaration")
throw new Error("fixture changed");
const object = statement.declarations[0]?.init;
if (object?.type !== "ObjectExpression") throw new Error("fixture changed");
let keyVisits = 0;
for (const property of object.properties) {
if (property.type === "SpreadElement") continue;
const key = property.key;
Object.defineProperty(property, "key", {
configurable: true,
get: () => {
keyVisits += 1;
return key;
},
});
}
expect(
analyzeParsedJavaScriptStaticSource(source, parsed).electron
.browser_windows[0],
).toMatchObject({ web_preferences_status: "object-literal" });
expect(keyVisits).toBeLessThanOrEqual(32 * size);
},
);
it("retains possible global code from a caller-owned receiver", () => {
const source = `
const { BrowserWindow } = require("electron");
function boot() { this.Function("createWindow()")(); }
boot();
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();
`;
expect(() =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) }),
).toThrow(/before initialization/);
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({ web_preferences_status: "dynamic", web_preferences: [] });
});
describe("Classic-script global function activation", () => {
it.each([
"const { nested: { value } } = { nested: { value: 1 } };",
"let value; ({ nested: { value } } = { nested: { value: 1 } });",
"export type { createWindow };",
"export { type createWindow };",
])("preserves settings across runtime-independent syntax: %s", (prefix) => {
const source = `${prefix === "export { type createWindow };" ? "import { BrowserWindow } from 'electron';" : "const { BrowserWindow } = require('electron');"}
${prefix}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();`;
const parsed = parseJavaScriptSource(source, "fixture.ts");
expect(parsed).not.toBeNull();
if (parsed === null) throw new Error("fixture did not parse");
expect(
analyzeParsedJavaScriptStaticSource(source, parsed).electron
.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it("retains possible classic activation after an erased type import", () => {
const source = `import type { T } from './types';
const { BrowserWindow } = require('electron'); globalThis.createWindow();
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();`;
expect(() =>
runInNewContext(source.replace("import type { T } from './types';", ""), {
require: () => ({ BrowserWindow: class {} }),
}),
).toThrow(/before initialization/);
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({ web_preferences_status: "dynamic", web_preferences: [] });
});
it("keeps a shadowed Function constructor independent of global activation", () => {
const source = `const { BrowserWindow } = require('electron');
const Function = () => () => {}; Function('createWindow()')();
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();`;
expect(() =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) }),
).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it.each(CLASSIC_GLOBAL_ACTIVATIONS)(
"keeps early global access unresolved: %s",
(activation) => {
const body = (early: boolean) => `
const { BrowserWindow } = require("electron");
${early ? activation : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
${early ? "" : activation}
createWindow();
`;
const runtime = () => ({ require: () => ({ BrowserWindow: class {} }) });
expect(() =>
runInNewContext(
"globalThis.window = globalThis; globalThis.self = globalThis;" +
body(true),
runtime(),
),
).toThrow(/before initialization/);
expect(() =>
runInNewContext(
"globalThis.window = globalThis; globalThis.self = globalThis;" +
body(false),
runtime(),
),
).not.toThrow();
expect(
analyzeJavaScriptStaticSource(body(true)).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
expect(
analyzeJavaScriptStaticSource(body(false)).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{
name: "nodeIntegration",
value: { status: "literal", value: true },
},
],
});
},
);
it.each([
"@dec(this.createWindow()) class Decorated {}",
"class Decorated { @dec(this.createWindow()) method() {} }",
])(
"tracks decorator expressions in the surrounding script: %s",
(decorated) => {
const source = (early: boolean) => `
const { BrowserWindow } = require("electron");
function dec() {}
${early ? decorated : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
${early ? "" : decorated}
createWindow();
`;
expect(
analyzeJavaScriptStaticSource(source(true)).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
expect(
analyzeJavaScriptStaticSource(source(false)).electron
.browser_windows[0],
).toMatchObject({ web_preferences_status: "object-literal" });
},
);
it("keeps callbacks in hoisted default parameters uncertain", () => {
const source = `
const { BrowserWindow } = require("electron");
createWindow();
const webPreferences = { nodeIntegration: true };
function createWindow(make = () => new BrowserWindow({ webPreferences })) { return make(); }
`;
expect(() =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) }),
).toThrow(/before initialization/);
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({ web_preferences_status: "dynamic", web_preferences: [] });
});
});
describe("Classic-script global receiver identity", () => {
it("keeps a long flat global alias chain stack safe", () => {
const aliases = ["const root0 = globalThis;"];
for (let index = 1; index < 6000; index++)
aliases.push(
"const root" +
index +
" = root" +
(index - 1) +
"; root" +
index +
".document;",
);
const source = `
const { BrowserWindow } = require("electron");
${aliases.join("\n")}
root5999.createWindow();
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();
`;
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({ web_preferences_status: "dynamic", web_preferences: [] });
});
it("does not confuse an unrelated parameter with the module function", () => {
const source = `
const { BrowserWindow } = require("electron");
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
function helper(createWindow) { createWindow(); }
createWindow();
`;
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it("does not treat an ES module function as a global property", () => {
const source = `
import { BrowserWindow } from "electron";
globalThis.createWindow();
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();
`;
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it.each([
"function unused() { other.createWindow; }",
"for (const value of [1,2]) { other.createWindow; }",
"const alias = other; function unused() { alias.createWindow; }",
])(
"preserves no-effect receiver identity across deferred reads: %s",
(read) => {
const source = `const { BrowserWindow } = require("electron");
const other = { createWindow() {} }; ${read}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({webPreferences}); }
createWindow();`;
expect(() =>
runInNewContext(source, {
require: () => ({ BrowserWindow: class {} }),
}),
).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{
name: "nodeIntegration",
value: { status: "literal", value: true },
},
],
});
},
);
it.each([
"String.prototype.extra = 1; [].createWindow;",
"const Reflect = { get() { return () => {}; } }; Reflect.get(globalThis, 'createWindow')();",
"Reflect.get({createWindow(){}}, 'createWindow')();",
"try { throw {createWindow(){}}; } catch ({createWindow:start}) { start(); }",
"Object.extra = 1; function unused() { [].createWindow; }",
"Array.extra = 1; function unused() { [].createWindow; }",
"Function.extra = 1; function unused() { (function(){}).createWindow; }",
"[].createWindow; Object.setPrototypeOf(Array.prototype, globalThis);",
"Function.prototype.extra = 1; [].createWindow;",
])("keeps unrelated or later prototype effects isolated: %s", (effect) => {
const source = `const { BrowserWindow } = require("electron"); ${effect}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({webPreferences}); }
createWindow();`;
expect(() =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) }),
).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
it("keeps unrelated same-named member calls independent", () => {
const source = `
const { BrowserWindow } = require("electron");
const other = { createWindow() {} };
let root = other;
root = globalThis;
root.document;
other.createWindow();
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();
`;
expect(() =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) }),
).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
});
describe("Electron option evidence", () => {
it.each([
'({ webPreferences: { preload: "./preload.js", sandbox: true } } as const)',
@@ -473,3 +1188,60 @@ describe("Electron empty option keys", () => {
},
);
});
it.each([
"function boot({ createWindow: start }) { start(); } boot(globalThis);",
"function boot({ createWindow: start } = {}) { start(); } boot(globalThis);",
])(
"retains unresolved caller-supplied parameter property reads: %s",
(activation) => {
for (const early of [true, false]) {
const source = `
const { BrowserWindow } = require("electron");
${early ? activation : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
${early ? "" : activation}
createWindow();
`;
const run = () =>
runInNewContext(source, {
require: () => ({ BrowserWindow: class {} }),
});
if (early) expect(run).toThrow(/before initialization/);
else expect(run).not.toThrow();
// No argument-source or interprocedural activation proof is established.
// A default value does not exclude a caller-supplied ambient receiver.
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
}
},
);
it.each([
"const Reflect = { get() { return () => {}; } }; const read = Reflect.get; read(globalThis, 'createWindow')();",
"const Reflect = { get() { return () => {}; } }; Reflect.get?.(globalThis, 'createWindow')();",
])("keeps shadowed reflected-call spellings isolated: %s", (activation) => {
const source = `
const { BrowserWindow } = require("electron");
${activation}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
createWindow();
`;
expect(() =>
runInNewContext(source, { require: () => ({ BrowserWindow: class {} }) }),
).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});
@@ -32,7 +32,7 @@ const optionObjectCandidate = (
!binding.declarationKinds.has("const") ||
initializer === undefined ||
initializer.projection.length !== 0 ||
!semanticBindingAtRead(binding, node, index.bindings.parentsByNode)
!semanticBindingAtRead(binding, node, index.bindings)
.hasDefiniteInitializer
)
return undefined;
@@ -75,7 +75,7 @@ export const resolveElectronBrowserWindowReceiver = (
if (
initializer === undefined ||
initializer.projection.length !== 0 ||
!semanticBindingAtRead(binding, receiver, index.bindings.parentsByNode)
!semanticBindingAtRead(binding, receiver, index.bindings)
.hasDefiniteInitializer
)
return undefined;
@@ -89,7 +89,7 @@ export const collectJavaScriptExports = (
const read = semanticBindingAtRead(
withoutUnusedFactoryDefaults(binding, roles),
reference,
bindings.parentsByNode,
bindings,
);
if (read.status !== "ordered") return "unresolved";
return read.binding.initializers.length === 0 &&
+74 -2
View File
@@ -1,8 +1,28 @@
import * as t from "@babel/types";
import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js";
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
import { semanticArrayIndex } from "./javascriptSemanticPropertyPaths.js";
/** Computed definition keys use the surrounding evaluation environment. */
export const javaScriptComputedDefinitionKey = (
parent: t.Node | undefined,
child: t.Node,
): boolean =>
(t.isObjectMethod(parent) ||
t.isClassMethod(parent) ||
t.isClassProperty(parent) ||
t.isClassAccessorProperty(parent)) &&
parent.computed &&
parent.key === child;
/** Class decorators and computed definition keys use the enclosing environment. */
export const javaScriptDefinitionExpression = (
parent: t.Node | undefined,
child: t.Node,
): boolean =>
t.isDecorator(child) || javaScriptComputedDefinitionKey(parent, child);
/** Type-only class fields neither initialize own slots nor shadow inherited members. */
export const isRuntimeJavaScriptClassProperty = (
node: t.Node,
@@ -444,13 +464,43 @@ const readStaticObjectProperty = (
return prototypeMaySupply ? { status: "dynamic" } : { status: "missing" };
};
/** Parse ordered members once for related property queries within one boundary. */
/** Index effective keys once for related property queries within one boundary. */
export const staticObjectPropertyReader = (
object: t.Node | undefined,
isNullish?: (node: t.Node) => boolean,
): ((name: string) => StaticObjectProperty) => {
const source = staticObjectPropertySource(object, isNullish);
return (name) => readStaticObjectProperty(source, name);
if (typeof source === "string") return () => ({ status: source });
const properties = new Map<string, StaticObjectProperty>();
let fallback: "missing" | "dynamic" = "missing";
let hasIndexedSpread = false;
for (let index = source.length - 1; index >= 0; index--) {
const member = source[index]!;
if (member.kind === "prototype-setter") {
if (member.prototype === "unknown") fallback = "dynamic";
} else if (
member.kind === "primitive-indices" ||
member.kind === "array-indices"
) {
hasIndexedSpread = true;
} else if (member.kind === "unknown" || member.key === null) {
fallback = "dynamic";
break;
} else if (!properties.has(member.key)) {
const { property, key } = member;
const value = objectDataPropertyValue(property);
properties.set(
key,
value === undefined
? { status: "dynamic" }
: { status: "explicit", property, value },
);
}
}
return (name) =>
hasIndexedSpread && semanticArrayIndex(name) !== null
? readStaticObjectProperty(source, name)
: (properties.get(name) ?? { status: fallback });
};
/** Find the effective explicit property, preserving unresolved overrides. */
@@ -461,6 +511,28 @@ export const staticObjectProperty = (
): StaticObjectProperty =>
readStaticObjectProperty(staticObjectPropertySource(object, isNullish), name);
/** Prove a literal's effective own member is a method, independent of its prototype. */
export const staticObjectOwnMethodReader = (
object: t.Node,
): ((name: string) => boolean) => {
if (!t.isObjectExpression(object)) return () => false;
const methods = new Map<string, boolean>();
const members = staticObjectMembers(object, 0);
for (let index = members.length - 1; index >= 0; index--) {
const member = members[index]!;
if (member.kind === "prototype-setter") continue;
if (member.kind === "unknown") break;
if (member.kind !== "property") continue;
if (member.key === null) break;
if (!methods.has(member.key))
methods.set(
member.key,
t.isObjectMethod(member.property) && member.property.kind === "method",
);
}
return (name) => methods.get(name) === true;
};
export type StaticObjectPropertyNames =
| { readonly status: "missing" | "dynamic" }
| { readonly status: "partial"; readonly names: readonly string[] }
@@ -175,11 +175,7 @@ const canonicalWebpackInteropBindings = (
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined) continue;
if (loaderBindingIds.has(binding.bindingId)) return true;
const selected = semanticBindingAtRead(
binding,
node,
state.parentsByNode,
).binding;
const selected = semanticBindingAtRead(binding, node, state).binding;
for (const source of [
...selected.initializers,
...selected.referenceInitializers,
@@ -147,16 +147,27 @@ export const resolveJavaScriptGenericInvocation = (
* Binding a function alone is not a write; only its eventual call is examined.
*/
export const resolveJavaScriptIntrinsicInvocation = (
node: t.CallExpression | t.OptionalCallExpression,
node:
| t.CallExpression
| t.OptionalCallExpression
| t.NewExpression
| t.TaggedTemplateExpression,
intrinsicPath: (node: t.Node) => readonly string[] | undefined,
boundIntrinsic: (node: t.Node) => JavaScriptIntrinsicInvocation | undefined,
intrinsicReadChanged: (node: t.Node, path: readonly string[]) => boolean,
nativeArrayIteration: JavaScriptArrayIterationProof = unknownArrayIteration,
): JavaScriptIntrinsicInvocation | undefined => {
const visited = new Set<t.Node>();
let callee: t.Node = node.callee;
let callee: t.Node = t.isTaggedTemplateExpression(node)
? node.tag
: node.callee;
let arguments_: JavaScriptIntrinsicInvocation["arguments"] =
expandJavaScriptInvocationArguments(node.arguments, nativeArrayIteration);
t.isTaggedTemplateExpression(node)
? undefined
: expandJavaScriptInvocationArguments(
node.arguments,
nativeArrayIteration,
);
let receiver: t.Node | undefined;
let explicitReceiver = false;
for (;;) {
@@ -240,6 +251,15 @@ export const resolveJavaScriptIntrinsicInvocation = (
arguments_ = selected.context.arguments;
continue;
}
if (path.join(".") === "intrinsic.Reflect.construct") {
const callable = arguments_?.[0];
if (!t.isExpression(callable)) return undefined;
callee = callable;
receiver = undefined;
explicitReceiver = true;
arguments_ = arrayArguments(arguments_?.[1], nativeArrayIteration);
continue;
}
if (path.join(".") === "intrinsic.Reflect.apply") {
const callable = arguments_?.[0];
if (!t.isExpression(callable)) return undefined;
@@ -626,7 +626,11 @@ export const createSemanticModuleInterop = (
callOrigin,
intrinsicPath,
intrinsicInvocation: (
node: t.CallExpression | t.OptionalCallExpression,
node:
| t.CallExpression
| t.OptionalCallExpression
| t.NewExpression
| t.TaggedTemplateExpression,
callablePath = intrinsicPath,
) =>
resolveJavaScriptIntrinsicInvocation(
@@ -89,8 +89,7 @@ const isXhrOpenRequest = (
if (
initializer === undefined ||
initializer.projection.length !== 0 ||
!semanticBindingAtRead(binding, receiver, bindings.parentsByNode)
.hasDefiniteInitializer
!semanticBindingAtRead(binding, receiver, bindings).hasDefiniteInitializer
)
return false;
receiver = unwrapJavaScriptExpression(initializer.node).node;
+4 -10
View File
@@ -23,10 +23,8 @@ import {
type JavaScriptBindingIndex,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import {
semanticCopiedReferencePath,
semanticReferenceSourcesInactive,
} from "./javascriptSemanticReferenceSources.js";
import { semanticReferenceSourcesInactive } from "./javascriptSemanticReferenceSources.js";
import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js";
import {
semanticAliasSelectionChoices,
type SemanticAliasSelectionPath,
@@ -452,7 +450,7 @@ export class JavaScriptRouteScan {
const binding = semanticBindingAtRead(
declared,
node,
this.index.bindings.parentsByNode,
this.index.bindings,
).binding;
for (const initializers of [
binding.initializers,
@@ -1020,11 +1018,7 @@ export class JavaScriptRouteScan {
seen.add(node);
const binding = index.bindingForReferenceAt(node);
if (binding === undefined) return;
const read = semanticBindingAtRead(
binding,
node,
index.bindings.parentsByNode,
);
const read = semanticBindingAtRead(binding, node, index.bindings);
if (!read.hasDefiniteInitializer) return;
const initializer = semanticStableInitializer(
read.binding,
@@ -1,3 +1,4 @@
import { javaScriptThisOwner } from "./javascriptSourceParser.js";
import * as t from "@babel/types";
import {
@@ -38,10 +39,10 @@ import {
} from "./javascriptSemanticGlobalReference.js";
import { readSemanticRequireLoader } from "./javascriptSemanticRequireOriginSlot.js";
import {
semanticCopiedReferencePath,
semanticReferenceIterationUnknown,
semanticReferenceSourcesInactive,
} from "./javascriptSemanticReferenceSources.js";
import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js";
export {
isUnmodifiedGlobalReference,
@@ -144,6 +145,7 @@ type AmbientIdentityUnknownReason =
interface AmbientResolution {
readonly paths: readonly AmbientPath[];
readonly callerPaths: readonly AmbientPath[];
readonly ownership: "local" | "ambient" | "unknown";
readonly unknownReasons: readonly AmbientIdentityUnknownReason[];
}
@@ -179,6 +181,7 @@ export const createAmbientPathResolver = (
const prior = possibleWrites ? undefined : cached.get(node);
if (prior !== undefined) return prior;
const result = new Map<string, AmbientPath>();
const callerPaths = new Map<string, AmbientPath>();
let hasLocalIdentity = false;
const unknownReasons = new Set<AmbientIdentityUnknownReason>();
// The caller's write site selects initializers for the written target.
@@ -254,6 +257,7 @@ export const createAmbientPathResolver = (
if (!possibleWrites) {
const unresolved: AmbientResolution = {
paths: [],
callerPaths: [...callerPaths.values()],
ownership: "unknown",
unknownReasons: ["widened-projection"],
};
@@ -319,6 +323,22 @@ export const createAmbientPathResolver = (
resolvedIdentity = true;
continue;
}
if (t.isThisExpression(root)) {
const owner = javaScriptThisOwner(
root,
state.program,
state.parentsByNode,
);
if (owner === "global") {
retain(["global", ...(suffix ?? [])]);
resolvedIdentity = true;
continue;
}
if (owner === "caller" && suffix !== undefined) {
const path = canonicalAmbientPath(["global", ...suffix]);
callerPaths.set(JSON.stringify(path), path);
}
}
if (t.isIdentifier(root)) {
// The wrapper module object is distinct from globalThis.module. Keep
// its identity through the same alias/projection owner used for writes.
@@ -345,7 +365,7 @@ export const createAmbientPathResolver = (
const readAt = current.readsAtSelf ? root : capture;
const atRead =
possibleWrites && readAt !== undefined
? semanticBindingAtRead(binding, readAt, state.parentsByNode)
? semanticBindingAtRead(binding, readAt, state)
: undefined;
const selected = atRead?.binding ?? binding;
if (possibleWrites && atRead?.hasDefiniteInitializer === false)
@@ -552,6 +572,7 @@ export const createAmbientPathResolver = (
unknownReasons.add("mixed-identities");
const resolution: AmbientResolution = {
paths,
callerPaths: [...callerPaths.values()],
ownership:
unknownReasons.size > 0
? "unknown"
@@ -1,3 +1,4 @@
import { semanticPatternPropertyReadSources } from "./javascriptSemanticIterationSources.js";
import {
createSemanticOpaqueGlobalObserver,
indexSemanticOpaqueEffects,
@@ -9,7 +10,7 @@ import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFac
import * as t from "@babel/types";
import {
semanticComputedMethodKey,
semanticMethodDefinitionExpression,
semanticBranchMayBeSkipped,
semanticPriorStatementMayExit,
semanticIterationCardinality,
@@ -48,6 +49,8 @@ import type {
} from "./javascriptSemanticState.js";
import {
currentSemanticScope,
semanticPrototypeOwners,
type SemanticPrototypeOwner,
resolveSemanticBindingState,
semanticResolutionBlocked,
resolveSemanticBindingFromScope,
@@ -228,6 +231,7 @@ function* indexSemanticMemberMutationFactsSteps(
(site, reference, path) =>
referenceEffectsMayHaveChanged(state, site, reference, path),
);
state.memberMutationFactsComplete = true;
indexedMemberMutationStates.add(state);
invalidateSemanticRequireDelegateWrites(state);
}
@@ -1041,9 +1045,10 @@ const createState = (
};
return {
arrayIterationUnknown: false,
memberMutationFactsComplete: false,
program,
opaqueEffects: {
mayReferenceGlobalEval: false,
mayReferenceGlobalCode: false,
sites: new WeakSet(),
sloppySites: new WeakSet(),
byScope: new Map(),
@@ -1063,6 +1068,8 @@ const createState = (
bindingWrites: new Map(),
prototypeRequireMutationObserved: false,
prototypeEnumerationMayAffect: false,
prototypeMutationSites: [],
implicitPropertyReads: new WeakMap(),
callables: [],
callableNodesById: new Map(),
moduleLinks: [],
@@ -1081,8 +1088,9 @@ const createState = (
};
};
const globalEvalRoots = new Set([
const globalCodeRoots = new Set([
"eval",
"Function",
"globalThis",
"global",
"window",
@@ -1120,6 +1128,8 @@ function* collectDefinitionsSteps(
scope: JavaScriptSemanticScopeState;
}[] = [];
let needsPrototypeFacts = false;
const implicitReadCalls: (t.CallExpression | t.OptionalCallExpression)[] = [];
let mayReferenceReflect = false;
const functionBodies: {
readonly body: t.BlockStatement;
readonly scope: JavaScriptSemanticScopeState;
@@ -1138,15 +1148,17 @@ function* collectDefinitionsSteps(
if (
t.isWithStatement(node) ||
t.isForInStatement(node) ||
t.isForOfStatement(node)
t.isForOfStatement(node) ||
t.isArrayExpression(node) ||
t.isFunction(node)
)
needsPrototypeFacts = true;
let parentScope = currentSemanticScope(stack);
if (
semanticComputedMethodKey(parent ?? undefined, node) &&
semanticMethodDefinitionExpression(parent ?? undefined, node) &&
parentScope.parentScopeId !== null
) {
// A computed method key is evaluated before entering the method's
// A method definition expression is evaluated before entering the method's
// parameter and body environments.
const keyScope = state.scopesById.get(parentScope.parentScopeId);
if (keyScope !== undefined) {
@@ -1211,12 +1223,26 @@ function* collectDefinitionsSteps(
state,
});
bindInnerDeclaration(node, parent, scope, state);
if (
t.isIdentifier(node, { name: "Reflect" }) ||
t.isStringLiteral(node, { value: "Reflect" }) ||
(t.isTemplateLiteral(node) &&
semanticStaticPropertyKey(node, true) === "Reflect")
)
mayReferenceReflect = true;
// Aliases and optional/call/apply/bind forms share the intrinsic owner;
// callee spelling alone cannot identify the property-read operation.
if (t.isCallExpression(node) || t.isOptionalCallExpression(node))
implicitReadCalls.push(node);
if (t.isThrowStatement(node))
thrownValues.push({ node, ancestors: readAncestors() });
// Every supported global eval origin starts with one of these decoded
// Every supported global-code origin starts with one of these decoded
// identifiers, including computed properties and destructured aliases.
if (t.isIdentifier(node) && globalEvalRoots.has(node.name))
state.opaqueEffects.mayReferenceGlobalEval = true;
if (
t.isThisExpression(node) ||
(t.isIdentifier(node) && globalCodeRoots.has(node.name))
)
state.opaqueEffects.mayReferenceGlobalCode = true;
if (
t.isCallExpression(node) &&
t.isIdentifier(unwrapJavaScriptExpression(node.callee).node, {
@@ -1328,6 +1354,15 @@ function* collectDefinitionsSteps(
parent.handler != null &&
child === parent.block
) {
if (parent.handler.param != null)
state.implicitPropertyReads.set(
node,
semanticPatternPropertyReadSources(
parent.handler.param,
{ node: node.argument, projection: [] },
node,
),
);
const scope = state.scopeByNode.get(parent.handler);
if (scope !== undefined && parent.handler.param != null)
bindPattern({
@@ -1398,36 +1433,85 @@ function* collectDefinitionsSteps(
if (t.isIdentifier(identifier))
copyParameterToBody({ identifier, body, scope, state });
}
const readInterop = mayReferenceReflect
? createSemanticModuleInterop(state)
: undefined;
for (const node of readInterop === undefined ? [] : implicitReadCalls) {
const invocation = readInterop?.intrinsicInvocation(node);
if (
invocation?.kind !== "known" ||
invocation.path.join(".") !== "intrinsic.Reflect.get"
)
continue;
const [receiver, key] = invocation.arguments ?? [];
const receiverUnknown =
invocation.arguments === undefined || t.isSpreadElement(receiver);
if (!receiverUnknown && !t.isExpression(receiver)) continue;
const selected =
!receiverUnknown && (t.isStringLiteral(key) || t.isNumericLiteral(key))
? key.value
: null;
state.implicitPropertyReads.set(node, [
{
node: receiverUnknown ? node : (receiver ?? node),
projection: receiverUnknown ? [null, selected] : [selected],
mutation: node,
lookup: "prototype",
},
]);
}
indexSemanticOpaqueEffects(opaqueCandidates, state);
if (state.opaqueEffects.byScope.size > 0) clearSemanticValueCaches(state);
}
type PrototypeOwner = "Object" | "Array" | "String";
const prototypeOwners = ["Object", "Array", "String"] as const;
/** Preserve uncertainty when source can change or expose a built-in prototype. */
function* indexPrototypeUncertaintySteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): Generator<void, void> {
const taint = (owner: PrototypeOwner): void => {
let effectSite: t.Node = program;
const seen = new Map<SemanticPrototypeOwner, WeakSet<t.Node>>();
const possiblePrototypeReferences = createAmbientPathResolver(state, false);
const taint = (owner: SemanticPrototypeOwner, inherited = true): void => {
let sites = seen.get(owner);
if (sites === undefined) {
sites = new WeakSet();
seen.set(owner, sites);
}
if (inherited && !sites.has(effectSite)) {
sites.add(effectSite);
state.prototypeMutationSites.push({ owner, node: effectSite });
}
if (owner === "Object") state.prototypeRequireMutationObserved = undefined;
state.prototypeEnumerationMayAffect = undefined;
};
const globalOwner = (node: t.Node): PrototypeOwner | undefined => {
const globalOwner = (node: t.Node): SemanticPrototypeOwner | undefined => {
const value = unwrapJavaScriptExpression(node).node;
for (const owner of prototypeOwners) {
if (isUnmodifiedGlobalReference(value, state, owner)) return owner;
if (
t.isIdentifier(value, { name: owner }) &&
resolveSemanticBindingState(state, value, owner) === undefined
)
return owner;
const member =
t.isMemberExpression(value) || t.isOptionalMemberExpression(value);
const key = t.isIdentifier(value)
? value.name
: member
? semanticStaticPropertyKey(value.property, value.computed)
: null;
const owner = semanticPrototypeOwners.find(
(candidate) => candidate === key,
);
if (owner === undefined) return undefined;
if (isUnmodifiedGlobalReference(value, state, owner)) return owner;
if (member) {
const { paths, callerPaths } = possiblePrototypeReferences(value);
const selectsOwner = (path: readonly (string | null)[]) =>
path.length === 2 && path[0] === "global" && path[1] === owner;
return paths.some(selectsOwner) || callerPaths.some(selectsOwner)
? owner
: undefined;
}
return undefined;
return resolveSemanticBindingState(state, value, owner) === undefined
? owner
: undefined;
};
const prototypeOwner = (node: t.Node): PrototypeOwner | undefined => {
const prototypeOwner = (node: t.Node): SemanticPrototypeOwner | undefined => {
const value = unwrapJavaScriptExpression(node).node;
return (t.isMemberExpression(value) ||
t.isOptionalMemberExpression(value)) &&
@@ -1451,6 +1535,7 @@ function* indexPrototypeUncertaintySteps(
]);
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
enter: (node, parent) => {
effectSite = node;
const write = semanticWriteSiteTarget(node);
if (write !== null) {
for (const target of semanticWriteTargets(write)) {
@@ -1459,9 +1544,15 @@ function* indexPrototypeUncertaintySteps(
t.isMemberExpression(value) ||
t.isOptionalMemberExpression(value)
) {
const owner =
prototypeOwner(value.object) ?? globalOwner(value.object);
if (owner !== undefined) taint(owner);
const prototype = prototypeOwner(value.object);
const owner = prototype ?? globalOwner(value.object);
if (owner !== undefined)
taint(
owner,
prototype !== undefined ||
semanticStaticPropertyKey(value.property, value.computed) ===
"prototype",
);
} else {
const owner = globalOwner(value);
if (owner !== undefined) taint(owner);
@@ -235,7 +235,7 @@ const emitterIdentity = (
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding !== undefined) {
const read = semanticBindingAtRead(binding, node, state.parentsByNode);
const read = semanticBindingAtRead(binding, node, state);
const origins = [
...read.binding.initializers,
...read.binding.referenceInitializers,
@@ -383,7 +383,7 @@ const collectTimer = (input: TimerCollectionInput): void => {
const snapshot = semanticBindingAtRead(
handle,
node.arguments[0],
state.parentsByNode,
state,
);
// A const handle has the same value inside a later closure even
// when the source positions belong to different function bodies.
@@ -4,6 +4,7 @@ import { expect, it, vi } from "vitest";
import {
analyzeJavaScriptSemantics,
indexJavaScriptBindingsSteps,
indexJavaScriptBindingsWithReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import {
resolveLocalCallables,
@@ -186,6 +187,73 @@ it("bounds shared partial callback alias expansion by AST size", () => {
expect(large.referenceReads).toBeLessThan(3 * small.referenceReads);
});
it("reuses the immutable enclosing-function position across distinct captured reads", () => {
const { state } = indexed(
`const make=()=>1; ${"0;".repeat(64)} function use(){${"make();".repeat(64)}} use();`,
);
const binding = [...state.bindingsById.values()].find(
(b) => b.name === "make",
);
const fn = state.program.body.find(
(n) => t.isFunctionDeclaration(n) && n.id?.name === "use",
);
if (binding === undefined || fn === undefined)
throw Error("Missing fixture binding");
const lookup = vi.spyOn(state.program.body, "indexOf");
let reads = 0;
try {
traverseJavaScriptAst(fn, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "make" })
) {
reads++;
expect(
semanticBindingAtRead(binding, node.callee, state)
.hasDefiniteInitializer,
).toBe(true);
}
},
});
expect(reads).toBe(64);
expect(
lookup.mock.calls.filter(([node]) => node === fn).length,
).toBeLessThanOrEqual(1);
} finally {
lookup.mockRestore();
}
});
it.each([true, false])(
"orders a module direct-eval activation with early=%s",
(early) => {
const { state } = indexed(
`export {}; ${early ? "eval('open()');" : ""} const make = () => 1; function open() { return make(); } ${early ? "" : "eval('open()');"} open();`,
);
const binding = [...state.bindingsById.values()].find(
(binding) => binding.name === "make",
);
if (binding === undefined) throw new Error("Missing make binding");
let reads = 0;
traverseJavaScriptAst(state.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "make" })
) {
reads++;
expect(
semanticBindingAtRead(binding, node.callee, state)
.hasDefiniteInitializer,
).toBe(!early);
}
},
});
expect(reads).toBe(1);
},
);
it.each([
["if(flag){cb=fallback;cb();}", true],
["if(flag)cb=fallback;cb();", false],
@@ -207,7 +275,7 @@ it.each([
) {
reads++;
expect(
semanticBindingAtRead(binding, node.callee, state.parentsByNode)
semanticBindingAtRead(binding, node.callee, state)
.hasDefiniteInitializer,
).toBe(expected);
}
@@ -412,7 +480,7 @@ it.each(["||=", "??="])(
) {
reads++;
expect(
semanticBindingAtRead(binding, node.callee, state.parentsByNode)
semanticBindingAtRead(binding, node.callee, state)
.hasDefiniteInitializer,
).toBe(true);
}
@@ -540,3 +608,87 @@ it("resolves calls in computed method keys before subsequent pattern writes", ()
calleeCallableIds: [original?.callableId],
});
});
it.each([64, 128])(
"indexes receiver effects once for %s reads before distinct writes",
(count) => {
const source = `const object = {}; ${"object.x;".repeat(count)} ${Array.from({ length: count }, (_, i) => `object.p${i}=1;`).join("")} const make=()=>1; function use(){make();} use();`;
const file = parseJavaScriptSource(source, "fixture.js");
if (file === null) throw Error("Invalid fixture");
const steps = indexJavaScriptBindingsWithReceiversSteps(file.program);
let step = steps.next();
while (!step.done) step = steps.next();
const index = step.value;
for (const _ of index.indexMemberMutationFactsSteps()) {
void _;
}
const state = index.bindings;
const object = [...state.bindingsById.values()].find(
(b) => b.name === "object",
);
const make = [...state.bindingsById.values()].find(
(b) => b.name === "make",
);
if (object === undefined || make === undefined)
throw Error("Missing fixture bindings");
let visits = 0;
Object.defineProperty(object, "memberWrites", {
value: new Proxy(object.memberWrites, {
get(target, key, receiver) {
if (typeof key === "string" && /^\d+$/.test(key)) visits++;
return Reflect.get(target, key, receiver);
},
}),
});
let checked = 0;
traverseJavaScriptAst(file.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "make" })
) {
expect(
semanticBindingAtRead(make, node.callee, state)
.hasDefiniteInitializer,
).toBe(true);
checked++;
}
},
});
expect(checked).toBe(1);
expect(object.memberWrites.length).toBe(count);
expect(visits).toBeLessThanOrEqual(count * 2);
},
);
it("indexes one statement body for distinct captured functions", () => {
const { state } = indexed(
`const make=()=>1; ${Array.from({ length: 128 }, (_, i) => `function use${i}(){make();} use${i}();`).join(" ")}`,
);
const binding = [...state.bindingsById.values()].find(
(b) => b.name === "make",
);
if (binding === undefined) throw Error("Missing fixture binding");
const lookup = vi.spyOn(state.program.body, "indexOf");
let reads = 0;
try {
traverseJavaScriptAst(state.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "make" })
) {
reads++;
expect(
semanticBindingAtRead(binding, node.callee, state)
.hasDefiniteInitializer,
).toBe(true);
}
},
});
expect(reads).toBe(128);
expect(lookup.mock.calls.length).toBeLessThanOrEqual(1);
} finally {
lookup.mockRestore();
}
});
@@ -104,11 +104,7 @@ const resolveIdentifier = (
complete: false,
reason: `Unresolved call target ${node.name}.`,
};
const read = semanticBindingAtRead(
binding,
node,
context.state.parentsByNode,
);
const read = semanticBindingAtRead(binding, node, context.state);
// A full read re-enters the same candidate set wherever the chain returns
// to this binding. Close that cycle at the binding; position-narrowed reads
// keep their per-initializer cycle handling.
@@ -1,6 +1,6 @@
import * as t from "@babel/types";
import { semanticComputedMethodKey } from "./javascriptSemanticEvaluationOrder.js";
import { semanticMethodDefinitionExpression } from "./javascriptSemanticEvaluationOrder.js";
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
/**
@@ -49,7 +49,7 @@ export const createSemanticCallableStack = <Callable>(
enter: (node, parent) => {
if (
parent !== null &&
semanticComputedMethodKey(parent, node) &&
semanticMethodDefinitionExpression(parent, node) &&
frames.at(-1)?.callableId === semanticCallableIdForNode(parent)
) {
const method = pop();
@@ -340,11 +340,7 @@ const resolveChildCandidates = (
// An alias captures its source at its initializer read, not at the later
// interaction. The shared selector retains all sources across uncertain
// control flow, loops and writes in other activations.
const snapshot = semanticBindingAtRead(
binding,
current,
state.parentsByNode,
);
const snapshot = semanticBindingAtRead(binding, current, state);
const selected = snapshot.binding;
const initializers = [
...selected.initializers,
@@ -0,0 +1,57 @@
import {
semanticArrayIndex,
type JavaScriptSemanticPropertyPath as PropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
interface CopiedReferenceSource {
readonly projection: PropertyPath;
readonly copyKind?: "object-rest" | "array-rest";
readonly copyProjectionOffset?: number;
readonly copyExcludedKeys?: readonly string[];
readonly copyStartIndex?: number;
}
/** Map copied child selections once; the fresh copy itself keeps its own identity. */
export const semanticCopiedReferencePath = (
initializer: CopiedReferenceSource,
path: PropertyPath,
effect: "write" | "escape" | "identity",
): PropertyPath | null => {
const offset = initializer.copyProjectionOffset;
if (offset === undefined) return [...initializer.projection, ...path];
const copiedPath = [...initializer.projection.slice(offset + 1), ...path];
if (effect === "identity" && copiedPath.length === 0) return null;
if (effect === "write" && copiedPath.length < 2) return null;
let [key, ...remaining] = copiedPath;
if (initializer.copyKind === "array-rest") {
const startIndex = initializer.copyStartIndex ?? 0;
if (key === undefined || key === null)
key = { excludedKeys: [], startIndex };
else if (typeof key === "object")
key = {
excludedKeys: key.excludedKeys.flatMap((name) => {
const index = semanticArrayIndex(name);
return index === null ? [] : [String(index + startIndex)];
}),
startIndex: (key.startIndex ?? 0) + startIndex,
};
else {
const index = semanticArrayIndex(key);
if (index === null) return null;
key = index + startIndex;
}
} else {
const excludedKeys = initializer.copyExcludedKeys ?? [];
if (key === undefined || key === null) key = { excludedKeys };
else if (typeof key === "object")
key = {
...key,
excludedKeys: [...new Set([...key.excludedKeys, ...excludedKeys])].sort(
compareUnicodeCodePoints,
),
};
else if (excludedKeys.includes(String(key))) return null;
}
return [...initializer.projection.slice(0, offset), key, ...remaining];
};
@@ -278,11 +278,7 @@ const soleWriteReachesRead = (
request: t.Node | undefined,
): boolean => {
if (binding === undefined || request === undefined) return false;
const snapshot = semanticBindingAtRead(
binding,
read,
context.state.parentsByNode,
);
const snapshot = semanticBindingAtRead(binding, read, context.state);
const initializers = snapshot.binding.initializers.filter(
({ node }) => semanticEffectTiming(read, node, context.state) !== "after",
);
@@ -1,6 +1,7 @@
import * as t from "@babel/types";
import {
javaScriptDefinitionExpression,
readExactJavaScriptLiteral,
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
@@ -15,14 +16,15 @@ export type JavaScriptSemanticIterationCardinality =
| "nonempty"
| "unknown";
/** Whether a method's computed key executes in the enclosing environment. */
export const semanticComputedMethodKey = (
/** Whether a method child executes in the enclosing environment. */
export const semanticMethodDefinitionExpression = (
parent: t.Node | undefined,
child: t.Node,
): boolean =>
(t.isObjectMethod(parent) || t.isClassMethod(parent)) &&
parent.computed &&
parent.key === child;
(t.isObjectMethod(parent) ||
t.isClassMethod(parent) ||
t.isClassPrivateMethod(parent)) &&
javaScriptDefinitionExpression(parent, child);
/** Exact source-only cardinality for literal loop heads. */
export const semanticIterationCardinality = (
@@ -96,7 +96,7 @@ export const createSemanticExpressionShapeResolver = (
const initializer = semanticStableInitializer(binding, state);
if (initializer === undefined || initializer.projection.length > 0)
return "unknown";
const read = semanticBindingAtRead(binding, node, state.parentsByNode);
const read = semanticBindingAtRead(binding, node, state);
if (read.status !== "ordered" || !read.hasDefiniteInitializer)
return "unknown";
node = initializer.node;
@@ -208,7 +208,7 @@ export class GetterResultWalker {
const { binding: origins } = semanticBindingAtRead(
binding,
expression,
state.parentsByNode,
state,
);
const aliases: AliasVisit = {
identity,
@@ -11,7 +11,10 @@ import {
type JavaScriptSemanticPropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { semanticCopiedReferencePath } from "./javascriptSemanticReferenceSources.js";
import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js";
import { semanticMethodDefinitionExpression } from "./javascriptSemanticEvaluationOrder.js";
import { childNodes } from "./javascriptSemanticTraversal.js";
import type {
IterationSourceValue,
SemanticIterationSource,
@@ -274,6 +277,50 @@ const emptySourceFacts: SemanticSourceFacts = {
iterations: [],
propertyReads: [],
};
// Immutable AST subtree summaries are reused by enclosing and nested catches.
// Property reads can invoke getters; tags and coercions can execute user code.
const guardedExecutions = new WeakMap<t.Node, boolean>();
const guardedBodyMayCall = (body: t.BlockStatement): boolean => {
const pending: { node: t.Node; parent: t.Node | null; finish: boolean }[] = [
{ node: body, parent: null, finish: false },
];
while (pending.length > 0) {
const frame = pending.pop();
if (frame === undefined || guardedExecutions.has(frame.node)) continue;
const { node, parent } = frame;
const children = [...childNodes(node)].filter(
(child) =>
!t.isFunction(node) || semanticMethodDefinitionExpression(node, child),
);
if (!frame.finish) {
pending.push({ node, parent, finish: true });
for (const child of children)
pending.push({ node: child, parent: node, finish: false });
} else {
const direct =
t.isCallExpression(node) ||
t.isOptionalCallExpression(node) ||
t.isNewExpression(node) ||
t.isTaggedTemplateExpression(node) ||
t.isMemberExpression(node) ||
t.isOptionalMemberExpression(node) ||
t.isBinaryExpression(node) ||
t.isUnaryExpression(node) ||
t.isUpdateExpression(node) ||
t.isSpreadElement(node) ||
t.isForOfStatement(node) ||
t.isAwaitExpression(node) ||
((t.isVariableDeclarator(node) || t.isAssignmentExpression(node)) &&
semanticSourceFacts(node, parent).propertyReads.length > 0);
guardedExecutions.set(
node,
direct ||
children.some((child) => guardedExecutions.get(child) === true),
);
}
}
return guardedExecutions.get(body) === true;
};
const sourceFacts = new WeakMap<
t.Node,
{ readonly parent: t.Node | null; readonly facts: SemanticSourceFacts }
@@ -291,6 +338,7 @@ const semanticSourceFacts = (
t.isForOfStatement(node) ||
t.isSpreadElement(node) ||
t.isYieldExpression(node) ||
t.isCatchClause(node) ||
t.isFunction(node)
)
)
@@ -330,9 +378,19 @@ const semanticSourceFacts = (
node.argument != null
)
add(sources, { node: node.argument, projection: [] }, node, {});
else if (
t.isCatchClause(node) &&
node.param != null &&
t.isTryStatement(parent) &&
guardedBodyMayCall(parent.block)
)
visit(context, node.param, { node: node.param, projection: [null] });
else if (t.isFunction(node))
for (const parameter of runtimeFunctionParameters(node))
visit(context, parameter, undefined);
// A caller-supplied value is unknown, not an absent property-read source.
// Retain the owning pattern as an unresolved source, including alongside
// default-value projections, instead of dropping its property lookups.
visit(context, parameter, { node: parameter, projection: [] });
const facts = { iterations: sources, propertyReads };
sourceFacts.set(node, { parent, facts });
return facts;
@@ -344,8 +402,28 @@ export const semanticIterationSources = (
): readonly SemanticIterationSource[] =>
semanticSourceFacts(node, parent).iterations;
/** Pattern reads from an explicitly retained source, including thrown values. */
export const semanticPatternPropertyReadSources = (
pattern: t.Node,
source: IterationSourceValue,
mutation: t.Node,
): readonly SemanticPropertyReadSource[] => {
const propertyReads: SemanticPropertyReadSource[] = [];
visit({ sources: [], propertyReads, mutation }, pattern, source);
return propertyReads;
};
export const semanticPropertyReadSources = (
node: t.Node,
parent: t.Node | null,
): readonly SemanticPropertyReadSource[] =>
semanticSourceFacts(node, parent).propertyReads;
supplemental?: {
readonly implicitPropertyReads: WeakMap<
t.Node,
readonly SemanticPropertyReadSource[]
>;
},
): readonly SemanticPropertyReadSource[] => {
const ordinary = semanticSourceFacts(node, parent).propertyReads;
const implicit = supplemental?.implicitPropertyReads.get(node);
return implicit === undefined ? ordinary : [...ordinary, ...implicit];
};
@@ -54,8 +54,7 @@ export const isIntrinsicMapLookup = (
if (
initializer === undefined ||
initializer.projection.length !== 0 ||
!semanticBindingAtRead(binding, receiver, bindings.parentsByNode)
.hasDefiniteInitializer
!semanticBindingAtRead(binding, receiver, bindings).hasDefiniteInitializer
)
return false;
receiver = unwrapJavaScriptExpression(initializer.node).node;
@@ -69,11 +69,11 @@ import {
import { semanticCoercionOperands } from "./javascriptSemanticLanguageInvocations.js";
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
import {
semanticCopiedReferencePath,
semanticKnownArrayIteration,
semanticReferenceSourcesInactive,
semanticSelectedReferenceSlot,
} from "./javascriptSemanticReferenceSources.js";
import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js";
import {
semanticArrayIndex,
joinSemanticPropertyPaths,
@@ -162,11 +162,7 @@ const localAccessorMethods = (
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined) continue;
const selected = semanticBindingAtRead(
binding,
node,
state.parentsByNode,
).binding;
const selected = semanticBindingAtRead(binding, node, state).binding;
for (const source of [
...selected.initializers,
...selected.referenceInitializers,
@@ -1652,7 +1648,7 @@ export function* collectSemanticMemberMutationsSteps(
else if (node.init != null) markEscaped(node.init, exit);
}
}
for (const source of semanticPropertyReadSources(node, parent)) {
for (const source of semanticPropertyReadSources(node, parent, state)) {
if (semanticReferenceSourcesInactive(source, state)) continue;
for (const effect of getterEffects(
source.node,
@@ -1,20 +1,35 @@
import * as t from "@babel/types";
import {
isErasedJavaScriptValueChild,
traverseJavaScriptAst,
} from "./javascriptSemanticTraversal.js";
import { isTypeOnlyExportDeclaration } from "./javascriptModuleDeclaration.js";
import {
javaScriptRuntimeContext,
javaScriptThisOwner,
} from "./javascriptSourceParser.js";
import { semanticPropertyReadSources } from "./javascriptSemanticIterationSources.js";
import {
delayedInstanceFieldValue,
semanticComputedMethodKey,
semanticMethodDefinitionExpression,
semanticBranchMayBeSkipped,
semanticIterationCardinality,
} from "./javascriptSemanticEvaluationOrder.js";
import {
staticObjectPropertyReader,
staticObjectOwnMethodReader,
semanticStaticPropertyKey,
semanticWriteSiteReadsFirst,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
semanticScopeId,
semanticPrototypeOwners,
type SemanticPrototypeOwner,
type JavaScriptSemanticBindingState,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticEffectTiming,
@@ -55,6 +70,22 @@ interface InitializerPositionCache {
readonly mutation: WeakMap<t.Node, StatementPosition | null>;
readonly reference: WeakMap<t.Node, StatementPosition | null>;
readonly read: WeakMap<t.Node, BindingReadPosition>;
readonly enclosingFunction: WeakMap<t.Node, t.Function | null>;
readonly functionPosition: WeakMap<t.Function, StatementPosition | null>;
readonly statementIndices: WeakMap<
readonly t.Statement[],
ReadonlyMap<t.Statement, number>
>;
readonly moduleFunctionReads: WeakMap<
readonly t.Statement[],
{
readonly reads: WeakMap<t.FunctionDeclaration, number | null>;
readonly memberEffectsComplete: boolean;
readonly opaquePositions: WeakMap<t.Node, number | null>;
opaqueSitesIndexed: number;
firstOpaqueRead: number | null;
}
>;
readonly evaluationChildren: WeakMap<
t.Node,
ReadonlyMap<t.Node, number> | null
@@ -80,6 +111,10 @@ const initializerPositionCache = (
mutation: new WeakMap(),
reference: new WeakMap(),
read: new WeakMap(),
enclosingFunction: new WeakMap(),
functionPosition: new WeakMap(),
statementIndices: new WeakMap(),
moduleFunctionReads: new WeakMap(),
evaluationChildren: new WeakMap(),
expressionAncestry: undefined,
};
@@ -101,7 +136,13 @@ const statementPosition = (
: statement;
const parent = parents.get(enclosing);
if (!t.isProgram(parent) && !t.isBlockStatement(parent)) return null;
const index = parent.body.indexOf(enclosing);
const cache = initializerPositionCache(parents).statementIndices;
let positions = cache.get(parent.body);
if (positions === undefined) {
positions = new Map(parent.body.map((entry, index) => [entry, index]));
cache.set(parent.body, positions);
}
const index = positions.get(enclosing) ?? -1;
return index < 0
? null
: {
@@ -135,7 +176,8 @@ const computeTopLevelStatementPosition = (
while (node !== undefined) {
if (
(t.isFunction(node) &&
(child === undefined || !semanticComputedMethodKey(node, child))) ||
(child === undefined ||
!semanticMethodDefinitionExpression(node, child))) ||
t.isLoop(node)
)
return null;
@@ -731,6 +773,38 @@ export const semanticMutationInitializers = (
};
};
/** Function ownership is independent of uncertain ordering within a loop. */
const bindingEnclosingFunction = (
read: t.Node,
parents: WeakMap<t.Node, t.Node>,
): t.Function | undefined => {
const cache = initializerPositionCache(parents).enclosingFunction;
const path: t.Node[] = [];
let current = read;
let result: t.Function | null = null;
while (true) {
const cached = cache.get(current);
if (cached !== undefined) {
result = cached;
break;
}
path.push(current);
const parent = parents.get(current);
if (parent === undefined || delayedInstanceFieldValue(parent, current))
break;
if (
t.isFunction(parent) &&
!semanticMethodDefinitionExpression(parent, current)
) {
if (current === parent.body) result = parent;
break;
}
current = parent;
}
for (const node of path) cache.set(node, result);
return result ?? undefined;
};
/** Reuse immutable ancestry facts across reads nested in the same expression. */
const bindingReadPosition = (
read: t.Node,
@@ -754,7 +828,8 @@ const bindingReadPosition = (
}
if (
parent === undefined ||
(t.isFunction(parent) && !semanticComputedMethodKey(parent, current)) ||
(t.isFunction(parent) &&
!semanticMethodDefinitionExpression(parent, current)) ||
delayedInstanceFieldValue(parent, current)
) {
result = { position: null, repeatingLoop: undefined };
@@ -820,11 +895,608 @@ type SemanticBindingRead = {
readonly binding: JavaScriptSemanticBindingState;
};
/** A hoisted function needs a module-level use after initialization, not a later declaration. */
const moduleFunctionFirstRead = (
declaration: t.FunctionDeclaration,
body: readonly t.Statement[],
parents: WeakMap<t.Node, t.Node>,
state?: JavaScriptSemanticAnalysisState,
): number | null => {
const cache = initializerPositionCache(parents).moduleFunctionReads;
let retained = cache.get(body);
const memberEffectsComplete = state?.memberMutationFactsComplete === true;
if (retained?.memberEffectsComplete !== memberEffectsComplete)
retained = undefined;
if (retained === undefined) {
const reads = new WeakMap<t.FunctionDeclaration, number | null>();
retained = {
reads,
memberEffectsComplete,
opaquePositions: new WeakMap(),
opaqueSitesIndexed: 0,
firstOpaqueRead: Infinity,
};
cache.set(body, retained);
const functions = new Map<string, t.FunctionDeclaration | null>();
for (const statement of body) {
const exported =
t.isExportNamedDeclaration(statement) ||
t.isExportDefaultDeclaration(statement);
if (
t.isExportNamedDeclaration(statement) &&
isTypeOnlyExportDeclaration(statement)
)
continue;
const node = exported ? statement.declaration : statement;
if (!t.isFunctionDeclaration(node)) continue;
reads.set(node, exported ? null : Infinity);
if (node.id == null) continue;
const name = node.id.name;
if (functions.has(name)) {
const previous = functions.get(name);
if (previous != null) reads.set(previous, null);
reads.set(node, null);
functions.set(name, null);
} else functions.set(name, node);
}
const indexedReads = reads;
const firstStatement = body[0];
const program =
firstStatement === undefined ? undefined : parents.get(firstStatement);
const classicScript =
t.isProgram(program) && javaScriptRuntimeContext(program) === "unknown";
const globalReceivers = new WeakMap<t.Node, boolean | null>();
const propertyReaders = new WeakMap<
t.Node,
ReturnType<typeof staticObjectPropertyReader>
>();
const ownMethodReaders = new WeakMap<t.Node, (name: string) => boolean>();
const ownMethodAtSource = (node: t.Node, name: string): boolean => {
let read = ownMethodReaders.get(node);
if (read === undefined) {
read = staticObjectOwnMethodReader(node);
ownMethodReaders.set(node, read);
}
return read(name);
};
const receiverPrototypeOwners = new WeakMap<
t.Node,
ReadonlySet<SemanticPrototypeOwner>
>();
const bindingReceivers = new WeakMap<
JavaScriptSemanticBindingState,
t.Node
>();
const globalReceiver = (input: t.Node): boolean | null => {
const pending: { node: t.Node; finish: boolean }[] = [
{ node: input, finish: false },
];
const visiting = new Set<t.Node>();
const origins = new WeakMap<
t.Node,
{
dependencies: t.Node[];
unknown: boolean;
global: boolean;
owners: Set<SemanticPrototypeOwner>;
}
>();
while (pending.length > 0) {
const frame = pending.pop();
if (frame === undefined) continue;
const current = frame.node;
if (frame.finish) {
const fact = origins.get(current);
if (fact === undefined) continue;
let global = fact.global;
let unknown = fact.unknown;
for (const dependency of fact.dependencies) {
for (const owner of receiverPrototypeOwners.get(dependency) ?? [])
fact.owners.add(owner);
const answer = globalReceivers.get(dependency);
if (answer === true) global = true;
if (answer === null || answer === undefined) unknown = true;
}
receiverPrototypeOwners.set(current, fact.owners);
globalReceivers.set(current, global ? true : unknown ? null : false);
visiting.delete(current);
continue;
}
if (globalReceivers.has(current)) continue;
if (visiting.has(current)) {
// A back edge has no finite identity proof; retain uncertainty.
globalReceivers.set(current, null);
continue;
}
visiting.add(current);
const fact = {
dependencies: [] as t.Node[],
unknown: false,
global: false,
owners: new Set<SemanticPrototypeOwner>(),
};
const node = unwrapJavaScriptExpression(current).node;
if (t.isArrayExpression(node)) fact.owners.add("Array");
else if (t.isFunction(node)) fact.owners.add("Function");
else if (t.isObjectExpression(node)) fact.owners.add("Object");
else if (t.isStringLiteral(node) || t.isTemplateLiteral(node))
fact.owners.add("String");
else if (t.isNumericLiteral(node)) fact.owners.add("Number");
else if (t.isBooleanLiteral(node)) fact.owners.add("Boolean");
else if (t.isRegExpLiteral(node)) fact.owners.add("RegExp");
else if (t.isBigIntLiteral(node)) fact.owners.add("BigInt");
if (node !== current) fact.dependencies.push(node);
else if (t.isThisExpression(node)) {
const owner = t.isProgram(program)
? javaScriptThisOwner(node, program, parents)
: "caller";
fact.global = owner === "global";
fact.unknown = owner === "caller";
} else if (t.isIdentifier(node)) {
const binding =
state === undefined
? undefined
: resolveSemanticBindingState(state, node, node.name);
if (binding === undefined) {
if (["globalThis", "window", "self"].includes(node.name))
fact.global = true;
else fact.unknown = true;
} else {
const representative = bindingReceivers.get(binding);
if (representative !== undefined && representative !== current) {
fact.dependencies.push(representative);
} else {
bindingReceivers.set(binding, current);
if (
binding.initializers.length === 0 ||
binding.referenceInitializers.length > 0
)
fact.unknown = true;
for (const initializer of binding.initializers) {
if (initializer.projection.length > 0) fact.unknown = true;
else fact.dependencies.push(initializer.node);
}
}
}
} else if (t.isObjectExpression(node)) {
fact.unknown = node.properties.some(
(property) =>
t.isSpreadElement(property) ||
(t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) ===
"__proto__"),
);
} else if (
!t.isArrayExpression(node) &&
!t.isLiteral(node) &&
!t.isFunction(node)
)
fact.unknown = true;
origins.set(current, fact);
pending.push({ node: current, finish: true });
for (const dependency of fact.dependencies)
pending.push({ node: dependency, finish: false });
}
return globalReceivers.get(input) ?? null;
};
interface ReceiverBound {
index: number;
node?: t.Node;
ambiguous: boolean;
}
const receiverBounds = new WeakMap<
JavaScriptSemanticBindingState,
ReceiverBound | null
>();
const receiverBound = (
root: JavaScriptSemanticBindingState,
): ReceiverBound | null => {
const pending = [{ binding: root, finish: false }];
const activeBindings = new Set<JavaScriptSemanticBindingState>();
const dependencies = new WeakMap<
JavaScriptSemanticBindingState,
JavaScriptSemanticBindingState[]
>();
const merge = (
left: ReceiverBound,
right: ReceiverBound,
): ReceiverBound => {
if (left.index < right.index) return left;
if (right.index < left.index) return right;
return {
...left,
ambiguous:
left.ambiguous || right.ambiguous || left.node !== right.node,
};
};
while (pending.length > 0) {
const frame = pending.pop()!;
const binding = frame.binding;
if (frame.finish) {
let bound: ReceiverBound | null = {
index: Infinity,
ambiguous: false,
};
for (const effects of [binding.memberWrites, binding.escapedPaths]) {
for (const effect of effects) {
const position =
effect.node === undefined
? null
: directMutationPosition(effect.node, parents);
if (
effect.node === undefined ||
position === null ||
position.body !== body
) {
bound = null;
break;
}
bound = merge(bound, {
index: position.index,
node: effect.node,
ambiguous: false,
});
}
if (bound === null) break;
}
for (const dependency of dependencies.get(binding) ?? []) {
const inherited = receiverBounds.get(dependency);
if (
bound === null ||
inherited === null ||
inherited === undefined
) {
bound = null;
break;
}
bound = merge(bound, inherited);
}
receiverBounds.set(binding, bound);
activeBindings.delete(binding);
continue;
}
if (receiverBounds.has(binding)) continue;
if (activeBindings.has(binding)) {
receiverBounds.set(binding, null);
continue;
}
activeBindings.add(binding);
const selected: JavaScriptSemanticBindingState[] = [];
for (const initializer of binding.initializers) {
const node = unwrapJavaScriptExpression(initializer.node).node;
if (!t.isIdentifier(node) || state === undefined) continue;
const dependency = resolveSemanticBindingState(
state,
node,
node.name,
);
if (dependency !== undefined) selected.push(dependency);
}
dependencies.set(binding, selected);
pending.push({ binding, finish: true });
for (const dependency of selected)
pending.push({ binding: dependency, finish: false });
}
return receiverBounds.get(root) ?? null;
};
// A single owner-specific bound serves every literal read in this body.
const prototypeBounds = new Map<
SemanticPrototypeOwner,
ReceiverBound | null
>();
for (const owner of semanticPrototypeOwners) {
let bound: ReceiverBound | null = { index: Infinity, ambiguous: false };
for (const effect of state?.prototypeMutationSites ?? []) {
if (effect.owner !== owner && effect.owner !== "Object") continue;
const position = directMutationPosition(effect.node, parents);
if (position === null || position.body !== body) {
bound = null;
break;
}
if (position.index < bound.index)
bound = {
index: position.index,
node: effect.node,
ambiguous: false,
};
else if (position.index === bound.index && effect.node !== bound.node)
bound = { ...bound, ambiguous: true };
}
prototypeBounds.set(owner, bound);
}
const receiverAtRead = (input: t.Node, read: t.Node): boolean | null => {
// Allocation provenance follows the same initializer DAG as receiver identity.
const identity = globalReceiver(input);
for (const owner of receiverPrototypeOwners.get(input) ?? []) {
const bound = prototypeBounds.get(owner);
if (bound === null) return null;
if (bound !== undefined && bound.index !== Infinity) {
const position = bindingReadPosition(read, parents);
if (
position.position === null ||
position.repeatingLoop ||
position.position.body !== body
)
return null;
if (position.position.index > bound.index) return null;
if (
position.position.index === bound.index &&
(bound.ambiguous ||
bound.node === undefined ||
state === undefined ||
semanticEffectTiming(read, bound.node, state) !== "after")
)
return null;
}
}
if (identity !== false || state === undefined || !memberEffectsComplete)
return identity;
const root = unwrapJavaScriptExpression(input).node;
if (!t.isIdentifier(root)) return identity;
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) return null;
const bound = receiverBound(binding);
if (bound === null) return null;
if (bound.index === Infinity) return identity;
const position = bindingReadPosition(read, parents);
if (
position.position === null ||
position.repeatingLoop ||
position.position.body !== body
)
return null;
if (position.position.index < bound.index) return identity;
if (position.position.index > bound.index || bound.ambiguous) return null;
return bound.node !== undefined &&
(semanticReadPrecedesEffectWithinActivation(
read,
bound.node,
parents,
) ||
semanticEffectTiming(read, bound.node, state) === "after")
? identity
: null;
};
const active = new Set<t.Function>();
const retainNamedRead = (
name: string,
parent: t.Node | null,
index: number,
): void => {
const fn = functions.get(name);
if (fn == null || active.has(fn)) return;
const current = indexedReads.get(fn);
if (current === null || current === undefined) return;
// Indirect activations and live exports have no proven module timing.
indexedReads.set(
fn,
active.size > 0 || t.isExportSpecifier(parent)
? null
: Math.min(current, index),
);
};
const opaquePositions = retained.opaquePositions;
const definitionKeys = new WeakMap<t.Node, t.Function>();
let unknownPropertyRead: number | null = Infinity;
for (let index = 0; index < body.length; index++) {
const statement = body[index];
if (statement === undefined) continue;
traverseJavaScriptAst(statement, {
skip: (node) =>
state?.erasedSyntaxNodes.has(node) === true ||
isErasedJavaScriptValueChild(node, parents.get(node) ?? null),
enter(node, parent) {
if (
semanticMethodDefinitionExpression(parent ?? undefined, node) &&
parent !== null &&
t.isFunction(parent)
) {
active.delete(parent);
definitionKeys.set(node, parent);
}
if (t.isFunction(node)) active.add(node);
// Direct eval sees lexical module/CommonJS bindings too. Its
// scope-owned sites are complete before ambient indexing starts.
if (
t.isCallExpression(node) &&
state?.opaqueEffects.sites.has(node)
) {
if (unknownPropertyRead !== null)
unknownPropertyRead =
active.size > 0 ? null : Math.min(unknownPropertyRead, index);
}
if (
classicScript &&
(t.isCallExpression(node) ||
t.isOptionalCallExpression(node) ||
t.isNewExpression(node) ||
t.isTaggedTemplateExpression(node))
) {
opaquePositions.set(node, active.size > 0 ? null : index);
}
if (classicScript) {
for (const source of semanticPropertyReadSources(
node,
parent,
state,
)) {
const projected =
source.iterationSources !== undefined ||
source.projection.length > 1;
if (!projected && receiverAtRead(source.node, node) === false)
continue;
// Select the receiver through the shared property-reference owner.
// Unknown/iterated sources retain possible activation.
if (projected && source.iterationSources === undefined) {
let receiver: t.Node | undefined = source.node;
for (const key of source.projection.slice(0, -1)) {
if (typeof key !== "string" && typeof key !== "number") {
receiver = undefined;
break;
}
if (receiver === undefined) break;
let read = propertyReaders.get(receiver);
if (read === undefined) {
read = staticObjectPropertyReader(receiver);
propertyReaders.set(receiver, read);
}
const selected = read(String(key));
receiver =
selected.status === "explicit" ? selected.value : undefined;
if (receiver === undefined) break;
}
if (
receiver !== undefined &&
receiverAtRead(receiver, node) === false
)
continue;
}
const key = source.projection[0];
if (!projected && typeof key === "string")
retainNamedRead(key, parent, index);
else if (unknownPropertyRead !== null)
unknownPropertyRead =
active.size > 0 ? null : Math.min(unknownPropertyRead, index);
}
}
let name: string | null = null;
if (
t.isIdentifier(node) &&
parent !== null &&
(t.isReferenced(node, parent) ||
(t.isAssignmentExpression(parent) &&
parent.left === node &&
semanticWriteSiteReadsFirst(parent)))
) {
const candidate = functions.get(node.name);
const binding =
state === undefined
? undefined
: resolveSemanticBindingState(state, node, node.name);
if (
candidate != null &&
state !== undefined &&
semanticResolutionBlocked(state, node, node.name)
) {
const current = indexedReads.get(candidate);
if (current !== null && current !== undefined)
indexedReads.set(
candidate,
active.size > 0 ? null : Math.min(current, index),
);
return;
}
if (
state === undefined ||
(candidate != null &&
binding?.initializers.some(
({ node: value }) => value === candidate,
))
)
name = node.name;
} else if (
classicScript &&
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
) {
const receiver = unwrapJavaScriptExpression(node.object).node;
const selected = semanticStaticPropertyKey(
node.property,
node.computed,
);
const ownMethod =
selected !== null &&
functions.has(selected) &&
(ownMethodAtSource(receiver, selected) ||
(state !== undefined &&
t.isIdentifier(receiver) &&
(() => {
const binding = resolveSemanticBindingState(
state,
receiver,
receiver.name,
);
if (
binding === undefined ||
receiverBound(binding)?.index !== Infinity
)
return false;
return (
binding.initializers.length === 1 &&
binding.initializers[0]!.projection.length === 0 &&
ownMethodAtSource(
unwrapJavaScriptExpression(
binding.initializers[0]!.node,
).node,
selected,
)
);
})()));
// Classic scripts expose hoisted functions through the global object.
// Property names are not referenced identifiers; keep their read timing too.
if (!ownMethod && receiverAtRead(receiver, node) !== false) {
name = selected;
if (name === null) {
if (unknownPropertyRead !== null)
unknownPropertyRead =
active.size > 0
? null
: Math.min(unknownPropertyRead, index);
return;
}
}
}
if (name !== null) retainNamedRead(name, parent, index);
},
exit(node) {
if (t.isFunction(node)) active.delete(node);
const method = definitionKeys.get(node);
if (method !== undefined) active.add(method);
},
});
}
// Unknown global keys share one timing fact across the module functions.
for (const fn of functions.values()) {
if (fn == null) continue;
const current = indexedReads.get(fn);
if (current !== null && current !== undefined)
indexedReads.set(
fn,
unknownPropertyRead === null
? null
: Math.min(current, unknownPropertyRead),
);
}
}
// Callee resolution can query timing during the ambient walk. Source reads
// stay fixed; consume each newly observed opaque call once without rescanning.
const sites = state?.opaqueEffects.globalSites ?? [];
while (retained.opaqueSitesIndexed < sites.length) {
const site = sites[retained.opaqueSitesIndexed++];
const position =
site === undefined ? undefined : retained.opaquePositions.get(site);
if (position === undefined || retained.firstOpaqueRead === null) continue;
retained.firstOpaqueRead =
position === null ? null : Math.min(retained.firstOpaqueRead, position);
}
const read = retained.reads.get(declaration);
const first =
read === undefined || read === null || retained.firstOpaqueRead === null
? null
: Math.min(read, retained.firstOpaqueRead);
return first !== undefined && Number.isFinite(first) ? first : null;
};
export const semanticBindingAtRead = (
binding: JavaScriptSemanticBindingState,
read: t.Node,
parents: WeakMap<t.Node, t.Node>,
context: WeakMap<t.Node, t.Node> | JavaScriptSemanticAnalysisState,
): SemanticBindingRead => {
const state = "parentsByNode" in context ? context : undefined;
const parents =
state === undefined
? (context as WeakMap<t.Node, t.Node>)
: state.parentsByNode;
if (
binding.initializers.length === 0 &&
binding.referenceInitializers.length === 0
@@ -919,11 +1591,66 @@ export const semanticBindingAtRead = (
binding,
hasDefiniteInitializer,
};
const moduleConstBinding =
binding.scopeId.startsWith("scope:program:") &&
binding.declarationKinds.size === 1 &&
binding.declarationKinds.has("const") &&
!binding.mutable;
const enclosingFunction = moduleConstBinding
? bindingEnclosingFunction(read, parents)
: undefined;
let functionPosition: StatementPosition | null = null;
if (enclosingFunction !== undefined) {
const positions = initializerPositionCache(parents).functionPosition;
const cached = positions.get(enclosingFunction);
if (cached !== undefined) functionPosition = cached;
else {
const compute = (): StatementPosition | null => {
if (t.isStatement(enclosingFunction))
return statementPosition(enclosingFunction, parents);
for (
let owner = parents.get(enclosingFunction);
owner !== undefined;
owner = parents.get(owner)
) {
if (t.isFunction(owner)) return null;
if (t.isStatement(owner)) return statementPosition(owner, parents);
}
return null;
};
functionPosition = compute();
positions.set(enclosingFunction, functionPosition);
}
}
const firstFunctionRead =
moduleConstBinding &&
sourcePosition !== null &&
functionPosition !== null &&
sourcePosition.body === functionPosition.body &&
t.isFunctionDeclaration(enclosingFunction)
? moduleFunctionFirstRead(
enclosingFunction,
sourcePosition.body,
parents,
state,
)
: null;
const moduleConstInitializedBeforeActivation =
moduleConstBinding &&
sourcePosition !== null &&
functionPosition !== null &&
sourcePosition.body === functionPosition.body &&
(t.isFunctionDeclaration(enclosingFunction)
? firstFunctionRead !== null && sourcePosition.index < firstFunctionRead
: positionPrecedes(sourcePosition, functionPosition));
const uncertainOrder =
repeatingLoop ||
sourcePosition === null ||
position === null ||
sourcePosition.body !== position.body;
if (uncertainOrder && moduleConstInitializedBeforeActivation)
// Initialization coverage does not order a capture in another activation.
return { status: "unresolved", binding, hasDefiniteInitializer: true };
if (uncertainOrder)
return { status: "unresolved", binding, hasDefiniteInitializer };
const selected = selectInitializers(binding, position, read, parents);
@@ -1271,7 +1998,7 @@ export const semanticBindingHasPriorValueWrite = (
): boolean => {
if (binding.definitions.some(({ kind }) => kind === "function")) return true;
const writes = state.bindingWrites.get(binding.bindingId) ?? [];
const read = semanticBindingAtRead(binding, site, state.parentsByNode);
const read = semanticBindingAtRead(binding, site, state);
const retained =
read.status === "ordered"
? new Set([
@@ -53,44 +53,68 @@ export const createSemanticOpaqueGlobalObserver = (
) => (
node: t.Node,
site: t.Node | undefined,
) => { paths: readonly (readonly (string | null)[])[] };
) => {
paths: readonly (readonly (string | null)[])[];
callerPaths: readonly (readonly (string | null)[])[];
};
createSemanticModuleInterop: (
state: JavaScriptSemanticAnalysisState,
options?: {
intrinsicPath: (node: t.Node) => readonly string[] | undefined;
},
) => {
intrinsicPath: (node: t.Node) => readonly string[] | undefined;
intrinsicInvocation: (
node: t.CallExpression | t.OptionalCallExpression,
node:
| t.CallExpression
| t.OptionalCallExpression
| t.NewExpression
| t.TaggedTemplateExpression,
) => { readonly path: readonly string[] } | undefined;
};
},
): ((node: t.Node) => void) | undefined => {
if (!state.opaqueEffects.mayReferenceGlobalEval) return undefined;
if (!state.opaqueEffects.mayReferenceGlobalCode) return undefined;
const ambientPaths = dependencies.createAmbientPathResolver(state, true);
let invocationSite: t.Node | undefined;
const interop = dependencies.createSemanticModuleInterop(state, {
intrinsicPath: (node) => {
const paths = ambientPaths(node, invocationSite).paths;
const path = paths.find((path) => path[1] === "eval") ?? paths[0];
const { paths, callerPaths } = ambientPaths(node, invocationSite);
const path =
paths.find((path) => path[1] === "eval" || path[1] === "Function") ??
callerPaths.find(
(path) => path[1] === "eval" || path[1] === "Function",
) ??
paths[0];
if (path?.[0] !== "global" || path.some((key) => key === null))
return undefined;
// Keep the global callable owner for generic call/apply/bind resolution.
return path[1] === "eval"
return path[1] === "eval" || path[1] === "Function"
? ["intrinsic", ...(path as readonly string[])]
: ["intrinsic", ...(path.slice(1) as readonly string[])];
},
});
return (node) => {
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return;
if (
!t.isCallExpression(node) &&
!t.isOptionalCallExpression(node) &&
!t.isNewExpression(node) &&
!t.isTaggedTemplateExpression(node)
)
return;
invocationSite = node;
const invocation = interop.intrinsicInvocation(node);
if (invocation?.path.join(".") === "intrinsic.global.eval")
const path = interop.intrinsicInvocation(node)?.path;
// Constructed code may escape and activate a classic-script global. Keep
// its creation as a conservative timing bound, not proof of execution.
if (
path?.join(".") === "intrinsic.global.eval" ||
path?.join(".") === "intrinsic.global.Function"
)
state.opaqueEffects.globalSites.push(node);
};
};
/** Indirect eval can change globals, but cannot rebind its caller's locals. */
/** Global code can affect globals, but cannot rebind its caller's locals. */
export const semanticOpaqueGlobalMayHaveChanged = (
read: t.Node,
state: JavaScriptSemanticAnalysisState,
@@ -18,10 +18,10 @@ import {
semanticTopLevelEffectFollowsCapture,
} from "./javascriptSemanticMutationInitializers.js";
import {
semanticCopiedReferencePath,
semanticReferenceIterationUnknown,
semanticReferenceSourcesInactive,
} from "./javascriptSemanticReferenceSources.js";
import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js";
import { semanticStoredSourceSelection } from "./javascriptSemanticIterationSources.js";
import {
semanticEffectPathCandidates,
@@ -227,7 +227,7 @@ const projectedSourceFactsAtRead = (
readonly retainsOrigin: boolean;
}
| undefined => {
const read = semanticBindingAtRead(binding, reference, state.parentsByNode);
const read = semanticBindingAtRead(binding, reference, state);
const captured = [
...read.binding.initializers,
...read.binding.referenceInitializers,
@@ -47,7 +47,7 @@ import {
type CommonJsExportSite,
} from "./javascriptCommonJsExportReceivers.js";
import type { JavaScriptSemanticPropertyPath } from "./javascriptSemanticPropertyPaths.js";
import { semanticComputedMethodKey } from "./javascriptSemanticEvaluationOrder.js";
import { semanticMethodDefinitionExpression } from "./javascriptSemanticEvaluationOrder.js";
interface CollectCallableInput {
readonly node: t.Node;
@@ -77,7 +77,7 @@ export const collectSemanticCallable = (input: CollectCallableInput): void => {
computedKeyLocation:
"key" in node &&
t.isNode(node.key) &&
semanticComputedMethodKey(node, node.key)
semanticMethodDefinitionExpression(node, node.key)
? range(node.key)
: null,
returnSites: [],
@@ -488,7 +488,7 @@ const resolvePromiseExpression = (
const binding = resolveSemanticBindingState(state, node, node.name);
if (binding === undefined || seenBindings.has(binding.bindingId))
return unresolvedPromise();
const read = semanticBindingAtRead(binding, node, state.parentsByNode);
const read = semanticBindingAtRead(binding, node, state);
if (
read.status !== "ordered" ||
!read.hasDefiniteInitializer ||
@@ -5,7 +5,6 @@ import {
semanticPropertyPathKeyMatches,
type JavaScriptSemanticPropertyPath as PropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
evaluateSemanticExpression,
semanticValueDefinitelyDefined,
@@ -147,55 +146,3 @@ export const semanticSelectedReferenceSlot = (
path,
);
};
interface CopiedReferenceSource {
readonly projection: PropertyPath;
readonly copyKind?: "object-rest" | "array-rest";
readonly copyProjectionOffset?: number;
readonly copyExcludedKeys?: readonly string[];
readonly copyStartIndex?: number;
}
/** Map copied child selections once; the fresh copy itself keeps its own identity. */
export const semanticCopiedReferencePath = (
initializer: CopiedReferenceSource,
path: PropertyPath,
effect: "write" | "escape" | "identity",
): PropertyPath | null => {
const offset = initializer.copyProjectionOffset;
if (offset === undefined) return [...initializer.projection, ...path];
const copiedPath = [...initializer.projection.slice(offset + 1), ...path];
if (effect === "identity" && copiedPath.length === 0) return null;
if (effect === "write" && copiedPath.length < 2) return null;
let [key, ...remaining] = copiedPath;
if (initializer.copyKind === "array-rest") {
const startIndex = initializer.copyStartIndex ?? 0;
if (key === undefined || key === null)
key = { excludedKeys: [], startIndex };
else if (typeof key === "object")
key = {
excludedKeys: key.excludedKeys.flatMap((name) => {
const index = semanticArrayIndex(name);
return index === null ? [] : [String(index + startIndex)];
}),
startIndex: (key.startIndex ?? 0) + startIndex,
};
else {
const index = semanticArrayIndex(key);
if (index === null) return null;
key = index + startIndex;
}
} else {
const excludedKeys = initializer.copyExcludedKeys ?? [];
if (key === undefined || key === null) key = { excludedKeys };
else if (typeof key === "object")
key = {
...key,
excludedKeys: [...new Set([...key.excludedKeys, ...excludedKeys])].sort(
compareUnicodeCodePoints,
),
};
else if (excludedKeys.includes(String(key))) return null;
}
return [...initializer.projection.slice(0, offset), key, ...remaining];
};
@@ -493,11 +493,7 @@ const selectedRequireSourcesAt = (
}
if (facts === undefined) {
unknown = true;
const retained = semanticBindingAtRead(
binding,
node,
state.parentsByNode,
).binding;
const retained = semanticBindingAtRead(binding, node, state).binding;
pending.push(
...inheritLoaderSourceContext(
current,
@@ -1265,7 +1261,7 @@ const hasCommonJsModuleSource = (
continue;
}
if (binding === undefined) continue;
const read = semanticBindingAtRead(binding, value, state.parentsByNode);
const read = semanticBindingAtRead(binding, value, state);
const retainsMainModule =
mainModulePath &&
commonJsWrapperBindingDeclaration(value, "require", state) !==
@@ -390,11 +390,7 @@ export const createSemanticReturnedReferences = (
result.precision = "widened";
if (sourceGenericInvocation) result.unresolvedInvocation = true;
}
const selected = semanticBindingAtRead(
binding,
node,
state.parentsByNode,
).binding;
const selected = semanticBindingAtRead(binding, node, state).binding;
for (const initializer of [
...selected.initializers,
...selected.referenceInitializers,
@@ -166,7 +166,7 @@ const callableIdsForBinding = (
const selected =
read === undefined
? binding
: semanticBindingAtRead(binding, read, state.parentsByNode).binding;
: semanticBindingAtRead(binding, read, state).binding;
const initializer = selected.initializers[0];
if (
initializer === undefined ||
@@ -1,3 +1,4 @@
import type { SemanticPropertyReadSource } from "./javascriptSemanticIterationSources.js";
import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
@@ -119,17 +120,37 @@ export interface JavaScriptSemanticScopeState {
/** One source owns sites and the minimal scope tree reaching them; O(AST) storage. */
export interface JavaScriptOpaqueScopeEffects {
/** Cheap source evidence collected with declarations, before alias queries. */
mayReferenceGlobalEval: boolean;
mayReferenceGlobalCode: boolean;
readonly sites: WeakSet<t.Node>;
readonly sloppySites: WeakSet<t.Node>;
readonly byScope: Map<string, t.CallExpression[]>;
readonly children: Map<string, Set<string>>;
readonly globalSites: (t.CallExpression | t.OptionalCallExpression)[];
readonly globalSites: (
| t.CallExpression
| t.OptionalCallExpression
| t.NewExpression
| t.TaggedTemplateExpression
)[];
}
/** Intrinsic prototypes tracked from mutation sites to inherited reads. */
export const semanticPrototypeOwners = [
"Object",
"Array",
"String",
"Function",
"Number",
"Boolean",
"RegExp",
"BigInt",
] as const;
export type SemanticPrototypeOwner = (typeof semanticPrototypeOwners)[number];
/** Shared state for semantic collection and evaluation. */
export interface JavaScriptSemanticAnalysisState {
arrayIterationUnknown: boolean;
/** Receiver isolation requires the completed member-effect inventory. */
memberMutationFactsComplete: boolean;
readonly program: t.Program;
readonly opaqueEffects: JavaScriptOpaqueScopeEffects;
/** Effective loader for static imports in this source artifact. */
@@ -154,6 +175,14 @@ export interface JavaScriptSemanticAnalysisState {
prototypeRequireMutationObserved: boolean | undefined;
/** Source evidence that Object.prototype may add inherited enumerable keys. */
prototypeEnumerationMayAffect: boolean | undefined;
readonly implicitPropertyReads: WeakMap<
t.Node,
readonly SemanticPropertyReadSource[]
>;
readonly prototypeMutationSites: {
readonly owner: SemanticPrototypeOwner;
readonly node: t.Node;
}[];
/** Effects on a cached require exports object, independent of local projections. */
readonly requiredModuleEffects: Map<
string,
@@ -682,11 +682,7 @@ const evaluateExpressionValue = (
const binding = resolveSemanticBindingState(context.state, node, node.name);
if (binding === undefined)
return { status: "unknown", reason: `Unbound identifier ${node.name}.` };
const read = semanticBindingAtRead(
binding,
node,
context.state.parentsByNode,
);
const read = semanticBindingAtRead(binding, node, context.state);
if (
read.status === "unresolved" &&
binding.mutable &&
@@ -1377,7 +1373,7 @@ const provenanceForBindingAtRead = (
const { binding: snapshot } = semanticBindingAtRead(
binding,
read,
context.state.parentsByNode,
context.state,
);
if (snapshot === binding) return provenanceForBinding(binding, context);
if (context.bindings.has(binding.bindingId))
@@ -7,9 +7,43 @@ import {
parserPluginsForPath,
javaScriptRuntimeContext,
javaScriptProgramStrictness,
javaScriptTopLevelThisMayBeGlobal,
} from "./javascriptSourceParser.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
it.each([
["class C extends (this.marker, Object) {}", true],
["class C { [this.marker]() {} }", true],
["class C { [this.marker] = 1 }", true],
["({[this.marker]() {}})", true],
["class C { value = this.marker }", false],
["class C { static value = this.marker }", false],
["class C { static { this.marker } }", false],
["class C { method() { this.marker } }", false],
["function f() { return class C extends (this.marker, Object) {} }", false],
["class C { value = class D { [this.marker]() {} } }", false],
["(() => class C { [this.marker]() {} })()", true],
] as const)(
"owns definition-time this separately from class bodies: %s",
(source, expected) => {
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected parsed fixture");
const parents = new WeakMap<t.Node, t.Node>();
let selected: t.ThisExpression | undefined;
traverseJavaScriptAst(parsed.program, {
enter(node, parent) {
if (parent !== null) parents.set(node, parent);
if (t.isThisExpression(node)) selected = node;
},
});
if (selected === undefined) throw new Error("Missing this fixture");
expect(
javaScriptTopLevelThisMayBeGlobal(selected, parsed.program, parents),
).toBe(expected);
},
);
it.each([
['import type {T} from "example";', "fixture.cts", "commonjs"],
['import type {T} from "example";', "fixture.mts", "module"],
@@ -1,5 +1,6 @@
import { parse, type ParserOptions, type ParserPlugin } from "@babel/parser";
import * as t from "@babel/types";
import { javaScriptDefinitionExpression } from "./javascriptAstValues.js";
import {
hasOnlyInlineTypeExportSpecifiers,
hasOnlyInlineTypeImportSpecifiers,
@@ -282,3 +283,51 @@ export const parseJavaScriptSource = (
return null;
}
};
/** A script's top-level lexical this may be its ambient global; wrappers and modules isolate it. */
export const javaScriptThisOwner = (
node: t.ThisExpression,
program: t.Program,
parents: WeakMap<t.Node, t.Node>,
): "global" | "caller" | "isolated" => {
let child: t.Node = node;
let classDefinitionExpression = false;
let current: t.Node | undefined = parents.get(node);
while (current !== undefined && current !== program) {
const definitionExpression = javaScriptDefinitionExpression(current, child);
if (definitionExpression && !t.isObjectMethod(current))
classDefinitionExpression = true;
if (
(t.isClassProperty(current) ||
t.isClassPrivateProperty(current) ||
t.isClassAccessorProperty(current)) &&
current.value === child
)
return "isolated";
if (t.isStaticBlock(current)) return "isolated";
if (
t.isFunction(current) &&
!t.isArrowFunctionExpression(current) &&
!definitionExpression
)
return "caller";
if (
t.isClass(current) &&
current.superClass !== child &&
!classDefinitionExpression
)
return "isolated";
if (t.isClass(current)) classDefinitionExpression = false;
child = current;
current = parents.get(current);
}
return current === program && javaScriptRuntimeContext(program) === "unknown"
? "global"
: "isolated";
};
export const javaScriptTopLevelThisMayBeGlobal = (
node: t.ThisExpression,
program: t.Program,
parents: WeakMap<t.Node, t.Node>,
): boolean => javaScriptThisOwner(node, program, parents) === "global";
@@ -0,0 +1,347 @@
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"preserves hoisted Electron initialization uncertainty through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-electron-initialization-");
await writeFixtureFiles(root, {
"package.json": JSON.stringify({ main: "early.cjs" }),
"early.cjs": `
const { BrowserWindow } = require("electron");
createWindow();
const webPreferences = { nodeIntegration: true };
function createWindow() {
return new BrowserWindow({ webPreferences });
}
require("./late.cjs");
require("./early-loop.cjs");
require("./late-loop.cjs");
throw new Error("must not execute source");
`,
"late.cjs": `
const { BrowserWindow } = require("electron");
function createWindow() {
return new BrowserWindow({ webPreferences });
}
const webPreferences = { nodeIntegration: true };
createWindow();
throw new Error("must not execute source");
`,
"early-loop.cjs": `
const { BrowserWindow } = require("electron");
createWindow();
const webPreferences = { nodeIntegration: true };
function createWindow() {
for (const item of [0]) return new BrowserWindow({ webPreferences });
}
throw new Error("must not execute source");
`,
"late-loop.cjs": `
const { BrowserWindow } = require("electron");
const webPreferences = { nodeIntegration: true };
function createWindow() {
for (const item of [0]) return new BrowserWindow({ webPreferences });
}
createWindow();
throw new Error("must not execute source");
`,
});
const result = await analyzeJavaScriptDirectory(surface, cli, root);
expect(result.statistics.parse_failures).toBe(0);
const windows = result.graph.nodes.filter(
({ kind }) => kind === "browser-window",
);
expect(windows).toHaveLength(4);
const observations = windows.flatMap(({ observations }) => observations);
const inSource = (source: string) =>
observations.filter(
({ evidence }) =>
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === source,
);
for (const source of ["early.cjs", "early-loop.cjs"]) {
expect(inSource(source)).toHaveLength(1);
expect(inSource(source)[0]?.properties).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
}
for (const source of ["late.cjs", "late-loop.cjs"]) {
expect(inSource(source)).toHaveLength(1);
expect(inSource(source)[0]?.properties).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{
name: "nodeIntegration",
value: { status: "literal", value: true },
},
],
});
}
},
);
cliTest.for([
["CLI", false],
["stdio MCP", false],
["CLI", true],
["stdio MCP", true],
["CLI", "cached-eval"],
["stdio MCP", "cached-eval"],
["CLI", "constructed-function"],
["stdio MCP", "constructed-function"],
["CLI", "erased-type"],
["stdio MCP", "erased-type"],
["CLI", "module-eval"],
["stdio MCP", "module-eval"],
["CLI", "bound-constructor"],
["stdio MCP", "bound-constructor"],
["CLI", "reflect-constructor"],
["stdio MCP", "reflect-constructor"],
["CLI", "prototype-effect"],
["stdio MCP", "prototype-effect"],
["CLI", "assign-effect"],
["stdio MCP", "assign-effect"],
["CLI", "this-constructor"],
["stdio MCP", "this-constructor"],
["CLI", "alias-prototype"],
["stdio MCP", "alias-prototype"],
["CLI", "alias-assign"],
["stdio MCP", "alias-assign"],
["CLI", "assignment-pattern"],
["stdio MCP", "assignment-pattern"],
["CLI", "iteration-pattern"],
["stdio MCP", "iteration-pattern"],
["CLI", "reflect-read"],
["stdio MCP", "reflect-read"],
["CLI", "catch-read"],
["stdio MCP", "catch-read"],
["CLI", "heritage"],
["stdio MCP", "heritage"],
["CLI", "computed-key"],
["stdio MCP", "computed-key"],
["CLI", "tagged-function"],
["stdio MCP", "tagged-function"],
["CLI", "array-prototype"],
["stdio MCP", "array-prototype"],
] as const)(
"preserves classic global initialization uncertainty through %s (%s)",
async ([surface, alias], { cli }) => {
const root = await createTestTempDirectory(
"rea-electron-global-initialization-",
);
const activation =
alias === "reflect-read"
? "Reflect.get(globalThis, 'createWindow')();"
: alias === "catch-read"
? "try { throw globalThis; } catch ({createWindow:start}) { start(); }"
: alias === "heritage"
? "class C extends (this.Function('createWindow()')(), Object) {}"
: alias === "computed-key"
? "({ [this.eval('createWindow()')]() {} });"
: alias === "tagged-function"
? "Function`createWindow()`();"
: alias === "array-prototype"
? "Object.setPrototypeOf(Array.prototype, globalThis); [].createWindow();"
: alias === "this-constructor"
? "this['Function']('createWindow()')();"
: alias === "alias-prototype"
? "const changed = {}; const indirect = changed; Object.setPrototypeOf(changed, globalThis); indirect.createWindow();"
: alias === "alias-assign"
? "const changed = {}; const indirect = changed; Object.assign(changed, globalThis); indirect.createWindow();"
: alias === "bound-constructor"
? "const F = Function.bind(null); new F('createWindow()')();"
: alias === "reflect-constructor"
? "Reflect.construct(Function, ['createWindow()'])();"
: alias === "prototype-effect"
? "const changed = {}; Object.setPrototypeOf(changed, globalThis); changed.createWindow();"
: alias === "assign-effect"
? "const changed = {}; Object.assign(changed, globalThis); changed.createWindow();"
: alias === "assignment-pattern"
? "let start; ({createWindow:start}=globalThis); start();"
: alias === "iteration-pattern"
? "for(const {createWindow:start} of [globalThis]) start();"
: alias === "module-eval"
? "eval('createWindow()');"
: alias === "erased-type"
? "globalThis.createWindow();"
: alias === "constructed-function"
? "new Function('createWindow()')();"
: alias === "cached-eval"
? "const noop = () => {}; function unused() { noop(); } (0, eval)('createWindow()');"
: alias
? "root.createWindow();"
: "globalThis.createWindow();";
const source = (early: boolean) => `
${alias === "erased-type" ? "import type { T } from './types';" : alias === "module-eval" ? "export {};" : ""}
${alias === "module-eval" ? 'import { BrowserWindow } from "electron";' : 'const { BrowserWindow } = require("electron");'}
${alias ? "const root = globalThis;" : ""}
${alias === "reflect-read" || alias === "catch-read" ? "Object.extra = 1; function unusedArrayRead() { [].createWindow; }" : ""}
const unrelated = { createWindow() {} };
function unusedReceiverRead() { unrelated.createWindow; }
for (const ignored of [1, 2]) { unrelated.createWindow; }
${early ? activation : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
${early || alias === "module-eval" ? "" : activation}
createWindow();
throw new Error("must not execute source");
`;
const extension =
alias === "erased-type" ? "ts" : alias === "module-eval" ? "mjs" : "js";
const earlyName = `early.${extension}`;
const lateName = `late.${extension}`;
await writeFixtureFiles(root, {
[earlyName]: source(true),
[lateName]: source(false),
"index.html": `<script src="${earlyName}"></script><script src="${lateName}"></script>`,
});
const result = await analyzeJavaScriptDirectory(surface, cli, root);
expect(result.statistics.parse_failures).toBe(0);
const windows = result.graph.nodes.filter(
({ kind }) => kind === "browser-window",
);
expect(windows).toHaveLength(2);
const inSource = (name: string) =>
windows
.flatMap(({ observations }) => observations)
.filter(
({ evidence }) =>
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === name,
);
expect(inSource(earlyName)).toHaveLength(1);
expect(inSource(earlyName)[0]?.properties).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
expect(inSource(lateName)).toHaveLength(1);
expect(inSource(lateName)[0]?.properties).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"preserves aliased prototype, reflected and parameter activation through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory(
"rea-electron-prototype-parameters-",
);
const activations = {
callerThis: "function boot() { this.createWindow(); } boot();",
boundThis:
"function boot() { this.createWindow(); } boot.call(globalThis);",
numericPrototype:
"Object.assign(Object.prototype, globalThis); (0).createWindow();",
booleanPrototype:
"Object.assign(Object.prototype, globalThis); true.createWindow();",
regexpPrototype:
"Object.assign(Object.prototype, globalThis); /x/.createWindow();",
nestedGlobalPrototype:
"Object.setPrototypeOf(globalThis.window.Array.prototype, globalThis); [].createWindow();",
taggedCatch:
"function fail() { throw globalThis; } try { fail``; } catch ({createWindow:start}) { start(); }",
getterCatch:
"const box = { get value() { throw globalThis; } }; try { box.value; } catch ({createWindow:start}) { start(); }",
reflectedApply:
"const args = [globalThis, 'createWindow']; Reflect.get.apply(Reflect, args)();",
projectedReceiver:
"const { root } = { root: globalThis, createWindow() {} }; const early = root.createWindow; early();",
assignedReceiver:
"let root; ({ root } = { root: globalThis, createWindow() {} }); const early = root.createWindow; early();",
nestedReceiver:
"const { nested: { root } } = { nested: { root: globalThis }, createWindow() {} }; root.createWindow();",
catchPropagated:
"function fail() { throw globalThis; } try { fail(); } catch ({ createWindow: start }) { start(); }",
catchNested:
"function fail() { throw globalThis; } try { if (true) fail(); } catch ({ createWindow: start }) { start(); }",
objectPrototype:
"Object.assign(Object.prototype, globalThis); ({}).createWindow();",
stringPrototype:
"Object.setPrototypeOf(String.prototype, globalThis); ''.createWindow();",
lexicalThisPrototype:
"Object.setPrototypeOf(this.Array.prototype, globalThis); [].createWindow();",
reflectedTemplate:
"globalThis[`Reflect`].get(globalThis, 'createWindow')();",
reflectedOptional: "Reflect.get?.(globalThis, 'createWindow')();",
reflectedNamespace:
"const R = Reflect; R.get(globalThis, 'createWindow')();",
reflectedMethod:
"const read = Reflect.get; read(globalThis, 'createWindow')();",
reflectedBound:
"const read = Reflect.get.bind(Reflect); read(globalThis, 'createWindow')();",
array:
"const receiver = []; const alias = receiver; Object.setPrototypeOf(Array.prototype, globalThis); alias.createWindow();",
function:
"const receiver = function(){}; const alias = receiver; Object.setPrototypeOf(Function.prototype, globalThis); alias.createWindow();",
parameter:
"function boot({ createWindow: start } = {}) { start(); } boot(globalThis);",
};
const files: Record<string, string> = {};
for (const [kind, activation] of Object.entries(activations)) {
for (const early of [true, false]) {
files[`${kind}-${early ? "early" : "late"}.js`] = `
const { BrowserWindow } = require("electron");
${early ? activation : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() { return new BrowserWindow({ webPreferences }); }
${early ? "" : activation}
createWindow();
throw new Error("must not execute source");
`;
}
}
files["index.html"] = Object.keys(files)
.map((name) => `<script src="${name}"></script>`)
.join("");
await writeFixtureFiles(root, files);
const result = await analyzeJavaScriptDirectory(surface, cli, root);
expect(result.statistics.parse_failures).toBe(0);
const windows = result.graph.nodes.filter(
({ kind }) => kind === "browser-window",
);
expect(windows).toHaveLength(50);
for (const source of Object.keys(files).filter((name) =>
name.endsWith(".js"),
)) {
const observations = windows
.flatMap(({ observations }) => observations)
.filter(
({ evidence }) =>
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === source,
);
expect(observations).toHaveLength(1);
const definite =
source.endsWith("-late.js") &&
!["parameter-", "callerThis-", "boundThis-"].some((prefix) =>
source.startsWith(prefix),
);
expect(observations[0]?.properties).toMatchObject(
definite
? {
web_preferences_status: "object-literal",
web_preferences: [
{
name: "nodeIntegration",
value: { status: "literal", value: true },
},
],
}
: { web_preferences_status: "dynamic", web_preferences: [] },
);
}
},
);