Files
scriptc/packages/compiler/test/lib-diagnostics-options.test.ts
T
Chris Tate 385555f2fd Reuse lib diagnostics across options that cannot change them
The stored and prebuilt answers for the declaration files scriptc supplies
were keyed by every compiler option, and the prebuilt ones are recorded
without a tsconfig. A project whose tsconfig sets noUnusedLocals,
noUncheckedIndexedAccess or a similar option therefore missed them and
checked the supplied files in every cold build.

The key now leaves out the options that cannot change what checking the
supplied declaration files reports: the unused-identifier options (the
checker reports none in declaration files), the statement options, the
access-expression options and allowSyntheticDefaultImports. A test checks
the supplied files with each of them set and unset, and checks that they
have none of the constructs those options apply to.
2026-10-28 08:56:40 -05:00

147 lines
6.2 KiB
TypeScript

import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, expect, test } from "vitest";
import { fallbackModuleSpecifiers } from "../src/frontend/dts-paths.js";
import { checkPreflightTs7 } from "../src/frontend/program-node.js";
import type { Node } from "../src/frontend/ts7/ast-types.js";
import { walkPreorder } from "../src/frontend/ts7/ast.js";
import { SyntaxKind } from "../src/frontend/ts7/enums.js";
import {
OPTIONS_WITHOUT_SUPPLIED_DIAGNOSTICS,
setLibDiagnosticsStore,
} from "../src/frontend/ts7/lib-diagnostics.js";
import { closeSourceParser, parseSourceFile } from "../src/frontend/ts7/source-parser-node.js";
// The supplied declaration files' answers leave out the options of
// OPTIONS_WITHOUT_SUPPLIED_DIAGNOSTICS. A program supplied with every one of
// those files (the TypeScript libs, scriptc's declarations and every Node
// module's fallback declarations) shows why each cannot change what checking
// them reports, and that it does not.
const root = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-options-"));
afterAll(() => {
setLibDiagnosticsStore(null);
closeSourceParser();
rmSync(root, { recursive: true, force: true });
});
const everyModule =
fallbackModuleSpecifiers()
.map((specifier) => `import ${JSON.stringify(specifier)};\n`)
.join("") + "export {};\n";
function project(options: Record<string, unknown>, files: Record<string, string>): string {
const dir = mkdtempSync(join(root, "project-"));
writeFileSync(join(dir, "tsconfig.json"), JSON.stringify({ compilerOptions: options }));
for (const [name, text] of Object.entries(files)) writeFileSync(join(dir, name), text);
return join(dir, "main.ts");
}
/** The supplied files' diagnostics of the full check of a program importing
* every Node module, with `options` in its tsconfig, by file name. */
function suppliedDiagnostics(options: Record<string, unknown>): Record<string, unknown> {
const entries = new Map<string, string>();
setLibDiagnosticsStore(
{ read: (key) => entries.get(key) ?? null, write: (key, text) => void entries.set(key, text) },
"test checker",
);
try {
checkPreflightTs7(project(options, { "main.ts": everyModule }));
} finally {
setLibDiagnosticsStore(null);
}
// Each full check stores the supplied files' diagnostics (a program the
// preflight builds twice stores two answers for the same files).
expect(entries.size).toBeGreaterThan(0);
const files: Record<string, unknown> = {};
for (const text of entries.values())
Object.assign(files, (JSON.parse(text) as { files: Record<string, unknown> }).files);
return files;
}
const base = { strict: true };
let unset: Record<string, unknown> | undefined;
function baseline(): Record<string, unknown> {
return (unset ??= suppliedDiagnostics(base));
}
test("the program is supplied with the libs and every Node module's fallback declarations", () => {
const names = Object.keys(baseline());
expect(names.some((name) => name.endsWith("/lib.es5.d.ts"))).toBe(true);
expect(names.filter((name) => name.includes("/node-fallback/")).length).toBe(
new Set(fallbackModuleSpecifiers().map((specifier) => specifier.replace(/^node:/, ""))).size,
);
});
test.each(OPTIONS_WITHOUT_SUPPLIED_DIAGNOSTICS.map((option) => [option]))(
"%s, set either way, does not change the supplied files' diagnostics",
(option) => {
for (const value of [true, false])
expect(suppliedDiagnostics({ ...base, [option]: value })).toEqual(baseline());
},
60_000,
);
test("noUnusedLocals and noUnusedParameters report nothing in declaration files", () => {
const options = { ...base, noUnusedLocals: true, noUnusedParameters: true };
const codes = (extension: string, text: string): string[] =>
checkPreflightTs7(
project(options, {
"main.ts": 'import "./extra.js";\nexport {};\n',
[`extra.${extension}`]: text,
}),
).diags.map((d) => d.message);
// The same unused names: reported in a source file, not in a declaration file.
expect(
codes("ts", "type Unused = number;\nexport function f(unused: number): void {}\n"),
).toEqual([
"'Unused' is declared but never used.",
"'unused' is declared but its value is never read.",
]);
expect(
codes("d.ts", "type Unused = number;\nexport declare function f(unused: number): void;\n"),
).toEqual([]);
});
test("the supplied files have no statements, no index accesses and no default imports", () => {
// The constructs the other options apply to: statements (noImplicitReturns,
// noFallthroughCasesInSwitch, allowUnusedLabels, allowUnreachableCode),
// element and property access expressions (noUncheckedIndexedAccess,
// noPropertyAccessFromIndexSignature; a property access in a computed
// property name, such as `[Symbol.iterator]`, reads a property of
// SymbolConstructor, which has no index signature, and one in a heritage
// clause, such as `extends NodeJS.WritableStream`, names a type) and default imports
// (allowSyntheticDefaultImports).
const found: string[] = [];
for (const name of Object.keys(baseline())) {
const file = parseSourceFile(name, readFileSync(name, "utf8"), "ts");
expect(file.isDeclarationFile).toBe(true);
walkPreorder(file as unknown as Node, (node) => {
switch (node.kind) {
case SyntaxKind.Block:
case SyntaxKind.ExpressionStatement:
case SyntaxKind.ReturnStatement:
case SyntaxKind.SwitchStatement:
case SyntaxKind.LabeledStatement:
case SyntaxKind.ElementAccessExpression:
found.push(`${name}: ${SyntaxKind[node.kind]}`);
break;
case SyntaxKind.PropertyAccessExpression:
if (
node.parent?.kind !== SyntaxKind.ComputedPropertyName &&
node.parent?.kind !== SyntaxKind.ExpressionWithTypeArguments &&
node.parent?.kind !== SyntaxKind.PropertyAccessExpression
)
found.push(`${name}: PropertyAccessExpression`);
break;
case SyntaxKind.ImportClause:
if ((node as unknown as { name?: unknown }).name !== undefined)
found.push(`${name}: default import`);
break;
}
});
}
expect(found).toEqual([]);
});