Files
Sebastian Poxhofer 914c64afbd test(linter): zod schema and infered type naming rule (#43736)
* ci: add linting rule for schema and infered types

* refactor: remove usages of Schema and differently named types
2026-06-02 12:07:35 +00:00

173 lines
4.1 KiB
TypeScript

import fs from 'fs-extra';
import upath from 'upath';
import { z } from 'zod/v4';
import type { CoverageInfo } from './types.ts';
const StatementLocation = z.object({
start: z.object({ line: z.number() }),
end: z.object({ line: z.number() }),
});
const CoverageEntry = z.object({
statementMap: z.record(z.string(), StatementLocation),
s: z.record(z.string(), z.number()),
});
const CoverageJson = z.record(z.string(), CoverageEntry);
type CoverageJson = z.infer<typeof CoverageJson>;
function groupConsecutiveNumbers(numbers: number[]): string[] {
if (numbers.length === 0) {
return [];
}
const sorted = [...numbers].sort((a, b) => a - b);
const ranges: string[] = [];
let start = sorted[0];
let end = sorted[0];
for (let i = 1; i < sorted.length; i++) {
if (sorted[i] === end + 1) {
end = sorted[i];
} else {
ranges.push(start === end ? `${start}` : `${start}-${end}`);
start = sorted[i];
end = sorted[i];
}
}
ranges.push(start === end ? `${start}` : `${start}-${end}`);
return ranges;
}
async function loadCoverageFile(
coverageDir: string,
): Promise<CoverageJson | null> {
const coverageFile = upath.resolve(coverageDir, 'coverage-final.json');
if (!(await fs.pathExists(coverageFile))) {
return null;
}
try {
const rawJson: unknown = JSON.parse(
await fs.readFile(coverageFile, 'utf8'),
);
const parseResult = CoverageJson.safeParse(rawJson);
return parseResult.success ? parseResult.data : null;
} catch {
return null;
}
}
function normalizeCoveragePaths(
data: CoverageJson,
): Map<string, CoverageJson[string]> {
const normalized = new Map<string, CoverageJson[string]>();
for (const [key, value] of Object.entries(data)) {
normalized.set(upath.normalize(key), value);
}
return normalized;
}
function isSourceFile(path: string): boolean {
return path.endsWith('.ts') && !path.endsWith('.spec.ts');
}
function extractFileCoverage(
file: string,
coverage: Map<string, CoverageJson[string]>,
): CoverageInfo | null {
const absPath = upath.resolve(process.cwd(), file);
const entry = coverage.get(absPath);
if (!entry) {
return null;
}
const { statementMap, s: statementCoverage } = entry;
const totalStatements = Object.keys(statementMap).length;
if (totalStatements === 0) {
return null;
}
const coveredStatements = Object.values(statementCoverage).filter(
(count) => count > 0,
).length;
const percentage = (coveredStatements / totalStatements) * 100;
if (percentage >= 100) {
return null;
}
const uncoveredLineSet = new Set<number>();
for (const [stmtId, count] of Object.entries(statementCoverage)) {
if (count !== 0) {
continue;
}
const stmt = statementMap[stmtId];
if (!stmt) {
continue;
}
for (let line = stmt.start.line; line <= stmt.end.line; line++) {
uncoveredLineSet.add(line);
}
}
return {
file,
percentage,
uncoveredLines: groupConsecutiveNumbers(Array.from(uncoveredLineSet)),
};
}
export async function loadCoverage(
coverageDir: string,
): Promise<Map<string, CoverageJson[string]> | null> {
const data = await loadCoverageFile(coverageDir);
if (!data) {
return null;
}
return normalizeCoveragePaths(data);
}
export function getCoverageForFiles(
coverage: Map<string, CoverageJson[string]>,
files: string[],
): CoverageInfo[] {
const results: CoverageInfo[] = [];
for (const file of files) {
if (!isSourceFile(file)) {
continue;
}
const info = extractFileCoverage(file, coverage);
if (info) {
results.push(info);
}
}
return results;
}
export function getCoverageForDir(
coverage: Map<string, CoverageJson[string]>,
dir: string,
): CoverageInfo[] {
const absDir = `${upath.resolve(process.cwd(), dir)}/`;
const results: CoverageInfo[] = [];
for (const absPath of coverage.keys()) {
if (!absPath.startsWith(absDir)) {
continue;
}
const relPath = upath.relative(process.cwd(), absPath);
if (!isSourceFile(relPath)) {
continue;
}
const info = extractFileCoverage(relPath, coverage);
if (info) {
results.push(info);
}
}
return results;
}