mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
The generator listed a field as a scalar when its walk read nothing, so a nested record holding only numbers and booleans got a ValuesNotRead check on the record itself, which fails because the record is an object, even after regenerating. A field is now listed as a scalar only when its type holds no string and no object; such a record is checked through its own fields, so a field added to it or a scalar in it that starts to hold a string still fails type checking with its name. The generator exports generateIrFields(irText) for the new test, which generates the readers for an ir.ts with such a record and type-checks them. The generated file for the current ir.ts is unchanged.
61 lines
2.7 KiB
TypeScript
61 lines
2.7 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import ts from "typescript";
|
|
import { expect, test } from "vitest";
|
|
import { generateIrFields } from "../../scripts/generate-ir-fields.mjs";
|
|
|
|
const compilerDir = join(import.meta.dirname, "../../packages/compiler");
|
|
const irPath = join(compilerDir, "src/ir/ir.ts");
|
|
const generatedPath = join(compilerDir, "src/ir/fields.generated.ts");
|
|
const irText = readFileSync(irPath, "utf8");
|
|
const arrayGet = '{ kind: "arrayGet"; arr: IrExpr; index: IrExpr; type: IrType; loc: SrcLoc }';
|
|
|
|
/** ir.ts with `field` added to the arrayGet node. */
|
|
function withArrayGetField(field: string): string {
|
|
expect(irText).toContain(arrayGet);
|
|
return irText.replace(arrayGet, arrayGet.replace("loc: SrcLoc }", `loc: SrcLoc; ${field} }`));
|
|
}
|
|
|
|
/** The type errors of `generated` as fields.generated.ts, with `ir` as ir.ts,
|
|
* under the compiler package's options. */
|
|
function generatedErrors(ir: string, generated: string): string[] {
|
|
const parsed = ts.getParsedCommandLineOfConfigFile(
|
|
join(compilerDir, "tsconfig.json"),
|
|
{ noEmit: true, incremental: false },
|
|
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
|
|
)!;
|
|
const host = ts.createCompilerHost(parsed.options);
|
|
const readSourceFile = host.getSourceFile;
|
|
host.getSourceFile = (file, languageVersion, ...rest) =>
|
|
file === irPath
|
|
? ts.createSourceFile(file, ir, languageVersion)
|
|
: file === generatedPath
|
|
? ts.createSourceFile(file, generated, languageVersion)
|
|
: readSourceFile.call(host, file, languageVersion, ...rest);
|
|
const program = ts.createProgram([generatedPath], parsed.options, host);
|
|
return ts
|
|
.getPreEmitDiagnostics(program, program.getSourceFile(generatedPath))
|
|
.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
|
|
}
|
|
|
|
test("a nested record of scalar fields passes the checks once the readers are regenerated", () => {
|
|
const ir = withArrayGetField("counts?: { hits: number; misses: number | undefined }");
|
|
expect(generatedErrors(ir, generateIrFields(ir))).toEqual([]);
|
|
});
|
|
|
|
test("the record's own fields are checked: an added field or a widened scalar fails with its name", () => {
|
|
const generated = generateIrFields(
|
|
withArrayGetField("counts?: { hits: number; misses: number | undefined }"),
|
|
);
|
|
const added = generatedErrors(
|
|
withArrayGetField("counts?: { hits: number; misses: number | undefined; label: string }"),
|
|
generated,
|
|
);
|
|
expect(added).toEqual([`Type '"label"' does not satisfy the constraint 'never'.`]);
|
|
const widened = generatedErrors(
|
|
withArrayGetField("counts?: { hits: number | string; misses: number | undefined }"),
|
|
generated,
|
|
);
|
|
expect(widened).toEqual([`Type 'string' does not satisfy the constraint 'never'.`]);
|
|
});
|