mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
75104040df
* fix(javascript): track public CommonJS export receiver lifetimes * fix(javascript): retain exports across uncertain identity effects
386 lines
12 KiB
TypeScript
386 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
analyzeJavaScriptSemantics,
|
|
analyzeParsedJavaScriptSemantics,
|
|
} from "./javascriptSemanticAnalysis.js";
|
|
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
|
import { moduleResolutionModeForJavaScriptReference } from "./javascriptModuleResolutionMode.js";
|
|
import {
|
|
origin,
|
|
topLevelBinding,
|
|
} from "./javascriptSemanticAnalysis.fixture.js";
|
|
|
|
describe("TypeScript import-equals links", () => {
|
|
it("retains runtime external import-equals as a require dependency", () => {
|
|
const links = analyzeJavaScriptSemantics(`
|
|
import dependency = require("./dependency.cjs");
|
|
import type types = require("./types");
|
|
import alias = Namespace.Dependency;
|
|
`).moduleLinks;
|
|
|
|
expect(links).toEqual([
|
|
expect.objectContaining({
|
|
kind: "require",
|
|
specifier: "./dependency.cjs",
|
|
importedName: null,
|
|
localName: "dependency",
|
|
exportedName: null,
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "type-import",
|
|
specifier: "./types",
|
|
localName: "types",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("keeps the exported binding for exported import-equals", () => {
|
|
const source = 'export import dependency = require("./dependency");';
|
|
const links = analyzeJavaScriptSemantics(source).moduleLinks;
|
|
|
|
expect(links).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "require",
|
|
specifier: "./dependency",
|
|
localName: "dependency",
|
|
exportedName: null,
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "export",
|
|
specifier: null,
|
|
localName: "dependency",
|
|
exportedName: "dependency",
|
|
}),
|
|
]),
|
|
);
|
|
expect(
|
|
links.find(({ kind }) => kind === "require")?.location.start,
|
|
).toEqual({ line: 1, column: 7 });
|
|
});
|
|
});
|
|
|
|
describe("CommonJS wrapper parameter redeclarations", () => {
|
|
it("does not infer a CommonJS wrapper from resolution mode in an explicit module", () => {
|
|
const path = "app/main.mjs";
|
|
const parsed = parseJavaScriptSource(
|
|
'var require; require("./dependency.cjs");',
|
|
path,
|
|
);
|
|
expect(parsed).not.toBeNull();
|
|
expect(parsed?.program.sourceType).toBe("module");
|
|
const ir = analyzeParsedJavaScriptSemantics(
|
|
parsed!,
|
|
moduleResolutionModeForJavaScriptReference(path, "static-import"),
|
|
);
|
|
expect(ir.moduleLinks).not.toContainEqual(
|
|
expect.objectContaining({ specifier: "./dependency.cjs" }),
|
|
);
|
|
});
|
|
|
|
it("retains ambient exports and require through uninitialized top-level vars", () => {
|
|
const path = "app/main.cjs";
|
|
const parsed = parseJavaScriptSource(
|
|
'var module, exports, require; module.exports = require("./dependency.cjs");',
|
|
path,
|
|
);
|
|
expect(parsed).not.toBeNull();
|
|
const ir = analyzeParsedJavaScriptSemantics(
|
|
parsed!,
|
|
moduleResolutionModeForJavaScriptReference(path, "static-import"),
|
|
);
|
|
expect(ir.moduleLinks).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "commonjs-export",
|
|
specifier: "./dependency.cjs",
|
|
localName: null,
|
|
exportedName: "default",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("retains the Node loader call on an uninitialized wrapper var assignment RHS", () => {
|
|
const path = "app/main.cjs";
|
|
const parsed = parseJavaScriptSource(
|
|
'var require; require = require("./bootstrap.cjs");',
|
|
path,
|
|
);
|
|
expect(parsed).not.toBeNull();
|
|
const ir = analyzeParsedJavaScriptSemantics(
|
|
parsed!,
|
|
moduleResolutionModeForJavaScriptReference(path, "static-import"),
|
|
);
|
|
expect(ir.moduleLinks).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "require",
|
|
specifier: "./bootstrap.cjs",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
'var require = local; require("./dependency.cjs");',
|
|
'var require; require = replacement; require("./dependency.cjs");',
|
|
"var module; module = replacement; module.exports = 1;",
|
|
"var exports; exports = replacement; exports.value = 1;",
|
|
'let require; require("./dependency.cjs");',
|
|
'function nested() { var require; require("./dependency.cjs"); }',
|
|
])("keeps non-wrapper declarations as shadows: %s", (source) => {
|
|
const path = "app/main.cjs";
|
|
const parsed = parseJavaScriptSource(source, path);
|
|
expect(parsed).not.toBeNull();
|
|
const ir = analyzeParsedJavaScriptSemantics(
|
|
parsed!,
|
|
moduleResolutionModeForJavaScriptReference(path, "static-import"),
|
|
);
|
|
expect(
|
|
ir.moduleLinks.filter(
|
|
({ kind }) => kind === "require" || kind === "commonjs-export",
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("TypeScript type-only module links", () => {
|
|
it("retains declarations without runtime bindings or callable claims", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
import type { Whole } from "./whole";
|
|
import { type Inline } from "./inline";
|
|
import { type Erased, value } from "./mixed";
|
|
export type { Reexported } from "./reexported";
|
|
export { type InlineExport } from "./inline-export";
|
|
export type LocalType = string;
|
|
export interface LocalInterface { value: string }
|
|
function hiddenFunction() { return 1; }
|
|
export type { hiddenFunction };
|
|
`);
|
|
|
|
const externalLinks = ir.moduleLinks
|
|
.filter(({ specifier }) => specifier !== null)
|
|
.map(
|
|
({ kind, specifier, importedName, exportedName }) =>
|
|
[kind, specifier, importedName, exportedName] as const,
|
|
);
|
|
expect(externalLinks).toEqual([
|
|
["type-import", "./whole", "Whole", null],
|
|
["type-import", "./inline", "Inline", null],
|
|
["type-import", "./mixed", "Erased", null],
|
|
["import", "./mixed", "value", null],
|
|
["type-import", "./reexported", "Reexported", "Reexported"],
|
|
["type-import", "./inline-export", "InlineExport", "InlineExport"],
|
|
]);
|
|
|
|
const typeLinks = ir.moduleLinks
|
|
.filter(({ kind }) => kind === "type-import")
|
|
.map(
|
|
({ specifier, localName, exportedName, callableId }) =>
|
|
[specifier, localName, exportedName, callableId] as const,
|
|
);
|
|
expect(typeLinks).toEqual([
|
|
["./whole", "Whole", null, null],
|
|
["./inline", "Inline", null, null],
|
|
["./mixed", "Erased", null, null],
|
|
["./reexported", null, "Reexported", null],
|
|
["./inline-export", null, "InlineExport", null],
|
|
[null, "LocalType", "LocalType", null],
|
|
[null, "LocalInterface", "LocalInterface", null],
|
|
[null, "hiddenFunction", "hiddenFunction", null],
|
|
]);
|
|
expect(ir.limitations).toContain(
|
|
"Compiler options are unavailable; inline type-only imports or re-exports may preserve a module side-effect dependency.",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("CommonJS dependency links", () => {
|
|
it("retains an import-equals binding and its module origin", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
import typed = require("dependency");
|
|
import type TypeOnly = require("type-only");
|
|
namespace Local { export class Marker {} }
|
|
import Alias = Local.Marker;
|
|
void typed;
|
|
`);
|
|
expect(topLevelBinding(ir, "typed")).toMatchObject({
|
|
kind: "import",
|
|
mutable: false,
|
|
provenance: {
|
|
origins: [{ specifier: "dependency", importedPath: [] }],
|
|
},
|
|
});
|
|
expect(
|
|
ir.moduleLinks.filter(({ specifier }) => specifier !== null),
|
|
).toEqual([
|
|
expect.objectContaining({
|
|
kind: "require",
|
|
specifier: "dependency",
|
|
importedName: null,
|
|
localName: "typed",
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "type-import",
|
|
specifier: "type-only",
|
|
importedName: null,
|
|
localName: "TypeOnly",
|
|
}),
|
|
]);
|
|
expect(ir.references.filter(({ name }) => name === "typed")).toHaveLength(
|
|
1,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
'require("dependency");',
|
|
'void require("dependency");',
|
|
'consume(require("dependency"));',
|
|
'require("dependency").member;',
|
|
'require("dependency")[key];',
|
|
'const {} = require("dependency");',
|
|
'const [] = require("dependency");',
|
|
'let assigned; assigned = require("dependency");',
|
|
'function run(value = require("dependency")) {}',
|
|
'(require("dependency") as unknown);',
|
|
])("retains an unbound dependency in %s", (source) => {
|
|
const ir = analyzeJavaScriptSemantics(source);
|
|
expect(ir.moduleLinks).toEqual([
|
|
expect.objectContaining({
|
|
kind: "require",
|
|
specifier: "dependency",
|
|
importedName: null,
|
|
localName: null,
|
|
exportedName: null,
|
|
callableId: null,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
'function run(require) { require("dependency"); }',
|
|
'require("dependency"); const require = loader;',
|
|
'function run() { require("dependency"); var require; }',
|
|
'with (environment) { require("dependency"); }',
|
|
"require(dynamicSpecifier);",
|
|
])("does not invent a static global dependency in %s", (source) => {
|
|
expect(analyzeJavaScriptSemantics(source).moduleLinks).toEqual([]);
|
|
});
|
|
|
|
it("keeps bindings and re-exports without extra dependency links", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const bound = (require("bound") as unknown);
|
|
const { left, right } = require("destructured");
|
|
module.exports = require("forwarded");
|
|
exports = module.exports;
|
|
exports.member = require("member").value;
|
|
exports.first = exports.second = require("chained");
|
|
`);
|
|
expect(
|
|
ir.moduleLinks.map(({ kind, specifier, localName, exportedName }) => ({
|
|
kind,
|
|
specifier,
|
|
localName,
|
|
exportedName,
|
|
})),
|
|
).toEqual([
|
|
{
|
|
kind: "require",
|
|
specifier: "bound",
|
|
localName: "bound",
|
|
exportedName: null,
|
|
},
|
|
{
|
|
kind: "require",
|
|
specifier: "destructured",
|
|
localName: "left",
|
|
exportedName: null,
|
|
},
|
|
{
|
|
kind: "require",
|
|
specifier: "destructured",
|
|
localName: "right",
|
|
exportedName: null,
|
|
},
|
|
{
|
|
kind: "commonjs-export",
|
|
specifier: "forwarded",
|
|
localName: null,
|
|
exportedName: "default",
|
|
},
|
|
{
|
|
kind: "commonjs-export",
|
|
specifier: "member",
|
|
localName: null,
|
|
exportedName: "member",
|
|
},
|
|
{
|
|
kind: "commonjs-export",
|
|
specifier: "chained",
|
|
localName: null,
|
|
exportedName: "first",
|
|
},
|
|
{
|
|
kind: "commonjs-export",
|
|
specifier: "chained",
|
|
localName: null,
|
|
exportedName: "second",
|
|
},
|
|
]);
|
|
expect(
|
|
ir.bindings.find(({ name }) => name === "bound")?.provenance.origins,
|
|
).toEqual([
|
|
{ specifier: "bound", importedPath: [], resolutionMode: "require" },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
'require("outer", require("inner"));',
|
|
'const bound = require("outer", require("inner"));',
|
|
'module.exports = require("outer", require("inner"));',
|
|
])("retains a distinct nested call in %s", (source) => {
|
|
const links = analyzeJavaScriptSemantics(source).moduleLinks;
|
|
expect(links.map(({ specifier }) => specifier)).toEqual(["outer", "inner"]);
|
|
expect(links[1]).toMatchObject({
|
|
kind: "require",
|
|
importedName: null,
|
|
localName: null,
|
|
exportedName: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
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"])
|
|
expect(origin(topLevelBinding(ir, name))).toEqual({
|
|
specifier: "electron",
|
|
importedPath: ["ipcMain"],
|
|
resolutionMode: "require",
|
|
});
|
|
expect(topLevelBinding(ir, "assigned").provenance).toMatchObject({
|
|
status: "ambiguous",
|
|
origins: [{ specifier: "electron", importedPath: ["ipcMain"] }],
|
|
});
|
|
expect(ir.moduleLinks.filter(({ kind }) => kind === "require")).toEqual([
|
|
expect.objectContaining({
|
|
specifier: "electron",
|
|
importedName: "ipcMain",
|
|
localName: "bus",
|
|
}),
|
|
expect.objectContaining({
|
|
specifier: "electron",
|
|
importedName: null,
|
|
localName: null,
|
|
}),
|
|
]);
|
|
});
|