mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
2bba39bc2b
* fix(domain): classify TypeScript parameter properties as declarations rather than identifier reads Recognize TSParameterProperty in isPatternOwnerDeclaration, isDeclarationIdentifier, and patternContains so parameter property definitions are not emitted as identifier reads, while retaining valid initializer reads. Fixes #4371 * test(domain): assert constructor body parameter property reads survive
658 lines
23 KiB
TypeScript
658 lines
23 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: 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("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([]);
|
|
});
|
|
});
|