monocart-coverage-reports
Version:
A code coverage tool to generate native V8 reports or Istanbul reports.
810 lines (644 loc) • 21.9 kB
JavaScript
const Util = require('../utils/util.js');
const BranchTypes = {
ConditionalExpression: 'ConditionalExpression',
LogicalExpression: 'LogicalExpression',
IfStatement: 'IfStatement',
SwitchStatement: 'SwitchStatement',
AssignmentPattern: 'AssignmentPattern'
};
const setGeneratedOnly = (block, generatedOnly) => {
if (block && generatedOnly) {
block.generatedOnly = true;
}
};
const getParentFunctionState = (reverseParents) => {
const parentFunction = reverseParents.find((it) => it._state && it._state.isFunction);
if (!parentFunction) {
return;
}
return parentFunction._state;
};
const getParentCount = (reverseParents, functionCount) => {
// parent count
const parent = reverseParents.find((it) => it._state);
if (parent) {
return parent._state.count;
}
// root function count
return functionCount;
};
const getFunctionRange = (start, end, type, coverageInfo) => {
const {
functionMap, functionNameMap, functionStaticRanges, functionUncoveredRanges
} = coverageInfo;
// exact matched in functionMap
const range = functionMap.get(start);
if (range) {
return range;
}
// exact matched in functionNameMap
const nameRange = functionNameMap.get(start);
if (nameRange) {
return nameRange;
}
if (type === 'StaticBlock' && functionStaticRanges.length) {
const staticRange = Util.findInRanges(start, end, functionStaticRanges, 'startOffset', 'endOffset');
if (staticRange) {
return staticRange;
}
}
// find in uncoveredRanges
return Util.findInRanges(start, end, functionUncoveredRanges, 'startOffset', 'endOffset');
};
const getFunctionBlock = (start, end, functionState) => {
if (!functionState) {
return;
}
const { range } = functionState;
if (!range) {
return;
}
const {
blockMap, blockUncoveredRanges, blockCoveredRanges
} = range;
if (!blockMap) {
return;
}
const block = blockMap.get(start);
if (block) {
return block;
}
const uncoveredBlock = Util.findInRanges(start, end, blockUncoveredRanges, 'startOffset', 'endOffset');
if (uncoveredBlock) {
return uncoveredBlock;
}
// the block is not exact correct, if there is a block wrapped
// [x8]{ var a = b || [x4]c }, b is no block, but it can be found in x8 block
const coveredBlocks = blockCoveredRanges.filter((it) => start >= it.startOffset && end <= it.endOffset);
if (coveredBlocks.length) {
return coveredBlocks.pop();
}
};
const addNodeCount = (item) => {
const { node, reverseParents } = item;
if (node._state) {
return;
}
const { start, end } = node;
const functionState = getParentFunctionState(reverseParents);
const block = getFunctionBlock(start, end, functionState);
if (block) {
node._state = {
count: block.count
};
}
};
// =======================================================================================
const createBranchGroup = (type, node, parents, branchMap) => {
const { start, end } = node;
// clone and reverse parents
const reverseParents = [].concat(parents).reverse();
const group = {
type,
start,
// could be updated if multiple locations
end,
locations: [],
reverseParents
};
if (type === BranchTypes.LogicalExpression) {
// && or ||
group.operator = node.operator;
}
// could be same start
const branchKey = `${start}_${end}`;
branchMap.set(branchKey, group);
return group;
};
const addBranch = (group, node, locationMap) => {
const {
start, end, type
} = node;
const branchKey = `${group.start}_${group.end}`;
const branchInfo = {
// for get previous group for LogicalExpression
branchKey,
start,
end,
// branch count default to 0
count: 0
};
if (type === 'SwitchCase') {
// console.log(node);
// check break
if (node.consequent) {
const breakItem = node.consequent.find((it) => it.type === 'BreakStatement');
if (breakItem) {
branchInfo.hasBreak = true;
}
}
// check default
if (!node.test) {
branchInfo.isDefault = true;
}
}
group.locations.push(branchInfo);
// update group end
if (end > group.end) {
group.end = end;
}
locationMap.set(start, branchInfo);
return branchInfo;
};
const addNoneBranch = (group) => {
group.locations.push({
none: true,
count: 0
});
};
// =======================================================================================
const updateBlockLocations = (locations) => {
const noBlockList = [];
let blockCount = 0;
locations.forEach((item, i) => {
if (item.block) {
item.count = item.block.count;
blockCount += item.count;
return;
}
// for calculate mo break branches
item.index = i;
noBlockList.push(item);
});
return {
noBlockList,
blockCount
};
};
// const a = tf1 ? 'true' : 'false';
const ConditionalExpression = (group, parentCount) => {
const { noBlockList, blockCount } = updateBlockLocations(group.locations);
if (!noBlockList.length) {
return;
}
let count = parentCount - blockCount;
noBlockList.forEach((item) => {
item.count = count;
count = 0;
});
};
const IfStatement = (group, parentCount) => {
const { noBlockList, blockCount } = updateBlockLocations(group.locations);
if (!noBlockList.length) {
return;
}
// console.log(parentCount, 'uncovered list', noBlockList.length, group.start);
let count = parentCount - blockCount;
noBlockList.forEach((item) => {
item.count = count;
count = 0;
});
};
// const b = tf2 || tf1 || a;
const LogicalExpression = (group, parentCount) => {
// from left to right
group.locations.forEach((item, i) => {
if (item.block) {
item.count = item.block.count;
} else {
item.count = parentCount;
}
});
};
const SwitchStatement = (group, parentCount) => {
const locations = group.locations;
const { noBlockList, blockCount } = updateBlockLocations(locations);
if (!noBlockList.length) {
return;
}
// calculate switch/case count
const countLeft = parentCount - blockCount;
noBlockList.forEach((item) => {
let hasCount = false;
// check no break branches
for (let i = item.index - 1; i >= 0; i--) {
const b = locations[i];
if (b && !b.hasBreak && b.count > 0) {
item.count += b.count;
hasCount = true;
continue;
}
break;
}
if (!hasCount) {
item.count = countLeft;
}
});
};
const AssignmentPattern = (group, parentCount) => {
group.locations.forEach((item) => {
item.count = parentCount;
});
};
// =======================================================================================
const updateBranchCount = (group) => {
const {
type, locations, reverseParents
} = group;
const functionState = getParentFunctionState(reverseParents);
const functionCount = functionState.count;
// default is 0, no need continue
if (functionCount === 0) {
return;
}
// parent is block statement or function
let parentCount = getParentCount(reverseParents, functionCount);
// parent is group range
const groupBlock = getFunctionBlock(group.start, group.end, functionState);
if (groupBlock) {
parentCount = groupBlock.count;
setGeneratedOnly(groupBlock, group.generatedOnly);
}
// calculate branches count
locations.forEach((item) => {
const {
start, end, none
} = item;
if (none) {
return;
}
item.block = getFunctionBlock(start, end, functionState);
setGeneratedOnly(item.block, group.generatedOnly);
});
const handlers = {
ConditionalExpression,
LogicalExpression,
IfStatement,
SwitchStatement,
AssignmentPattern
};
const handler = handlers[type];
if (handler) {
handler(group, parentCount);
}
};
const generateBranches = (branchMap) => {
// calculate count for all branches
branchMap.forEach((group) => {
updateBranchCount(group);
});
// init branches
const branches = [];
branchMap.forEach((group) => {
// add start/end for none with group start/end
group.locations.forEach((item) => {
if (item.none) {
item.start = group.start;
item.end = group.end;
}
});
const branch = {
type: group.type,
start: group.start,
end: group.end,
locations: group.locations
};
setGeneratedOnly(branch, group.generatedOnly);
branches.push(branch);
});
// sort branches
branches.sort((a, b) => {
return a.start - b.start;
});
return branches;
};
// =======================================================================================
// All programs in JavaScript are made of statements and they end with semicolons (;) except block statements which is used to group zero or more statements.
// Statements are just perform some actions but do not produce any value or output whereas expressions return some value.
// Expressions return value, statements do not.
const generateStatements = (statementNodes) => {
statementNodes.forEach((item) => {
const {
node,
reverseParents
} = item;
const { start, end } = node;
item.count = 1;
// isFunction
if (node._state) {
item.count = node._state.count;
return;
}
const functionState = getParentFunctionState(reverseParents);
if (!functionState) {
// there is a root range, it impossible not found
return;
}
// function uncovered
if (functionState.count === 0) {
item.count = 0;
return;
}
const block = getFunctionBlock(start, end, functionState);
if (block) {
item.count = block.count;
return;
}
item.count = functionState.count;
});
// remove block statement
statementNodes = statementNodes.filter((item) => {
return item.node.type !== 'BlockStatement';
});
const statements = statementNodes.map((item) => {
const {
node,
count
} = item;
const { start, end } = node;
return {
start,
end,
count
};
});
return statements;
};
// =======================================================================================
// handle special generated codes for original coverage
const webpackWrapHandler = (node) => {
// ignore webpack wrap for original function
// id: { type: 'Identifier', name: '__webpack_modules__' },
const name = node.id && node.id.name;
if (name === '__webpack_modules__' && node.init && node.init.properties) {
// mark all as wrap function
node.init.properties.forEach((it) => {
it.value._webpackWrapKey = it.key && it.key.value;
});
// console.log('==========================', node.type);
// console.log(name, parents.length);
}
};
const viteImportDefaultHandler = (node, group) => {
// ignore vite conditional expression for __esModule default
const testObj = node.test.object;
const testName = testObj && testObj.name;
if (testName && testName.startsWith('__vite__cjsImport')) {
setGeneratedOnly(group, true);
}
};
// =======================================================================================
const collectNodes = (ast) => {
const functionNodes = [];
const statementNodes = [];
const blockNodes = [];
const branchMap = new Map();
// locationMap for chain LogicalExpression locations
const locationMap = new Map();
Util.visitAst(ast, {
VariableDeclarator(node, parents) {
webpackWrapHandler(node);
},
// ===============================================================================
// statements
Statement: (node, parents) => {
const reverseParents = [].concat(parents).reverse();
statementNodes.push({
node,
reverseParents
});
},
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static
// as a function "functionName": "<static_initializer>",
StaticBlock: (node, parents) => {
const reverseParents = [].concat(parents).reverse();
functionNodes.push({
node,
reverseParents
});
},
// ===============================================================================
// functions
// Function include FunctionDeclaration, ArrowFunctionExpression, FunctionExpression
Function(node, parents) {
const reverseParents = [].concat(parents).reverse();
functionNodes.push({
node,
reverseParents
});
},
// ===============================================================================
// branches
// `for` block, the count not equal to parent
BlockStatement: (node, parents) => {
// fix branch count: BRDA
const reverseParents = [].concat(parents).reverse();
blockNodes.push({
node,
reverseParents
});
},
// default-arg assignment logic.
// function default arguments
AssignmentPattern: (node, parents) => {
const group = createBranchGroup(BranchTypes.AssignmentPattern, node, parents, branchMap);
addBranch(group, node, locationMap);
},
// cond-expr a ternary expression. e.g.: x ? y : z
// Ternary
// var b = a ? 'consequent' : 'alternate';
ConditionalExpression: (node, parents) => {
const { consequent, alternate } = node;
const group = createBranchGroup(BranchTypes.ConditionalExpression, node, parents, branchMap);
addBranch(group, consequent, locationMap);
addBranch(group, alternate, locationMap);
viteImportDefaultHandler(node, group);
},
// if an if statement; can also be else if.
// An IF statement always has exactly two branches:
// one where the condition is FALSE and one where the condition is TRUE
IfStatement: (node, parents) => {
const { consequent, alternate } = node;
const group = createBranchGroup(BranchTypes.IfStatement, node, parents, branchMap);
addBranch(group, consequent, locationMap);
// console.log('if type', consequent.type);
if (alternate) {
addBranch(group, alternate, locationMap);
} else {
// add none branch
addNoneBranch(group);
// no need update group end, there is no end
}
},
// binary-expr a logical expression with a binary operand. e.g.: x && y
// var b = a || b || c;
// do not use BinaryExpression
LogicalExpression: (node, parents) => {
const { left, right } = node;
// console.log(left.start, right.start);
// could be same branch start
// const da = arguments.length > 1 && typeof arguments[1] !== 'undefined' ? arguments[1] : true;
let group;
// link to same branch start if LogicalExpression
const prevLocation = locationMap.get(node.start);
if (prevLocation) {
// console.log('link branch ==================', type);
group = branchMap.get(prevLocation.branchKey);
} else {
group = createBranchGroup(BranchTypes.LogicalExpression, node, parents, branchMap);
addBranch(group, left, locationMap);
}
addBranch(group, right, locationMap);
// console.log(group.locations.map((it) => it.start));
// sort branch locations
// a || b || c
// first, left a and right c
// then, left a and right b
if (prevLocation) {
const { locations } = group;
locations.sort((a, b) => {
return a.start - b.start;
});
// update group end after sorted
const lastEnd = locations[locations.length - 1].end;
if (lastEnd > group.end) {
group.end = lastEnd;
}
}
},
// switch a switch statement.
SwitchStatement: (node, parents) => {
const group = createBranchGroup(BranchTypes.SwitchStatement, node, parents, branchMap);
const cases = node.cases;
cases.forEach((switchCase) => {
// console.log('switchCase', switchCase.start);
addBranch(group, switchCase, locationMap);
});
}
});
return {
functionNodes,
statementNodes,
blockNodes,
branchMap
};
};
const getRootFunctionState = (ast, coverageInfo) => {
const rootState = {
isFunction: true,
count: 1
};
const rootRange = coverageInfo.rootRange;
if (rootRange) {
rootState.range = rootRange;
rootState.count = rootRange.count;
// could be not from 0
// 0 881 { startOffset: 77, endOffset: 881,
// const { start, end } = ast;
// console.log(start, end, rootRange);
}
ast._state = rootState;
return rootState;
};
// eslint-disable-next-line complexity
const findFunctionRange = (item, coverageInfo) => {
const { node, reverseParents } = item;
const {
start, end, type
} = node;
// try function start/end
const functionRange = getFunctionRange(start, end, type, coverageInfo);
if (functionRange) {
return functionRange;
}
// `static async` case:
// [1]static [2]async [3]covered(active) {
// v8 start: 2
// ast start: 1,3
// fixed by async or static
// handle for static async
if (node.async) {
// 'async '.length
const asyncLen = 6;
const asyncStart = start - asyncLen;
// console.log(asyncStart, 'asyncStart ===============================================');
const asyncRange = getFunctionRange(asyncStart, end, type, coverageInfo);
if (asyncRange) {
return asyncRange;
}
}
// try if class MethodDefinition
// 0 is function self
const parent = reverseParents[1];
if (parent && parent.type === 'MethodDefinition') {
const parentRange = getFunctionRange(parent.start, parent.end, parent.type, coverageInfo);
if (parentRange) {
return parentRange;
}
if (parent.static) {
// 'static '.length
const staticLen = 7;
const staticStart = parent.start + staticLen;
// console.log(staticStart, ' staticStart ============================================');
const staticRange = getFunctionRange(staticStart, parent.end, parent.type, coverageInfo);
if (staticRange) {
return staticRange;
}
}
}
};
const collectAstInfo = (ast, coverageInfo) => {
const {
functionNodes, statementNodes, blockNodes, branchMap
} = collectNodes(ast);
// root function state
const rootState = getRootFunctionState(ast, coverageInfo);
const functions = [];
functionNodes.forEach((item) => {
const { node } = item;
const {
start, end, id
} = node;
const bodyStart = node.body.start;
const bodyEnd = node.body.end;
const functionName = id && id.name;
const _webpackWrapKey = node._webpackWrapKey;
const functionItem = {
start,
end,
bodyStart,
bodyEnd,
functionName,
count: rootState.count
};
setGeneratedOnly(functionItem, _webpackWrapKey);
functions.push(functionItem);
const functionState = {
isFunction: true,
count: functionItem.count
};
const functionRange = findFunctionRange(item, coverageInfo);
if (functionRange) {
setGeneratedOnly(functionRange, _webpackWrapKey);
functionState.range = functionRange;
functionState.count = functionRange.count;
functionItem.count = functionRange.count;
}
node._state = functionState;
// console.log(item.reverseParents.map((it) => it.type));
});
blockNodes.forEach((item) => {
addNodeCount(item);
});
const branches = generateBranches(branchMap);
const statements = generateStatements(statementNodes);
return {
functions,
branches,
statements
};
};
module.exports = {
BranchTypes,
collectAstInfo
};