Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44457d0990 | ||
|
|
2e860e2823 | ||
|
|
c98d326514 | ||
|
|
ad27b8b4ff | ||
|
|
0285701033 | ||
|
|
3e9bce6249 | ||
|
|
1e1a611012 | ||
|
|
b60248c462 | ||
|
|
2a9fdde5db | ||
|
|
3d1d5b56b2 | ||
|
|
6b460683d7 | ||
|
|
548a4334c4 | ||
|
|
b03df22ce4 | ||
|
|
5569e96162 | ||
|
|
47f8156efe | ||
|
|
824728d7ee | ||
|
|
ced8778094 | ||
|
|
6d470fc9cb | ||
|
|
9788112548 | ||
|
|
ebaa639cf8 |
@@ -323,15 +323,44 @@ substitute version-dependent Electron defaults for omitted `webPreferences`.
|
||||
Dynamic option objects, bridge keys, API objects, and IPC channel expressions
|
||||
remain unknown and make coverage partial.
|
||||
|
||||
Electron API identity follows proven lexical module aliases. Bundled CommonJS
|
||||
namespace imports are also followed through the interop helpers that bundlers
|
||||
and compilers emit (`__toESM`, `__importStar`, `_interopRequireWildcard`),
|
||||
including the `electron = __toESM(electron)` rewrap that Rolldown emits. This
|
||||
step is inferred: a helper is recognized by its emitted name and a single
|
||||
program-scope function definition, and its body is not evaluated, so a local
|
||||
function that reuses one of these names is trusted to pass the module through.
|
||||
Other reassigned bindings, dynamic selections, and visibly overwritten namespace
|
||||
members are unresolved.
|
||||
Electron API identity follows proven lexical module aliases. Namespace interop
|
||||
also preserves identity when the complete Rolldown helper operations, lexical
|
||||
roles, live getters, and captured intrinsic origins match, or when a stable
|
||||
helper simply returns its module parameter. Harmless single-statement control
|
||||
blocks preserve recognition; added operations still require proof. Literal
|
||||
member and destructuring projections are retained from proven results.
|
||||
Omitted or literal node-mode arguments preserve named exports; a module-valued
|
||||
default projection requires a truthy literal node mode. Computed or extra
|
||||
arguments remain unresolved.
|
||||
Recognition uses structure rather than emitted names; other helper bodies and ambiguous
|
||||
runtime fallbacks remain unresolved. Captured function identities survive later global-property
|
||||
replacement, including copies through initialized aliases. Reads before an
|
||||
alias initializer remain unresolved. Visible intrinsic writes before capture or
|
||||
conversion are checked in their owning callable scope, including identifier leaves in nested/default/rest
|
||||
destructuring and loop targets. Property keys and shadowed local bindings do not
|
||||
overwrite the global intrinsic identity. Cross-call side effects, including
|
||||
writes inside directly invoked local functions, are not evaluated. Other
|
||||
reassignments, dynamic selections, and visibly overwritten namespace members
|
||||
are unresolved.
|
||||
Known Object/Reflect mutators remain recognizable through literal destructuring
|
||||
of intrinsic objects and stable bound-function aliases. Calls retain bound
|
||||
argument prefixes and the original bound receiver through `call`, `apply`, and
|
||||
`Reflect.apply`, including generic `Function.prototype` invocation and binding
|
||||
creation. Arguments follow each actual callable invocation rather than property
|
||||
chain depth; rebinding retains the original bound receiver.
|
||||
Visibly replaced invocation intrinsics produce unknown effects instead
|
||||
of using their former call semantics; captured function aliases retain their
|
||||
earlier identity. Unexamined targets remain unresolved. `Reflect.set` examines
|
||||
its explicit receiver as well as its target, retaining unknown receiver effects.
|
||||
Creating an unused binding does not itself mutate an intrinsic. Variable
|
||||
redeclarations retain a single unconditional initializer
|
||||
when they introduce no assignment or second initializer.
|
||||
Stable identifier chains to fresh object, array, function,
|
||||
undecorated class or RegExp syntax keep those targets separate from intrinsic
|
||||
objects when initialized before use; opaque, decorated, reassigned or
|
||||
not-yet-initialized targets remain unresolved. Writes during class initialization
|
||||
are still retained. Function declarations are available before their textual
|
||||
position; variable and class initializers must precede use.
|
||||
Direct and destructuring assignment, loop, update, and delete targets are checked
|
||||
through namespace aliases; unrelated member writes do not discard observed API
|
||||
identity. This static check does
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
import { ROLLDOWN_MODULE_INTEROP } from "./javascriptModuleInterop.fixture.js";
|
||||
|
||||
const WINDOW =
|
||||
'new electron.BrowserWindow({webPreferences:{preload:"./preload.js"}});';
|
||||
const IMPORT = 'let electron = require("electron");';
|
||||
const WRAP = "electron = __toESM(electron, 1);";
|
||||
// The upstream runtime uses control-body braces that its bundled printer omits.
|
||||
// https://github.com/rolldown/rolldown/blob/22a0ab13732d81f2559ad2b7064b688699324889/crates/rolldown/src/runtime/runtime-base.js
|
||||
const BLOCK_MODULE_INTEROP = ROLLDOWN_MODULE_INTEROP.replace(
|
||||
'if (from && typeof from === "object" || typeof from === "function") for',
|
||||
'if (from && typeof from === "object" || typeof from === "function") { for',
|
||||
)
|
||||
.replace(
|
||||
"if (!__hasOwnProp.call(to, key) && key !== except) __defProp",
|
||||
"if (!__hasOwnProp.call(to, key) && key !== except) { __defProp",
|
||||
)
|
||||
.replace(" });", " });\n }")
|
||||
.replace(" return to;", " }\n return to;");
|
||||
|
||||
describe("Electron module interop identity", () => {
|
||||
it("preserves named APIs through the released Rolldown namespace conversion", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
${IMPORT} ${WRAP} ${WINDOW}
|
||||
new electron.BrowserWindow({webPreferences:{preload:runtimePath}});
|
||||
electron.ipcMain.handle("ping", () => "pong");
|
||||
electron.contextBridge.exposeInMainWorld("api", {ping:true});
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toMatchObject([
|
||||
{ preload_path: "./preload.js" },
|
||||
{ preload_path: null },
|
||||
]);
|
||||
expect(analysis.electron.ipc).toMatchObject([
|
||||
{ channel: "ping", side: "main" },
|
||||
]);
|
||||
expect(analysis.electron.context_bridge_apis).toMatchObject([
|
||||
{ api_key: "api" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("follows lexical identities when helper and parameter names change", () => {
|
||||
const source = `${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP} ${WINDOW}`
|
||||
.replaceAll("__toESM", "namespaceOf")
|
||||
.replaceAll("__copyProps", "copyNamed")
|
||||
.replaceAll("mod", "inputModule")
|
||||
.replaceAll("isNodeMode", "nodeMode")
|
||||
.replaceAll("target", "namespaceTarget")
|
||||
.replaceAll("from", "sourceObject");
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(source).electron.browser_windows,
|
||||
).toMatchObject([{ preload_path: "./preload.js" }]);
|
||||
});
|
||||
|
||||
it("preserves the same operations through control-body braces", () => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(
|
||||
`${BLOCK_MODULE_INTEROP}\n${IMPORT} ${WRAP} ${WINDOW}`,
|
||||
).electron.browser_windows,
|
||||
).toMatchObject([{ preload_path: "./preload.js" }]);
|
||||
});
|
||||
|
||||
it("keeps additional operations inside a control block unresolved", () => {
|
||||
const altered = BLOCK_MODULE_INTEROP.replace(
|
||||
"{ __defProp",
|
||||
"{ from = unrelated; __defProp",
|
||||
);
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(`${altered}\n${IMPORT} ${WRAP} ${WINDOW}`)
|
||||
.electron.browser_windows,
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
const BOUND_INTRINSIC_MUTATIONS: [string, string][] = [
|
||||
[
|
||||
"intrinsic changed through aliased prebound arguments",
|
||||
`const objectAlias = Object; const key = "create"; const define = Object.defineProperty; const mutate = define.bind(null, objectAlias, key); mutate({value:customCreate}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed through nested prebinding",
|
||||
`const first = Object.defineProperty.bind(null, Object); const second = first.bind(null, "create"); second({value:customCreate}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"a replaced bound mutator bind method is unknown",
|
||||
`const first = Object.defineProperty.bind(null, Object); first.bind = customBind; const second = first.bind(null, "create"); second({value:customCreate}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed through a computed prebound call",
|
||||
`const mutate = Object.defineProperty.bind(null, Object, "create"); mutate["call"](null, {value:customCreate}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed through a computed prebound apply",
|
||||
`const mutate = Object.defineProperty.bind(null, Object, "create"); mutate["apply"](null, [{value:customCreate}]); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"a replaced bind intrinsic makes a prebound mutator unknown",
|
||||
`Function.prototype.bind = customBind; const mutate = Object.defineProperty.bind(null, Object, "create"); mutate({value:customCreate}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
];
|
||||
|
||||
describe("Electron module interop uncertainty", () => {
|
||||
it.each([
|
||||
...BOUND_INTRINSIC_MUTATIONS,
|
||||
[
|
||||
"intrinsic changed through an unresolved mutator target",
|
||||
`Object.defineProperty(getTarget(), "create", {value:customCreate}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed via dynamic apply",
|
||||
`const args = [Object, "getOwnPropertyNames", {value:() => []}]; Object.defineProperty.apply(null, args); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed via spread call",
|
||||
`const args = [Object, "getOwnPropertyNames", {value:() => []}]; Object.defineProperty.call(null, ...args); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed via Reflect.apply",
|
||||
`const args = [Object, "getOwnPropertyNames", {value:() => []}]; Reflect.apply(Object.defineProperty, null, args); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed via spread Reflect.apply",
|
||||
`const args = [null, [Object, "create", {value:customCreate}]]; Reflect.apply(Object.defineProperty, ...args); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed via call",
|
||||
`Object.defineProperty.call(null, Object, "getOwnPropertyNames", {value:() => []}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed via apply",
|
||||
`Object.defineProperty.apply(null, [Object, "getOwnPropertyNames", {value:() => []}]); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed with defineProperty",
|
||||
`Object.defineProperty(Object, "getOwnPropertyNames", {value:() => []}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed with Reflect.set",
|
||||
`Reflect.set(Object, "getOwnPropertyNames", () => []); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed with an aliased mutator",
|
||||
`const change = Object.defineProperty; change(Object, "create", {value:customCreate}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed with assign",
|
||||
`Object.assign(Object, {getOwnPropertyNames:() => []}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"intrinsic changed with defineProperties",
|
||||
`Object.defineProperties(Function.prototype, {bind:{value:customBind}}); ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"overwritten Object global",
|
||||
`Object = customObject; ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"overwritten intrinsic through an alias",
|
||||
`const objectAlias = Object; objectAlias.create = customCreate; ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"overwritten intrinsic through globalThis",
|
||||
`globalThis.Object.create = customCreate; ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"helper called before initialization",
|
||||
`${IMPORT} ${WRAP} ${ROLLDOWN_MODULE_INTEROP}`,
|
||||
],
|
||||
[
|
||||
"fake helper",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n__toESM = (mod) => ({BrowserWindow:Fake}); ${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"changed getter",
|
||||
`${ROLLDOWN_MODULE_INTEROP.replace("from[k]", "to[k]")}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"replaced copy helper",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n__copyProps = () => ({}); ${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"replaced intrinsic alias",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n__defProp = () => ({}); ${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"shadowed Object",
|
||||
`const Object = customObject; ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"overwritten intrinsic",
|
||||
`Object.create = customCreate; ${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"overwritten alias call",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n__hasOwnProp.call = customCall; ${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"overwritten Function bind",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\nFunction.prototype.bind = customBind; ${IMPORT} ${WRAP}`,
|
||||
],
|
||||
[
|
||||
"conditional conversion",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} if (flag) { ${WRAP} }`,
|
||||
],
|
||||
[
|
||||
"compound assignment",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} electron ||= __toESM(electron, 1);`,
|
||||
],
|
||||
[
|
||||
"other input",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} electron = __toESM(other, 1);`,
|
||||
],
|
||||
[
|
||||
"later overwrite",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP} electron = unrelated;`,
|
||||
],
|
||||
[
|
||||
"overwritten constructor",
|
||||
`${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP} electron.BrowserWindow = Fake;`,
|
||||
],
|
||||
])("does not infer Electron identity after %s", (_name, setup) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(`${setup}\n${WINDOW}`).electron
|
||||
.browser_windows,
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron module interop helper results", () => {
|
||||
it.each([
|
||||
'const {BrowserWindow} = __toESM(require("electron"), 1); new BrowserWindow({});',
|
||||
'const {BrowserWindow: Window} = __toESM(require("electron"), 1); new Window({});',
|
||||
'const {default: {BrowserWindow}} = __toESM(require("electron"), 1); new BrowserWindow({});',
|
||||
'const Window = __toESM(require("electron"), 1).BrowserWindow; new Window({});',
|
||||
])("preserves a proven result projection: %s", (source) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}\n${source}`)
|
||||
.electron.browser_windows,
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'const {[key]: Window} = __toESM(require("electron"), 1); new Window({});',
|
||||
'const {...electron} = __toESM(require("electron"), 1); new electron.BrowserWindow({});',
|
||||
'let {BrowserWindow} = __toESM(require("electron"), 1); BrowserWindow = Fake; new BrowserWindow({});',
|
||||
])("keeps an uncertain result projection unresolved: %s", (source) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}\n${source}`)
|
||||
.electron.browser_windows,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"",
|
||||
", 0",
|
||||
", false",
|
||||
", true",
|
||||
", 1",
|
||||
', "node"',
|
||||
', ""',
|
||||
", null",
|
||||
])("preserves named exports with a literal node mode: %s", (mode) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
const fresh = __toESM(require("electron")${mode});
|
||||
new fresh.BrowserWindow({});
|
||||
const {BrowserWindow} = __toESM(require("electron")${mode});
|
||||
new BrowserWindow({});
|
||||
let electron = require("electron");
|
||||
electron = __toESM(electron${mode}); ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it.each(["", ", 0", ", false"])(
|
||||
"does not assume a module-valued default with node mode: %s",
|
||||
(mode) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
const original = require("electron");
|
||||
original.__esModule = true;
|
||||
original.default = {BrowserWindow: Fake};
|
||||
const {default: {BrowserWindow}} = __toESM(require("electron")${mode});
|
||||
new BrowserWindow({});
|
||||
const other = __toESM(require("electron")${mode}).default;
|
||||
new other.BrowserWindow({});
|
||||
new (__toESM(require("electron")${mode}).default.BrowserWindow)({});
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["__toESM", "identity"])(
|
||||
"rejects side-effecting extra arguments to %s",
|
||||
(helper) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
function identity(mod) { return mod; }
|
||||
const electron = ${helper}(require("electron"), mutate()); ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"const __toESM = () => ({BrowserWindow:FakeWindow});",
|
||||
"const __toESM = async (mod) => mod;",
|
||||
"function* __toESM(mod) { return mod; }",
|
||||
"const __toESM = (this && this.__toESM) || ((mod) => mod);",
|
||||
])("keeps an unproven fresh result unresolved: %s", (helper) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${helper}
|
||||
const electron = __toESM(require("electron"), 1); ${WINDOW}
|
||||
new (__toESM(require("electron"), 1)).BrowserWindow({});
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves a proven fresh namespace and a pure identity helper", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
const electron = __toESM(require("electron"), 1); ${WINDOW}
|
||||
new (__toESM(require("electron"), 1)).BrowserWindow({});
|
||||
function passthrough(value) {return value;}
|
||||
const plain = passthrough(require("electron"));
|
||||
new plain.BrowserWindow({});
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron module interop pattern writes", () => {
|
||||
it.each([
|
||||
"({Object} = replacements);",
|
||||
"[Function] = replacements;",
|
||||
"({nested:{Object}} = replacements);",
|
||||
"({replacement:Object = customObject} = replacements);",
|
||||
"({...Object} = replacements);",
|
||||
"[...Function] = replacements;",
|
||||
"for ({Object} of replacements) {}",
|
||||
"for ([Function] in replacements) {}",
|
||||
])("rejects an intrinsic replaced through %s", (write) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
${write} ${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"({Object:unrelated} = replacements);",
|
||||
"function decoy(Object) {({Object} = replacements);}",
|
||||
])("preserves intrinsic identity for an unrelated target: %s", (write) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
${write} ${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("preserves captured operations after later property destructuring", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
${ROLLDOWN_MODULE_INTEROP} ({create:Object.create} = replacements);
|
||||
${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron module interop intrinsic captures", () => {
|
||||
it.each([
|
||||
"var captured = Object.defineProperty;",
|
||||
"const {defineProperty:captured} = Object;",
|
||||
"var first = Object.defineProperty; const captured = first;",
|
||||
])("retains the original function capture through %s", (capture) => {
|
||||
const runtime = ROLLDOWN_MODULE_INTEROP.replace(
|
||||
"__defProp = Object.defineProperty",
|
||||
"__defProp = captured",
|
||||
);
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
${capture} Object.defineProperty = custom;
|
||||
${runtime} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each(["var", "const"])(
|
||||
"keeps an intrinsic alias read before its %s initializer unresolved",
|
||||
(kind) => {
|
||||
const runtime = ROLLDOWN_MODULE_INTEROP.replace(
|
||||
"__defProp = Object.defineProperty",
|
||||
"__defProp = later",
|
||||
);
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
${runtime} ${kind} later = Object.defineProperty;
|
||||
${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("requires the helper's intrinsic binding to initialize before conversion", () => {
|
||||
const runtime = ROLLDOWN_MODULE_INTEROP.replace(
|
||||
"var __defProp = Object.defineProperty;",
|
||||
"",
|
||||
);
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
var captured = Object.defineProperty;
|
||||
${runtime} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
var __defProp = captured;
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron module interop capture and scope", () => {
|
||||
it.each(["metadata", "alias"])(
|
||||
"recognizes a hoisted function target through %s",
|
||||
(target) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const alias = metadata;
|
||||
Object.defineProperty(${target}, "x", {value:1});
|
||||
function metadata() {}
|
||||
${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"var metadata = function () {};",
|
||||
"const metadata = () => {};",
|
||||
"class metadata {}",
|
||||
])("does not hoist fresh targets from %s", (declaration) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
Object.defineProperty(metadata, "x", {value:1});
|
||||
${declaration}
|
||||
${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'const {defineProperty:change} = Object; change(Object,"create",{value:customCreate});',
|
||||
"const objectAlias = Object; const {assign:change} = objectAlias; change(Object,{create:customCreate});",
|
||||
'const {set:change} = Reflect; change(Object,"create",customCreate);',
|
||||
'const {Object:{defineProperty:change}} = globalThis; change(Object,"create",{value:customCreate});',
|
||||
])("rejects a projected intrinsic mutator alias: %s", (write) => {
|
||||
const source = `${write} ${ROLLDOWN_MODULE_INTEROP}
|
||||
${IMPORT} ${WRAP} ${WINDOW}
|
||||
`.replaceAll("__toESM", "namespaceOf");
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"const metadata = {};",
|
||||
"const metadata = [];",
|
||||
"const initial = {}; const metadata = initial;",
|
||||
"const metadata = () => {};",
|
||||
"const metadata = function () {};",
|
||||
"const metadata = async () => {};",
|
||||
"const metadata = function* () {};",
|
||||
"const metadata = class {};",
|
||||
"const metadata = /local/;",
|
||||
"function metadata() {}",
|
||||
"class metadata {}",
|
||||
])(
|
||||
"keeps a fresh mutator target separate from intrinsic identity: %s",
|
||||
(setup) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
${setup} Object.defineProperty(metadata,"x",{value:1});
|
||||
${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"const metadata = getMetadata();",
|
||||
"let metadata = {}; metadata = Object;",
|
||||
"Object.defineProperty(metadata,'x',{value:1}); const metadata = {};",
|
||||
"const metadata = {intrinsic:Object}; Object.defineProperty(metadata.intrinsic,'create',{value:customCreate});",
|
||||
"const metadata = class {static {Object.create = customCreate;}};",
|
||||
"@(() => Object) class metadata {}",
|
||||
])("keeps an unproven mutator target unresolved: %s", (setup) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
${setup} Object.defineProperty(metadata,"x",{value:1});
|
||||
${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not reject unrelated intrinsic property definitions", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
Object.defineProperty(Object, "unrelated", {value:1});
|
||||
${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps opaque targets in a generic helper separate from outer module identity", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
function genericAssign() { Object.assign.apply(null, arguments); }
|
||||
${ROLLDOWN_MODULE_INTEROP} ${IMPORT} ${WRAP} ${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects an opaque intrinsic mutation in the namespace's own callable", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
function openWindow(args) {
|
||||
Object.defineProperty.apply(null, args);
|
||||
${IMPORT} ${WRAP} ${WINDOW}
|
||||
}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves captured intrinsics across later vendor polyfills and callable writes", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
Object.create = customCreate;
|
||||
${IMPORT} ${WRAP}
|
||||
function runLoop(key) { globalThis[key] = this; }
|
||||
function polyfill() { Object.create = customCreate; }
|
||||
${WINDOW}
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps a shadowed namespace separate from the converted module", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`${ROLLDOWN_MODULE_INTEROP}
|
||||
${IMPORT} ${WRAP} ${WINDOW}
|
||||
function decoy(electron) { ${WINDOW} }
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron module interop generic invocations", () => {
|
||||
it.each([
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty); change(null,Object,"create",{value:customCreate});',
|
||||
'const call = Function.prototype.call; const change = call.bind(Object.defineProperty); change(null,Object,"create",{value:customCreate});',
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null,Object,"create"); change({value:customCreate});',
|
||||
'Function.prototype.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'Function.prototype.call.apply(Object.defineProperty,[null,Object,"create",{value:customCreate}]);',
|
||||
'Reflect.apply(Function.prototype.call,Object.defineProperty,[null,Object,"create",{value:customCreate}]);',
|
||||
'const change = Function.prototype.apply.bind(Object.defineProperty); change(null,[Object,"create",{value:customCreate}]);',
|
||||
'Function.prototype.apply.call(Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
|
||||
'Reflect.apply(Function.prototype.apply,Object.defineProperty,[null,[Object,"create",{value:customCreate}]]);',
|
||||
'const first = Function.prototype.call.bind(Object.defineProperty,null,Object,"create"); const change = first.bind(fake); change({value:customCreate});',
|
||||
"const change = Function.prototype.call.bind(Object.defineProperty); change(null,...args);",
|
||||
"const change = Function.prototype.apply.bind(Object.defineProperty); change(null,args);",
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null,...args); change(metadata,"x",{value:1});',
|
||||
'Function.prototype.call.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'Object.defineProperty.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'Function.prototype.apply.apply(Object.defineProperty,[null,[Object,"create",{value:customCreate}]]);',
|
||||
'const change = Object.defineProperty.call.bind(Object.defineProperty); change(null,Object,"create",{value:customCreate});',
|
||||
'const change = Function.prototype.call.call.bind(Object.defineProperty,null,Object,"create"); change({value:customCreate});',
|
||||
])("follows the callable receiver of a generic invocation: %s", (write) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(
|
||||
`${write}\n${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP} ${WINDOW}`,
|
||||
).electron.browser_windows,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty); change(null,metadata,"x",{value:1});',
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null,metadata,"x"); change({value:1});',
|
||||
'Function.prototype.call.call(Object.defineProperty,null,metadata,"x",{value:1});',
|
||||
'const change = Function.prototype.apply.bind(Object.defineProperty); change(null,[metadata,"x",{value:1}]);',
|
||||
'Reflect.apply(Function.prototype.apply,Object.defineProperty,[null,[metadata,"x",{value:1}]]);',
|
||||
'Function.prototype.call.call.call(Object.defineProperty,null,metadata,"x",{value:1});',
|
||||
'Function.prototype.apply.apply(Object.defineProperty,[null,[metadata,"x",{value:1}]]);',
|
||||
'const change = Object.defineProperty.call.bind(Object.defineProperty); change(null,metadata,"x",{value:1});',
|
||||
])("preserves fresh targets through a generic invocation: %s", (write) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(
|
||||
`const metadata = {}; ${write}\n${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP} ${WINDOW}`,
|
||||
).electron.browser_windows,
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron module interop bound mutators", () => {
|
||||
it.each([
|
||||
'const change = Object.defineProperty.bind(null); change(Object,"create",{value:customCreate});',
|
||||
'const change = Object.defineProperty.bind(null,Object,"create"); change({value:customCreate});',
|
||||
'const first = Object.defineProperty.bind(null,Object); const change = first; change("create",{value:customCreate});',
|
||||
'const change = Object.defineProperty.bind(null,Object); change.call(null,"create",{value:customCreate});',
|
||||
'const change = Object.defineProperty.bind(null,Object); change.apply(null,["create",{value:customCreate}]);',
|
||||
'const change = Object.defineProperty.bind(null,Object); Reflect.apply(change,null,["create",{value:customCreate}]);',
|
||||
'Object.defineProperty.bind(null,Object,"create")({value:customCreate});',
|
||||
'const change = Object.defineProperty.bind(null,...args); change(Object,"create",{value:customCreate});',
|
||||
'const metadata = {}; Object.defineProperty.call(...args,metadata,"x",{value:1});',
|
||||
'const metadata = {}; Object.defineProperty.apply(...args,[metadata,"x",{value:1}]);',
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null); change(Object,"create",{value:customCreate});',
|
||||
'const first = Function.prototype.call.bind(Object.defineProperty,null); const change = first.bind(customReceiver,Object,"create"); change({value:customCreate});',
|
||||
'const change = Function.prototype.apply.bind(Object.defineProperty,null); change([Object,"create",{value:customCreate}]);',
|
||||
'Function.prototype.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'Function.prototype.apply.call(Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
|
||||
'const uncurry = Function.prototype.call.bind(Function.prototype.call); uncurry(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'const change = Function.prototype.bind.call(Object.defineProperty,null); change(Object,"create",{value:customCreate});',
|
||||
'const bind = Function.prototype.call.bind(Function.prototype.bind); const change = bind(Object.defineProperty,null); change(Object,"create",{value:customCreate});',
|
||||
'const change = Function.prototype.bind.apply(Object.defineProperty,[null,Object,"create"]); change({value:customCreate});',
|
||||
'const change = Reflect.apply(Function.prototype.bind,Object.defineProperty,[null,Object,"create"]); change({value:customCreate});',
|
||||
'const first = Object.defineProperty.bind(null,Object); const change = Function.prototype.bind.call(first,null,"create"); change({value:customCreate});',
|
||||
'Reflect.apply = Object.defineProperty; Reflect.apply(Object,"create",{value:customCreate});',
|
||||
'const apply = Reflect.apply; Reflect.apply = Object.defineProperty; Reflect.apply(Object,"create",{value:customCreate});',
|
||||
'const change = Reflect.set.bind(null,Object,"create"); change(customCreate);',
|
||||
'const change = Object.defineProperty.call; change.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
|
||||
'const {apply:change} = Object.defineProperty; change.call(Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
|
||||
'const change = Reflect.apply.call; change.call(Reflect.apply,null,Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
|
||||
'Reflect.set({},"create",customCreate,Object);',
|
||||
'const metadata = {}; Reflect.set(metadata,"create",customCreate,Object);',
|
||||
'const change = Reflect.set.bind(null,{},"create",customCreate); change(Object);',
|
||||
'Reflect.apply(Reflect.set,null,[{},"create",customCreate,Object]);',
|
||||
'Reflect.set({},"create",customCreate,...receivers);',
|
||||
'Reflect.set({},"create",...valuesAndReceivers);',
|
||||
"Reflect.set({},...keysValuesAndReceivers);",
|
||||
'Reflect.set({},"create",customCreate,receiver);',
|
||||
])("rejects intrinsic writes through a bound mutator: %s", (write) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(
|
||||
`${write}\n${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP} ${WINDOW}`,
|
||||
).electron.browser_windows,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'const change = Object.defineProperty.bind(null); change(metadata,"x",{value:1});',
|
||||
'const change = Object.defineProperty.bind(null,metadata,"x"); change({value:1});',
|
||||
'const first = Object.defineProperty.bind(null,metadata); const change = first.bind(null,"x"); change({value:1});',
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null); change(metadata,"x",{value:1});',
|
||||
'const change = Function.prototype.apply.bind(Object.defineProperty,null); change([metadata,"x",{value:1}]);',
|
||||
'const change = Function.prototype.bind.call(Object.defineProperty,null,metadata,"x"); change({value:1});',
|
||||
'const bind = Function.prototype.call.bind(Function.prototype.bind); const change = bind(Object.defineProperty,null,metadata,"x"); change({value:1});',
|
||||
'const change = Function.prototype.bind.apply(Object.defineProperty,[null,metadata,"x"]); change({value:1});',
|
||||
'const change = Reflect.apply(Function.prototype.bind,Object.defineProperty,[null,metadata,"x"]); change({value:1});',
|
||||
'const apply = Reflect.apply; Reflect.apply = replacement; apply(Object.defineProperty,null,[metadata,"x",{value:1}]);',
|
||||
'const change = Object.defineProperty.bind(null,Object,"create");',
|
||||
'Object.defineProperty.call(null,metadata,"x",{value:1});',
|
||||
'Object.defineProperty.apply(null,[metadata,"x",{value:1}]);',
|
||||
'const change = Object.defineProperty.call; change.call(Object.defineProperty,null,metadata,"x",{value:1});',
|
||||
'const {apply:change} = Object.defineProperty; change.call(Object.defineProperty,null,[metadata,"x",{value:1}]);',
|
||||
'Reflect.set(metadata,"x",1,{});',
|
||||
'const receiver = {}; Reflect.set({},"x",1,receiver);',
|
||||
])("keeps a fresh bound target or unused binding separate: %s", (write) => {
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(
|
||||
`const metadata = {}; ${write}\n${ROLLDOWN_MODULE_INTEROP}\n${IMPORT} ${WRAP} ${WINDOW}`,
|
||||
).electron.browser_windows,
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unrelated exports after an aliased prebound key write", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const electron = require("electron");
|
||||
const key = "Unrelated";
|
||||
const mutate = Object.defineProperty.bind(null, electron, key);
|
||||
mutate({value: custom});
|
||||
new electron.BrowserWindow({});
|
||||
`);
|
||||
expect(analysis.electron.browser_windows).toHaveLength(1);
|
||||
});
|
||||
@@ -77,7 +77,7 @@ describe("Electron API identity", () => {
|
||||
it("follows bundler namespace interop helpers without accepting other writes", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
var __toESM = (mod) => mod;
|
||||
var __importStar = (this && this.__importStar) || ((mod) => mod);
|
||||
var __importStar = (mod) => mod;
|
||||
let electron = require("electron");
|
||||
electron = __toESM(electron);
|
||||
new electron.BrowserWindow({ webPreferences: { preload: "./rolldown.js" } });
|
||||
@@ -120,4 +120,53 @@ describe("Electron API identity", () => {
|
||||
|
||||
expect(analysis.electron.browser_windows).toEqual([]);
|
||||
});
|
||||
|
||||
it("follows stable declaration aliases but not an assigned default parameter", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const direct = require("electron");
|
||||
const constAlias = direct;
|
||||
new constAlias.BrowserWindow({webPreferences:{preload:"./const.js"}});
|
||||
|
||||
let letAlias = require("electron");
|
||||
new letAlias.BrowserWindow({webPreferences:{preload:"./let.js"}});
|
||||
|
||||
const { BrowserWindow } = require("electron");
|
||||
new BrowserWindow({webPreferences:{preload:"./destructured.js"}});
|
||||
|
||||
function withDefault(electron = require("./not-electron")) {
|
||||
if (flag) electron = require("electron");
|
||||
new electron.BrowserWindow({webPreferences:{preload:"./default.js"}});
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
analysis.electron.browser_windows.map(({ preload_path }) => preload_path),
|
||||
).toEqual(["./const.js", "./let.js", "./destructured.js"]);
|
||||
});
|
||||
|
||||
it("preserves one initializer across variable redeclarations without accepting writes", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
var electron;
|
||||
var electron = require("electron");
|
||||
var electron;
|
||||
new electron.BrowserWindow({webPreferences:{preload:"./redeclared.js"}});
|
||||
|
||||
var replaced;
|
||||
var replaced = require("electron");
|
||||
replaced = fake;
|
||||
new replaced.BrowserWindow({webPreferences:{preload:"./replaced.js"}});
|
||||
|
||||
var initializedTwice = require("electron");
|
||||
var initializedTwice = fake;
|
||||
new initializedTwice.BrowserWindow({});
|
||||
|
||||
var conditional;
|
||||
if (flag) { var conditional = require("electron"); }
|
||||
new conditional.BrowserWindow({});
|
||||
`);
|
||||
|
||||
expect(
|
||||
analysis.electron.browser_windows.map(({ preload_path }) => preload_path),
|
||||
).toEqual(["./redeclared.js"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,16 +86,20 @@ export const semanticObjectPatternKeys = (
|
||||
return key === null ? [] : [key];
|
||||
});
|
||||
|
||||
/** Visit member write targets in direct and destructuring assignment forms. */
|
||||
export function* semanticMemberWriteTargets(
|
||||
/** Visit binding and member leaves in direct and destructuring write targets. */
|
||||
export function* semanticWriteTargets(
|
||||
root: t.Node,
|
||||
): Generator<t.MemberExpression | t.OptionalMemberExpression> {
|
||||
): Generator<t.Identifier | t.MemberExpression | t.OptionalMemberExpression> {
|
||||
const pending = [root];
|
||||
while (pending.length > 0) {
|
||||
const next = pending.pop();
|
||||
if (next === undefined) break;
|
||||
const node = unwrapJavaScriptExpression(next).node;
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
|
||||
if (
|
||||
t.isIdentifier(node) ||
|
||||
t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node)
|
||||
)
|
||||
yield node;
|
||||
else if (t.isRestElement(node)) pending.push(node.argument);
|
||||
else if (t.isAssignmentPattern(node)) pending.push(node.left);
|
||||
|
||||
@@ -2,10 +2,9 @@ import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
semanticMemberWriteTargets,
|
||||
semanticWriteTargets,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
||||
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
|
||||
|
||||
/** Match Electron's supported module entry points. */
|
||||
@@ -24,7 +23,7 @@ export const electronExportPath = (
|
||||
|
||||
/** Static identity recovery cannot establish the absence of Electron usage. */
|
||||
export const ELECTRON_IDENTITY_LIMITATION =
|
||||
"Electron API identity follows proven lexical module aliases, including results of namespace interop helpers (__toESM, __importStar, _interopRequireWildcard) recognized by name and a single program-scope function definition; helper bodies are not evaluated. Other reassigned bindings, dynamic selections, and visibly overwritten namespace members are unresolved; missing findings do not establish absence. Arbitrary call side effects and runtime registration are not evaluated.";
|
||||
"Electron API identity follows proven lexical module aliases and structurally verified module-namespace interop. Other reassignments, dynamic selections, and visibly overwritten namespace members are unresolved; missing findings do not establish absence. Arbitrary call side effects and runtime registration are not evaluated.";
|
||||
|
||||
/** Preserve exact property segments instead of flattening literal dotted keys. */
|
||||
export const electronMemberPath = (
|
||||
@@ -42,14 +41,98 @@ export const electronMemberPath = (
|
||||
return { root, members: members.toReversed() };
|
||||
};
|
||||
|
||||
/** Index visible writes through proven Electron aliases, without executing them. */
|
||||
export function* collectElectronMemberWritesSteps(
|
||||
/** A known intrinsic callable and its examined positional arguments. */
|
||||
export interface JavaScriptIntrinsicInvocation {
|
||||
readonly kind: "known" | "opaque";
|
||||
/** The original bound receiver; rebinding cannot replace it. */
|
||||
readonly receiver?: t.Node;
|
||||
readonly path: readonly string[];
|
||||
readonly arguments:
|
||||
| readonly t.CallExpression["arguments"][number][]
|
||||
| undefined;
|
||||
}
|
||||
|
||||
/** Index member and binding writes through caller-proven lexical identities. */
|
||||
export function* collectJavaScriptIdentityWritesSteps(
|
||||
program: t.Program,
|
||||
origin: (root: t.Node) => readonly string[] | undefined,
|
||||
): Generator<void, SemanticPropertyPathCoverage> {
|
||||
const writes = new SemanticPropertyPathCoverage();
|
||||
origin: (
|
||||
root: t.Node,
|
||||
kind: "binding" | "member" | "opaque-call",
|
||||
) => readonly string[] | undefined,
|
||||
intrinsicInvocation: (
|
||||
node: t.CallExpression | t.OptionalCallExpression,
|
||||
) => JavaScriptIntrinsicInvocation | undefined,
|
||||
staticString: (node: t.Node | undefined) => string | undefined,
|
||||
freshObject: (node: t.Node) => boolean,
|
||||
retain: (node: t.Node, path: readonly (string | null)[]) => void,
|
||||
): Generator<void> {
|
||||
const objectMutators = new Set([
|
||||
"defineProperty",
|
||||
"defineProperties",
|
||||
"assign",
|
||||
"setPrototypeOf",
|
||||
]);
|
||||
const reflectMutators = new Set([
|
||||
"set",
|
||||
"deleteProperty",
|
||||
"defineProperty",
|
||||
"setPrototypeOf",
|
||||
]);
|
||||
const keyedMutators = new Set(["set", "deleteProperty", "defineProperty"]);
|
||||
yield* traverseJavaScriptAstSteps(program, {
|
||||
enter: (node) => {
|
||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
|
||||
const invocation = intrinsicInvocation(node);
|
||||
if (invocation?.kind === "opaque") {
|
||||
const scope = origin(node, "opaque-call");
|
||||
if (scope !== undefined) retain(node, scope);
|
||||
return;
|
||||
}
|
||||
const callee = invocation?.path;
|
||||
const arguments_ = invocation?.arguments;
|
||||
const method = callee?.at(-1);
|
||||
const owner = callee?.length === 3 ? callee[1] : undefined;
|
||||
if (
|
||||
(owner === "Object" && objectMutators.has(method ?? "")) ||
|
||||
(owner === "Reflect" && reflectMutators.has(method ?? ""))
|
||||
) {
|
||||
// Reflect.set can write through its explicit receiver as well as
|
||||
// dispatching through its target. Both identities need examination.
|
||||
const targets =
|
||||
owner === "Reflect" &&
|
||||
method === "set" &&
|
||||
arguments_ !== undefined &&
|
||||
(arguments_.length >= 4 || arguments_.some(t.isSpreadElement))
|
||||
? [
|
||||
arguments_[0],
|
||||
arguments_.some(t.isSpreadElement)
|
||||
? undefined
|
||||
: arguments_[3],
|
||||
]
|
||||
: [arguments_?.[0]];
|
||||
for (const target of targets) {
|
||||
// A known mutator with an opaque argument list can overwrite any
|
||||
// captured intrinsic. Do not turn an unexamined target into proof.
|
||||
if (target === undefined || t.isSpreadElement(target)) {
|
||||
const scope = origin(node, "opaque-call");
|
||||
if (scope !== undefined) retain(node, scope);
|
||||
}
|
||||
if (t.isNode(target) && !t.isSpreadElement(target)) {
|
||||
const { root, members } = electronMemberPath(target);
|
||||
const exported = origin(root, "member");
|
||||
if (exported !== undefined) {
|
||||
const key = keyedMutators.has(method ?? "")
|
||||
? [staticString(arguments_?.[1]) ?? null]
|
||||
: [];
|
||||
retain(node, [...exported, ...members, ...key]);
|
||||
} else if (!freshObject(target)) {
|
||||
const scope = origin(node, "opaque-call");
|
||||
if (scope !== undefined) retain(node, scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const target = t.isAssignmentExpression(node)
|
||||
? node.left
|
||||
: t.isUpdateExpression(node)
|
||||
@@ -60,12 +143,16 @@ export function* collectElectronMemberWritesSteps(
|
||||
? node.left
|
||||
: undefined;
|
||||
if (target === undefined) return;
|
||||
for (const member of semanticMemberWriteTargets(target)) {
|
||||
const { root, members } = electronMemberPath(member);
|
||||
const exported = origin(root);
|
||||
if (exported !== undefined) writes.retain([...exported, ...members]);
|
||||
for (const leaf of semanticWriteTargets(target)) {
|
||||
if (t.isIdentifier(leaf)) {
|
||||
const exported = origin(leaf, "binding");
|
||||
if (exported !== undefined) retain(node, exported);
|
||||
continue;
|
||||
}
|
||||
const { root, members } = electronMemberPath(leaf);
|
||||
const exported = origin(root, "member");
|
||||
if (exported !== undefined) retain(node, [...exported, ...members]);
|
||||
}
|
||||
},
|
||||
});
|
||||
return writes;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type { JavaScriptIntrinsicInvocation } from "./javascriptElectronMemberWrites.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
const arrayArguments = (array: t.Node | undefined) =>
|
||||
t.isArrayExpression(array) &&
|
||||
array.elements.every((element) => t.isExpression(element))
|
||||
? array.elements.filter((element): element is t.Expression =>
|
||||
t.isExpression(element),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const invocationArguments = (
|
||||
method: "call" | "apply",
|
||||
arguments_: JavaScriptIntrinsicInvocation["arguments"],
|
||||
) =>
|
||||
t.isSpreadElement(arguments_?.[0])
|
||||
? undefined
|
||||
: method === "apply"
|
||||
? arrayArguments(arguments_?.[1])
|
||||
: arguments_?.slice(1);
|
||||
|
||||
/**
|
||||
* Normalize known invocation syntax once, retaining bound argument prefixes.
|
||||
* Binding a function alone is not a write; only its eventual call is examined.
|
||||
*/
|
||||
export const resolveJavaScriptIntrinsicInvocation = (
|
||||
node: t.CallExpression | t.OptionalCallExpression,
|
||||
intrinsicPath: (node: t.Node) => readonly string[] | undefined,
|
||||
boundIntrinsic: (node: t.Node) => JavaScriptIntrinsicInvocation | undefined,
|
||||
intrinsicReadChanged: (node: t.Node, path: readonly string[]) => boolean,
|
||||
): JavaScriptIntrinsicInvocation | undefined => {
|
||||
const visited = new Set<t.Node>();
|
||||
let callee: t.Node = node.callee;
|
||||
let arguments_: JavaScriptIntrinsicInvocation["arguments"] = node.arguments;
|
||||
let receiver: t.Node | undefined;
|
||||
let explicitReceiver = false;
|
||||
for (;;) {
|
||||
callee = unwrapJavaScriptExpression(callee).node;
|
||||
if (visited.has(callee)) return undefined;
|
||||
visited.add(callee);
|
||||
let path = intrinsicPath(callee);
|
||||
if (path !== undefined && intrinsicReadChanged(callee, path))
|
||||
return { kind: "opaque", path, arguments: undefined };
|
||||
if (path === undefined) {
|
||||
if (
|
||||
t.isMemberExpression(callee) ||
|
||||
t.isOptionalMemberExpression(callee)
|
||||
) {
|
||||
const method = semanticStaticPropertyKey(
|
||||
callee.property,
|
||||
callee.computed,
|
||||
);
|
||||
if (method === "call" || method === "apply") {
|
||||
const bound = boundIntrinsic(callee.object);
|
||||
if (
|
||||
bound !== undefined &&
|
||||
intrinsicReadChanged(callee, [...bound.path, method])
|
||||
)
|
||||
return { kind: "opaque", path: bound.path, arguments: undefined };
|
||||
receiver = arguments_?.[0];
|
||||
explicitReceiver = true;
|
||||
callee = callee.object;
|
||||
arguments_ = invocationArguments(method, arguments_);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const bound = boundIntrinsic(callee);
|
||||
if (bound === undefined) return undefined;
|
||||
if (bound.kind === "opaque") return bound;
|
||||
path = bound.path;
|
||||
receiver = bound.receiver;
|
||||
explicitReceiver = true;
|
||||
arguments_ =
|
||||
bound.arguments === undefined
|
||||
? undefined
|
||||
: [...bound.arguments, ...(arguments_ ?? [])];
|
||||
}
|
||||
const method = path.length > 3 ? path.at(-1) : undefined;
|
||||
if (method === "call" || method === "apply") {
|
||||
// These properties hold generic function values. Their receiver selects
|
||||
// the callable; property-chain depth does not consume more arguments.
|
||||
const callable: t.Node | undefined = explicitReceiver
|
||||
? receiver
|
||||
: t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)
|
||||
? callee.object
|
||||
: undefined;
|
||||
if (!t.isExpression(callable)) return undefined;
|
||||
callee = callable;
|
||||
receiver = arguments_?.[0];
|
||||
explicitReceiver = true;
|
||||
arguments_ = invocationArguments(method, arguments_);
|
||||
continue;
|
||||
}
|
||||
if (path.join(".") === "intrinsic.Reflect.apply") {
|
||||
const callable = arguments_?.[0];
|
||||
if (!t.isExpression(callable)) return undefined;
|
||||
callee = callable;
|
||||
receiver = arguments_?.[1];
|
||||
explicitReceiver = true;
|
||||
arguments_ = arrayArguments(arguments_?.[2]);
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
kind: "known",
|
||||
path,
|
||||
arguments: arguments_,
|
||||
...(receiver === undefined ? {} : { receiver }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// Rolldown runtime from the public T3 Code nightly 20261011.2967 main.cjs.
|
||||
// The input is inert source: none of these helpers or Electron APIs is executed.
|
||||
export const ROLLDOWN_MODULE_INTEROP = `
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
||||
key = keys[i];
|
||||
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
||||
get: ((k) => from[k]).bind(null, key),
|
||||
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
||||
});
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
||||
value: mod,
|
||||
enumerable: true
|
||||
}) : target, mod));
|
||||
`;
|
||||
@@ -0,0 +1,774 @@
|
||||
import { parseExpression } from "@babel/parser";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
isUnshadowedGlobal,
|
||||
resolveSemanticBindingState,
|
||||
semanticScopeId,
|
||||
semanticStableInitializer,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import {
|
||||
childNodes,
|
||||
javaScriptAstChildKeys,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
electronMemberPath,
|
||||
type JavaScriptIntrinsicInvocation,
|
||||
} from "./javascriptElectronMemberWrites.js";
|
||||
import { resolveJavaScriptIntrinsicInvocation } from "./javascriptIntrinsicInvocation.js";
|
||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
// Match the complete Rolldown runtime operations, including its live getters.
|
||||
// Names and formatting are irrelevant; lexical roles, operators and keys are not.
|
||||
const TO_ESM = parseExpression(`(mod, isNodeMode, target) => (
|
||||
target = mod != null ? __create(__getProtoOf(mod)) : {},
|
||||
__copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default")
|
||||
? __defProp(target, "default", {value: mod, enumerable: true}) : target, mod)
|
||||
)`);
|
||||
const COPY_PROPS = parseExpression(`(to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function")
|
||||
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
||||
key = keys[i];
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, {
|
||||
get: ((k) => from[k]).bind(null, key),
|
||||
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
||||
});
|
||||
}
|
||||
return to;
|
||||
}`);
|
||||
const INTRINSICS: Readonly<Record<string, readonly string[]>> = {
|
||||
__create: ["Object", "create"],
|
||||
__getProtoOf: ["Object", "getPrototypeOf"],
|
||||
__defProp: ["Object", "defineProperty"],
|
||||
__getOwnPropNames: ["Object", "getOwnPropertyNames"],
|
||||
__getOwnPropDesc: ["Object", "getOwnPropertyDescriptor"],
|
||||
__hasOwnProp: ["Object", "prototype", "hasOwnProperty"],
|
||||
};
|
||||
|
||||
interface IntrinsicDependency {
|
||||
readonly path: readonly string[];
|
||||
readonly capturedAt?: t.Node;
|
||||
}
|
||||
|
||||
interface InteropOrigin {
|
||||
readonly origin: JavaScriptModuleOrigin;
|
||||
readonly dependencies: readonly IntrinsicDependency[];
|
||||
readonly defaultIsModule: boolean;
|
||||
}
|
||||
|
||||
interface InteropHelper {
|
||||
readonly dependencies: readonly IntrinsicDependency[];
|
||||
readonly readyAfter: number;
|
||||
readonly identity: boolean;
|
||||
}
|
||||
|
||||
/** Prove one inert namespace conversion without relaxing general assignment semantics. */
|
||||
export const createSemanticModuleInterop = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
) => {
|
||||
const helpers = new Map<string, InteropHelper | null>();
|
||||
const origins = new Map<string, InteropOrigin>();
|
||||
const calls = new WeakMap<t.Node, InteropOrigin>();
|
||||
const intrinsicBindings = new Map<string, readonly string[] | null>();
|
||||
const writes: {
|
||||
readonly scope: string;
|
||||
readonly offset: number;
|
||||
readonly coverage: SemanticPropertyPathCoverage;
|
||||
}[] = [];
|
||||
const invalidatedBindings = new Map<string, boolean>();
|
||||
const invalidatedCalls = new WeakMap<t.Node, boolean>();
|
||||
const opaqueMutationPath = (node: t.Node): readonly string[] => {
|
||||
let current: t.Node | undefined = node;
|
||||
while (current !== undefined) {
|
||||
if (t.isFunction(current))
|
||||
return ["opaque-intrinsic", semanticScopeId("function", current)];
|
||||
current = state.parentsByNode.get(current);
|
||||
}
|
||||
return ["intrinsic"];
|
||||
};
|
||||
|
||||
const intrinsicPath = (node: t.Node): readonly string[] | undefined => {
|
||||
let current = node;
|
||||
const visited = new Set<string>();
|
||||
const pending: { id: string; suffixLength: number }[] = [];
|
||||
let suffix: readonly string[] = [];
|
||||
const finish = (
|
||||
path: readonly string[] | undefined,
|
||||
): readonly string[] | undefined => {
|
||||
for (const binding of pending)
|
||||
intrinsicBindings.set(
|
||||
binding.id,
|
||||
path === undefined
|
||||
? null
|
||||
: path.slice(0, path.length - binding.suffixLength),
|
||||
);
|
||||
return path;
|
||||
};
|
||||
for (;;) {
|
||||
const { root, members } = electronMemberPath(current);
|
||||
if (members.some((member) => member === null)) return finish(undefined);
|
||||
suffix = [...(members as readonly string[]), ...suffix];
|
||||
for (const global of ["Object", "Function", "Reflect"])
|
||||
if (isUnshadowedGlobal(root, state, global))
|
||||
return finish(["intrinsic", global, ...suffix]);
|
||||
for (const global of ["globalThis", "global", "window", "self"])
|
||||
if (isUnshadowedGlobal(root, state, global))
|
||||
return finish(["intrinsic", ...suffix]);
|
||||
if (!t.isIdentifier(root)) return finish(undefined);
|
||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||
const cached =
|
||||
binding === undefined
|
||||
? undefined
|
||||
: intrinsicBindings.get(binding.bindingId);
|
||||
if (cached !== undefined)
|
||||
return finish(cached === null ? undefined : [...cached, ...suffix]);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
initializer.projection.some((segment) => segment === null) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return finish(undefined);
|
||||
visited.add(binding.bindingId);
|
||||
pending.push({ id: binding.bindingId, suffixLength: suffix.length });
|
||||
suffix = [...initializer.projection.map(String), ...suffix];
|
||||
current = initializer.node;
|
||||
}
|
||||
};
|
||||
|
||||
// A nominal alias path is sufficient for conservative mutation lookup, but
|
||||
// helper proof also needs each read to follow initialization. An alias copy
|
||||
// retains the earlier function capture; a member read captures it anew.
|
||||
const capturedIntrinsic = (node: t.Node): IntrinsicDependency | undefined => {
|
||||
const path = intrinsicPath(node);
|
||||
if (path === undefined) return undefined;
|
||||
const visited = new Set<string>();
|
||||
let current = node;
|
||||
let capturedAt = node;
|
||||
let suffixLength = 0;
|
||||
for (;;) {
|
||||
const { root, members } = electronMemberPath(current);
|
||||
suffixLength += members.length;
|
||||
if (!t.isIdentifier(root)) return undefined;
|
||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||
if (binding === undefined) return { path, capturedAt };
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
initializer === undefined ||
|
||||
(initializer.node.end ?? Infinity) >= (root.start ?? -1) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return undefined;
|
||||
visited.add(binding.bindingId);
|
||||
if (suffixLength === 0) capturedAt = initializer.node;
|
||||
suffixLength += initializer.projection.length;
|
||||
current = initializer.node;
|
||||
}
|
||||
};
|
||||
|
||||
const staticString = (node: t.Node | undefined): string | undefined => {
|
||||
if (node === undefined) return undefined;
|
||||
const visited = new Set<string>();
|
||||
let current = node;
|
||||
for (;;) {
|
||||
const literal = readExactJavaScriptLiteral(current);
|
||||
if (literal.found)
|
||||
return typeof literal.value === "string" ? literal.value : undefined;
|
||||
const expression = unwrapJavaScriptExpression(current).node;
|
||||
if (!t.isIdentifier(expression)) return undefined;
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
expression,
|
||||
expression.name,
|
||||
);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
initializer.projection.length !== 0 ||
|
||||
(initializer.node.end ?? Infinity) >= (expression.start ?? -1) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return undefined;
|
||||
visited.add(binding.bindingId);
|
||||
current = initializer.node;
|
||||
}
|
||||
};
|
||||
|
||||
const intrinsicReadChanged = (
|
||||
node: t.Node,
|
||||
path: readonly string[],
|
||||
): boolean => {
|
||||
const capture = capturedIntrinsic(node)?.capturedAt ?? node;
|
||||
const scope = opaqueMutationPath(capture).join(".");
|
||||
const method = path.at(-1);
|
||||
const invocationMethod =
|
||||
method === "bind" || method === "call" || method === "apply";
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capture.start ?? -1) &&
|
||||
(write.coverage.covers(path) ||
|
||||
(invocationMethod &&
|
||||
write.coverage.covers([
|
||||
"intrinsic",
|
||||
"Function",
|
||||
"prototype",
|
||||
method,
|
||||
]))),
|
||||
);
|
||||
};
|
||||
|
||||
const resolveBoundIntrinsic = (
|
||||
node: t.Node,
|
||||
): JavaScriptIntrinsicInvocation | undefined => {
|
||||
const visited = new Set<string>();
|
||||
const argumentGroups: t.CallExpression["arguments"][] = [];
|
||||
const bindReads: t.Node[] = [];
|
||||
let opaqueArguments = false;
|
||||
let receiver: t.Node | undefined;
|
||||
let current = node;
|
||||
for (;;) {
|
||||
current = unwrapJavaScriptExpression(current).node;
|
||||
if (t.isIdentifier(current)) {
|
||||
const binding = resolveSemanticBindingState(
|
||||
state,
|
||||
current,
|
||||
current.name,
|
||||
);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
initializer.projection.length !== 0 ||
|
||||
(initializer.node.end ?? Infinity) >= (current.start ?? -1) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return undefined;
|
||||
visited.add(binding.bindingId);
|
||||
current = initializer.node;
|
||||
continue;
|
||||
}
|
||||
if (!t.isCallExpression(current)) return undefined;
|
||||
const { root, members } = electronMemberPath(current.callee);
|
||||
let bindTarget = root;
|
||||
let bindingArguments: JavaScriptIntrinsicInvocation["arguments"] =
|
||||
current.arguments;
|
||||
let captured = capturedIntrinsic(current.callee);
|
||||
let terminal = captured?.path.at(-1) === "bind";
|
||||
let path: readonly string[] | undefined = captured?.path.slice(0, -1);
|
||||
if (members.at(-1) === "bind") {
|
||||
// A direct .bind may continue a bound callable, without discarding members.
|
||||
if (!terminal && members.length !== 1) return undefined;
|
||||
bindReads.push(captured?.capturedAt ?? current.callee);
|
||||
} else {
|
||||
// Reuse the invocation owner for generic call/apply/Reflect.apply of bind.
|
||||
const invocation = resolveJavaScriptIntrinsicInvocation(
|
||||
current,
|
||||
intrinsicPath,
|
||||
boundIntrinsic,
|
||||
intrinsicReadChanged,
|
||||
);
|
||||
if (
|
||||
invocation?.kind !== "known" ||
|
||||
invocation.path.join(".") !== "intrinsic.Function.prototype.bind" ||
|
||||
invocation.receiver === undefined
|
||||
)
|
||||
return undefined;
|
||||
bindTarget = invocation.receiver;
|
||||
bindingArguments = invocation.arguments;
|
||||
captured = capturedIntrinsic(bindTarget);
|
||||
terminal = captured !== undefined;
|
||||
path = captured?.path;
|
||||
}
|
||||
receiver = bindingArguments?.[0];
|
||||
const ownArguments = bindingArguments?.slice(1) ?? [];
|
||||
opaqueArguments ||=
|
||||
bindingArguments === undefined || t.isSpreadElement(receiver);
|
||||
opaqueArguments ||= ownArguments.some((argument) =>
|
||||
t.isSpreadElement(argument),
|
||||
);
|
||||
argumentGroups.push(ownArguments);
|
||||
if (!terminal || path === undefined) {
|
||||
current = bindTarget;
|
||||
continue;
|
||||
}
|
||||
const boundPath = [...path, "bind"];
|
||||
const functionBind = ["intrinsic", "Function", "prototype", "bind"];
|
||||
opaqueArguments ||= bindReads.some((capture) => {
|
||||
const scope = opaqueMutationPath(capture).join(".");
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capture.start ?? -1) &&
|
||||
(write.coverage.covers(boundPath) ||
|
||||
write.coverage.covers(functionBind)),
|
||||
);
|
||||
});
|
||||
return {
|
||||
kind: opaqueArguments ? "opaque" : "known",
|
||||
path,
|
||||
...(receiver === undefined ? {} : { receiver }),
|
||||
// Rebinding appends arguments to the earlier bound argument list.
|
||||
arguments: opaqueArguments
|
||||
? undefined
|
||||
: argumentGroups.toReversed().flat(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const activeBoundValues = new WeakSet<t.Node>();
|
||||
const boundIntrinsic = (
|
||||
node: t.Node,
|
||||
): JavaScriptIntrinsicInvocation | undefined => {
|
||||
if (activeBoundValues.has(node)) return undefined;
|
||||
activeBoundValues.add(node);
|
||||
try {
|
||||
return resolveBoundIntrinsic(node);
|
||||
} finally {
|
||||
activeBoundValues.delete(node);
|
||||
}
|
||||
};
|
||||
|
||||
const isFreshObject = (node: t.Node): boolean => {
|
||||
const visited = new Set<string>();
|
||||
let current = node;
|
||||
for (;;) {
|
||||
current = unwrapJavaScriptExpression(current).node;
|
||||
if (
|
||||
t.isObjectExpression(current) ||
|
||||
t.isArrayExpression(current) ||
|
||||
t.isFunction(current) ||
|
||||
(t.isClass(current) && !current.decorators?.length) ||
|
||||
t.isRegExpLiteral(current)
|
||||
)
|
||||
return true;
|
||||
if (!t.isIdentifier(current)) return false;
|
||||
const binding = resolveSemanticBindingState(state, current, current.name);
|
||||
const initializer = unprojectedInitializer(binding, state);
|
||||
if (
|
||||
binding === undefined ||
|
||||
initializer === undefined ||
|
||||
(!t.isFunctionDeclaration(initializer) &&
|
||||
(initializer.end ?? Infinity) >= (current.start ?? -1)) ||
|
||||
visited.has(binding.bindingId)
|
||||
)
|
||||
return false;
|
||||
visited.add(binding.bindingId);
|
||||
current = initializer;
|
||||
}
|
||||
};
|
||||
|
||||
const helperProof = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): InteropHelper | undefined => {
|
||||
const cached = helpers.get(binding.bindingId);
|
||||
if (cached !== undefined) return cached ?? undefined;
|
||||
helpers.set(binding.bindingId, null);
|
||||
if (state.scopesById.get(binding.scopeId)?.kind !== "program")
|
||||
return undefined;
|
||||
const wrapper = unprojectedInitializer(binding, state);
|
||||
if (wrapper === undefined) return undefined;
|
||||
if (identityFunction(wrapper, state)) {
|
||||
const proof = {
|
||||
dependencies: [],
|
||||
readyAfter: t.isFunctionDeclaration(wrapper)
|
||||
? -1
|
||||
: (wrapper.end ?? Infinity),
|
||||
identity: true,
|
||||
};
|
||||
helpers.set(binding.bindingId, proof);
|
||||
return proof;
|
||||
}
|
||||
const wrapperRoles = canonicalRoles(wrapper, TO_ESM, state);
|
||||
const copyBinding = wrapperRoles?.get("__copyProps");
|
||||
const copy = unprojectedInitializer(copyBinding, state);
|
||||
const copyRoles =
|
||||
copy === undefined ? undefined : canonicalRoles(copy, COPY_PROPS, state);
|
||||
if (
|
||||
wrapperRoles === undefined ||
|
||||
copyRoles === undefined ||
|
||||
copy === undefined
|
||||
)
|
||||
return undefined;
|
||||
const dependencies: IntrinsicDependency[] = [];
|
||||
let readyAfter = Math.max(wrapper.end ?? Infinity, copy.end ?? Infinity);
|
||||
for (const roles of [wrapperRoles, copyRoles])
|
||||
for (const [role, dependency] of roles) {
|
||||
const expected = INTRINSICS[role];
|
||||
if (expected === undefined) continue;
|
||||
const initializer = unprojectedInitializer(dependency, state);
|
||||
const captured =
|
||||
initializer === undefined
|
||||
? undefined
|
||||
: capturedIntrinsic(initializer);
|
||||
if (
|
||||
initializer === undefined ||
|
||||
captured === undefined ||
|
||||
JSON.stringify(captured.path) !==
|
||||
JSON.stringify(["intrinsic", ...expected])
|
||||
)
|
||||
return undefined;
|
||||
readyAfter = Math.max(readyAfter, initializer.end ?? Infinity);
|
||||
dependencies.push(captured);
|
||||
if (role === "__hasOwnProp")
|
||||
dependencies.push({ path: [...captured.path, "call"] });
|
||||
}
|
||||
dependencies.push(
|
||||
{ path: ["intrinsic", "Function", "prototype", "bind"] },
|
||||
{ path: ["intrinsic", "Function", "prototype", "call"] },
|
||||
);
|
||||
const proof = {
|
||||
dependencies,
|
||||
identity: false,
|
||||
readyAfter,
|
||||
};
|
||||
helpers.set(binding.bindingId, proof);
|
||||
return proof;
|
||||
};
|
||||
|
||||
const provenCall = (node: t.Node) => {
|
||||
if (!t.isCallExpression(node) || !t.isIdentifier(node.callee))
|
||||
return undefined;
|
||||
const helper = resolveSemanticBindingState(
|
||||
state,
|
||||
node.callee,
|
||||
node.callee.name,
|
||||
);
|
||||
const proof = helper === undefined ? undefined : helperProof(helper);
|
||||
const input = node.arguments[0];
|
||||
const mode = node.arguments[1];
|
||||
const literalMode =
|
||||
mode === undefined
|
||||
? { found: true, value: false }
|
||||
: t.isExpression(mode)
|
||||
? readExactJavaScriptLiteral(mode)
|
||||
: { found: false };
|
||||
if (
|
||||
proof === undefined ||
|
||||
proof.readyAfter >= (node.start ?? -1) ||
|
||||
!t.isExpression(input) ||
|
||||
node.arguments.length > 2 ||
|
||||
!literalMode.found
|
||||
)
|
||||
return undefined;
|
||||
return {
|
||||
input,
|
||||
// A truthy node mode forces the module-valued default. Other modes
|
||||
// preserve named exports without proving the source's default property.
|
||||
defaultIsModule: !proof.identity && Boolean(literalMode.value),
|
||||
dependencies: proof.dependencies.map((dependency) => ({
|
||||
...dependency,
|
||||
capturedAt: dependency.capturedAt ?? node,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const callOrigin = (node: t.Node): InteropOrigin | undefined => {
|
||||
const cached = calls.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
const { root, members } = electronMemberPath(node);
|
||||
if (!members.every((member): member is string => member !== null))
|
||||
return undefined;
|
||||
const converted = provenCall(root);
|
||||
const required =
|
||||
converted === undefined
|
||||
? undefined
|
||||
: semanticRequireOrigin(converted.input, state)?.origin;
|
||||
if (
|
||||
converted === undefined ||
|
||||
required === undefined ||
|
||||
required.importedPath.length !== 0 ||
|
||||
(members[0] === "default" && !converted.defaultIsModule)
|
||||
)
|
||||
return undefined;
|
||||
const result = {
|
||||
origin: { ...required, importedPath: members },
|
||||
dependencies: converted.dependencies,
|
||||
defaultIsModule: converted.defaultIsModule,
|
||||
};
|
||||
calls.set(node, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const origin = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): InteropOrigin | undefined => {
|
||||
const single = semanticStableInitializer(binding, state);
|
||||
if (single !== undefined) {
|
||||
if (
|
||||
!single.projection.every(
|
||||
(key): key is string => typeof key === "string",
|
||||
)
|
||||
)
|
||||
return undefined;
|
||||
const converted = callOrigin(single.node);
|
||||
if (
|
||||
converted?.origin.importedPath.length === 0 &&
|
||||
single.projection[0] === "default" &&
|
||||
!converted.defaultIsModule
|
||||
)
|
||||
return undefined;
|
||||
const result =
|
||||
converted === undefined
|
||||
? undefined
|
||||
: {
|
||||
...converted,
|
||||
origin: {
|
||||
...converted.origin,
|
||||
importedPath: [
|
||||
...converted.origin.importedPath,
|
||||
...single.projection,
|
||||
],
|
||||
},
|
||||
};
|
||||
if (result !== undefined) origins.set(binding.bindingId, result);
|
||||
return result;
|
||||
}
|
||||
const [initial, converted] = binding.initializers;
|
||||
if (
|
||||
binding.definitions.length !== 2 ||
|
||||
binding.definitions[0]?.kind !== "variable" ||
|
||||
binding.definitions[1]?.kind !== "assignment" ||
|
||||
binding.initializers.length !== 2 ||
|
||||
initial === undefined ||
|
||||
converted === undefined ||
|
||||
initial.projection.length !== 0 ||
|
||||
converted.projection.length !== 0 ||
|
||||
state.conditionalInitializers.has(initial.node)
|
||||
)
|
||||
return undefined;
|
||||
const call = converted.node;
|
||||
const assignment = state.parentsByNode.get(call);
|
||||
const statement =
|
||||
assignment === undefined
|
||||
? undefined
|
||||
: state.parentsByNode.get(assignment);
|
||||
const declaration = state.parentsByNode.get(initial.node);
|
||||
const declarationStatement =
|
||||
declaration === undefined
|
||||
? undefined
|
||||
: state.parentsByNode.get(declaration);
|
||||
if (
|
||||
!t.isCallExpression(call) ||
|
||||
!t.isIdentifier(call.callee) ||
|
||||
!t.isAssignmentExpression(assignment, { operator: "=" }) ||
|
||||
assignment.right !== call ||
|
||||
!t.isIdentifier(assignment.left, { name: binding.name }) ||
|
||||
!t.isExpressionStatement(statement) ||
|
||||
!t.isVariableDeclarator(declaration) ||
|
||||
!t.isVariableDeclaration(declarationStatement) ||
|
||||
state.parentsByNode.get(statement) !==
|
||||
state.parentsByNode.get(declarationStatement) ||
|
||||
(initial.node.start ?? Infinity) >= (call.start ?? -1)
|
||||
)
|
||||
return undefined;
|
||||
const convertedCall = provenCall(call);
|
||||
const input = convertedCall?.input;
|
||||
if (
|
||||
!t.isIdentifier(input) ||
|
||||
resolveSemanticBindingState(state, input, input.name) !== binding
|
||||
)
|
||||
return undefined;
|
||||
const required = semanticRequireOrigin(initial.node, state)?.origin;
|
||||
if (required === undefined || required.importedPath.length !== 0)
|
||||
return undefined;
|
||||
if (convertedCall === undefined) return undefined;
|
||||
const result = {
|
||||
origin: required,
|
||||
dependencies: convertedCall.dependencies,
|
||||
defaultIsModule: convertedCall.defaultIsModule,
|
||||
};
|
||||
origins.set(binding.bindingId, result);
|
||||
return result;
|
||||
};
|
||||
const invalidatedOrigin = (record: InteropOrigin | undefined): boolean =>
|
||||
record?.dependencies.some(({ path, capturedAt }) => {
|
||||
if (capturedAt === undefined) return true;
|
||||
const scope = opaqueMutationPath(capturedAt).join(".");
|
||||
// Captured function values survive later replacement of global
|
||||
// properties. Other callable bodies are not execution evidence.
|
||||
return writes.some(
|
||||
(write) =>
|
||||
(write.scope === scope || write.scope === "intrinsic") &&
|
||||
write.offset < (capturedAt.start ?? -1) &&
|
||||
write.coverage.covers(path),
|
||||
);
|
||||
}) ?? false;
|
||||
return {
|
||||
origin,
|
||||
callOrigin,
|
||||
intrinsicPath,
|
||||
intrinsicInvocation: (node: t.CallExpression | t.OptionalCallExpression) =>
|
||||
resolveJavaScriptIntrinsicInvocation(
|
||||
node,
|
||||
intrinsicPath,
|
||||
boundIntrinsic,
|
||||
intrinsicReadChanged,
|
||||
),
|
||||
boundIntrinsic,
|
||||
staticString,
|
||||
isFreshObject,
|
||||
opaqueMutationPath,
|
||||
retainWrite: (node: t.Node, path: readonly (string | null)[]) => {
|
||||
const coverage = new SemanticPropertyPathCoverage();
|
||||
coverage.retain(path[0] === "opaque-intrinsic" ? ["intrinsic"] : path);
|
||||
writes.push({
|
||||
scope: opaqueMutationPath(node).join("."),
|
||||
offset: node.start ?? Infinity,
|
||||
coverage,
|
||||
});
|
||||
},
|
||||
invalidated: (binding: JavaScriptSemanticBindingState): boolean => {
|
||||
const cached = invalidatedBindings.get(binding.bindingId);
|
||||
if (cached !== undefined) return cached;
|
||||
const result = invalidatedOrigin(origins.get(binding.bindingId));
|
||||
invalidatedBindings.set(binding.bindingId, result);
|
||||
return result;
|
||||
},
|
||||
invalidatedCall: (node: t.Node): boolean => {
|
||||
const cached = invalidatedCalls.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
const result = invalidatedOrigin(calls.get(node));
|
||||
invalidatedCalls.set(node, result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const identityFunction = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
if (
|
||||
!t.isFunction(node) ||
|
||||
node.async ||
|
||||
node.generator ||
|
||||
node.params.length !== 1
|
||||
)
|
||||
return false;
|
||||
const parameter = node.params[0];
|
||||
const result = t.isBlockStatement(node.body)
|
||||
? node.body.body.length === 1 && t.isReturnStatement(node.body.body[0])
|
||||
? node.body.body[0].argument
|
||||
: undefined
|
||||
: node.body;
|
||||
return (
|
||||
t.isIdentifier(parameter) &&
|
||||
t.isIdentifier(result) &&
|
||||
resolveSemanticBindingState(state, parameter, parameter.name) !==
|
||||
undefined &&
|
||||
resolveSemanticBindingState(state, parameter, parameter.name) ===
|
||||
resolveSemanticBindingState(state, result, result.name)
|
||||
);
|
||||
};
|
||||
|
||||
const unprojectedInitializer = (
|
||||
binding: JavaScriptSemanticBindingState | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): t.Node | undefined => {
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
return initializer?.projection.length === 0 ? initializer.node : undefined;
|
||||
};
|
||||
|
||||
/** Match operations and lexical roles, allowing single-statement control blocks. */
|
||||
const canonicalRoles = (
|
||||
actual: t.Node,
|
||||
pattern: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): ReadonlyMap<string, JavaScriptSemanticBindingState> | undefined => {
|
||||
const roles = new Map<string, JavaScriptSemanticBindingState>();
|
||||
const used = new Map<string, string>();
|
||||
const pending: { actual: t.Node; pattern: t.Node; parent: t.Node | null }[] =
|
||||
[{ actual, pattern, parent: null }];
|
||||
while (pending.length !== 0) {
|
||||
const pair = pending.pop();
|
||||
if (pair === undefined) return undefined;
|
||||
const { parent } = pair;
|
||||
const controlBody =
|
||||
(t.isIfStatement(parent) &&
|
||||
(parent.consequent === pair.pattern ||
|
||||
parent.alternate === pair.pattern)) ||
|
||||
(t.isForStatement(parent) && parent.body === pair.pattern);
|
||||
const node = controlBody ? controlBodyOperation(pair.actual) : pair.actual;
|
||||
const expected = controlBody
|
||||
? controlBodyOperation(pair.pattern)
|
||||
: pair.pattern;
|
||||
if (node.type !== expected.type) return undefined;
|
||||
const literalKey =
|
||||
parent !== null &&
|
||||
((t.isMemberExpression(parent) &&
|
||||
!parent.computed &&
|
||||
parent.property === expected) ||
|
||||
(t.isObjectProperty(parent) &&
|
||||
!parent.computed &&
|
||||
parent.key === expected));
|
||||
if (t.isIdentifier(expected) && t.isIdentifier(node) && !literalKey) {
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
if (
|
||||
binding === undefined ||
|
||||
(roles.has(expected.name) && roles.get(expected.name) !== binding) ||
|
||||
(used.has(binding.bindingId) &&
|
||||
used.get(binding.bindingId) !== expected.name)
|
||||
)
|
||||
return undefined;
|
||||
roles.set(expected.name, binding);
|
||||
used.set(binding.bindingId, expected.name);
|
||||
}
|
||||
const visitorKeys = javaScriptAstChildKeys(expected);
|
||||
for (const [key, field] of Object.entries(
|
||||
t.NODE_FIELDS[expected.type] ?? {},
|
||||
)) {
|
||||
if (
|
||||
visitorKeys.includes(key) ||
|
||||
(t.isIdentifier(expected) && key === "name" && !literalKey)
|
||||
)
|
||||
continue;
|
||||
const fallback: unknown = Reflect.get(field, "default");
|
||||
if (
|
||||
JSON.stringify(Reflect.get(node, key) ?? fallback) !==
|
||||
JSON.stringify(Reflect.get(expected, key) ?? fallback)
|
||||
)
|
||||
return undefined;
|
||||
}
|
||||
const children = childNodes(node),
|
||||
expectedChildren = childNodes(expected);
|
||||
if (children.length !== expectedChildren.length) return undefined;
|
||||
for (const [index, child] of children.entries()) {
|
||||
const expectedChild = expectedChildren[index];
|
||||
if (expectedChild !== undefined)
|
||||
pending.push({
|
||||
actual: child,
|
||||
pattern: expectedChild,
|
||||
parent: expected,
|
||||
});
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
};
|
||||
|
||||
// Braces around one control-body statement do not add operations. Keep other
|
||||
// blocks, directives and extra statements intact; their semantics need proof.
|
||||
// Lexical declarations/references still pass through canonicalRoles' bijection.
|
||||
const controlBodyOperation = (node: t.Node): t.Node => {
|
||||
while (
|
||||
t.isBlockStatement(node) &&
|
||||
node.directives.length === 0 &&
|
||||
node.body.length === 1
|
||||
) {
|
||||
const statement = node.body[0];
|
||||
if (statement === undefined) break;
|
||||
node = statement;
|
||||
}
|
||||
return node;
|
||||
};
|
||||
@@ -195,6 +195,32 @@ describe("tRPC binding identity and bundled syntax", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains one client initializer across variable redeclarations", () => {
|
||||
expect(
|
||||
rpcEndpoints(`
|
||||
var c;
|
||||
var c = createTRPCClient({});
|
||||
var c;
|
||||
c.users.query();
|
||||
|
||||
var replaced;
|
||||
var replaced = createTRPCClient({});
|
||||
replaced = pool;
|
||||
replaced.posts.query();
|
||||
|
||||
var initializedTwice = createTRPCClient({});
|
||||
var initializedTwice = pool;
|
||||
initializedTwice.comments.query();
|
||||
|
||||
var conditional;
|
||||
if (flag) { var conditional = createTRPCClient({}); }
|
||||
conditional.groups.query();
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: "network", value: "users", mechanism: "trpc:procedure:query" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognizes surviving factory names inside bundled call wrappers", () => {
|
||||
expect(
|
||||
rpcEndpoints(`
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import {
|
||||
semanticObjectPatternKeys,
|
||||
semanticStaticPropertyKey,
|
||||
semanticWriteTargets,
|
||||
} from "./javascriptAstValues.js";
|
||||
import type {
|
||||
JavaScriptSemanticAnalysisState,
|
||||
@@ -31,16 +32,14 @@ import {
|
||||
resolveSemanticBindingFromScope,
|
||||
semanticBindingId,
|
||||
semanticScopeId,
|
||||
semanticStableInitializer,
|
||||
semanticVariableScope,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import {
|
||||
completeSemanticSteps,
|
||||
traverseJavaScriptAstSteps,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
semanticNamespaceInteropOperand,
|
||||
semanticRequireOrigin,
|
||||
} from "./javascriptSemanticRequireOrigin.js";
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { collectSemanticMemberMutationsSteps } from "./javascriptSemanticMemberMutations.js";
|
||||
import {
|
||||
collectSemanticReturns,
|
||||
@@ -49,7 +48,7 @@ import {
|
||||
import { collectJavaScriptDerivedSemanticsSteps } from "./javascriptSemanticDerivedAnalysis.js";
|
||||
import { evaluateSemanticProvenance } from "./javascriptSemanticValues.js";
|
||||
import {
|
||||
collectElectronMemberWritesSteps,
|
||||
collectJavaScriptIdentityWritesSteps,
|
||||
electronExportPath,
|
||||
electronMemberPath,
|
||||
ELECTRON_MODULE,
|
||||
@@ -64,6 +63,8 @@ import {
|
||||
} from "./javascriptSourceParser.js";
|
||||
|
||||
import { isJavaScriptRpcClientFactory } from "./javascriptRpcEndpoints.js";
|
||||
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
||||
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
|
||||
|
||||
interface BindPatternInput {
|
||||
readonly pattern: t.Node;
|
||||
@@ -205,13 +206,10 @@ export function* classifyParsedJavaScriptRpcClientRootsSteps(
|
||||
if (binding === undefined) return;
|
||||
let client = clients.get(binding.bindingId);
|
||||
if (client === undefined) {
|
||||
const [initializer] = binding.initializers;
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
client =
|
||||
initializer !== undefined &&
|
||||
initializer.projection.length === 0 &&
|
||||
binding.initializers.length === 1 &&
|
||||
!binding.definitions.some(({ kind }) => kind === "assignment") &&
|
||||
!state.conditionalInitializers.has(initializer.node) &&
|
||||
isJavaScriptRpcClientFactory(initializer.node);
|
||||
clients.set(binding.bindingId, client);
|
||||
}
|
||||
@@ -374,6 +372,9 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
|
||||
const facts = new Map<number, readonly string[]>();
|
||||
const state = createState(file.program);
|
||||
yield* collectDefinitionsSteps(file.program, state);
|
||||
const interop = createSemanticModuleInterop(state);
|
||||
const writes = new SemanticPropertyPathCoverage();
|
||||
let collectedWrites = false;
|
||||
const exportsByBinding = new Map<string, readonly string[] | null>();
|
||||
const rootExport = (root: t.Node): readonly string[] | undefined => {
|
||||
if (t.isIdentifier(root)) {
|
||||
@@ -382,23 +383,45 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
|
||||
if (binding === undefined) return undefined;
|
||||
let exported = exportsByBinding.get(binding.bindingId);
|
||||
if (exported === undefined) {
|
||||
exported = electronExport(binding, state);
|
||||
exported = electronExport(binding, state, interop);
|
||||
exportsByBinding.set(binding.bindingId, exported);
|
||||
}
|
||||
return exported ?? undefined;
|
||||
return collectedWrites && interop.invalidated(binding)
|
||||
? undefined
|
||||
: (exported ?? undefined);
|
||||
}
|
||||
const origin = semanticRequireOrigin(
|
||||
semanticNamespaceInteropOperand(root, state) ?? root,
|
||||
state,
|
||||
)?.origin;
|
||||
const wrapped = interop.callOrigin(root);
|
||||
const origin =
|
||||
wrapped === undefined
|
||||
? semanticRequireOrigin(root, state)?.origin
|
||||
: collectedWrites && interop.invalidatedCall(root)
|
||||
? undefined
|
||||
: wrapped.origin;
|
||||
return origin !== undefined && ELECTRON_MODULE.test(origin.specifier)
|
||||
? electronExportPath(origin.importedPath)
|
||||
: undefined;
|
||||
};
|
||||
const writes = yield* collectElectronMemberWritesSteps(
|
||||
yield* collectJavaScriptIdentityWritesSteps(
|
||||
file.program,
|
||||
rootExport,
|
||||
(root, kind) => {
|
||||
if (kind === "opaque-call") return interop.opaqueMutationPath(root);
|
||||
const exported = kind === "binding" ? undefined : rootExport(root);
|
||||
return exported === undefined
|
||||
? (interop.intrinsicPath(root) ??
|
||||
(kind === "member"
|
||||
? interop.boundIntrinsic(root)?.path
|
||||
: undefined))
|
||||
: ["electron", ...exported];
|
||||
},
|
||||
interop.intrinsicInvocation,
|
||||
interop.staticString,
|
||||
interop.isFreshObject,
|
||||
(node, path) => {
|
||||
if (path[0] === "electron") writes.retain(path);
|
||||
else interop.retainWrite(node, path);
|
||||
},
|
||||
);
|
||||
collectedWrites = true;
|
||||
yield* traverseJavaScriptAstSteps(file.program, {
|
||||
enter: (node) => {
|
||||
if (
|
||||
@@ -409,7 +432,10 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
|
||||
return;
|
||||
const { root, members } = electronMemberPath(node.callee);
|
||||
const exported = rootExport(root);
|
||||
if (exported !== undefined && !writes.covers([...exported, ...members]))
|
||||
if (
|
||||
exported !== undefined &&
|
||||
!writes.covers(["electron", ...exported, ...members])
|
||||
)
|
||||
facts.set(root.start ?? -1, exported);
|
||||
},
|
||||
});
|
||||
@@ -421,19 +447,16 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
|
||||
const electronExport = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
interop: ReturnType<typeof createSemanticModuleInterop>,
|
||||
): readonly string[] | null => {
|
||||
const initializers = namespacePreservingInitializers(binding, state);
|
||||
if (initializers === undefined) return null;
|
||||
const directOrigin = bindingOrigin(binding, initializers, state);
|
||||
const [initializer] = initializers;
|
||||
if (
|
||||
directOrigin === undefined &&
|
||||
(initializer === undefined ||
|
||||
initializers.length !== 1 ||
|
||||
initializers.length !== binding.initializers.length ||
|
||||
state.conditionalInitializers.has(initializer.node))
|
||||
)
|
||||
return null;
|
||||
const reassigned = binding.definitions.some(
|
||||
({ kind }) => kind === "assignment",
|
||||
);
|
||||
const converted = interop.origin(binding);
|
||||
if (reassigned && converted === undefined) return null;
|
||||
const directOrigin = converted?.origin ?? bindingOrigin(binding, state);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (directOrigin === undefined && initializer === undefined) return null;
|
||||
const provenance =
|
||||
directOrigin === undefined
|
||||
? evaluateSemanticProvenance(binding, state)
|
||||
@@ -448,66 +471,17 @@ const electronExport = (
|
||||
: null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the initializers that can change which module a binding names.
|
||||
* Bundlers emit `name = __toESM(name)` after a CommonJS require; rewrapping
|
||||
* the binding's own value keeps every member, so it is not a reassignment.
|
||||
*/
|
||||
const namespacePreservingInitializers = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticBindingState["initializers"] | undefined => {
|
||||
const initializers = binding.initializers.filter(
|
||||
({ node }) => !rewrapsOwnNamespace(binding, node, state),
|
||||
);
|
||||
const rewraps = binding.initializers.length - initializers.length;
|
||||
const assignments = binding.definitions.filter(
|
||||
({ kind }) => kind === "assignment",
|
||||
).length;
|
||||
return assignments === rewraps ? initializers : undefined;
|
||||
};
|
||||
|
||||
const rewrapsOwnNamespace = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
const assignment = state.parentsByNode.get(node);
|
||||
const operand = semanticNamespaceInteropOperand(node, state);
|
||||
return (
|
||||
t.isAssignmentExpression(assignment) &&
|
||||
assignment.operator === "=" &&
|
||||
assignment.right === node &&
|
||||
t.isIdentifier(assignment.left, { name: binding.name }) &&
|
||||
t.isIdentifier(operand, { name: binding.name }) &&
|
||||
!semanticResolutionBlocked(state, operand, operand.name) &&
|
||||
resolveSemanticBindingState(state, operand, operand.name) === binding
|
||||
);
|
||||
};
|
||||
|
||||
const bindingOrigin = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
initializers: JavaScriptSemanticBindingState["initializers"],
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptModuleOrigin | undefined => {
|
||||
if (binding.directOrigins.length > 0)
|
||||
return binding.directOrigins.length === 1
|
||||
? binding.directOrigins[0]
|
||||
: undefined;
|
||||
const [initializer] = initializers;
|
||||
// A var/let declaration permits writes without proving one occurred.
|
||||
// Require one unconditional initializer and no actual binding writes.
|
||||
if (
|
||||
initializer === undefined ||
|
||||
initializers.length !== 1 ||
|
||||
state.conditionalInitializers.has(initializer.node)
|
||||
)
|
||||
return undefined;
|
||||
const origin = semanticRequireOrigin(
|
||||
semanticNamespaceInteropOperand(initializer.node, state) ??
|
||||
initializer.node,
|
||||
state,
|
||||
)?.origin;
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
if (initializer === undefined) return undefined;
|
||||
const origin = semanticRequireOrigin(initializer.node, state)?.origin;
|
||||
const projection = initializer.projection;
|
||||
return origin === undefined ||
|
||||
!projection.every((key): key is string => typeof key === "string")
|
||||
@@ -650,10 +624,11 @@ function* collectDefinitionsSteps(
|
||||
for (const [index, { body, scope, parameters }] of functionBodies.entries()) {
|
||||
if (index % 64 === 0) yield;
|
||||
for (const parameter of parameters)
|
||||
for (const identifier of assignedPatternIdentifiers(
|
||||
for (const identifier of semanticWriteTargets(
|
||||
t.isTSParameterProperty(parameter) ? parameter.parameter : parameter,
|
||||
))
|
||||
copyParameterToBody({ identifier, body, scope, state });
|
||||
if (t.isIdentifier(identifier))
|
||||
copyParameterToBody({ identifier, body, scope, state });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,16 +776,18 @@ const bindAssignment = (
|
||||
state,
|
||||
);
|
||||
else
|
||||
for (const identifier of assignedPatternIdentifiers(node.left))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
for (const identifier of semanticWriteTargets(node.left))
|
||||
if (t.isIdentifier(identifier))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
} else if (t.isUpdateExpression(node) && t.isIdentifier(node.argument))
|
||||
addAssignment(node.argument, node, scope, state);
|
||||
else if (t.isForOfStatement(node) || t.isForInStatement(node))
|
||||
for (const pattern of t.isVariableDeclaration(node.left)
|
||||
? node.left.declarations.map(({ id }) => id)
|
||||
: [node.left]) {
|
||||
for (const identifier of assignedPatternIdentifiers(pattern))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
for (const identifier of semanticWriteTargets(pattern))
|
||||
if (t.isIdentifier(identifier))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
if (t.isForOfStatement(node))
|
||||
// Keep the loop value unknown, but preserve references yielded into
|
||||
// each bound slot so writes/escapes can reach their iterable origins.
|
||||
@@ -825,30 +802,6 @@ const bindAssignment = (
|
||||
}
|
||||
};
|
||||
|
||||
const assignedPatternIdentifiers = (
|
||||
pattern: t.Node,
|
||||
): readonly t.Identifier[] => {
|
||||
const identifiers: t.Identifier[] = [];
|
||||
const pending = [pattern];
|
||||
while (pending.length > 0) {
|
||||
const node = pending.pop();
|
||||
if (node === undefined) continue;
|
||||
if (t.isIdentifier(node)) identifiers.push(node);
|
||||
else if (t.isRestElement(node)) pending.push(node.argument);
|
||||
else if (t.isAssignmentPattern(node)) pending.push(node.left);
|
||||
else if (t.isArrayPattern(node)) {
|
||||
for (const element of node.elements)
|
||||
if (element !== null) pending.push(element);
|
||||
} else if (t.isObjectPattern(node)) {
|
||||
for (const property of node.properties)
|
||||
pending.push(
|
||||
t.isRestElement(property) ? property.argument : property.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
return identifiers;
|
||||
};
|
||||
|
||||
const nestedScope = (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
|
||||
@@ -33,7 +33,7 @@ import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
semanticMemberWriteTargets,
|
||||
semanticWriteTargets,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
@@ -401,14 +401,15 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
markEscaped(expression.object, mutation);
|
||||
};
|
||||
const markTarget = (node: t.Node, mutation: t.Node): void => {
|
||||
for (const target of semanticMemberWriteTargets(node))
|
||||
markValue(
|
||||
target.object,
|
||||
[semanticStaticPropertyKey(target.property, target.computed)],
|
||||
new Set(),
|
||||
"write",
|
||||
mutation,
|
||||
);
|
||||
for (const target of semanticWriteTargets(node))
|
||||
if (!t.isIdentifier(target))
|
||||
markValue(
|
||||
target.object,
|
||||
[semanticStaticPropertyKey(target.property, target.computed)],
|
||||
new Set(),
|
||||
"write",
|
||||
mutation,
|
||||
);
|
||||
};
|
||||
yield* traverseJavaScriptAstSteps(program, {
|
||||
enter: (node, parent) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
@@ -59,73 +58,3 @@ export const semanticRequireOrigin = (
|
||||
call: current,
|
||||
};
|
||||
};
|
||||
|
||||
// esbuild/Rolldown, TypeScript, and Babel emit these helpers for namespace
|
||||
// imports of CommonJS modules; each returns an object exposing every own
|
||||
// member of its operand, so member paths through the result are unchanged.
|
||||
// Helper bodies are not evaluated: recognition trusts a single program-scope
|
||||
// function definition under one of these emitted names.
|
||||
const NAMESPACE_INTEROP_HELPERS = new Set([
|
||||
"__importStar",
|
||||
"__toESM",
|
||||
"_interopRequireWildcard",
|
||||
]);
|
||||
|
||||
/** Return the module operand of a locally declared namespace interop helper. */
|
||||
export const semanticNamespaceInteropOperand = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): t.Expression | undefined => {
|
||||
const call = unwrapJavaScriptExpression(node).node;
|
||||
if (
|
||||
!t.isCallExpression(call) ||
|
||||
!t.isIdentifier(call.callee) ||
|
||||
!NAMESPACE_INTEROP_HELPERS.has(call.callee.name) ||
|
||||
semanticResolutionBlocked(state, call.callee, call.callee.name)
|
||||
)
|
||||
return undefined;
|
||||
const helper = resolveSemanticBindingState(
|
||||
state,
|
||||
call.callee,
|
||||
call.callee.name,
|
||||
);
|
||||
const [operand] = call.arguments;
|
||||
return helper !== undefined &&
|
||||
emittedInteropHelper(helper, state) &&
|
||||
t.isExpression(operand)
|
||||
? operand
|
||||
: undefined;
|
||||
};
|
||||
|
||||
// Emitters declare each helper once at program scope as a function, an
|
||||
// arrow/function expression, or `(this && this.name) || <function>`.
|
||||
const emittedInteropHelper = (
|
||||
helper: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
const [initializer] = helper.initializers;
|
||||
if (
|
||||
initializer === undefined ||
|
||||
helper.initializers.length !== 1 ||
|
||||
helper.definitions.length !== 1 ||
|
||||
(helper.kind !== "function" && helper.kind !== "variable") ||
|
||||
state.scopesById.get(helper.scopeId)?.kind !== "program"
|
||||
)
|
||||
return false;
|
||||
const value = unwrapJavaScriptExpression(initializer.node).node;
|
||||
const emitted =
|
||||
t.isLogicalExpression(value, { operator: "||" }) &&
|
||||
t.isLogicalExpression(unwrapJavaScriptExpression(value.left).node, {
|
||||
operator: "&&",
|
||||
})
|
||||
? unwrapJavaScriptExpression(value.right).node
|
||||
: value;
|
||||
return (
|
||||
t.isFunctionDeclaration(emitted) ||
|
||||
t.isFunctionExpression(emitted) ||
|
||||
t.isArrowFunctionExpression(emitted) ||
|
||||
(t.isCallExpression(emitted) &&
|
||||
(t.isFunctionExpression(emitted.callee) ||
|
||||
t.isArrowFunctionExpression(emitted.callee)))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,6 +75,30 @@ export interface JavaScriptSemanticAnalysisState {
|
||||
readonly conditionalInitializers: WeakSet<t.Node>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return one unconditional source initializer for a declared lexical binding.
|
||||
* Repeated variable declarations without another initializer do not write it.
|
||||
* This establishes a stable source fact only; it does not prove immutability or
|
||||
* that the declaration has executed at a particular program point.
|
||||
*/
|
||||
export const semanticStableInitializer = (
|
||||
binding: JavaScriptSemanticBindingState | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticBindingState["initializers"][number] | undefined => {
|
||||
const [initializer] = binding?.initializers ?? [];
|
||||
const [definition] = binding?.definitions ?? [];
|
||||
return binding !== undefined &&
|
||||
((definition?.kind === "variable" &&
|
||||
binding.definitions.every(({ kind }) => kind === "variable")) ||
|
||||
(binding.definitions.length === 1 &&
|
||||
(definition?.kind === "function" || definition?.kind === "class"))) &&
|
||||
binding.initializers.length === 1 &&
|
||||
initializer !== undefined &&
|
||||
!state.conditionalInitializers.has(initializer.node)
|
||||
? initializer
|
||||
: undefined;
|
||||
};
|
||||
|
||||
/** Return the active scope from a non-empty construction stack. */
|
||||
export const currentSemanticScope = (
|
||||
stack: readonly JavaScriptSemanticScopeState[],
|
||||
|
||||
@@ -82,8 +82,7 @@ interface TraversalFrame {
|
||||
* not be reintroduced (they diverge on loc/comment fields and ordering).
|
||||
*/
|
||||
export const childNodes = (node: t.Node): t.Node[] => {
|
||||
const keys = t.VISITOR_KEYS[node.type];
|
||||
if (keys === undefined) return [];
|
||||
const keys = javaScriptAstChildKeys(node);
|
||||
const children: t.Node[] = [];
|
||||
for (const key of keys) {
|
||||
const value: unknown = Reflect.get(node, key);
|
||||
@@ -98,3 +97,7 @@ export const childNodes = (node: t.Node): t.Node[] => {
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
/** Child-bearing fields; shared by traversal and structural AST comparison. */
|
||||
export const javaScriptAstChildKeys = (node: t.Node): readonly string[] =>
|
||||
t.VISITOR_KEYS[node.type] ?? [];
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { ROLLDOWN_MODULE_INTEROP } from "../../../src/domain/javascript/javascriptModuleInterop.fixture.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const INTRINSIC_MUTATIONS = [
|
||||
["poisoned.cjs", "({Object} = replacements);"],
|
||||
[
|
||||
"projected.cjs",
|
||||
'const {defineProperty:change} = Object; change(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"decorated.cjs",
|
||||
'@(() => Object) class metadata {}; Object.defineProperty(metadata,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"uncurried.cjs",
|
||||
'const change = Function.prototype.call.bind(Object.defineProperty,null); change(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"generic-bind.cjs",
|
||||
'const change = Function.prototype.bind.call(Object.defineProperty,null); change(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"replaced-apply.cjs",
|
||||
'Reflect.apply = Object.defineProperty; Reflect.apply(Object,"create",{value:customCreate});',
|
||||
],
|
||||
[
|
||||
"bound.cjs",
|
||||
'const change = Object.defineProperty.bind(null,Object,"create"); change({value:customCreate});',
|
||||
],
|
||||
["receiver.cjs", 'Reflect.set({},"create",customCreate,Object);'],
|
||||
] as const;
|
||||
|
||||
const writePoisonedModules = async (root: string): Promise<void> => {
|
||||
for (const [name, mutation] of INTRINSIC_MUTATIONS)
|
||||
await writeFile(
|
||||
join(root, name),
|
||||
`${mutation}
|
||||
${ROLLDOWN_MODULE_INTEROP}
|
||||
let poisoned = require("electron");
|
||||
poisoned = __toESM(poisoned, 1);
|
||||
new poisoned.BrowserWindow({});
|
||||
throw new Error("must not execute poisoned source");`,
|
||||
);
|
||||
};
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"retains converted Electron windows and preload evidence through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-electron-interop-");
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ main: "main.cjs" }),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "preload.js"),
|
||||
'throw new Error("must stay inert");',
|
||||
);
|
||||
await writePoisonedModules(root);
|
||||
const source = `const metadata = {};
|
||||
Object.defineProperty(metadata,"x",{value:1});
|
||||
const decorateMetadata = Object.defineProperty.bind(null,metadata,"bound");
|
||||
decorateMetadata({value:1});
|
||||
const decorateViaCall = Function.prototype.call.bind(Object.defineProperty);
|
||||
decorateViaCall(null,metadata,"uncurried",{value:1});
|
||||
const localFunction = () => {};
|
||||
Object.defineProperty(localFunction,"x",{value:1});
|
||||
const localClass = class {};
|
||||
Object.defineProperty(localClass,"x",{value:1});
|
||||
const localRegExp = /local/;
|
||||
Object.defineProperty(localRegExp,"x",{value:1});
|
||||
Object.defineProperty(hoistedFunction,"x",{value:1});
|
||||
function hoistedFunction() {}
|
||||
const captured = Object.defineProperty;
|
||||
Object.defineProperty = custom;
|
||||
${ROLLDOWN_MODULE_INTEROP.replace("__defProp = Object.defineProperty", "__defProp = captured")}
|
||||
let electron = require("electron");
|
||||
electron = __toESM(electron, 1);
|
||||
new electron.BrowserWindow({webPreferences:{preload:"./preload.js"}});
|
||||
new electron.BrowserWindow({webPreferences:{preload:runtimePath}});
|
||||
var direct;
|
||||
var direct = require("electron");
|
||||
var direct;
|
||||
new direct.BrowserWindow({});
|
||||
const fresh = __toESM(require("electron"), 1);
|
||||
new fresh.BrowserWindow({});
|
||||
const {BrowserWindow: ProjectedWindow} = __toESM(require("electron"));
|
||||
new ProjectedWindow({});
|
||||
const nonNode = __toESM(require("electron"), 0);
|
||||
new nonNode.BrowserWindow({});
|
||||
function identity(mod) {return mod;}
|
||||
const passthrough = identity(require("electron"));
|
||||
new passthrough.BrowserWindow({});
|
||||
const __importStar = () => ({BrowserWindow:FakeWindow});
|
||||
const fake = __importStar(require("electron"));
|
||||
new fake.BrowserWindow({});
|
||||
function decoy(electron) {new electron.BrowserWindow({});}
|
||||
require("./poisoned.cjs");
|
||||
require("./projected.cjs");
|
||||
require("./decorated.cjs");
|
||||
require("./bound.cjs");
|
||||
require("./uncurried.cjs");
|
||||
require("./generic-bind.cjs");
|
||||
require("./replaced-apply.cjs");
|
||||
require("./receiver.cjs");
|
||||
throw new Error("must not execute main");
|
||||
`;
|
||||
await writeFile(join(root, "main.cjs"), source);
|
||||
let document: unknown;
|
||||
if (surface === "CLI") {
|
||||
const response = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--json"],
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
document = response.json;
|
||||
} else {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const response = await call("analyze_javascript_application", {
|
||||
input_path: root,
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
document = response.structuredContent;
|
||||
}
|
||||
const result = z
|
||||
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
||||
.parse(document).normalized_result;
|
||||
expect(result.summary).toMatchObject({
|
||||
browser_windows: 7,
|
||||
preload_entrypoints: 1,
|
||||
});
|
||||
expect(result.statistics.parse_failures).toBe(0);
|
||||
expect(result.statistics.parsed_javascript_files).toBe(10);
|
||||
const windows = result.graph.nodes.filter(
|
||||
({ kind }) => kind === "browser-window",
|
||||
);
|
||||
expect(windows).toHaveLength(7);
|
||||
const expectedLines = source
|
||||
.split("\n")
|
||||
.flatMap((line, index) =>
|
||||
/^\s*new (?:(?:electron|direct|fresh|passthrough|nonNode)\.BrowserWindow|ProjectedWindow)\(/u.test(
|
||||
line,
|
||||
)
|
||||
? [index + 1]
|
||||
: [],
|
||||
);
|
||||
const observedLines = windows.flatMap((window) => {
|
||||
const location = window.observations[0]?.evidence.location;
|
||||
return location?.available && location.value.kind === "source-range"
|
||||
? [location.value.start.line]
|
||||
: [];
|
||||
});
|
||||
expect(observedLines.toSorted()).toEqual(expectedLines.toSorted());
|
||||
for (const window of windows) {
|
||||
expect(window.observations[0]?.evidence.location).toMatchObject({
|
||||
available: true,
|
||||
value: { kind: "source-range", source: "main.cjs" },
|
||||
});
|
||||
}
|
||||
expect(
|
||||
windows.flatMap((window) =>
|
||||
window.observations.map(({ properties }) => properties.web_preferences),
|
||||
),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "preload",
|
||||
value: {
|
||||
status: "dynamic",
|
||||
value: null,
|
||||
expression: "runtimePath",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user