codecheck-type-coverage
Version:
Track missing type coverage in TypeScript projects to ensure type safety
112 lines (90 loc) • 3.42 kB
text/typescript
import { codeChecks, CodeChecksReport } from "@codechecks/client";
import { lint as getTypeCoverageInfo } from "type-coverage";
import { RawTypeCoverageReport, TypeCoverageArtifact, Options, SymbolInfo } from "./types";
import { groupBy } from "lodash";
const ARTIFACT_KEY = "type-coverage";
const defaultOptions: Required<Options> = {
tsconfigPath: "tsconfig.json",
};
export async function typeCoverageWatcher(_options: Options): Promise<void> {
const options: Required<Options> = {
...defaultOptions,
..._options,
};
const _typeCoverage = await getTypeCoverageInfo(options.tsconfigPath, true, false);
const typeCoverage = normalizeTypeCoverage(_typeCoverage);
await codeChecks.saveValue(ARTIFACT_KEY, typeCoverage);
if (!codeChecks.isPr()) {
return;
}
const baseTypeCoverage = await codeChecks.getValue<TypeCoverageArtifact>(ARTIFACT_KEY);
const report = getReport(typeCoverage, baseTypeCoverage);
await codeChecks.report(report);
}
function getReport(
headTypeCoverageArtifact: TypeCoverageArtifact,
baseTypeCoverageArtifact: TypeCoverageArtifact | undefined,
): CodeChecksReport {
const headTypeCoverage = (headTypeCoverageArtifact.typedSymbols / headTypeCoverageArtifact.totalSymbols) * 100;
const baseTypeCoverage = baseTypeCoverageArtifact
? (baseTypeCoverageArtifact.typedSymbols / baseTypeCoverageArtifact.totalSymbols) * 100
: 0;
const coverageDiff = headTypeCoverage - baseTypeCoverage;
const shortDescription = `Change: ${renderSign(coverageDiff)}${perc(coverageDiff)} Total: ${perc(headTypeCoverage)}`;
const newUntypedSymbols = findNew(
headTypeCoverageArtifact.allUntypedSymbols,
baseTypeCoverageArtifact ? baseTypeCoverageArtifact.allUntypedSymbols : [],
);
let longDescription = `New untyped symbols: ${newUntypedSymbols.length}`;
if (newUntypedSymbols.length > 0) {
longDescription += `
| File | line:character | Symbol |
|:-----:|:-----:|:-----:|
${newUntypedSymbols
.slice(0, 100)
.map(s => `| ${s.filename} | ${s.line}:${s.character} | ${s.symbol} |`)
.join("\n")}`;
}
return {
name: "Type Coverage",
status: "success",
shortDescription,
longDescription,
};
}
function findNew(headSymbols: SymbolInfo[], baseSymbols: SymbolInfo[]): SymbolInfo[] {
const baseSymbolsByFilename = groupBy(baseSymbols, s => s.filename);
const newSymbols: SymbolInfo[] = [];
for (const headSymbol of headSymbols) {
// we assess if symbols are the same only by looking at filename and symbol name (we ignore line and character)
const baseSymbolMatch =
(baseSymbolsByFilename[headSymbol.filename] || []).filter(s => s.symbol === headSymbol.symbol).length > 0;
if (!baseSymbolMatch) {
newSymbols.push(headSymbol);
}
}
return newSymbols;
}
function perc(n: number): string {
return n.toFixed(2) + "%";
}
function normalizeTypeCoverage(rawTypeCoverage: RawTypeCoverageReport): TypeCoverageArtifact {
return {
typedSymbols: rawTypeCoverage.correctCount,
totalSymbols: rawTypeCoverage.totalCount,
allUntypedSymbols: rawTypeCoverage.anys.map(a => ({
filename: a.file,
line: a.line,
character: a.character,
symbol: a.text,
})),
};
}
function renderSign(value: number): string {
if (value > 0) {
return "+";
} else {
// we dont' render signs for negative (it's part of a number xD) or 0
return "";
}
}