Files
scriptc/packages/compiler/test/structural-value-boundaries.test.ts
T
Malte Ubl 6c9c39b3ab Keep class instance identity through structural record views
A class instance passed, stored, returned or asserted as an anonymous
object type or an interface it does not `implements` was copied into a
native record, so writes, identity checks and inspection through the view
missed the instance (`function add(o: { count: number }, n: number)
{ o.count += n }` left a Counter at 0).

ClassProtocols now scans flow sites (arguments, typed initializers and
fields, defaults, returns, arrow bodies, yields, assignments, array
elements, property values, assertions through `unknown`, and annotated
callback parameters) and marks every program object type a class
instance flows into, then propagates the mark along flows between
structural types. Marked types are class-backed protocols: their values
are checked views that keep the original object, like implemented
interfaces already were. Only sites whose source carries identity query
the checker for their target, and a checker panic skips the site.
Assertions read or written member-wise in place keep their direct field
access, and `typeof C` is not an instance.

An assertion of a class instance to such a view boxes it like an
implicit flow, and `view.n++` on a number member lowers to the checked
expanded form.
2026-10-16 14:02:02 -07:00

114 lines
3.8 KiB
TypeScript

import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { analyze } from "../src/index.js";
const nominal = `
class Row { left = 1; right = 2; }
const rows = [new Row()];
function choose(): string { return "left"; }
`;
test.each([
[
"optional nominal missing read",
nominal + `console.log((rows[0] as unknown as { missing: number })["missing"]);`,
"structural views of class instances",
],
[
"optional nominal missing write",
nominal + `(rows[0] as unknown as { missing: number })["missing"] = 3;`,
"structural views of class instances",
],
[
"optional nominal computed read",
nominal + `console.log((rows[0] as unknown as Record<string, number>)[choose()]);`,
"structural views of class instances",
],
[
"optional nominal computed write",
nominal + `(rows[0] as unknown as Record<string, number>)[choose()] = 3;`,
"structural views of class instances",
],
[
"nominal computed write",
nominal + `(new Row() as unknown as Record<string, number>)[choose()] = 3;`,
"structural views of class instances",
],
[
"getter-only structural write",
`class Row { get value(): number { return 1; } }
(new Row() as { value: number })["value"] = 2;`,
"getter-only property",
],
[
"setter-only structural read",
`class Row { set value(value: number) { console.log(value); } }
console.log((new Row() as { value: number })["value"]);`,
"only a setter",
],
[
"opaque optional class view cannot discard prototype accessors",
nominal +
`
console.log((rows[0] as { left: unknown }).left);`,
"opaque structural views",
],
[
"opaque record view cannot discard accessors",
`
const source = { get value(): number { return 1; } };
console.log((source as { value: unknown }).value);`,
"opaque structural views",
],
[
"runtime keys cannot supply required fields",
`
const values: Record<string, unknown> = { value: 1 };
const result: { value: number } = { ...values } as { value: number };
console.log(result.value);`,
"required field",
],
])(
"%s reports a frontend refusal instead of selecting the wrong layout",
(_name, source, message) => {
const dir = mkdtempSync(
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-structural-refusal-"),
);
try {
const entry = join(dir, "main.ts");
writeFileSync(entry, source);
const { coverage } = analyze(entry, {});
expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false);
expect(coverage.diagnostics).toContainEqual(
expect.objectContaining({ code: "SC1090", message: expect.stringContaining(message) }),
);
expect(coverage.diagnostics.some((diagnostic) => diagnostic.code === "SC9001")).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
},
);
test("opaque class view keeps prototype accessors through the instance itself", () => {
const dir = mkdtempSync(
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-structural-view-"),
);
try {
const entry = join(dir, "main.ts");
writeFileSync(
entry,
`class Row { get value(): number { return 1; } }
const opaque = new Row() as { value: unknown }; console.log(opaque.value);`,
);
const { coverage } = analyze(entry, {});
expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false);
// The view is the instance itself, so the build has no blocker; the
// differential corpus (5767) pins the behavior against Node.
expect(coverage.diagnostics).toEqual([]);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});