monocart-coverage-reports
Version:
A code coverage tool to generate native V8 reports or Istanbul reports.
981 lines (793 loc) • 25.4 kB
JavaScript
const { diffSequence } = require('../packages/monocart-coverage-vendor.js');
const Util = require('../utils/util.js');
const EC = Util.EC;
const findMapping = (list, offset) => {
let start = 0;
let end = list.length - 1;
while (end - start > 1) {
const i = Math.floor((start + end) * 0.5);
const item = list[i];
if (offset < item.generatedOffset) {
end = i;
continue;
}
if (offset > item.generatedOffset) {
start = i;
continue;
}
return list[i];
}
// last two items, less is start
const startItem = list[start];
if (offset === startItem.generatedOffset) {
return startItem;
}
const endItem = list[end];
if (offset === endItem.generatedOffset) {
return endItem;
}
// between two mappings
if (offset > startItem.generatedOffset && offset < endItem.generatedOffset) {
return [startItem, endItem];
}
// not found the mappings
};
// ========================================================================================================
const alignText = (gt, ot, info) => {
const { diffCache } = info.state;
if (diffCache.has(gt)) {
const subMap = diffCache.get(gt);
if (subMap.has(ot)) {
return subMap.get(ot);
}
}
const toList = (s) => {
return s.split('').map((v, i) => {
return {
index: i,
value: v
};
});
};
const mergeList = (gList, oList) => {
const oLen = oList.length;
if (!oLen) {
return;
}
const gLen = gList.length;
if (gLen === oLen) {
gList.forEach((item, i) => {
item.original = oList[i];
});
}
};
const gl = toList(gt);
const ol = toList(ot);
// console.log(gl);
// console.log(ol);
const commonItems = [];
diffSequence(gl.length, ol.length, (gi, oi) => {
return gl[gi].value === ol[oi].value;
}, (len, gi, oi) => {
commonItems.push([gi, oi, len]);
});
// console.log(commonItems);
// console.log('================================================================');
// merge list
// previous index
let gpi = 0;
let opi = 0;
commonItems.forEach(([gi, oi, len]) => {
if (gpi !== gi) {
const gList = gl.slice(gpi, gi);
const oList = ol.slice(opi, oi);
mergeList(gList, oList);
}
for (let i = 0; i < len; i++) {
const gii = gi + i;
const oii = oi + i;
const gItem = gl[gii];
gItem.original = ol[oii];
// matching a word end
if (i === len - 1 && len > 1) {
gItem.wordEnd = true;
}
}
gpi = gi + len;
opi = oi + len;
});
// console.log(JSON.stringify(data.gt));
// console.log(JSON.stringify(data.ot));
if (gpi < gl.length && opi < ol.length) {
const gList = gl.slice(gpi);
const oList = ol.slice(opi);
mergeList(gList, oList);
}
if (diffCache.has(gt)) {
const subMap = diffCache.get(gt);
subMap.set(ot, gl);
} else {
const subMap = new Map();
diffCache.set(gt, subMap);
subMap.set(ot, gl);
}
// console.log(gl);
return gl;
};
const getWordEndPosition = (list, gp, direction) => {
// for end only
if (direction !== 'end') {
return;
}
const prev = list[gp - 1];
if (prev && prev.original && prev.wordEnd) {
const op = prev.original.index + 1;
// console.log(gp, JSON.stringify(gt));
// console.log(op, JSON.stringify(ot));
return {
pos: op
};
}
};
const getAlignPosition = (info, direction) => {
const gt = info.generatedText;
const gp = info.generatedPos;
const ot = info.originalText;
// const alignTextItem = {
// gt, ot, gp
// };
// info.state.alignTextList.push(it);
// there is no need to align for long text
const maxLength = 100;
if (gt.length > maxLength || ot.length > maxLength) {
// console.log(gt.length, ot.length, gp);
return;
}
// generatedText: '"false";\n',
// originalText: "'false';\r\n ",
// generatedPos: 7
// left matched
// originalText: '1;',
// generatedText: '1;else',
// generatedLeft: '1;'
// right matched
// only for original first line text
// exclusive
const list = alignText(gt, ot, info);
const item = list[gp];
if (item && item.original) {
// alignTextItem.op = item.original.index;
return {
pos: item.original.index
};
}
// inclusive, for end only
return getWordEndPosition(list, gp, direction);
};
// ========================================================================================================
const getBlockStartPosition = (originalLineText) => {
// originalText: 'argument) {',
// generatedLeft: 'o',
// generatedRight: '&&'
// function/block could be started with {(
const startBlockIndex = originalLineText.search(/[<{(]/);
if (startBlockIndex !== -1) {
return {
pos: startBlockIndex
};
}
// end a block
const list = ['>', '}', ')'];
for (const s of list) {
const endBlockIndex = originalLineText.lastIndexOf(s);
if (endBlockIndex !== -1) {
return {
pos: endBlockIndex + 1
};
}
}
// ============================
// end characters
// ends with ">" in vue
// <span v-if="data.distFile">
// originalText: '">',
// generatedText: ' ? ((0,vue__.openBlock)(), '
// originalMethod?.apply
// originalText: '?.' no ?
const indexEndBlock = originalLineText.search(/(?<=[;,:"'\s])/);
if (indexEndBlock !== -1) {
return {
pos: indexEndBlock
};
}
};
const getBlockEndPosition = (originalLineText) => {
// generatedText: 'e),',
// generatedPos: 2,
// originalText: 'prop))'
// generatedText: ' = false)',
// generatedPos: 8,
// originalText: '=false"'
// generatedText: '), 1 /* TEXT */)])])) : (0,vue__...)("v-if", true)], 6 /* CLASS, STYLE */);',
// generatedPos: 17,
// originalText: ' }}</slot>'
// end marks
const list = ['>', '}', ')'];
for (const s of list) {
const endBlockIndex = originalLineText.lastIndexOf(s);
if (endBlockIndex !== -1) {
return {
pos: endBlockIndex + 1
};
}
}
const startBlockIndex = originalLineText.search(/[<{(]/);
if (startBlockIndex !== -1) {
return {
pos: startBlockIndex
};
}
};
// ========================================================================================================
const getComparedPosition = (info, direction) => {
const gt = info.generatedText;
const gp = info.generatedPos;
const ot = info.originalText;
const olt = info.originalLineText;
// seems direction is start only
if (gp === 0) {
return;
}
// whole generated text
if (gp >= gt.length) {
// console.log(JSON.stringify(gt), gt.length, gp);
// console.log(JSON.stringify(ot));
return {
pos: olt.length
};
}
// =============================
// trim
const gtt = gt.trim();
// no generated content after trim
if (!gtt) {
return;
}
const ott = ot.trim();
// no original content after trim
if (!ott) {
return {
pos: direction === 'start' ? ot.length : 0
};
}
// same content
if (gtt === ott) {
// fix indent
const blankBlock = /\S/;
const gi = gt.search(blankBlock);
const oi = ot.search(blankBlock);
return {
pos: gp - gi + oi
};
}
// =============================
return getAlignPosition(info, direction);
};
const getSimilarPosition = (info, direction) => {
const originalText = info.originalText;
// never cross line, using first line of original text
// trim end remove \r\n and \n
const originalLines = originalText.split(/\n/);
if (originalLines.length === 1) {
// already single line
info.originalLineText = originalText.trimEnd();
} else {
// multiple liens
info.originalLineText = originalLines[0].trimEnd();
}
// no need comparison for fake source
if (info.state.fake) {
return;
}
const textPos = getComparedPosition(info, direction);
if (textPos) {
return textPos;
}
// console.log('====================================================================');
// console.log(`${EC.magenta(direction)} similar position can NOT be fixed`, info.originalState.sourcePath);
// console.log({
// direction,
// generatedText: info.generatedText,
// generatedPos: info.generatedPos,
// originalText
// });
};
const getFixedPosition = (info, direction) => {
const similarPos = getSimilarPosition(info, direction);
if (similarPos) {
return similarPos.pos;
}
// generatedText: never cross lines
// originalText: could be multiple lines
// originalLineText: single line originalText
const { originalLineText } = info;
if (direction === 'start') {
const blockPos = getBlockStartPosition(originalLineText);
if (blockPos) {
return blockPos.pos;
}
return 0;
}
const blockPos = getBlockEndPosition(originalLineText);
if (blockPos) {
return blockPos.pos;
}
return originalLineText.length;
};
// ========================================================================================================
const getOriginalEndOffset = (m, originalState) => {
if (!m.originalEndOffset) {
const line = originalState.locator.getLine(m.originalLine + 1);
m.originalEndOffset = line ? line.end : m.originalOffset;
}
return m.originalEndOffset;
};
const getOriginalText = (m1, m2, originalState, startNextLine) => {
const originalLocator = originalState.locator;
const o1 = m1.originalOffset;
const o2 = m2.originalOffset;
const sameLine = m2.originalLine === m1.originalLine;
// o1 < o2: most of time
if (o1 < o2) {
// could be in comments
// it must be wrong sourcemap, do not fix it here
// if (originalLocator.lineParser.commentParser.isComment(o1, o2)) {
// console.log('in comments', originalLocator.getSlice(o1, o2), originalLocator.offsetToLocation(o1), originalLocator.offsetToLocation(o2), cache.originalState.sourcePath);
// return {
// originalOffset: o1,
// originalText: ''
// };
// }
if (sameLine) {
return {
originalOffset: o1,
originalText: originalLocator.getSlice(o1, o2)
};
}
// start from next line
if (startNextLine) {
const nextLine = originalState.locator.getLine(m1.originalLine + 2);
if (nextLine) {
// console.log(nextLine);
return {
originalOffset: nextLine.start + nextLine.indent,
originalText: nextLine.text.slice(nextLine.indent)
};
}
}
// could be multiple lines for original text
return {
originalOffset: o1,
originalText: originalLocator.getSlice(o1, getOriginalEndOffset(m1, originalState))
};
}
// esbuild fixing two mapping have same original
// m1 to end line
if (o1 === o2) {
return {
originalOffset: o1,
originalText: originalLocator.getSlice(o1, getOriginalEndOffset(m1, originalState))
};
}
// o1 > o2: should be wrong sourcemap
// just reverse offsets if same line
if (sameLine) {
return {
originalOffset: o2,
originalText: originalLocator.getSlice(o2, o1)
};
}
// if (direction === 'start') {
// should be m2 ?
// console.log(direction, cache.originalState.sourcePath);
// console.log(m1, m2);
// }
// should be wrong, ignore m2
return {
originalOffset: o1,
originalText: originalLocator.getSlice(o1, getOriginalEndOffset(m1, originalState))
};
};
const getGeneratedText = (m1, m2, state, offset) => {
const generatedLocator = state.locator;
const o1 = m1.generatedOffset;
const o2 = m2.generatedOffset;
// same line
if (m1.generatedLine === m2.generatedLine) {
return {
generatedText: generatedLocator.getSlice(o1, o2),
generatedPos: offset - o1
};
}
// =======================================
// different lines
// never cross lines
// 1-base
const lineInfo = generatedLocator.offsetToLocation(offset);
// 0-base
const targetLine = lineInfo.line - 1;
// m1 p (case 1, right to end)
// p (case 2, mid line)
// p (case 3) m2
// case 1
if (targetLine === m1.generatedLine) {
return {
generatedText: generatedLocator.getSlice(o1, lineInfo.end),
generatedPos: offset - o1
};
}
// case 3
if (targetLine === m2.generatedLine) {
return {
generatedText: generatedLocator.getSlice(lineInfo.start, o2),
generatedPos: offset - lineInfo.start
};
}
// case 2
return {
generatedText: lineInfo.text,
generatedPos: offset - lineInfo.start
};
};
// ========================================================================================================
const getFunctionName = (mp, state, options) => {
if (options.checkName && typeof mp.nameIndex !== 'undefined') {
return state.sourceMapNames[mp.nameIndex];
}
};
const getFixedOriginalStart = (start, mappings, state, cache, options) => {
const [m1, m2] = mappings;
const { originalState, crossStart } = cache;
// cross file from start of line
if (crossStart) {
return {
originalStart: m2.originalOffset,
originalName: getFunctionName(m2, state, options)
};
}
// skip last mapping
let startNextLine = false;
if (m1.last) {
const locStart = state.locator.offsetToLocation(start);
// 1-base
const lineIndex = locStart.line - 1;
if (lineIndex > m1.generatedLine) {
startNextLine = true;
// console.log(originalState.sourcePath, start, m1, m2);
}
}
const { originalText, originalOffset } = getOriginalText(m1, m2, originalState, startNextLine);
const { generatedText, generatedPos } = getGeneratedText(m1, m2, state, start);
const direction = 'start';
const info = {
state,
generatedText,
generatedPos,
originalText,
originalState
};
const originalPos = getFixedPosition(info, direction);
// if (start === 2660) {
// console.log('=====================================================');
// console.log('fixed start', originalState.sourcePath);
// console.log({
// generatedText,
// generatedPos,
// originalText,
// originalPos
// });
// }
return {
originalStart: originalOffset + originalPos,
originalName: getFunctionName(m1, state, options)
};
};
const getFixedOriginalEnd = (end, mappings, state, cache) => {
const [m1, m2] = mappings;
const { originalState, crossEnd } = cache;
// cross file until e1 line end (not file end)
if (crossEnd) {
const originalEndOffset = getOriginalEndOffset(m1, originalState);
return {
originalEnd: originalEndOffset
};
}
// end is exclusive
// quick check previous one if exact match
// most of case is matching "}"
if (m1.generatedOffset === end - 1) {
return {
originalEnd: m1.originalOffset + 1
};
}
const { originalText, originalOffset } = getOriginalText(m1, m2, originalState);
const { generatedText, generatedPos } = getGeneratedText(m1, m2, state, end);
const direction = 'end';
const info = {
state,
offset: end,
generatedText,
generatedPos,
originalText,
originalState
};
const originalPos = getFixedPosition(info, direction);
// if (end === 1304578) {
// console.log('=====================================================');
// console.log('fixed end', originalState.sourcePath);
// console.log({
// generatedText,
// generatedPos,
// originalText,
// originalPos,
// originalLineText: info.originalLineText
// });
// }
return {
originalEnd: originalOffset + originalPos
};
};
// ========================================================================================================
const getOriginalStartPosition = (cache, state, options) => {
const { start, startMappings } = cache;
if (Array.isArray(startMappings)) {
return getFixedOriginalStart(start, startMappings, state, cache, options);
}
// Exact match
return {
originalStart: startMappings.originalOffset,
originalName: getFunctionName(startMappings, state, options)
};
};
const getOriginalExclusiveEnd = (cache, state) => {
// end, exclusive mappings
const { end } = cache;
const { decodedMappings } = state;
const endMappings = findMapping(decodedMappings, end);
if (!endMappings) {
return {
error: true,
errors: ['not found end mappings']
};
}
if (Array.isArray(endMappings)) {
return getFixedOriginalEnd(end, endMappings, state, cache);
}
// Exact match end
return {
originalEnd: endMappings.originalOffset
};
};
const getOriginalEndPosition = (cache, state) => {
const { endMappings, originalState } = cache;
// (end - 1), inclusive
if (Array.isArray(endMappings)) {
return getOriginalExclusiveEnd(cache, state);
}
// Exact match (end - 1)
// check end char
const oi = endMappings.originalOffset;
const originalEndChar = originalState.locator.getSlice(oi, oi + 1);
// the char should never end with "{" or "("
if (['{', '('].includes(originalEndChar)) {
return getOriginalExclusiveEnd(cache, state);
}
// inclusive to exclusive
return {
originalEnd: oi + 1
};
};
// ========================================================================================================
const checkSourceFileIndexes = (startIndexes, endIndexes) => {
const checkIndex11 = (s1, e1) => {
if (s1 === e1) {
return {
sourceIndex: s1
};
}
};
const checkIndex21 = (s1, s2, e1) => {
if (s1 === e1 || s2 === e1) {
if (s1 === s2) {
return {
sourceIndex: e1
};
}
return {
crossStart: true,
sourceIndex: e1
};
}
};
const checkIndex12 = (s1, e1, e2) => {
if (s1 === e1 || s1 === e2) {
if (e1 === e2) {
return {
sourceIndex: s1
};
}
return {
crossEnd: true,
sourceIndex: s1
};
}
};
const checkIndex22 = (s1, s2, e1, e2) => {
// 4 same
if (s1 === s2 && e1 === e2 && s1 === e1) {
return {
sourceIndex: s1
};
}
if (e1 === e2) {
return checkIndex21(s1, s2, e1);
}
if (s1 === s2) {
return checkIndex12(s1, e1, e2);
}
if (s2 === e1) {
return {
crossStart: true,
crossEnd: true,
sourceIndex: s2
};
}
// both between two mappings: 38,39 ~ 38,39
// if (s1 === e1 && s2 === e2) {
// }
};
// both exact matched
if (startIndexes.length === 1 && endIndexes.length === 1) {
return checkIndex11(startIndexes[0], endIndexes[0]);
}
if (startIndexes.length === 2 && endIndexes.length === 1) {
return checkIndex21(startIndexes[0], startIndexes[1], endIndexes[0]);
}
if (startIndexes.length === 1 && endIndexes.length === 2) {
return checkIndex12(startIndexes[0], endIndexes[0], endIndexes[1]);
}
// both 2 mappings
return checkIndex22(startIndexes[0], startIndexes[1], endIndexes[0], endIndexes[1]);
};
const getOriginalState = (start, end, state, originalMap) => {
const mappingInfo = getMappingInfo(start, end, state);
if (mappingInfo.error) {
return mappingInfo;
}
const { startMappings, endMappings } = mappingInfo;
// check source file indexes
const startIndexes = [].concat(startMappings).map((it) => it.sourceIndex);
const endIndexes = [].concat(endMappings).map((it) => it.sourceIndex);
const results = checkSourceFileIndexes(startIndexes, endIndexes);
if (!results) {
return {
error: true,
errors: [`invalid source indexes: ${EC.yellow(`${startIndexes} ~ ${endIndexes}`)}`]
};
}
const {
sourceIndex, crossStart, crossEnd
} = results;
// if (crossStart || crossEnd) {
// console.log(EC.magenta('cross file'), EC.yellow(`${startIndexes} ~ ${endIndexes}`));
// }
const originalState = originalMap.get(sourceIndex);
if (!originalState) {
return {
error: true,
errors: [`not found original file: ${EC.yellow(sourceIndex)}`]
};
}
// cache for original info
return {
// for debug
// startIndexes,
// endIndexes,
start,
end,
startMappings,
endMappings,
sourceIndex,
crossStart,
crossEnd,
originalState
};
};
const getMappingInfo = (start, end, state) => {
const { decodedMappings } = state;
// possible no length
if (decodedMappings.length < 2) {
return {
error: true,
errors: ['invalid decoded mappings (length < 2)']
};
}
// start: inclusive
const startMappings = findMapping(decodedMappings, start);
if (!startMappings) {
return {
error: true,
errors: ['not found start mappings']
};
}
// end: exclusive
const endMappings = findMapping(decodedMappings, end - 1);
if (!endMappings) {
return {
error: true,
errors: ['not found end mappings']
};
}
// could be 4 mappings found
return {
startMappings,
endMappings
};
};
const findOriginalRange = (start, end, state, originalMap, options = {}) => {
const { sourcePath, rangeCache } = state;
const key = `${start}_${end}_${Boolean(options.fixOriginalRange)}`;
if (rangeCache.has(key)) {
return rangeCache.get(key);
}
const createMappingError = (errors) => {
const res = {
error: true,
start,
end,
sourcePath,
errors
};
// cache error response
rangeCache.set(key, res);
return res;
};
const cache = getOriginalState(start, end, state, originalMap);
if (cache.error) {
return createMappingError(cache.errors);
}
const originalStartResult = getOriginalStartPosition(cache, state, options);
const { originalStart, originalName } = originalStartResult;
// could be used for end
cache.originalStart = originalStart;
const originalEndResult = getOriginalEndPosition(cache, state);
if (originalEndResult.error) {
return createMappingError(originalEndResult.errors);
}
const { originalEnd } = originalEndResult;
// range start > end
if (originalStart > originalEnd) {
return createMappingError([`invalid original start > end: ${EC.yellow(originalStart)} > ${EC.yellow(originalEnd)}`]);
}
const { originalState } = cache;
const locator = originalState.locator;
const inComment = locator.lineParser.commentParser.isComment(originalStart, originalEnd);
if (inComment) {
return createMappingError(['the range in a original comment']);
}
const res = {
start: originalStart,
end: originalEnd,
name: originalName,
originalState
};
if (options.fixOriginalRange) {
const { fixedStart, fixedEnd } = Util.fixSourceRange(locator, originalStart, originalEnd);
res.start = fixedStart;
res.end = fixedEnd;
}
// cache response
rangeCache.set(key, res);
return res;
};
module.exports = findOriginalRange;