mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
b1da50466d
tsconfig.strict.json is the build config with strictNullChecks and noImplicitAny on. lint:strict runs tsc over it and holds the errors per file to scripts/strict-baseline.json (204 in 109 files today): a file may only lose errors, a new file starts at none, and an entry above its count fails until --update lowers it. An error outside any file or an unreadable tsc run fails closed. It runs in the server job; the build itself stays on strict: false.
138 lines
5.8 KiB
TypeScript
138 lines
5.8 KiB
TypeScript
/**
|
|
* scripts/strict-lint.mjs holds the strict type errors (strictNullChecks +
|
|
* noImplicitAny) per file to scripts/strict-baseline.json. The counting
|
|
* (scripts/lib/tsc-errors.mjs) is tested on tsc output directly; the
|
|
* comparison runs the real script as a child process against throwaway
|
|
* server roots (--dir) fed a saved tsc output (--tsc-output), so no test pays
|
|
* for a type check.
|
|
*/
|
|
import { countErrors } from '../../../scripts/lib/tsc-errors.mjs';
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
const SCRIPT = path.join(__dirname, '../../../scripts/strict-lint.mjs');
|
|
|
|
interface ExecError {
|
|
status: number | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
const roots: string[] = [];
|
|
|
|
const err = (file: string, line = 1, code = 'TS2322') =>
|
|
`${file}(${line},5): error ${code}: Type 'x' is not assignable.`;
|
|
|
|
function serverRoot(files: Record<string, string>, baseline: unknown, tscOutput: string): string {
|
|
const dir = mkdtempSync(path.join(tmpdir(), 'trek-strict-lint-'));
|
|
roots.push(dir);
|
|
mkdirSync(path.join(dir, 'scripts'), { recursive: true });
|
|
for (const [file, text] of Object.entries(files)) {
|
|
mkdirSync(path.dirname(path.join(dir, file)), { recursive: true });
|
|
writeFileSync(path.join(dir, file), text);
|
|
}
|
|
if (baseline !== null) {
|
|
writeFileSync(
|
|
path.join(dir, 'scripts/strict-baseline.json'),
|
|
typeof baseline === 'string' ? baseline : JSON.stringify(baseline),
|
|
);
|
|
}
|
|
writeFileSync(path.join(dir, 'tsc.out'), tscOutput);
|
|
return dir;
|
|
}
|
|
|
|
function run(dir: string, ...args: string[]): { status: number | null; out: string } {
|
|
try {
|
|
const stdout = execFileSync(
|
|
process.execPath,
|
|
[SCRIPT, `--dir=${dir}`, `--tsc-output=${path.join(dir, 'tsc.out')}`, ...args],
|
|
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
|
|
);
|
|
return { status: 0, out: stdout };
|
|
} catch (e) {
|
|
const { status, stdout, stderr } = e as ExecError;
|
|
return { status, out: `${stdout}${stderr}` };
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of roots.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('strict-lint countErrors', () => {
|
|
it('STRICT-001: counts one error per error line and file, not its continuation lines', () => {
|
|
const output = [
|
|
err('src/a.ts', 1),
|
|
" Type 'string | undefined' is not assignable to type 'string'.",
|
|
" Type 'undefined' is not assignable to type 'string'.",
|
|
err('src/a.ts', 9, 'TS7006'),
|
|
err('src/nest/b/b.service.ts', 3),
|
|
'',
|
|
].join('\n');
|
|
expect(countErrors(output)).toEqual({ counts: { 'src/a.ts': 2, 'src/nest/b/b.service.ts': 1 }, global: [] });
|
|
});
|
|
|
|
it('STRICT-002: reads CRLF output and Windows paths the same as LF and POSIX ones', () => {
|
|
const output = `${err('src\\nest\\a.ts')}\r\n${err('./src/b.ts')}\r\n`;
|
|
expect(countErrors(output).counts).toEqual({ 'src/nest/a.ts': 1, 'src/b.ts': 1 });
|
|
});
|
|
|
|
it('STRICT-003: keeps an error that names no file apart, and ignores anything else', () => {
|
|
const output = [
|
|
"error TS5023: Unknown compiler option 'x'.",
|
|
'Found 3 errors in 2 files.',
|
|
'src/a.ts is fine',
|
|
].join('\n');
|
|
expect(countErrors(output)).toEqual({ counts: {}, global: ["error TS5023: Unknown compiler option 'x'."] });
|
|
});
|
|
});
|
|
|
|
describe('strict-lint.mjs', () => {
|
|
it('STRICT-010: passes when every file sits at or under its entry', () => {
|
|
const dir = serverRoot({ 'src/a.ts': '' }, { 'src/a.ts': 2 }, `${err('src/a.ts')}\n${err('src/a.ts', 2)}\n`);
|
|
const { status, out } = run(dir);
|
|
expect(status).toBe(0);
|
|
expect(out).toContain('strict: 2 error(s) in 1 file(s), baseline 2 in 1');
|
|
});
|
|
|
|
it('STRICT-011: fails a new file with any strict error, and a baselined file past its entry', () => {
|
|
const fresh = run(serverRoot({}, {}, `${err('src/new.ts')}\n`));
|
|
expect(fresh.status).toBe(1);
|
|
expect(fresh.out).toContain('FAIL src/new.ts: 1 strict error(s), a file without a baseline entry may have none.');
|
|
const grown = run(serverRoot({ 'src/a.ts': '' }, { 'src/a.ts': 1 }, `${err('src/a.ts')}\n${err('src/a.ts', 2)}\n`));
|
|
expect(grown.status).toBe(1);
|
|
expect(grown.out).toContain('FAIL src/a.ts: 2 strict error(s), its baseline is 1.');
|
|
});
|
|
|
|
it('STRICT-012: fails an entry above its count or for a gone file until --update lowers it, and never adds one', () => {
|
|
const dir = serverRoot(
|
|
{ 'src/a.ts': '', 'src/b.ts': '' },
|
|
{ 'src/a.ts': 3, 'src/b.ts': 1, 'src/gone.ts': 2 },
|
|
`${err('src/a.ts')}\n${err('src/new.ts')}\n`,
|
|
);
|
|
const before = run(dir);
|
|
expect(before.status).toBe(1);
|
|
expect(before.out).toContain('FAIL src/a.ts is held at 3 in scripts/strict-baseline.json, but there are 1 now.');
|
|
expect(before.out).toContain(
|
|
'FAIL src/gone.ts is held at 2 in scripts/strict-baseline.json, but the file is gone.',
|
|
);
|
|
expect(before.out).toContain('npm run lint:strict -- --update');
|
|
// The new file still fails after the update; the baseline only lost entries.
|
|
expect(run(dir, '--update').status).toBe(1);
|
|
expect(JSON.parse(readFileSync(path.join(dir, 'scripts/strict-baseline.json'), 'utf8'))).toEqual({ 'src/a.ts': 1 });
|
|
});
|
|
|
|
it('STRICT-013: fails closed on a missing or broken baseline and on an error outside any file', () => {
|
|
expect(run(serverRoot({}, null, '')).out).toContain('strict-baseline.json cannot be read');
|
|
expect(run(serverRoot({}, '{ broken', '')).status).toBe(1);
|
|
expect(run(serverRoot({}, { 'src/a.ts': 0 }, '')).status).toBe(1);
|
|
const config = run(serverRoot({}, {}, "error TS5023: Unknown compiler option 'x'.\n"));
|
|
expect(config.status).toBe(1);
|
|
expect(config.out).toContain('tsc reported errors outside any file');
|
|
});
|
|
});
|