mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
83d807781e
The client compiles with strict: false, so nullable contract fields arrive as if always present and implicit anys stay invisible. tsconfig.strict.json is the same program with strictNullChecks and noImplicitAny, and lint:strict (scripts/strict-lint.mjs) counts its errors per file against scripts/strict-baseline.json: a file may not gain one, a new file has to be clean, and an entry above its count fails until --update lowers it. Nothing is converted here; today's 534 errors in 165 files are frozen. allowJs stays: the tests import the .mjs scripts and read their JSDoc types through it. The todo MSW handler maps a cleared priority to its default so the test helper written in this branch starts at zero. The check runs after the typecheck in client-quality.
119 lines
4.2 KiB
JavaScript
119 lines
4.2 KiB
JavaScript
/*
|
|
* The tally and the comparison behind lint:strict (scripts/strict-lint.mjs).
|
|
*/
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createRequire } from 'node:module';
|
|
import { join } from 'node:path';
|
|
import {
|
|
countMap,
|
|
lowerCounts,
|
|
RatchetError,
|
|
readBaseline,
|
|
reportStale,
|
|
staleCounts,
|
|
writeBaseline,
|
|
} from './ratchet.mjs';
|
|
|
|
// `src/a.ts(12,5): error TS2531: Object is possibly 'null'.` as tsc prints it with --pretty false.
|
|
const FILE_ERROR = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
|
|
// An error tied to no file, such as a broken tsconfig, starts the same way without the position.
|
|
const OTHER_ERROR = /^(?:.+?: )?error (TS\d+): (.*)$/;
|
|
|
|
/**
|
|
* The strict errors per file in tsc's output, with each error's line, and the
|
|
* errors tied to no file (a broken config), which always fail. A file key is
|
|
* the path tsc printed, relative to the client root, with forward slashes; the
|
|
* indented lines that continue a message belong to the error above them.
|
|
|
|
*
|
|
* @param {string} output
|
|
* @returns {{ counts: Record<string, number>, listed: Record<string, string[]>, general: string[] }}
|
|
*/
|
|
export function parseTscOutput(output) {
|
|
const counts = {};
|
|
const listed = {};
|
|
const general = [];
|
|
for (const line of output.replace(/\r\n?/g, '\n').split('\n')) {
|
|
const hit = FILE_ERROR.exec(line);
|
|
if (hit) {
|
|
const key = hit[1].split('\\').join('/');
|
|
counts[key] = (counts[key] ?? 0) + 1;
|
|
(listed[key] ??= []).push(`${hit[2]}:${hit[3]} ${hit[4]} ${hit[5]}`);
|
|
continue;
|
|
}
|
|
if (!/^\s/.test(line) && OTHER_ERROR.test(line)) general.push(line.trim());
|
|
}
|
|
return { counts, listed, general };
|
|
}
|
|
|
|
/** tsc over tsconfig.strict.json, as its plain-text output. */
|
|
export function runTsc(root) {
|
|
const tsc = createRequire(join(root, 'package.json')).resolve('typescript/bin/tsc');
|
|
const result = spawnSync(process.execPath, [tsc, '-p', 'tsconfig.strict.json', '--noEmit', '--pretty', 'false'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
maxBuffer: 256 * 1024 * 1024,
|
|
});
|
|
if (result.error) throw new RatchetError(`tsc did not run: ${result.error.message}`);
|
|
// tsc exits 0 when clean and 1 or 2 when it reports errors; anything else is a crash.
|
|
if (![0, 1, 2].includes(result.status ?? -1))
|
|
throw new RatchetError(
|
|
`tsc stopped with exit code ${result.status}: ${(result.stderr || result.stdout).slice(0, 500)}`
|
|
);
|
|
return `${result.stdout}\n${result.stderr}`;
|
|
}
|
|
|
|
const sum = (counts) => Object.values(counts).reduce((a, b) => a + b, 0);
|
|
|
|
/**
|
|
* Runs tsc (or tsc, in a test) and compares the strict errors per file with
|
|
* the baseline at baselinePath. Returns the exit code.
|
|
*/
|
|
export function check({
|
|
root,
|
|
baselinePath = join(root, 'scripts/strict-baseline.json'),
|
|
update = false,
|
|
list = false,
|
|
tsc = runTsc,
|
|
log = console.log,
|
|
error = console.error,
|
|
}) {
|
|
let baseline = readBaseline(baselinePath, countMap);
|
|
const { counts, listed, general } = parseTscOutput(tsc(root));
|
|
|
|
if (list) {
|
|
for (const [file, errors] of Object.entries(listed).sort()) {
|
|
log(`${file} (${errors.length}, baseline ${baseline[file] ?? 0})`);
|
|
for (const line of errors) log(` ${line}`);
|
|
}
|
|
}
|
|
|
|
if (update) {
|
|
baseline = lowerCounts(baseline, counts);
|
|
writeBaseline(baselinePath, baseline);
|
|
}
|
|
|
|
for (const line of general) error(`FAIL ${line}`);
|
|
const grown = Object.entries(counts).filter(([file, n]) => n > (baseline[file] ?? 0));
|
|
for (const [file, n] of grown) {
|
|
const first = listed[file].slice(0, 3).join('; ');
|
|
error(
|
|
`FAIL ${file}: ${n} error(s) under strictNullChecks and noImplicitAny, baseline ${baseline[file] ?? 0}. ` +
|
|
`First: ${first}`
|
|
);
|
|
}
|
|
if (grown.length) {
|
|
error(
|
|
'Handle the null or type the value rather than asserting it away with `!` or `any`: both are counted by ' +
|
|
'lint:warnings. Run npm run lint:strict -- --list for every error.'
|
|
);
|
|
}
|
|
const stale = staleCounts(baseline, counts);
|
|
reportStale(stale, { file: 'strict-baseline.json', command: 'lint:strict', root, error });
|
|
log(
|
|
`strict: ${sum(counts)} error(s) in ${Object.keys(counts).length} file(s) under strictNullChecks and ` +
|
|
`noImplicitAny, baseline allows ${sum(baseline)}`
|
|
);
|
|
return general.length || grown.length || stale.length ? 1 : 0;
|
|
}
|