Files
reverse-engineering-anything/tests/acceptance/applications/unboundRequires.test.ts
T
morlutoandShuaixinHuang b46f4fcebd fix(javascript): preserve loader authority for exact suffixes (#9477)
* fix(javascript): require exact loader suffix case

* fix(javascript): share exact loader mode across import resolution

* test(process): align symlink fixture with filesystem checkpoints

* test(javascript): normalize the module resolution oracle path separators

* test(frida): assert the CLI spawn Evidence contract

* test(mcp): use an unsupported analysis-view parent

* test(browser): separate source-location line bounds from parity

* fix(javascript): preserve uncertain loader provenance

* test(javascript): cover uncertain require resolution

* fix(javascript): retain conditional Electron evidence

* fix(javascript): preserve timed semantic provenance

* fix(javascript): retain conditional builtin effects

* test(javascript): distinguish bundled host authority

* fix(javascript): preserve nested loader invocation identity

* test(javascript): qualify Electron fixture authority

* fix(javascript): preserve loader invocation authority

* fix(javascript): respect source order for resolver escapes

* fix(javascript): check nested loader callable mutations

* fix(javascript): retain conditional constructor requests

* fix(reference): retain constructor loader relationships

* fix(reference): follow nested loader callable paths

* refactor(javascript): share callable mutation interpretation

* fix(javascript): require exact package loader suffixes

* refactor(javascript): unify conditional relation evidence

* refactor(javascript): unify conditional node evidence

* fix(javascript): retain unknown authority for mixed case sources

* fix(javascript): preserve loader uncertainty for dotfiles

* fix(reference): classify javascript suffix dotfiles

* test(reference): cover suffix dotfiles through cli

* fix(javascript): retain equivalent conditional built-in origins

* fix(javascript): preserve wrapped resolver operation

* fix(javascript): retain nested conditional built-in receivers

* test(javascript): declare CommonJS in native addon fixtures

* fix(javascript): retain wrapped resolver candidates

* style: format merged native command schema

* fix(javascript): preserve uncertain loader authority

* test(javascript): require candidate preload evidence

* test(javascript): cover reference loader member writes

---------

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
2026-11-09 13:21:24 +08:00

1217 lines
42 KiB
TypeScript

import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, relative } from "node:path";
import { promisify } from "node:util";
import { expect } from "vitest";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const execute = promisify(execFile);
const expectResolverCandidates = (
graph: ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>["graph"],
): void => {
for (const source of [
"upper-resolve.MJS",
"wrapped-resolve.TS",
"wrapped-receiver.TS",
])
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
kind: "require",
operation: "resolve",
specifier: "./dep.cjs",
loader_authority: "candidate",
resolved_path: null,
resolution_mode: null,
}),
evidence: expect.objectContaining({
confidence: "low",
location: expect.objectContaining({
value: expect.objectContaining({ source }),
}),
}),
}),
);
for (const source of [
"upper-shadow.MJS",
"upper-mutated.MJS",
"wrapped-shadow.TS",
"wrapped-mutated.TS",
]) {
expect(
graph.edges.some(
({ properties, evidence }) =>
properties.loader_authority === "candidate" &&
properties.specifier === "./dep.cjs" &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === source,
),
).toBe(false);
}
};
const expectUnknownLoaderCandidates = (
graph: ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>["graph"],
): void => {
const upper = sourceRequireFacts(graph, "upper-module.MJS");
expect(upper.module_format).toBe("unknown");
expect(upper.require_targets).toContainEqual([
"./dep.cjs",
null,
"unavailable",
]);
const candidate = graph.edges.find(
({ relation, properties, evidence }) =>
relation === "imports" &&
properties.specifier === "./dep.cjs" &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === "upper-module.MJS",
);
expect(candidate?.properties.resolution_mode).toBeNull();
expect(candidate?.evidence.confidence).toBe("low");
expect(candidate?.evidence.limitations.join(" ")).toContain(
"loader is unknown",
);
const staticCandidate = graph.edges.find(
({ relation, properties, evidence }) =>
relation === "imports" &&
properties.kind === "require" &&
properties.specifier === "./dep.cjs" &&
properties.loader_authority === "candidate" &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === "upper-module.MJS",
);
expect(staticCandidate).toMatchObject({
properties: {
resolution_mode: null,
resolved_path: null,
resolution_status: null,
},
evidence: {
confidence: "low",
},
});
expect(staticCandidate?.evidence.limitations.join(" ")).toContain(
"loader is unknown",
);
expectResolverCandidates(graph);
const candidateExports = graph.edges.filter(
({ relation, properties, evidence }) =>
relation === "exposes" &&
properties.loader_authority === "candidate" &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === "upper-export.MJS",
);
expect(candidateExports.length).toBeGreaterThan(0);
expect(candidateExports[0]?.evidence).toMatchObject({
confidence: "low",
});
expect(candidateExports[0]?.evidence.limitations.join(" ")).toContain(
"wrapper is unknown",
);
for (const path of ["upper-alias.MJS", "upper-var-export.MJS"]) {
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "exposes",
properties: expect.objectContaining({
loader_authority: "candidate",
}),
evidence: expect.objectContaining({
confidence: "low",
location: expect.objectContaining({
value: expect.objectContaining({ source: path }),
}),
}),
}),
);
}
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
module_link_kind: "commonjs-export",
loader_authority: "candidate",
specifier: "./dep.cjs",
resolved_path: null,
resolution_status: "unavailable",
}),
evidence: expect.objectContaining({
confidence: "low",
location: expect.objectContaining({
value: expect.objectContaining({ source: "upper-alias.MJS" }),
}),
}),
}),
);
const candidateExportSource = graph.nodes
.flatMap(({ observations }) => observations)
.find(
({ properties }) =>
properties.semantic_role === "source-module" &&
properties.path === "upper-export.MJS",
);
expect(
candidateExportSource?.properties.omitted_export_names,
).toBeGreaterThan(0);
expect(candidateExportSource?.evidence.limitations.join(" ")).toContain(
"candidate relationships are retained",
);
};
const expectSelectedLoaderEdges = (
graph: ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>["graph"],
): void => {
const loaderEdge = (file: string, specifier: string) =>
graph.edges.find(
({ properties, evidence }) =>
properties.specifier === specifier &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === file,
);
expect
.soft(
loaderEdge("candidate-alias.MJS", "./alias-dep.cjs")?.properties
.loader_authority,
)
.toBe("candidate");
for (const source of ["candidate-new.MJS", "candidate-new-alias.MJS"]) {
expect
.soft(loaderEdge(source, "./alias-dep.cjs")?.properties.loader_authority)
.toBe("candidate");
}
for (const source of [
"candidate-new-shadow.MJS",
"candidate-new-replaced.MJS",
]) {
expect.soft(loaderEdge(source, "./alias-dep.cjs")).toBeUndefined();
}
expect
.soft(loaderEdge("benign-resolve.cjs", "./benign-dep.cjs"))
.toBeDefined();
expect.soft(loaderEdge("benign-call.cjs", "./call-dep.cjs")).toBeDefined();
expect
.soft(loaderEdge("consecutive-methods.cjs", "./benign-dep.cjs"))
.toBeDefined();
expect
.soft(loaderEdge("consecutive-methods.cjs", "./call-dep.cjs"))
.toBeDefined();
expect
.soft(loaderEdge("nested-loader.cjs", "./nested-false.cjs"))
.toBeUndefined();
expect
.soft(loaderEdge("nested-child.cjs", "./nested-valid.cjs"))
.toBeDefined();
expect
.soft(loaderEdge("wrapped-call.cts", "./false-dep.cjs"))
.toBeUndefined();
};
cliTest.for(["CLI", "stdio MCP"] as const)(
"distinguishes exact loader suffixes from uppercase artifact names through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-loader-suffix-case-");
const source = "console.log(42);";
await writeFixtureFiles(root, {
"package.json": JSON.stringify({
type: "commonjs",
main: "upper-module.MJS",
}),
"upper-module.MJS":
'const value = require("./dep.cjs"); console.log(value);',
"dep.cjs": "module.exports = 42;",
"upper-export.MJS": "module.exports = { value: 42 };",
"upper-resolve.MJS": 'require.resolve("./dep.cjs");',
"wrapped-resolve.TS": '(require.resolve as any)("./dep.cjs");',
"wrapped-receiver.TS": '(require as any).resolve("./dep.cjs");',
"wrapped-shadow.TS":
'const require = {resolve() {}}; (require as any).resolve("./dep.cjs");',
"wrapped-mutated.TS":
'require.resolve = custom; (require as any).resolve("./dep.cjs");',
"upper-shadow.MJS":
'const require = { resolve() {} }; require.resolve("./dep.cjs");',
"upper-mutated.MJS":
'require.resolve = custom; require.resolve("./dep.cjs");',
"upper-alias.MJS":
'var module; const dep = require("./dep.cjs"); module.exports = dep;',
"upper-var-export.MJS": "var exports; exports.name = 42;",
"nested-mutated.MJS":
'require.call=custom;require.call.call(require,"./receiver.cjs","./invented.node");',
"nested-mutated.cjs":
'require.call=custom;require.call.apply(require,["./receiver.cjs","./invented.node"]);',
"candidate-alias.MJS": 'const load = require; load("./alias-dep.cjs");',
"candidate-new.MJS": 'new require("./alias-dep.cjs");',
"candidate-new-alias.MJS":
'const load = require; new load("./alias-dep.cjs");',
"candidate-new-shadow.MJS":
'const require = function() {}; new require("./alias-dep.cjs");',
"candidate-new-replaced.MJS":
'require = function() {}; const load = require; new load("./alias-dep.cjs");',
"benign-resolve.cjs":
'require.resolve.paths = custom; require.resolve("./benign-dep.cjs");',
"benign-call.cjs":
'require.call.extra = 1; require.call(null, "./call-dep.cjs");',
"consecutive-methods.cjs":
'require.resolve.paths = custom; require.resolve("./benign-dep.cjs"); require.call.extra = 1; require.call(null, "./call-dep.cjs");',
"nested-loader.cjs":
'const box = {load: require}; box.load.call = () => 0; box.load.call(null, "./nested-false.cjs");',
"nested-child.cjs":
'const box = {load: require}; box.load.call.extra = 1; box.load.call(null, "./nested-valid.cjs");',
"wrapped-call.cts":
'require.call = () => 0; (require.call as any)(null, "./false-dep.cjs");',
"alias-dep.cjs": "module.exports = 1;",
"benign-dep.cjs": "module.exports = 2;",
"call-dep.cjs": "module.exports = 3;",
"false-dep.cjs": "module.exports = 4;",
"nested-false.cjs": "module.exports = 5;",
"nested-valid.cjs": "module.exports = 6;",
"upper-wrapper.CJS": source,
"lower-module.mjs": source,
"lower-wrapper.cjs": source,
});
const control = await execute(
process.execPath,
[join(root, "upper-module.MJS")],
{
cwd: root,
timeout: 5_000,
},
);
expect(control.stdout.trim()).toBe("42");
const result = await analyzeJavaScriptDirectory(surface, cli, root);
expect(result.statistics.parse_failures).toBe(0);
expectUnknownLoaderCandidates(result.graph);
for (const path of ["nested-mutated.MJS", "nested-mutated.cjs"]) {
expect(
result.graph.edges.some(
({ properties, evidence }) =>
(properties.specifier === "./receiver.cjs" ||
properties.specifier === "./invented.node") &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === path,
),
).toBe(false);
}
expectSelectedLoaderEdges(result.graph);
const modules = result.graph.nodes
.filter((node) => node.kind === "javascript-module")
.flatMap((node) =>
node.observations.map((observation) => observation.properties),
);
expect(modules).toEqual(
expect.arrayContaining([
expect.objectContaining({
path: "upper-module.MJS",
module_format: "unknown",
}),
expect.objectContaining({
path: "upper-wrapper.CJS",
module_format: "unknown",
}),
expect.objectContaining({
path: "upper-export.MJS",
module_format: "unknown",
export_names: [],
}),
expect.objectContaining({
path: "lower-module.mjs",
module_format: "esm",
}),
expect.objectContaining({
path: "lower-wrapper.cjs",
module_format: "commonjs",
}),
]),
);
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"retains conditional Electron boundaries without resolving their roles through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-candidate-electron-");
await writeFixtureFiles(root, {
"entry.MJS": [
'const { BrowserWindow, ipcMain, protocol, contextBridge, ipcRenderer } = require("electron");',
'const win = new BrowserWindow({ webPreferences: { preload: "./preload.cjs" } });',
'win.loadFile("./index.html");',
'win.loadURL("https://candidate.example.test/app");',
'ipcMain.handle("ping", () => 1);',
'protocol.handle("demo", () => null);',
'contextBridge.exposeInMainWorld("api", { ping: () => ipcRenderer.invoke("ping") });',
].join("\n"),
"preload.cjs": "console.log(1);",
"index.html": "<html></html>",
"dynamic.MJS": [
'const name = "electron";',
"const { contextBridge, ipcRenderer } = require(name);",
'contextBridge.exposeInMainWorld("dynamicApi", { ping: () => ipcRenderer.invoke("dynamicPing") });',
].join("\n"),
});
const result = await analyzeJavaScriptDirectory(surface, cli, root);
for (const kind of ["browser-window", "ipc-handler", "url-scheme"]) {
expect(result.graph.nodes).toContainEqual(
expect.objectContaining({
kind,
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
loader_authority: "candidate",
}),
evidence: expect.objectContaining({
confidence: "low",
}),
}),
]),
}),
);
}
expect(
result.graph.nodes.some(({ kind }) => kind === "electron-preload"),
).toBe(false);
expect(
result.graph.nodes.some(({ kind }) => kind === "electron-renderer"),
).toBe(false);
expect(result.graph.nodes).toContainEqual(
expect.objectContaining({
kind: "unknown",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
candidate_role: "renderer",
loader_authority: "candidate",
declared_path: "./index.html",
resolution_status: "unavailable",
resolved_path: null,
}),
evidence: expect.objectContaining({ confidence: "low" }),
}),
]),
}),
);
expect(result.graph.nodes).toContainEqual(
expect.objectContaining({
kind: "endpoint",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
value: "https://candidate.example.test/app",
interpretation: "candidate",
}),
evidence: expect.objectContaining({ confidence: "low" }),
}),
]),
}),
);
expect(result.graph.nodes).toContainEqual(
expect.objectContaining({
kind: "javascript-expression",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
loader_authority: "candidate",
terminal: "electron.ipcRenderer.invoke",
}),
evidence: expect.objectContaining({
confidence: "low",
}),
}),
]),
}),
);
expect(result.graph.nodes).toContainEqual(
expect.objectContaining({
kind: "javascript-expression",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
loader_authority: "candidate",
terminal: "electron.ipcRenderer.invoke",
channel: "dynamicPing",
}),
evidence: expect.objectContaining({ confidence: "low" }),
}),
]),
}),
);
expect(result.summary).toMatchObject({
browser_windows: 0,
preload_entrypoints: 0,
protocol_registrations: 0,
ipc: { operations: 0, main_handlers: 0 },
});
expect(result.graph.limitations.join(" ")).toContain(
"Conditional Electron findings",
);
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"retains loader uncertainty in Electron semantic paths through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory(
"rea-electron-semantic-authority-",
);
const source = [
'const { contextBridge, ipcRenderer } = require("electron");',
'contextBridge.exposeInMainWorld("api", { ping: () => ipcRenderer.invoke("ping") });',
].join("\n");
await writeFixtureFiles(root, {
"candidate.MJS": source,
"proven.cjs": source,
});
const result = await analyzeJavaScriptDirectory(surface, cli, root);
const graph = result.semantic_graph;
const pathNodes = graph.nodes.filter((node) =>
node.identity.role_key.startsWith("electron-path:"),
);
const candidate = pathNodes.filter(
(node) => node.identity.module_path === "candidate.MJS",
);
const proven = pathNodes.filter(
(node) => node.identity.module_path === "proven.cjs",
);
expect(candidate.length).toBeGreaterThan(0);
expect(proven.length).toBeGreaterThan(0);
const evidenceFor = (contextId: string) =>
graph.evidence_contexts.find(
({ context_id }) => context_id === contextId,
);
for (const node of candidate) {
expect(node.properties.loader_authority).toBe("candidate");
const evidence = evidenceFor(node.evidence.context_id);
expect(evidence).toMatchObject({ state: "inferred", confidence: "low" });
expect(evidence?.limitations.join(" ")).toContain("loader is unknown");
}
for (const node of proven) {
expect(node.properties.loader_authority).toBeUndefined();
expect(evidenceFor(node.evidence.context_id)).toMatchObject({
state: "observed",
confidence: "exact",
});
}
const relations = graph.relations.filter(
({ properties }) => properties.loader_authority === "candidate",
);
expect(relations.length).toBeGreaterThan(0);
for (const relation of relations) {
const evidence = evidenceFor(relation.evidence.context_id);
expect(evidence).toMatchObject({ state: "inferred", confidence: "low" });
expect(evidence?.limitations.join(" ")).toContain("loader is unknown");
}
},
);
const compiler = join(
dirname(createRequire(import.meta.url).resolve("typescript/package.json")),
"bin",
"tsc",
);
const typedLines = [
'export import typed = require("typed-package");',
'import type TypeOnly = require("./type-only.cjs");',
] as const;
const typedSource = typedLines.join("\n");
const aliasSource = [
'import raw = require("typed-package");',
"const alias = raw;",
"export { alias };",
].join("\n");
const mixedSource = [
'import { value as imported } from "mixed-package";',
'import required = require("mixed-package");',
"const mixed = Date.now() > 0 ? imported : required.value;",
"export { imported as staticChoice, mixed };",
"export const requireChoice = required.value;",
].join("\n");
const typescriptSourceFiles = {
"typed.cts": typedSource,
"typed.mts": typedSource,
"alias.cts": aliasSource,
"alias.mts": aliasSource,
"mixed.cts": mixedSource,
"mixed.mts": mixedSource,
};
const typedPackageFiles = {
"node_modules/typed-package/package.json": JSON.stringify({
exports: { require: "./actual.cjs", import: "./decoy.mjs" },
}),
"node_modules/typed-package/actual.cjs": "module.exports = 42;",
"node_modules/typed-package/actual.d.cts":
"declare const value: number; export = value;",
"node_modules/typed-package/decoy.mjs": "export default 99;",
"node_modules/typed-package/decoy.d.mts":
"declare const value: number; export default value;",
"node_modules/mixed-package/package.json": JSON.stringify({
exports: { require: "./actual.cjs", import: "./decoy.mjs" },
}),
"node_modules/mixed-package/actual.cjs": "exports.value = 42;",
"node_modules/mixed-package/actual.d.cts":
"export declare const value: number;",
"node_modules/mixed-package/decoy.mjs": "export const value = 99;",
"node_modules/mixed-package/decoy.d.mts":
"export declare const value: number;",
};
const expectNativeModuleControls = async (root: string): Promise<void> => {
const emitted = await createTestTempDirectory("rea-import-equals-output-");
await writeFixtureFiles(emitted, typedPackageFiles);
await execute(
process.execPath,
[
compiler,
"--module",
"NodeNext",
"--moduleResolution",
"NodeNext",
"--target",
"ES2022",
"--skipLibCheck",
"--allowJs",
"--outDir",
emitted,
join(root, "typed.cts"),
join(root, "typed.mts"),
join(root, "alias.cts"),
join(root, "alias.mts"),
join(root, "mixed.cts"),
join(root, "mixed.mts"),
],
{ cwd: root, timeout: 10_000 },
);
for (const [filename, script] of [
[
"typed.cjs",
'console.log(require("./typed.cjs").typed, require("./alias.cjs").alias);',
],
[
"typed.mjs",
'Promise.all([import("./typed.mjs"), import("./alias.mjs")]).then(([first, second]) => console.log(first.typed, second.alias));',
],
] as const) {
const typedControl = await execute(process.execPath, ["-e", script], {
cwd: emitted,
timeout: 5_000,
});
expect(typedControl.stdout.trim()).toBe("42 42");
expect(await readFile(join(emitted, filename), "utf8")).not.toContain(
"./type-only.cjs",
);
}
for (const [filename, expected] of [
["mixed.cjs", "42 42 42"],
["mixed.mjs", "99 42 99"],
] as const) {
const control = await execute(
process.execPath,
[
"-e",
`import("./${filename}").then(({staticChoice, requireChoice, mixed}) => console.log(staticChoice, requireChoice, mixed));`,
],
{ cwd: emitted, timeout: 5_000 },
);
expect(control.stdout.trim()).toBe(expected);
}
const nativeRequire = createRequire(join(root, "main.cjs"));
for (const name of ["dependency", "nested"])
expect(
relative(root, nativeRequire.resolve(`./${name}.cjs`)).replaceAll(
"\\",
"/",
),
).toBe(`${name}.cjs`);
};
const expectAliasRelationships = (
graph: ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>["graph"],
): void => {
for (const source of ["alias.cts", "alias.mts"]) {
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
module_link_kind: "export",
local_name: "alias",
exported_name: "alias",
resolution_mode: "require",
resolved_path: "node_modules/typed-package/actual.cjs",
resolution_status: "resolved",
}),
evidence: expect.objectContaining({
artifact: expect.objectContaining({
sha256: createHash("sha256").update(aliasSource).digest("hex"),
}),
location: expect.objectContaining({
value: expect.objectContaining({ source }),
}),
}),
}),
);
}
};
const expectMixedRelationships = (
graph: ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>["graph"],
): void => {
const exported = (source: string, name: string) =>
graph.edges.filter(
({ relation, properties, evidence }) =>
relation === "imports" &&
properties.module_link_kind === "export" &&
properties.exported_name === name &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === source,
);
for (const source of ["mixed.cts", "mixed.mts"]) {
for (const [name, target] of [
["staticChoice", source.endsWith(".cts") ? "actual.cjs" : "decoy.mjs"],
["requireChoice", "actual.cjs"],
] as const)
expect(exported(source, name)).toMatchObject([
{
properties: {
resolved_path: `node_modules/mixed-package/${target}`,
resolution_status: "resolved",
},
evidence: {
artifact: {
sha256: createHash("sha256").update(mixedSource).digest("hex"),
},
},
},
]);
}
expect(exported("mixed.cts", "mixed")).toMatchObject([
{
properties: {
resolved_path: "node_modules/mixed-package/actual.cjs",
resolution_status: "resolved",
},
evidence: {
artifact: {
sha256: createHash("sha256").update(mixedSource).digest("hex"),
},
},
},
]);
expect(exported("mixed.mts", "mixed")).toEqual([]);
};
const expectImportEqualsRelationships = (
graph: ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>["graph"],
): void => {
for (const [sourcePath, format] of [
["typed.cts", "commonjs"],
["typed.mts", "esm"],
] as const) {
const typedLinks = graph.edges.filter(
({ relation, properties, evidence: edgeEvidence }) =>
relation === "imports" &&
properties.module_link_kind === "require" &&
edgeEvidence.location.available &&
edgeEvidence.location.value.kind === "source-range" &&
edgeEvidence.location.value.source === sourcePath,
);
expect(typedLinks).toHaveLength(1);
expect(typedLinks[0]).toMatchObject({
properties: {
specifier: "typed-package",
imported_name: null,
local_name: "typed",
resolved_path: "node_modules/typed-package/actual.cjs",
resolution_status: "resolved",
},
evidence: {
artifact: {
available: true,
sha256: createHash("sha256").update(typedSource).digest("hex"),
},
location: {
available: true,
value: {
source: sourcePath,
start: { line: 1, column: 7 },
end: { line: 1, column: typedLines[0].length },
},
},
},
});
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
module_link_kind: "export",
local_name: "typed",
resolution_mode: "require",
resolved_path: "node_modules/typed-package/actual.cjs",
}),
evidence: expect.objectContaining({
location: expect.objectContaining({
value: expect.objectContaining({ source: sourcePath }),
}),
}),
}),
);
expect(
graph.nodes.flatMap(({ observations }) => observations),
).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
semantic_role: "source-module",
path: sourcePath,
export_names: ["typed"],
export_relationships: 1,
}),
}),
);
expect(
graph.edges.filter(
({ relation, evidence: edgeEvidence }) =>
relation === "exposes" &&
edgeEvidence.location.available &&
edgeEvidence.location.value.kind === "source-range" &&
edgeEvidence.location.value.source === sourcePath,
),
).toHaveLength(1);
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
kind: "require",
specifier: "typed-package",
resolved_path: "node_modules/typed-package/actual.cjs",
}),
}),
);
expect(
graph.nodes.flatMap(({ observations }) => observations),
).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
semantic_role: "source-module",
path: sourcePath,
module_format: format,
import_relationships: 1,
}),
}),
);
}
expect(
graph.edges.filter(
({ relation, properties }) =>
relation === "imports" && properties.specifier === "./type-only.cjs",
),
).toHaveLength(0);
};
/** Source-module facts and semantic require edges of one analyzed source. */
const sourceRequireFacts = (
graph: ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>["graph"],
path: string,
) => {
const module = graph.nodes
.flatMap(({ observations }) => observations)
.find(
({ properties }) =>
properties.semantic_role === "source-module" &&
properties.path === path,
)?.properties;
if (module === undefined)
throw new Error(`Source module ${path} is absent from the analysis graph`);
const requires = graph.edges.filter(
({ relation, properties, evidence }) =>
relation === "imports" &&
properties.module_link_kind === "require" &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
evidence.location.value.source === path,
);
return {
module_format: module.module_format,
import_relationships: module.import_relationships,
require_targets: requires.map(({ properties }) => [
properties.specifier,
properties.resolved_path,
properties.resolution_status,
]),
};
};
cliTest(
"follows the nearest package type for an unbound require in .js sources",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-package-type-requires-");
const source = (dependency: string) =>
`try { console.log(require(${JSON.stringify(dependency)})); } catch (error) { console.log(error.name); }\n`;
// The nearest package.json owns each .js file's scope (Node packages.md,
// "Determining module system"). Requiring Electron, directly, through a
// local loader, or in a bundler factory default that never runs, does not
// give a Node-loaded module a require.
await writeFixtureFiles(root, {
"package.json": JSON.stringify({ type: "module" }),
"main.js": source("./dep.cjs"),
"electron-main.js":
'try { require("electron"); console.log(require("./dep.cjs")); } catch (error) { console.log(error.name); }\n',
"renderer-looking-main.js":
'try { require("electron").ipcRenderer.send("ready"); require("./dep.cjs"); } catch (error) { console.log(error.name); }',
"shadowed.js": `function load(require) { return require("electron").contextBridge; }\n${source("./dep.cjs")}`,
"dead-default.js": [
source("./dep.cjs"),
"globalThis.webpackChunkApp = [];",
"globalThis.webpackChunkApp.push([[1], {",
' 7(module = require("electron").ipcRenderer.send("unused"), exports, load) {}',
"}]);",
].join("\n"),
"dep.cjs": "module.exports = 1;\n",
"legacy/package.json": JSON.stringify({ type: "commonjs" }),
"legacy/main.js": source("../dep.cjs"),
"untyped/package.json": JSON.stringify({}),
"untyped/main.js": source("../dep.cjs"),
});
for (const [path, expected] of [
["main.js", "ReferenceError"],
["electron-main.js", "ReferenceError"],
["renderer-looking-main.js", "ReferenceError"],
["shadowed.js", "ReferenceError"],
["dead-default.js", "ReferenceError"],
["legacy/main.js", "1"],
["untyped/main.js", "1"],
] as const) {
const control = await execute(process.execPath, [join(root, path)], {
cwd: root,
timeout: 5_000,
});
expect(control.stdout.trim(), path).toBe(expected);
}
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(response.json).normalized_result,
);
for (const path of [
"main.js",
"electron-main.js",
"renderer-looking-main.js",
"shadowed.js",
"dead-default.js",
])
expect(sourceRequireFacts(graph, path), path).toEqual({
module_format: "unknown",
import_relationships: 0,
require_targets: [],
});
for (const path of ["legacy/main.js", "untyped/main.js"])
expect(sourceRequireFacts(graph, path), path).toEqual({
module_format: "commonjs",
import_relationships: 1,
require_targets: [["../dep.cjs", "dep.cjs", "resolved"]],
});
},
);
cliTest(
"keeps Electron's script loader for a .js preload in a module package scope",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-package-type-preload-");
// Electron's preload loader ignores "type": "module" ("ES Modules (ESM)
// in Electron"); main.mjs is an ES module by its suffix.
await writeFixtureFiles(root, {
"package.json": JSON.stringify({ type: "module", main: "main.mjs" }),
"main.mjs": [
'import { app, BrowserWindow, ipcMain } from "electron";',
'import { fileURLToPath } from "node:url";',
'ipcMain.handle("ping", () => "pong");',
"app.whenReady().then(() => {",
" const window = new BrowserWindow({",
' webPreferences: { sandbox: false, preload: fileURLToPath(new URL("./preload.js", import.meta.url)) },',
" });",
' window.loadFile("index.html");',
"});",
].join("\n"),
"preload.js": [
'const { contextBridge, ipcRenderer } = require("electron");',
'function expose() { contextBridge.exposeInMainWorld("api", { ping: () => ipcRenderer.invoke("ping") }); } expose();',
].join("\n"),
"index.html": "<!doctype html><title>app</title>\n",
});
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
const { graph, summary } = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(response.json).normalized_result,
);
expect(sourceRequireFacts(graph, "preload.js")).toEqual({
module_format: "commonjs",
import_relationships: 2,
require_targets: [
["electron", null, "external"],
["electron", null, "external"],
],
});
expect(summary.ipc).toMatchObject({
main_handlers: 1,
renderer_transmissions: 1,
paired_renderer_transmissions: 1,
});
},
);
cliTest(
"does not infer preload loader authority from a candidate Electron window",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-candidate-preload-");
await writeFixtureFiles(root, {
"package.json": JSON.stringify({ type: "module", main: "main.MJS" }),
"main.MJS":
'const {BrowserWindow}=require("electron"); new BrowserWindow({webPreferences:{preload:"./preload.js"}});',
"preload.js": 'require("./helper.cjs");',
"helper.cjs": "module.exports = 1;",
});
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(response.json).normalized_result,
);
expect(graph.nodes).toContainEqual(
expect.objectContaining({
kind: "browser-window",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
loader_authority: "candidate",
}),
}),
]),
}),
);
expect(sourceRequireFacts(graph, "preload.js").module_format).toBe(
"unknown",
);
expect(
sourceRequireFacts(graph, "preload.js").require_targets,
).not.toContainEqual(["./helper.cjs", "helper.cjs", "resolved"]);
},
);
cliTest(
"keeps a declared preload's helper require without renderer API text",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-helper-only-preload-");
await writeFixtureFiles(root, {
"package.json": JSON.stringify({ type: "module", main: "main.mjs" }),
"main.mjs":
'import { BrowserWindow } from "electron"; import { fileURLToPath } from "node:url"; new BrowserWindow({webPreferences:{sandbox:false,preload:fileURLToPath(new URL("./aaa-preload.js",import.meta.url))}});',
"aaa-preload.js": 'require("./helper.cjs");',
"helper.cjs": "module.exports = 1;",
});
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(response.json).normalized_result,
);
expect(sourceRequireFacts(graph, "aaa-preload.js")).toEqual({
module_format: "commonjs",
import_relationships: 1,
require_targets: [["./helper.cjs", "helper.cjs", "resolved"]],
});
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"retains unbound CommonJS semantic relationships through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-unbound-requires-");
const lines = [
'require("./dependency.cjs");',
'void require("./dependency.cjs");',
'consume(require("./dependency.cjs"));',
'require("./dependency.cjs").member;',
'const {} = require("./dependency.cjs");',
'const bound = require("./dependency.cjs");',
'const { left, right } = require("./dependency.cjs");',
'module.exports = require("./dependency.cjs", require("./nested.cjs"));',
'function local(require) { require("./shadow.cjs"); }',
"require(dynamicSpecifier);",
'let assigned; assigned = require("./dependency.cjs");',
] as const;
const source = lines.join("\n");
const files = {
"main.cjs": source,
"imports.mjs": 'import "./dependency.cjs";',
"dependency.cjs": 'throw new Error("fixture must not execute");',
"nested.cjs": 'throw new Error("fixture must not execute");',
...typescriptSourceFiles,
"type-only.d.cts": "declare const value: unknown; export = value;",
};
await Promise.all(
Object.entries(files).map(([path, text]) =>
writeFile(join(root, path), text),
),
);
await writeFixtureFiles(root, typedPackageFiles);
await expectNativeModuleControls(root);
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
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,
format: "directory",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
const evidence = parseEvidence(document);
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expectImportEqualsRelationships(graph);
expectAliasRelationships(graph);
expectMixedRelationships(graph);
const links = graph.edges.filter(
({ relation, properties, evidence: edgeEvidence }) =>
relation === "imports" &&
properties.module_link_kind === "require" &&
edgeEvidence.location.available &&
edgeEvidence.location.value.kind === "source-range" &&
edgeEvidence.location.value.source === "main.cjs",
);
expect(links).toHaveLength(10);
for (const [line, count] of [
[1, 1],
[2, 1],
[3, 1],
[4, 1],
[5, 1],
[6, 1],
[7, 2],
[8, 1],
[11, 1],
] as const)
expect(
links.filter(
({ evidence: edgeEvidence }) =>
edgeEvidence.location.available &&
edgeEvidence.location.value.kind === "source-range" &&
edgeEvidence.location.value.start.line === line,
),
`semantic links on line ${line}`,
).toHaveLength(count);
const digest = createHash("sha256").update(source).digest("hex");
for (const edge of links) {
expect(edge.evidence.artifact).toMatchObject({
available: true,
sha256: digest,
});
expect(edge.properties.resolution_status).toBe("resolved");
expect(edge.properties.resolved_path).toBe(
edge.properties.specifier === "./nested.cjs"
? "nested.cjs"
: "dependency.cjs",
);
}
const bare = links.find(
({ evidence: edgeEvidence }) =>
edgeEvidence.location.available &&
edgeEvidence.location.value.kind === "source-range" &&
edgeEvidence.location.value.start.line === 1,
);
expect(bare).toMatchObject({
properties: {
imported_name: null,
local_name: null,
exported_name: null,
},
evidence: {
location: {
available: true,
value: {
source: "main.cjs",
start: { line: 1, column: 0 },
end: { line: 1, column: lines[0].length - 1 },
},
},
},
});
expect(
graph.nodes
.flatMap(({ observations }) => observations)
.find(
({ properties }) =>
properties.semantic_role === "source-module" &&
properties.path === "main.cjs",
)?.properties.import_relationships,
).toBe(10);
expect(
graph.edges.some(
({ relation, properties }) =>
relation === "imports" &&
properties.kind === "require" &&
properties.specifier === "./dependency.cjs",
),
).toBe(true);
expect(
graph.edges.filter(
({ relation, properties }) =>
relation === "imports" &&
properties.module_link_kind === "commonjs-export" &&
properties.specifier === "./dependency.cjs",
),
).toHaveLength(1);
},
);