Files
rea/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts
T
rodricksz4h5 bec88b9d77 fix(javascript): retain CommonJS exports defined with Object.defineProperty (#4725)
* fix(javascript): retain CommonJS exports defined with Object.defineProperty

tsc, Babel and Rollup emit Object.defineProperty(exports, name, descriptor) for CommonJS re-exports. The semantic projection only recognised assignments, so these exports were missing from export_names and exposes edges. Recognise the call on exports or module.exports with the existing unmodified-global and receiver checks, track its receiver lifetime like an assignment, and link a data descriptor's value like the assigned value; an accessor's result stays unknown.

* fix(javascript): unwrap assignment chains in defined CommonJS export values

A descriptor value such as holder.parse = require("./parser").parse lost its require origin because only the assignment path took the rightmost assigned value. Do it once in the shared export builder.
2026-10-17 23:23:47 +08:00

840 lines
29 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import {
programScope,
bindingById,
bindingsNamed,
onlyBinding,
topLevelBinding,
origin,
} from "./javascriptSemanticAnalysis.fixture.js";
describe("JavaScript semantic analysis: ESM import origins", () => {
it.each(["", "a", "0"])(
"preserves exact ESM import name %j in binding origins",
(name) => {
const ir = analyzeJavaScriptSemantics(
"import { " +
JSON.stringify(name) +
' as selected } from "./module.mjs"; const alias = selected; export { alias };',
);
for (const binding of ["selected", "alias"])
expect(origin(topLevelBinding(ir, binding))).toEqual({
specifier: "./module.mjs",
importedPath: [name],
});
expect(ir.moduleLinks).toContainEqual(
expect.objectContaining({
kind: "import",
importedName: name,
localName: "selected",
}),
);
},
);
it("keeps default and namespace import origins distinct from an empty named import", () => {
const ir = analyzeJavaScriptSemantics(
'import value from "./module.mjs"; import * as namespace from "./module.mjs";',
);
expect(origin(topLevelBinding(ir, "value"))).toEqual({
specifier: "./module.mjs",
importedPath: ["default"],
});
expect(origin(topLevelBinding(ir, "namespace"))).toEqual({
specifier: "./module.mjs",
importedPath: [],
});
});
});
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"],
resolutionMode: "require",
});
expect(origin(topLevelBinding(ir, "forwarded"))).toEqual({
specifier: "electron",
importedPath: ["ipcMain"],
resolutionMode: "require",
});
expect(topLevelBinding(ir, "assigned").provenance).toMatchObject({
status: "ambiguous",
origins: [{ 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: [],
resolutionMode: "require",
});
const shadow = onlyBinding(ir, "ipcRenderer");
expect(shadow.provenance).toEqual({
status: "unknown",
origins: [],
reason: "Parameter provenance depends on caller arguments.",
});
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(bindingById(ir, shadowReference.bindingId)).toEqual(shadow);
expect(
ir.references.find(
({ location }) =>
location.start.line === shadowReference.location.start.line &&
location.start.column === 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("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: loop lexical names", () => {
it.each([
`for (let value = "inside"; ready; step()) { consume(value); }`,
`for (const value of entries) { consume(value); }`,
`for (const value in entries) { consume(value); }`,
])("does not leak lexical loop bindings into the enclosing scope", (loop) => {
const ir = analyzeJavaScriptSemantics(`
const value = "outside";
${loop}
consume(value);
`);
expect(topLevelBinding(ir, "value").value).toEqual({
status: "literal",
value: "outside",
});
expect(bindingsNamed(ir, "value")).toHaveLength(2);
const reads = ir.references.filter(
({ name, role }) => name === "value" && role === "read",
);
expect(reads).toHaveLength(2);
expect(reads[0]?.bindingId).not.toBe(reads[1]?.bindingId);
expect(reads[1]?.bindingId).toBe(topLevelBinding(ir, "value").bindingId);
});
it("resolves closures inside a loop against the loop binding", () => {
const ir = analyzeJavaScriptSemantics(`
const value = "outside";
for (const value of entries) { callbacks.push(() => value); }
consume(value);
`);
const reads = ir.references.filter(
({ name, role }) => name === "value" && role === "read",
);
expect(reads).toHaveLength(2);
expect(reads[0]?.bindingId).not.toBe(
topLevelBinding(ir, "value").bindingId,
);
expect(reads[1]?.bindingId).toBe(topLevelBinding(ir, "value").bindingId);
expect(ir.closureCaptures).toContainEqual(
expect.objectContaining({
bindingId: reads[0]?.bindingId,
}),
);
});
it("keeps var loop declarations in the enclosing function", () => {
const ir = analyzeJavaScriptSemantics(`
function run() { for (var value of entries) { consume(value); } return value; }
`);
const binding = onlyBinding(ir, "value");
const callable = ir.callables.find(({ name }) => name === "run");
expect(binding.scopeId).toBe(callable?.bodyScopeId);
expect(
ir.references
.filter(({ name, role }) => name === "value" && role === "read")
.every(({ bindingId }) => bindingId === binding.bindingId),
).toBe(true);
});
});
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: class member keys", () => {
it("does not read an outer binding through noncomputed class member names", () => {
const ir = analyzeJavaScriptSemantics(`
const marker = "seed";
class Holder {
marker() {}
static marker() {}
get marker() { return 1; }
set marker(value) {}
marker = 1;
static marker = 2;
#marker = 3;
#read() { return this.#marker; }
}
`);
expect(ir.references.filter(({ name }) => name === "marker")).toEqual([]);
});
it("does not read an outer binding through TypeScript class member names", () => {
const ir = analyzeJavaScriptSemantics(`
const marker = "seed";
abstract class Holder {
declare marker: string;
abstract marker(): void;
abstract [marker](): void;
}
class Overloads {
marker(): void;
marker(value?: number) {}
}
`);
// Only the computed abstract key evaluates the outer binding.
expect(
ir.references
.filter(({ name }) => name === "marker")
.map(({ role, location }) => `${role}@${location.start.line}`),
).toEqual(["read@6"]);
});
it("does not capture an outer binding through a noncomputed method name", () => {
const ir = analyzeJavaScriptSemantics(`
function outer() {
const marker = 1;
class Holder { marker() { return 2; } }
return Holder;
}
`);
expect(ir.closureCaptures).toEqual([]);
});
it("keeps reads from computed class keys and field initializers", () => {
const ir = analyzeJavaScriptSemantics(`
const marker = "seed";
class Holder {
[marker]() {}
field = marker;
}
`);
const reads = ir.references.filter(
({ name, role }) => name === "marker" && role === "read",
);
expect(reads.map(({ resolution }) => resolution)).toEqual([
"resolved",
"resolved",
]);
});
});
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("does not invent export or method names from dynamic keys", () => {
const ir = analyzeJavaScriptSemantics(`
const key = resolveKey();
exports[key] = addon;
module.exports[key] = addon;
const target = { [key]() { return 1; } };
class Holder { [key]() { return 2; } }
`);
// The assignment is real, but its name is not knowable, so the only
// honest exported_name is the wildcard.
expect(
ir.moduleLinks
.filter(({ kind }) => kind === "commonjs-export")
.map(({ exportedName }) => exportedName),
).toEqual(["*", "*"]);
const names = ir.callables.map(({ name }) => name);
expect(names).not.toContain("key");
expect(names.filter((name) => name?.startsWith("[computed@")).length).toBe(
2,
);
});
it("resolves the value of a simple assignment expression exported through CommonJS", () => {
const ir = analyzeJavaScriptSemantics(`
function parse() { return { kind: "result" }; }
const holder = {};
module.exports = holder.value = parse;
`);
expect(ir.moduleLinks).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "commonjs-export",
localName: "parse",
exportedName: "default",
}),
]),
);
});
it.each(["parse", "default"])(
"does not export a %s property write to a replaced module.exports receiver",
(name) => {
const ir = analyzeJavaScriptSemantics(`
module.exports.${name} = (module.exports = () => ({ kind: "result" }));
`);
expect(ir.moduleLinks).toEqual([
expect.objectContaining({
kind: "commonjs-export",
exportedName: "default",
}),
]);
},
);
});
describe("CommonJS receiver lifetimes", () => {
it.each([
'Object.defineProperty(module.exports, "other", { value: exports });',
"Object.assign(module.exports, { other: exports });",
])(
"keeps the receiver identity intact when mutating its contents: %s",
(mutation) => {
const ir = analyzeJavaScriptSemantics(
`exports.parse = () => 1; ${mutation}`,
);
const exported = ir.moduleLinks.find(
({ exportedName }) => exportedName === "parse",
);
expect(exported?.callableId).toEqual(expect.any(String));
},
);
it.each([
"module.exports = {}; exports.parse = () => 1;",
"exports = {}; exports.parse = () => 1;",
"exports.parse = () => 1; module.exports = {};",
"module.exports.parse = () => 1; module.exports = [];",
'module.exports = {}; exports.status = "enabled";',
"module.exports = exports = {}; module.exports = {}; exports.parse = () => 1;",
'module.exports = {}; Reflect.set(module, "other", exports); exports.parse = () => 1;',
'module.exports = {}; Object.defineProperty(globalThis.module, "exports", { value: exports }); exports.parse = () => 1;',
"module.exports = {}; let wrapper = module; wrapper = {}; Object.assign(wrapper, { exports }); exports.parse = () => 1;",
])(
"excludes a detached CommonJS receiver across statements: %s",
(source) => {
const ir = analyzeJavaScriptSemantics(source);
expect(
ir.moduleLinks.filter(
({ exportedName }) =>
exportedName === "parse" || exportedName === "status",
),
).toEqual([]);
},
);
it.each([
"exports.parse = () => 1;",
"module.exports = {}; module.exports.parse = () => 1;",
"module.exports = {}; exports = module.exports; exports.parse = () => 1;",
"module.exports = exports = {}; exports.parse = () => 1;",
"exports = module.exports = {}; exports.parse = () => 1;",
"module.exports = module.exports; exports.parse = () => 1;",
"module.exports = external; exports = module.exports; exports.parse = () => 1;",
"module.exports = external; module.exports.parse = () => 1;",
"exports.parse = (exports = {} , () => 1);",
"exports.parse = (module.exports = {}); module.exports = exports;",
"(module.exports = {}, exports = module.exports, exports.parse = () => 1);",
])("retains a connected CommonJS receiver: %s", (source) => {
expect(analyzeJavaScriptSemantics(source).moduleLinks).toContainEqual(
expect.objectContaining({
kind: "commonjs-export",
exportedName: "parse",
}),
);
});
it.each([
"module.exports = external; exports.parse = () => 1;",
"if (flag) module.exports = {}; exports.parse = () => 1;",
"flag && (module.exports = {}); exports.parse = () => 1;",
"holder ||= (module.exports = {}); exports.parse = () => 1;",
"invoke?.(module.exports = {}); exports.parse = () => 1;",
"const { value = (module.exports = {}) } = source; exports.parse = () => 1;",
"module[key] = {}; exports.parse = () => 1;",
"exports.parse = () => 1; if (flag) module.exports = {};",
"function replace() { module.exports = {}; } exports.parse = () => 1; replace();",
"function replace() { exports = {}; } replace(); exports.parse = () => 1;",
"module.exports = {}; function install() { exports.parse = () => 1; } install();",
"for (exports of values) {} exports.parse = () => 1;",
"({ exports } = external); exports.parse = () => 1;",
"module = {}; module.exports = {}; module.exports.parse = () => 1;",
'const holder = {}; ({ [(module.exports = exports, "x")]: holder.x } = (module.exports = {})); exports.parse = () => 1;',
'const { [(module.exports = exports, "x")]: value } = (module.exports = {}); exports.parse = () => 1;',
"const [value = (module.exports = exports)] = (module.exports = []); exports.parse = () => 1;",
"module.exports = {}; Object.assign(module, { exports }); exports.parse = () => 1;",
'module.exports = {}; Object.defineProperty(module, "exports", { value: exports }); exports.parse = () => 1;',
'module.exports = {}; Reflect.set(module, "exports", exports); exports.parse = () => 1;',
"module.exports = {}; const assign = Object.assign; assign.call(null, module, { exports }); exports.parse = () => 1;",
"module.exports = {}; const wrapper = module; Object.assign(wrapper, { exports }); exports.parse = () => 1;",
])("keeps uncertain CommonJS receiver identity unresolved: %s", (source) => {
expect(analyzeJavaScriptSemantics(source).moduleLinks).toContainEqual(
expect.objectContaining({
kind: "commonjs-export",
exportedName: "parse",
callableId: null,
localName: null,
}),
);
});
});
describe("CommonJS exports defined with Object.defineProperty", () => {
const exportsOf = (source: string) =>
analyzeJavaScriptSemantics(source).moduleLinks.filter(
({ kind }) => kind === "commonjs-export",
);
const defineParse =
'Object.defineProperty(exports, "parse", { value: () => 1 });';
it("retains tsc CommonJS re-exports and data descriptors", () => {
const links = exportsOf(`
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VERSION = void 0;
exports.VERSION = "1.0";
var client_1 = require("./client");
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_1.Client; } });
`);
expect(links.map(({ exportedName }) => exportedName)).toEqual([
"__esModule",
"VERSION",
"VERSION",
"Client",
]);
// An accessor's result is not proven by the descriptor.
expect(links.at(-1)).toMatchObject({
localName: null,
callableId: null,
specifier: null,
});
});
it.each([
["() => 1", "parse"],
['require("./parser")', "parse"],
['require("./parser").parse', "parse"],
["parse", "parse"],
['holder.parse = require("./parser").parse', "parse"],
["first = second = () => 1", "parse"],
])(
"links a data descriptor value %s like the equivalent assignment",
(value, name) => {
const strip = (links: ReturnType<typeof exportsOf>) =>
links.map(({ location: _location, callableId, ...link }) => ({
...link,
callable: callableId !== null,
}));
const assigned = exportsOf(`exports.${name} = ${value};`);
const defined = exportsOf(
`Object.defineProperty(exports, "${name}", { value: ${value} });`,
);
expect(strip(defined)).toEqual(strip(assigned));
expect(defined).toHaveLength(1);
},
);
it.each([
defineParse,
`module.exports = {}; ${defineParse.replace("(exports", "(module.exports")}`,
`module.exports = {}; exports = module.exports; ${defineParse}`,
])("retains a connected receiver: %s", (source) => {
expect(exportsOf(source)).toContainEqual(
expect.objectContaining({
exportedName: "parse",
callableId: expect.any(String),
}),
);
});
it.each([
`module.exports = {}; ${defineParse}`,
`exports = {}; ${defineParse}`,
`${defineParse} module.exports = {};`,
])("excludes a detached receiver: %s", (source) => {
expect(
exportsOf(source).filter(({ exportedName }) => exportedName === "parse"),
).toEqual([]);
});
it.each([
`if (flag) module.exports = {}; ${defineParse}`,
`function replace() { exports = {}; } replace(); ${defineParse}`,
`module.exports = {}; Object.defineProperty(module, "exports", { value: exports }); ${defineParse}`,
`if (flag) { ${defineParse} }`,
])("keeps an uncertain receiver unresolved: %s", (source) => {
expect(exportsOf(source)).toContainEqual(
expect.objectContaining({
exportedName: "parse",
callableId: null,
localName: null,
}),
);
});
it.each([
'const Object = { defineProperty() {} }; Object.defineProperty(exports, "parse", { value: 1 });',
'Object.defineProperty = patched; Object.defineProperty(exports, "parse", { value: 1 });',
'Object.defineProperty(other, "parse", { value: 1 });',
'Object.defineProperty(module, "parse", { value: 1 });',
'function local(exports) { Object.defineProperty(exports, "parse", { value: 1 }); }',
'Object.defineProperty(exports, "parse");',
"Object.defineProperty(...args);",
])("does not treat other defineProperty calls as exports: %s", (source) => {
expect(
exportsOf(source).filter(({ exportedName }) => exportedName === "parse"),
).toEqual([]);
});
it("keeps a dynamic defined key unknown", () => {
expect(
exportsOf("Object.defineProperty(exports, key, { value: 1 });"),
).toEqual([expect.objectContaining({ exportedName: "*" })]);
});
});
describe("JavaScript semantic analysis: write references", () => {
it("classifies destructuring and loop targets as writes, not reads", () => {
const ir = analyzeJavaScriptSemantics(`
let a, b, c;
({ a } = source);
[b] = list;
({ nested: { c } } = source);
for (a of list) {}
let d;
try { risky(); } catch ({ message }) { report(message); }
`);
const roles = (name: string): string[] =>
ir.references
.filter((reference) => reference.name === name)
.map(({ role }) => role);
expect(roles("a")).toEqual(["write", "write"]);
expect(roles("b")).toEqual(["write"]);
expect(roles("c")).toEqual(["write"]);
// A catch binding is a declaration, so its only later use is a read.
expect(roles("message")).toEqual(["read"]);
});
it("retains the read a compound assignment or update performs", () => {
const ir = analyzeJavaScriptSemantics(`
let total = 0;
total += 1;
total++;
total = 2;
const observed = total;
`);
const roles = ir.references
.filter(({ name }) => name === "total")
.map(({ role }) => role);
expect(roles).toEqual([
"read",
"write", // total += 1
"read",
"write", // total++
"write", // total = 2
"read", // const observed = total
]);
});
it("treats TypeScript parameter-property declarations as bindings rather than reads", () => {
const ir = analyzeJavaScriptSemantics(`
const seed = 1;
class Service {
constructor(
public x: number,
public y = 1,
public z = seed,
) {
console.log(this.x);
console.log(y);
}
}
`);
const referencesFor = (name: string) =>
ir.references.filter((reference) => reference.name === name);
// Parameter properties are definitions, not source identifier reads.
expect(referencesFor("x")).toEqual([]);
expect(referencesFor("z")).toEqual([]);
// Actual reads in constructor body survive.
expect(referencesFor("y")).toEqual([
expect.objectContaining({
name: "y",
role: "read",
}),
]);
// Actual reads in parameter default values remain references.
expect(referencesFor("seed")).toEqual([
expect.objectContaining({
name: "seed",
role: "read",
}),
]);
});
});
describe("JavaScript semantic analysis: dynamic and case scopes", () => {
it("marks with and eval as unresolved dynamic scope instead of staying silent", () => {
const ir = analyzeJavaScriptSemantics(`
function load(source) {
with (host) { consume(target); }
eval("var injected = 1;");
}
`);
const reasons = ir.frontiers
.filter(({ kind }) => kind === "dynamic-scope")
.map(({ reason }) => reason);
expect(reasons).toHaveLength(2);
expect(reasons.join("\n")).toContain("`with`");
expect(reasons.join("\n")).toContain("`eval`");
});
it("does not assign CommonJS exports to lexically shadowed module or exports", () => {
const ir = analyzeJavaScriptSemantics(`
function shadowed(module, exports) {
module.exports.value = 1;
exports.other = 2;
exports[key] = 3;
module.exports[key] = 4;
unrelated[key] = 5;
}
`);
expect(
ir.moduleLinks.filter(({ kind }) => kind === "commonjs-export"),
).toEqual([]);
});
it("shares one lexical scope across switch cases and isolates static blocks", () => {
const switchIr = analyzeJavaScriptSemantics(`
const kind = select();
switch (kind) {
case "a": let shared = 1; break;
case "b": shared = 2; break;
}
`);
// One declaration, one binding — and it belongs to a scope of its own, so
// it cannot leak past the switch statement.
const shared = switchIr.bindings.filter(({ name }) => name === "shared");
expect(shared).toHaveLength(1);
expect(shared[0]?.scopeId).not.toBe(
topLevelBinding(switchIr, "kind").scopeId,
);
const classIr = analyzeJavaScriptSemantics(`
class Holder {
static { let isolated = 1; }
static { let isolated = 2; }
}
`);
expect(
classIr.bindings.filter(({ name }) => name === "isolated"),
).toHaveLength(2);
});
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([]);
});
});