Compare commits

...
Author SHA1 Message Date
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822morluto
44457d0990 fix(javascript): integrate Reflect.set receiver mutation coverage
Retain concurrent explicit receiver and opaque argument coverage while
preserving corrected call/apply receiver controls and the public CLI/MCP
fixture. Keep the complete invocation follow-up history without rewriting it.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-11 17:06:34 +00:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822morluto
2e860e2823 fix(javascript): integrate intrinsic binding and receiver proofs
Preserve generic binding factories and replaced-intrinsic checks from the
concurrent follow-up. Resolve call/apply by actual callable receiver rather
than nominal property-chain depth. Retain deep invocation, bound receiver,
fresh-target and public CLI/MCP regression coverage.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-11 17:01:30 +00:00
morluto c98d326514 fix(javascript): retain Reflect set receiver effects 2026-10-12 01:01:18 +08:00
morluto ad27b8b4ff fix(javascript): prove generic binding and invocation effects 2026-10-12 00:55:02 +08:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
0285701033 fix(javascript): preserve callable receivers through generic invocations
Follow actual callable receivers for Function.prototype.call and apply,
including bound and rebinding forms. Treat call/apply projections as generic
function values instead of stripping arguments by property-chain depth.
Keep opaque arguments conservative and preserve fresh local target identity.
Cover uncurry, nested chains and the public CLI/MCP boundary.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 16:50:52 +00:00
morluto 3e9bce6249 fix(javascript): preserve bound receivers for intrinsic invocation 2026-10-12 00:39:01 +08:00
morluto 1e1a611012 fix(javascript): retain bound intrinsic mutation effects 2026-10-12 00:34:35 +08:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
b60248c462 fix(javascript): integrate current main stability fixes
Include the merged Hopper reconnect-listener readiness fix so local and CI
validation use the same current main recovery boundary.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 16:25:37 +00:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
2a9fdde5db fix(javascript): retain stable declarations and bound intrinsic writes
Keep repeated variable declarations with one unconditional initializer while
rejecting assignments, conditional initialization and multiple values.
Normalize known intrinsic calls, aliases and bound argument prefixes at the
semantic owner, retaining opaque targets without tainting unused bindings or
fresh local objects. Cover both regressions through source and CLI/MCP tests.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 16:25:15 +00:00
morluto 3d1d5b56b2 fix(javascript): preserve proven interop result projections 2026-10-12 00:10:02 +08:00
morluto 6b460683d7 fix(javascript): respect hoisted fresh function declarations
Function declarations provide fresh targets before their textual position. Retain initialization-order checks for variable and class values, including aliases. Cover the reviewed regression with source controls and the public CLI/MCP fixture.
2026-10-11 23:56:06 +08:00
morluto 548a4334c4 fix(javascript): preserve intrinsic alias capture timing
Validate each alias read after initialization and retain the original function capture across copies. Share declaration initializer facts with RPC and Electron analysis, including class declarations used for fresh-target identity. Accept single-statement control blocks while keeping lexical roles and added operations checked. Cover the capture regressions through inert source and public CLI/MCP workflows.
2026-10-11 23:53:55 +08:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
b03df22ce4 fix(javascript): retain fresh function and class target identity
Recognize stable local function, undecorated class and RegExp targets before
widening intrinsic mutations. Keep decorated classes opaque because their
binding value can be replaced, and preserve class-initialization writes.
Cover fresh targets and replacement decorators in inert CLI/MCP fixtures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 15:41:00 +00:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
5569e96162 fix(javascript): preserve intrinsic projections and fresh target identity
Resolve known mutators through static binding projections and prove stable
identifier chains to fresh literals before widening unknown mutation targets.
Keep reassigned, uninitialized and member-projected targets conservative.
Cover both review regressions through source tests and inert CLI/MCP fixtures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 15:22:12 +00:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
47f8156efe fix(javascript): integrate current provider timeout recovery
Include the main-branch Hopper startup timeout normalization already merged
in #1866 so combined CI retains launcher diagnostics when either timer fires.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 14:53:16 +00:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
824728d7ee fix(javascript): retain destructured intrinsic writes
Share one pattern-target walker across lexical definitions, member mutation
tracking and Electron identity writes. Keep overwritten globals unresolved
without treating property keys or shadowed locals as global replacements.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 14:43:17 +00:00
N0zoM1z0factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
ced8778094 refactor(javascript): share AST child field metadata
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 14:19:51 +00:00
N0zoM1z0andfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 6d470fc9cb fix(javascript): integrate current managed and stability fixes
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-11 14:07:53 +00:00
N0zoM1z0andfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 9788112548 fix(javascript): integrate verified namespace interop with main
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-11 14:06:55 +00:00
N0zoM1z0 ebaa639cf8 fix(javascript): prove Electron module namespace conversion 2026-10-11 13:59:19 +00:00
15 changed files with 2089 additions and 221 deletions
+38 -9
View File
@@ -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"]);
});
});
+8 -4
View File
@@ -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,9 +624,10 @@ 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,
))
if (t.isIdentifier(identifier))
copyParameterToBody({ identifier, body, scope, state });
}
}
@@ -801,7 +776,8 @@ const bindAssignment = (
state,
);
else
for (const identifier of assignedPatternIdentifiers(node.left))
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);
@@ -809,7 +785,8 @@ const bindAssignment = (
for (const pattern of t.isVariableDeclaration(node.left)
? node.left.declarations.map(({ id }) => id)
: [node.left]) {
for (const identifier of assignedPatternIdentifiers(pattern))
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
@@ -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,7 +401,8 @@ export function* collectSemanticMemberMutationsSteps(
markEscaped(expression.object, mutation);
};
const markTarget = (node: t.Node, mutation: t.Node): void => {
for (const target of semanticMemberWriteTargets(node))
for (const target of semanticWriteTargets(node))
if (!t.isIdentifier(target))
markValue(
target.object,
[semanticStaticPropertyKey(target.property, target.computed)],
@@ -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",
},
}),
]),
]),
);
},
);