mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vitest";
|
|
import { compile, deserializeModule } from "../src/index.js";
|
|
import type { IrModule } from "../src/ir/ir.js";
|
|
|
|
async function lowerModule(source: string, file = "main.ts"): Promise<IrModule> {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-class-attributes-"));
|
|
try {
|
|
const entry = join(dir, file);
|
|
const outPath = join(dir, "main.ir.json");
|
|
await writeFile(entry, source);
|
|
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
|
if (!result.ok)
|
|
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
return deserializeModule(await readFile(outPath, "utf8"));
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function fieldsOf(mod: IrModule, className: string): string[] {
|
|
const cls = (mod.classes ?? []).find(
|
|
(c) => c.name === className || c.name.endsWith(`.${className}`),
|
|
);
|
|
if (!cls) throw new Error(`no class ${className}`);
|
|
return cls.fields.map((f) => f.name);
|
|
}
|
|
|
|
function functionsCalling(mod: IrModule, fn: string): string[] {
|
|
return mod.functions
|
|
.filter((f) => JSON.stringify(f.body).includes(`"fn":"${fn}"`))
|
|
.map((f) => f.name);
|
|
}
|
|
|
|
test("only hierarchies passed to an integrity call get attribute state and write checks", async () => {
|
|
const mod = await lowerModule(`
|
|
export {};
|
|
class Point { x = 1; }
|
|
class Other { x = 1; }
|
|
function movePoint(p: Point): void { p.x = 2; }
|
|
function moveOther(o: Other): void { o.x = 2; }
|
|
const p = new Point();
|
|
Object.freeze(p);
|
|
movePoint(p);
|
|
moveOther(new Other());
|
|
`);
|
|
expect(fieldsOf(mod, "Point")).toContain("%attributes");
|
|
expect(fieldsOf(mod, "Other")).not.toContain("%attributes");
|
|
const checked = functionsCalling(mod, "class.throwReadOnly");
|
|
expect(checked).toContain("movePoint");
|
|
expect(checked).not.toContain("moveOther");
|
|
});
|
|
|
|
test("a redefined field gets its own attribute word", async () => {
|
|
const mod = await lowerModule(`
|
|
export {};
|
|
class Config { mode = "dev"; level = 1; }
|
|
const c = new Config();
|
|
Object.defineProperty(c, "mode", { writable: false });
|
|
`);
|
|
const fields = fieldsOf(mod, "Config");
|
|
expect(fields).toContain("%attributes");
|
|
expect(fields).toContain("%attr:mode");
|
|
expect(fields).not.toContain("%attr:level");
|
|
});
|
|
|
|
test("sloppy-mode writes skip a read-only field without throwing", async () => {
|
|
const mod = await lowerModule(
|
|
`
|
|
class Box { constructor() { this.x = 1; } }
|
|
const b = new Box();
|
|
Object.freeze(b);
|
|
b.x = 2;
|
|
`,
|
|
"main.cjs",
|
|
);
|
|
expect(fieldsOf(mod, "Box")).toContain("%attributes");
|
|
// Class bodies are strict mode code; the top-level write is not.
|
|
const throwing = functionsCalling(mod, "class.throwReadOnly");
|
|
expect(throwing.length).toBeGreaterThan(0);
|
|
expect(throwing.every((name) => name.includes("constructor"))).toBe(true);
|
|
});
|