@plugjs/cov8
Version:
V8 Coverage Plugin for the PlugJS Build System ==============================================
222 lines (221 loc) • 7.69 kB
JavaScript
// report.ts
import { pathToFileURL } from "node:url";
import { parse } from "@babel/parser";
import {
isDeclaration,
isExportDeclaration,
isFile,
isIfStatement,
isImportDeclaration,
isProgram,
isTryStatement,
isTSDeclareMethod,
isTSTypeReference,
isTypeScript,
VISITOR_KEYS
} from "@babel/types";
import { readFile } from "@plugjs/plug/fs";
import { $p } from "@plugjs/plug/logging";
var COVERAGE_SKIPPED = -2;
var COVERAGE_IGNORED = -1;
var ignoreRegexp = /^\s+(coverage|istanbul)\s+ignore\s+(test|if|else|try|catch|finally|next|prev|file)(\s|$)/g;
async function coverageReport(analyser, sourceFiles, log) {
const results = {};
const nodes = {
coveredNodes: 0,
missingNodes: 0,
ignoredNodes: 0,
totalNodes: 0,
coverage: 0
};
for (const file of sourceFiles) {
const url = pathToFileURL(file).toString();
const code = await readFile(file, "utf-8");
let tree;
try {
tree = parse(code, {
allowImportExportEverywhere: true,
allowAwaitOutsideFunction: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
attachComment: true,
errorRecovery: false,
sourceType: "unambiguous",
sourceFilename: file,
startLine: 1,
startColumn: 0,
plugins: ["typescript"],
strictMode: false,
ranges: false,
tokens: false,
createParenthesizedExpressions: true
});
} catch (error) {
log.fail(`Error parsing ${$p(file)}`, error);
}
const codeCoverage = new Array(code.length).fill(0);
const nodeCoverage = {
coveredNodes: 0,
missingNodes: 0,
ignoredNodes: 0,
totalNodes: 0,
coverage: 0
};
const setCodeCoverage = (node, coverage, recursive) => {
if (!node) return;
if (Array.isArray(node)) {
for (const n of node) setCodeCoverage(n, coverage, recursive);
return;
}
if (node.start != null && node.end != null) {
for (let i = node.start; i < node.end; i++) {
codeCoverage[i] = coverage;
}
}
if (coverage == COVERAGE_IGNORED) {
nodeCoverage.ignoredNodes++;
} else if (coverage === 0) {
nodeCoverage.missingNodes++;
} else if (coverage > 0) {
nodeCoverage.coveredNodes++;
}
if (!recursive) return;
const keys = VISITOR_KEYS[node.type] || /* coverage ignore next */
[];
for (const key of keys) {
const value = node[key];
if (Array.isArray(value)) {
for (const child of value) {
setCodeCoverage(child, coverage, true);
}
} else if (value) {
setCodeCoverage(value, coverage, true);
}
}
};
const visitChildren = (node, depth) => {
const keys = VISITOR_KEYS[node.type] || /* coverage ignore next */
[];
for (const key of keys) {
const children = node[key];
if (Array.isArray(children)) {
for (const child of children) {
if (child) visitNode(child, depth + 1);
}
} else if (children) {
visitNode(children, depth + 1);
}
}
};
const maybeIgnoreNode = (condition, node, depth) => {
if (condition) {
setCodeCoverage(node, COVERAGE_IGNORED, true);
} else if (node) {
visitNode(node, depth);
}
};
const visitNode = (node, depth) => {
log.trace("-".padStart(depth * 2 + 1, " "), node.type, `${node.loc?.start.line}:${node.loc?.start.column}`);
if (isFile(node)) return visitChildren(node, depth);
if (isProgram(node)) return visitChildren(node, depth);
const ignores = [];
for (const comment of node.leadingComments || []) {
for (const match of comment.value.matchAll(ignoreRegexp)) {
if (match[2] !== "prev") ignores.push(match[2]);
}
}
for (const comment of node.trailingComments || []) {
for (const match of comment.value.matchAll(ignoreRegexp)) {
if (match[2] === "prev") ignores.push(match[2]);
}
}
if (ignores.includes("next")) return setCodeCoverage(node, COVERAGE_IGNORED, true);
if (ignores.includes("prev")) return setCodeCoverage(node, COVERAGE_IGNORED, true);
if (isTypeScript(node)) {
if (isTSDeclareMethod(node)) return setCodeCoverage(node, COVERAGE_SKIPPED, true);
if (isTSTypeReference(node)) return setCodeCoverage(node, COVERAGE_SKIPPED, true);
if (isDeclaration(node)) return setCodeCoverage(node, COVERAGE_SKIPPED, true);
setCodeCoverage(node, COVERAGE_SKIPPED, false);
return visitChildren(node, depth);
}
if (isExportDeclaration(node) && node.exportKind === "type") {
return setCodeCoverage(node, COVERAGE_SKIPPED, true);
}
if (isImportDeclaration(node) && node.importKind === "type") {
return setCodeCoverage(node, COVERAGE_SKIPPED, true);
}
let coverage = 0;
if (node.loc) {
const { line, column } = node.loc.start;
const c = analyser.coverage(url, line, column);
if (c == null) {
log.warn(`No coverage for ${node.type} at ${$p(file)}:${line}:${column}`);
} else {
coverage = c;
}
}
setCodeCoverage(node, coverage, false);
if (isIfStatement(node)) {
maybeIgnoreNode(ignores.includes("test"), node.test, depth + 1);
maybeIgnoreNode(ignores.includes("if"), node.consequent, depth + 1);
maybeIgnoreNode(ignores.includes("else"), node.alternate, depth + 1);
return;
}
if (isTryStatement(node)) {
maybeIgnoreNode(ignores.includes("try"), node.block, depth + 1);
maybeIgnoreNode(ignores.includes("catch"), node.handler, depth + 1);
maybeIgnoreNode(ignores.includes("finally"), node.finalizer, depth + 1);
return;
}
visitChildren(node, depth);
};
codeCoverage.fill(COVERAGE_SKIPPED);
setCodeCoverage(tree.program.directives, 0, true);
setCodeCoverage(tree.program.body, 0, true);
nodeCoverage.coveredNodes = 0;
nodeCoverage.missingNodes = 0;
nodeCoverage.ignoredNodes = 0;
let ignoreFileCoverage = false;
for (const comment of tree.program.body[0]?.leadingComments || []) {
for (const match of comment.value.matchAll(ignoreRegexp)) {
if (match[2] === "file") {
ignoreFileCoverage = true;
break;
}
}
if (ignoreFileCoverage) break;
}
if (ignoreFileCoverage) {
setCodeCoverage(tree.program, COVERAGE_IGNORED, true);
} else {
visitChildren(tree.program, -1);
}
setCodeCoverage(tree.comments, COVERAGE_SKIPPED, false);
updateNodeCoverageResult(nodeCoverage);
nodes.coveredNodes += nodeCoverage.coveredNodes;
nodes.missingNodes += nodeCoverage.missingNodes;
nodes.ignoredNodes += nodeCoverage.ignoredNodes;
nodes.totalNodes += nodeCoverage.totalNodes;
results[file] = { code, codeCoverage, nodeCoverage };
}
updateNodeCoverageResult(nodes);
return { results, nodes };
}
function updateNodeCoverageResult(result) {
const { coveredNodes, missingNodes, ignoredNodes } = result;
const totalNodes = result.totalNodes = coveredNodes + missingNodes + ignoredNodes;
if (totalNodes === 0) {
result.coverage = null;
} else if (totalNodes === ignoredNodes) {
result.coverage = null;
} else {
result.coverage = Math.floor(100 * coveredNodes / (totalNodes - ignoredNodes));
}
}
export {
COVERAGE_IGNORED,
COVERAGE_SKIPPED,
coverageReport
};
//# sourceMappingURL=report.mjs.map