mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 15:01:49 +00:00
914c64afbd
* ci: add linting rule for schema and infered types * refactor: remove usages of Schema and differently named types
173 lines
4.1 KiB
TypeScript
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;
|
|
}
|