fix(javascript): trace wrapped Electron IPC paths in large bundles (#1874)

Co-authored-by: David <17119984+fengdwx@users.noreply.github.com>
This commit is contained in:
David
2026-10-12 09:28:59 +08:00
committed by GitHub
parent d93fbda390
commit 87b9793c53
46 changed files with 3213 additions and 355 deletions
@@ -0,0 +1,72 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { projectJavaScriptSemanticFileSteps } from "./JavaScriptSemanticGraphBuilder.js";
const source = `${Array.from({ length: 40 }, (_, i) => `function library${String(i)}(){return ${String(i)}}`).join("\n")}
const e=require("electron");
function register(channel,callback){e.ipcMain.addListener(channel,event=>allowed(event)&&callback(event))}
register("test:copy",event=>event.sender.copy());`;
const file: JavaScriptArtifactFile = {
path: "main.js",
sha256: "a".repeat(64),
container_sha256: "b".repeat(64),
bytes: source.length,
inventory_artifact_id: `art_${"a".repeat(64)}`,
kind: "javascript",
unpacked: false,
text: { included: true, value: source },
};
const facts = analyzeJavaScriptStaticSource(source);
const ir = analyzeJavaScriptSemantics(source);
const project = (budget: number) => {
const steps = projectJavaScriptSemanticFileSteps(
file,
ir,
budget,
facts.electron.call_paths,
);
let result = steps.next();
while (!result.done) result = steps.next();
return result.value;
};
it("retains Electron calls before library definitions consume the node budget", () => {
const result = project(20);
expect(result.nodes).toHaveLength(20);
expect(result.truncated).toBe(true);
const terminal = result.nodes.find(
(node) => node.label === "ipc-event.sender.copy",
);
expect(terminal).toBeDefined();
expect(
result.relations.some(
(r) =>
r.target_node_id === terminal?.node_id && r.resolution === "candidate",
),
).toBe(true);
expect(
result.nodes.some(
(node) =>
node.properties.entry === "test:copy" &&
Array.isArray(node.properties.conditions) &&
node.properties.conditions.length === 1,
),
).toBe(true);
});
it("reports truncation and keeps referential integrity even within a boundary path", () => {
const result = project(3);
expect(result.nodes).toHaveLength(3);
expect(result.truncated).toBe(true);
expect(
result.nodes.some((node) => node.label === "ipc-event.sender.copy"),
).toBe(false);
const retained = new Set(result.nodes.map((node) => node.node_id));
expect(
result.relations.every(
(r) => retained.has(r.source_node_id) && retained.has(r.target_node_id),
),
).toBe(true);
});