Files
reverse-engineering-anything/src/domain/javascriptSemanticAnalysis.structure.test.ts
T
Rudy Mizrahi Celekli 55f0267c40 fix(javascript): bind named class expressions in their class scope (#510)
* fix(javascript): bind named class expressions in their class scope

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* test(javascript): isolate class expression lexical names regression cases

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-05 14:59:01 +08:00

287 lines
9.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import {
semanticBinding,
semanticReferenceAt,
} from "./javascriptSemanticIr.js";
import {
programScope,
bindingsNamed,
onlyBinding,
topLevelBinding,
origin,
} from "./javascriptSemanticAnalysis.fixture.js";
describe("JavaScript semantic analysis: structure 1", () => {
it("resolves imports, require destructuring, aliases, assignments, and shadowing", () => {
const ir = analyzeJavaScriptSemantics(`
import { ipcRenderer as ir } from "electron";
const { ipcMain: bus } = require("electron");
const forwarded = bus;
let assigned;
assigned = ir;
ir.invoke("outside");
bus.handle("main", handler);
function local(ipcRenderer) {
const bus = require("./local-bus.js");
ipcRenderer.send("shadowed");
return bus;
}
`);
expect(ir.coverage).toEqual({
status: "complete",
omittedCount: 0,
});
expect(origin(topLevelBinding(ir, "ir"))).toEqual({
specifier: "electron",
importedPath: ["ipcRenderer"],
});
expect(origin(topLevelBinding(ir, "bus"))).toEqual({
specifier: "electron",
importedPath: ["ipcMain"],
});
expect(origin(topLevelBinding(ir, "forwarded"))).toEqual({
specifier: "electron",
importedPath: ["ipcMain"],
});
expect(origin(topLevelBinding(ir, "assigned"))).toEqual({
specifier: "electron",
importedPath: ["ipcRenderer"],
});
expect(ir.moduleLinks).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "require",
specifier: "electron",
importedName: "ipcMain",
localName: "bus",
}),
]),
);
const innerBus = bindingsNamed(ir, "bus").find(
({ scopeId }) => scopeId !== programScope(ir).scopeId,
);
expect(innerBus).toBeDefined();
if (innerBus === undefined) return;
expect(origin(innerBus)).toEqual({
specifier: "./local-bus.js",
importedPath: [],
});
const shadow = onlyBinding(ir, "ipcRenderer");
expect(shadow.provenance).toEqual({
status: "local",
origins: [],
reason: null,
});
const shadowReference = ir.references.find(
({ name, bindingId, role }) =>
name === "ipcRenderer" &&
bindingId === shadow.bindingId &&
role === "read",
);
expect(shadowReference?.resolution).toBe("resolved");
expect(shadowReference?.bindingId).not.toBe(
topLevelBinding(ir, "ir").bindingId,
);
if (shadowReference === undefined || shadowReference.bindingId === null)
throw new Error("Missing shadow reference");
expect(semanticBinding(ir, shadowReference.bindingId)).toEqual(shadow);
expect(
semanticReferenceAt(
ir,
shadowReference.location.start.line,
shadowReference.location.start.column,
),
).toEqual(shadowReference);
});
it.each([
`function run(require) { const bus = require("electron").ipcMain; return bus; }`,
`function run() { const { ipcMain: bus } = require("electron"); function require() {} return bus; }`,
`{ const bus = require("electron").ipcMain; const require = localLoader; }`,
`import require from "./loader.js"; const bus = require("electron").ipcMain;`,
])(
"does not assign CommonJS origins to lexically shadowed require",
(source) => {
const ir = analyzeJavaScriptSemantics(source);
const bus = onlyBinding(ir, "bus");
expect(bus.provenance.origins).toEqual([]);
expect(ir.moduleLinks.filter(({ kind }) => kind === "require")).toEqual(
[],
);
},
);
it("preserves unshadowed require origins through aliases and assignments", () => {
const ir = analyzeJavaScriptSemantics(`
const { ipcMain: bus } = require("electron");
const forwarded = bus;
let assigned;
assigned = require("electron").ipcMain;
{ const require = localLoader; require("./local.js"); }
`);
for (const name of ["bus", "forwarded", "assigned"])
expect(origin(topLevelBinding(ir, name))).toEqual({
specifier: "electron",
importedPath: ["ipcMain"],
});
expect(
ir.moduleLinks.filter(({ kind }) => kind === "require"),
).toHaveLength(1);
});
it("propagates literal, template, object, conditional, and destructured values", () => {
const ir = analyzeJavaScriptSemantics(`
const prefix = "rea";
const suffix = "open";
const channel = \`${"${prefix}"}:${"${suffix}"}\`;
const options = {
channel,
mode: enabled ? "read" : "write",
};
const selected = options.channel;
const { mode } = options;
const [first] = ["zero", "one"];
`);
expect(topLevelBinding(ir, "channel").value).toEqual({
status: "literal",
value: "rea:open",
});
expect(topLevelBinding(ir, "selected").value).toEqual({
status: "literal",
value: "rea:open",
});
expect(topLevelBinding(ir, "mode").value).toEqual({
status: "union",
values: ["read", "write"],
});
expect(topLevelBinding(ir, "first").value).toEqual({
status: "literal",
value: "zero",
});
});
});
describe("JavaScript semantic analysis: class expression lexical names", () => {
it("resolves named class expressions inside the class without changing the outer binding", () => {
const ir = analyzeJavaScriptSemantics(`
const Service = "outside";
const Wrapper = class Service {
create() { return new Service(); }
};
consume(Service);
`);
expect(bindingsNamed(ir, "Service")).toHaveLength(2);
expect(topLevelBinding(ir, "Service").value).toEqual({
status: "literal",
value: "outside",
});
const references = ir.references.filter(
({ name, role }) => name === "Service" && role === "read",
);
expect(references).toHaveLength(2);
expect(references[0]?.bindingId).not.toBe(references[1]?.bindingId);
expect(references[0]?.resolution).toBe("resolved");
const classScope = ir.scopes.find(({ kind }) => kind === "class");
expect(
bindingsNamed(ir, "Service").some(
({ scopeId }) => scopeId === classScope?.scopeId,
),
).toBe(true);
});
it("does not expose a named class expression outside its class", () => {
const ir = analyzeJavaScriptSemantics(`
const Wrapper = class Internal { create() { return new Internal(); } };
consume(Internal);
`);
const references = ir.references.filter(
({ name, role }) => name === "Internal" && role === "read",
);
expect(references.map(({ resolution }) => resolution)).toEqual([
"resolved",
"unbound",
]);
expect(topLevelBinding(ir, "Wrapper").provenance.status).toBe("local");
});
});
describe("JavaScript semantic analysis: structure 2", () => {
it("retains ESM, re-export, require, and CommonJS export relationships", () => {
const ir = analyzeJavaScriptSemantics(`
export { ipcRenderer as bridge } from "electron";
export * from "./wrapper.js";
export const localValue = 1;
const addon = require("./native.node");
module.exports.addon = addon;
export default function namedDefault() {}
`);
expect(ir.moduleLinks).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "re-export",
specifier: "electron",
importedName: "ipcRenderer",
exportedName: "bridge",
}),
expect.objectContaining({
kind: "re-export",
specifier: "./wrapper.js",
importedName: "*",
exportedName: "*",
}),
expect.objectContaining({
kind: "export",
localName: "localValue",
exportedName: "localValue",
}),
expect.objectContaining({
kind: "require",
specifier: "./native.node",
localName: "addon",
}),
expect.objectContaining({
kind: "commonjs-export",
localName: "addon",
exportedName: "addon",
}),
expect.objectContaining({
kind: "export",
localName: "namedDefault",
exportedName: "default",
}),
]),
);
});
it("keeps function, class, and method identities separate from bindings", () => {
const ir = analyzeJavaScriptSemantics(`
class Service {
run() {}
get value() { return 1; }
#privateMethod() {}
}
const arrow = () => 1;
const object = { method() {} };
`);
expect(ir.callables).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "class", name: "Service" }),
expect.objectContaining({ kind: "method", name: "run" }),
expect.objectContaining({ kind: "method", name: "value" }),
expect.objectContaining({ kind: "method", name: "#privateMethod" }),
expect.objectContaining({ kind: "function", name: "arrow" }),
expect.objectContaining({ kind: "method", name: "method" }),
]),
);
expect(bindingsNamed(ir, "run")).toEqual([]);
expect(bindingsNamed(ir, "method")).toEqual([]);
});
});