UNPKG

codecheck-type-coverage

Version:

Track missing type coverage in TypeScript projects to ensure type safety

112 lines (90 loc) 3.42 kB
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 ""; } }