mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
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:
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 &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [] },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user