@plugjs/cov8
Version:
V8 Coverage Plugin for the PlugJS Build System ==============================================
210 lines (209 loc) • 7.4 kB
JavaScript
// analysis.ts
import { fileURLToPath, pathToFileURL } from "node:url";
import { assert } from "@plugjs/plug/asserts";
import { readFile } from "@plugjs/plug/fs";
import { $gry, $p } from "@plugjs/plug/logging";
import { SourceMapConsumer } from "source-map";
var CoverageAnalyserImpl = class {
constructor(_log) {
this._log = _log;
}
};
var CoverageResultAnalyser = class extends CoverageAnalyserImpl {
constructor(log, _result) {
super(log);
this._result = _result;
const _coverage = [];
for (const coveredFunction of _result.functions) {
for (const range of coveredFunction.ranges) {
for (let i = range.startOffset; i < range.endOffset; i++) {
_coverage[i] = range.count;
}
}
}
this._coverage = _coverage;
}
/** Number of passes at each character in the result */
_coverage;
/** Internal private field for init/_lineLengths getter */
_lineLengths;
async init() {
const filename = fileURLToPath(this._result.url);
const source = await readFile(filename, "utf-8");
this._lineLengths = source.split("\n").map((line) => line.length);
return this;
}
destroy() {
}
/** Return the number of coverage passes for the given location */
coverage(source, line, column) {
assert(this._lineLengths, "Analyser not initialized");
assert(source === this._result.url, `Wrong source ${source} (should be ${this._result.url})`);
const { _lineLengths, _coverage } = this;
let offset = 0;
for (let l = line - 2; l >= 0; l--) offset += _lineLengths[l] + 1;
return _coverage[offset + column] || 0;
}
};
var CoverageSitemapAnalyser = class extends CoverageResultAnalyser {
constructor(log, result, _sourceMapCache, _sourceMapBias) {
super(log, result);
this._sourceMapCache = _sourceMapCache;
this._sourceMapBias = _sourceMapBias;
this._lineLengths = _sourceMapCache.lineLengths;
}
_preciseMappings = /* @__PURE__ */ new Map();
_sourceMap;
_key(source, line, column) {
return `${line}:${column}:${source}`;
}
async init() {
const sourceMap = this._sourceMapCache.data;
assert(sourceMap, "Missing source map data from cache");
this._sourceMap = await new SourceMapConsumer(sourceMap);
if (this._sourceMapBias === "none") {
this._sourceMap.eachMapping((m) => {
const location = { line: m.generatedLine, column: m.generatedColumn };
const key = this._key(m.source, m.originalLine, m.originalColumn);
this._preciseMappings.set(key, location);
});
}
return this;
}
destroy() {
this._sourceMap?.destroy();
}
coverage(source, line, column) {
assert(this._sourceMap, "Analyser not initialized");
if (this._sourceMapBias === "none") {
const key = this._key(source, line, column);
const location = this._preciseMappings.get(key);
if (!location) {
this._log.debug(`No precise mapping for ${source}:${line}:${column}`);
return 0;
} else {
return super.coverage(this._result.url, location.line, location.column);
}
}
const bias = mapBias(this._sourceMapBias);
const generated = this._sourceMap.generatedPositionFor({ source, line, column, bias });
if (!generated) {
this._log.debug(`No position generated for ${source}:${line}:${column}`);
return 0;
}
if (generated.line == null) {
this._log.debug(`No line generated for ${source}:${line}:${column}`);
return 0;
}
if (generated.column == null) {
this._log.debug(`No column generated for ${source}:${line}:${column}`);
return 0;
}
return super.coverage(this._result.url, generated.line, generated.column);
}
};
function mapBias(bias) {
if (bias === "greatest_lower_bound") return SourceMapConsumer.GREATEST_LOWER_BOUND;
if (bias === "least_upper_bound") return SourceMapConsumer.LEAST_UPPER_BOUND;
return void 0;
}
function combineCoverage(analysers, source, line, column) {
let coverage = 0;
if (!analysers) return coverage;
for (const analyser of analysers) {
coverage += analyser.coverage(source, line, column);
}
return coverage;
}
var SourcesCoverageAnalyser = class extends CoverageAnalyserImpl {
constructor(log, _filename) {
super(log);
this._filename = _filename;
}
_mappings = /* @__PURE__ */ new Map();
hasMappings() {
return this._mappings.size > 0;
}
add(source, analyser) {
const analysers = this._mappings.get(source) || /* @__PURE__ */ new Set();
analysers.add(analyser);
this._mappings.set(source, analysers);
}
async init() {
this._log.debug("SourcesCoverageAnalyser", $p(this._filename), $gry(`(${this._mappings.size} mappings)`));
for (const analysers of this._mappings.values()) {
for (const analyser of analysers) {
await analyser.init();
}
}
return this;
}
destroy() {
for (const analysers of this._mappings.values()) {
for (const analyser of analysers) {
analyser.destroy();
}
}
}
coverage(source, line, column) {
const analysers = this._mappings.get(source);
return combineCoverage(analysers, source, line, column);
}
};
var CombiningCoverageAnalyser = class extends CoverageAnalyserImpl {
_analysers = /* @__PURE__ */ new Set();
add(analyser) {
this._analysers.add(analyser);
}
async init() {
this._log.debug("CombiningCoverageAnalyser", $gry(`(${this._analysers.size} analysers)`));
this._log.enter();
try {
for (const analyser of this._analysers) await analyser.init();
return this;
} finally {
this._log.leave();
}
}
destroy() {
for (const analyser of this._analysers) analyser.destroy();
}
coverage(source, line, column) {
return combineCoverage(this._analysers, source, line, column);
}
};
async function createAnalyser(sourceFiles, coverageFiles, sourceMapBias, log) {
const urls = sourceFiles.map((path) => pathToFileURL(path).toString());
const analyser = new CombiningCoverageAnalyser(log);
for await (const coverageFile of coverageFiles) {
const coverageFileAnalyser = new SourcesCoverageAnalyser(log, coverageFile);
log.info("Parsing coverage file", $p(coverageFile));
const contents = await readFile(coverageFile, "utf-8");
const coverage = JSON.parse(contents);
for (const result of coverage.result) {
if (!result.url.startsWith("node:")) {
log.debug("Found coverage data for", result.url);
}
const mapping = coverage["source-map-cache"]?.[result.url];
if (mapping) {
log.debug("Found source mapping for", result.url, mapping.data);
const matches = urls.filter((url) => mapping.data?.sources.includes(url));
if (matches.length) {
log.debug("Found source mapping matches", matches);
const sourceAnalyser = new CoverageSitemapAnalyser(log, result, mapping, sourceMapBias);
for (const match of matches) coverageFileAnalyser.add(match, sourceAnalyser);
}
} else if (urls.includes(result.url)) {
coverageFileAnalyser.add(result.url, new CoverageResultAnalyser(log, result));
}
}
if (coverageFileAnalyser.hasMappings()) analyser.add(coverageFileAnalyser);
}
return await analyser.init();
}
export {
CombiningCoverageAnalyser,
SourcesCoverageAnalyser,
createAnalyser
};
//# sourceMappingURL=analysis.mjs.map