UNPKG

@plugjs/cov8

Version:

V8 Coverage Plugin for the PlugJS Build System ==============================================

222 lines (221 loc) 7.69 kB
// 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