Files
rea/tests/acceptance/applications/electronModuleInteropCli.test.ts
T
Ysopwnable 9b3e59804d fix(javascript): prove namespace interop before attributing Electron APIs (#1857)
* fix(javascript): prove Electron module namespace conversion

* 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>

* 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>

* 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>

* 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>

* 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.

* 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.

* fix(javascript): preserve proven interop result projections

* 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>

* fix(javascript): retain bound intrinsic mutation effects

* fix(javascript): preserve bound receivers for intrinsic invocation

* fix(javascript): prove generic binding and invocation effects

* fix(javascript): retain Reflect set receiver effects

---------

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-12 01:06:33 +08:00

181 lines
6.7 KiB
TypeScript

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;
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");',
);
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");`,
);
const source = `const metadata = {};
Object.defineProperty(metadata,"x",{value:1});
const decorateMetadata = Object.defineProperty.bind(null,metadata,"bound");
decorateMetadata({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",
},
}),
]),
]),
);
},
);