monocart-coverage-reports
Version:
A code coverage tool to generate native V8 reports or Istanbul reports.
361 lines (282 loc) • 9.07 kB
JavaScript
const acorn = require('acorn');
const acornLoose = require('acorn-loose');
const { parseCss } = require('../packages/monocart-coverage-vendor.js');
const Util = require('../utils/util.js');
const { collectAstInfo } = require('./ast-visitor.js');
const getCoverageInfo = (coverageList) => {
const functionMap = new Map();
const functionNameMap = new Map();
const functionStaticRanges = [];
const functionUncoveredRanges = [];
let rootRange;
coverageList.forEach((block) => {
const {
functionName, ranges, root
} = block;
let functionRange;
const blockRanges = [];
ranges.forEach((range, i) => {
if (i === 0) {
// function range
functionRange = range;
return;
}
blockRanges.push(range);
});
functionRange.functionName = functionName;
// blocks
if (blockRanges.length) {
const blockMap = new Map();
blockRanges.forEach((item) => {
blockMap.set(item.startOffset, item);
});
functionRange.blockMap = blockMap;
// uncovered is unique
const blockUncoveredRanges = blockRanges.filter((it) => it.count === 0);
Util.sortOffsetRanges(blockUncoveredRanges);
functionRange.blockUncoveredRanges = blockUncoveredRanges;
functionRange.blockCoveredRanges = blockRanges.filter((it) => it.count > 0);
}
// root function from vm
if (root) {
rootRange = functionRange;
return;
}
// root function from first range
if (!rootRange) {
rootRange = functionRange;
}
functionMap.set(functionRange.startOffset, functionRange);
if (functionName) {
// can not handle `async functionName`
const possibleNameOffset = functionRange.startOffset + functionName.length;
functionNameMap.set(possibleNameOffset, functionRange);
}
if (functionName === '<static_initializer>') {
functionStaticRanges.push(functionRange);
}
// cache uncovered
if (functionRange.count === 0) {
functionUncoveredRanges.push(functionRange);
}
});
// sort ranges
Util.sortOffsetRanges(functionUncoveredRanges);
return {
functionMap,
functionNameMap,
functionStaticRanges,
functionUncoveredRanges,
rootRange
};
};
const getFakeAstInfo = (coverageList) => {
const functions = [];
const branches = [];
const statements = [];
coverageList.forEach((block) => {
const {
isBlockCoverage, functionName, ranges, root
} = block;
ranges.forEach((range, i) => {
const {
startOffset, endOffset, count
} = range;
if (i === 0 && root) {
return;
}
statements.push({
start: startOffset,
end: endOffset,
count
});
if (isBlockCoverage) {
// branches
branches.push({
type: 'branch',
start: startOffset,
end: endOffset,
locations: [{
start: startOffset,
end: endOffset,
count
}]
});
}
if (i === 0) {
// function range
functions.push({
start: startOffset,
end: endOffset,
bodyStart: startOffset,
bodyEnd: endOffset,
count,
functionName
});
}
});
});
// console.log('fake functions', functions.map((it) => it));
return {
functions,
branches,
statements
};
};
const getJsAstInfo = (item, coverageList) => {
const {
source, fake, empty
} = item;
if (fake) {
return getFakeAstInfo(coverageList);
}
const options = {
ecmaVersion: 'latest',
// most time for node.js file
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
allowAwaitOutsideFunction: true,
allowSuperOutsideMethod: true,
// first line: #!/usr/bin/env node
allowHashBang: true
};
let err;
let ast;
try {
ast = acorn.parse(source, options);
} catch (e) {
err = e;
}
if (err) {
// empty = untested file
// could be .ts, .vue and so on, that can not be parsed normally
if (empty) {
return {
functions: [],
branches: [],
statements: []
};
}
// runtime code should be parsed successful
Util.logInfo(`Unparsable source: ${item.sourcePath} ${err.message}`);
// it could be jsx even it is `.js`
// https://github.com/acornjs/acorn/tree/master/acorn-loose
// It is recommended to always try a parse with the regular acorn parser first,
// and only fall back to this parser when that one finds syntax errors.
ast = acornLoose.parse(source, options);
}
const coverageInfo = getCoverageInfo(coverageList);
coverageInfo.item = item;
return collectAstInfo(ast, coverageInfo);
};
// =========================================================================================================
const addRule = (item, coverageList, coveredRanges, count = 0) => {
const { source } = item;
const { start, end } = source;
const startOffset = start.offset;
const endOffset = end.offset;
if (!count) {
const coveredRange = Util.findInRanges(startOffset, endOffset, coveredRanges, 'startOffset', 'endOffset');
if (coveredRange) {
count = 1;
}
}
coverageList.push({
start: startOffset,
end: endOffset,
count
});
return count;
};
const addAtRule = (item, coverageList, coveredRanges, empty) => {
const { name, nodes } = item;
const defaultCount = empty ? 0 : 1;
if (['charset', 'import', 'namespace'].includes(name)) {
addRule(item, coverageList, coveredRanges, defaultCount);
return;
}
if (['media', 'supports', 'container', 'layer'].includes(name)) {
const childCoverageList = [];
const count = addCssRules(nodes, childCoverageList, coveredRanges, empty);
if (count) {
coverageList.push({
start: item.source.start.offset,
end: childCoverageList[0].start,
count: 1
});
let end;
childCoverageList.forEach((it) => {
coverageList.push(it);
end = it.end;
});
coverageList.push({
start: end,
end: item.source.end.offset,
count: 1
});
return;
}
}
addRule(item, coverageList, coveredRanges);
};
const addCssRules = (list, coverageList, coveredRanges, empty) => {
let count = 0;
// line and line is 1-base
list.forEach((item) => {
const { type } = item;
if (type === 'comment') {
return;
}
if (type === 'rule') {
count += addRule(item, coverageList, coveredRanges);
return;
}
// console.log('=============================================================================');
// Object.keys(item).forEach((k) => {
// if (k === 'parent') {
// return;
// }
// if (k === 'raws') {
// return;
// }
// if (k === 'nodes') {
// console.log(k, item[k].length);
// return;
// }
// if (k === 'source') {
// console.log(k, item[k].start, item[k].end);
// return;
// }
// console.log(k, item[k]);
// });
if (type === 'atrule') {
addAtRule(item, coverageList, coveredRanges, empty);
}
});
return count;
};
const getCssAstInfo = (item, coverageList) => {
const {
source, ranges, empty
} = item;
// sort covered ranges
ranges.sort((a, b) => a.start - b.start);
// to offset covered ranges
const coveredRanges = ranges.map((it) => {
return {
startOffset: it.start,
endOffset: it.end
};
});
const ast = parseCss(source);
addCssRules(ast.nodes, coverageList, coveredRanges, empty);
return {
functions: [],
branches: [],
statements: []
};
};
module.exports = {
getJsAstInfo,
getCssAstInfo
};