Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: merge ranges properly when contained by other ranges in set #750

Merged
merged 1 commit into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
129 changes: 110 additions & 19 deletions packages/istanbul-lib-coverage/lib/file-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,122 @@ function assertValidObject(obj) {
const keyFromLoc = ({ start, end }) =>
`${start.line}|${start.column}|${end.line}|${end.column}`;

// When merging, we can have a case where two ranges cover
// the same block of code with `hits=1`, and each carve out a
// different range with `hits=0` to indicate it's uncovered.
// Find the nearest container so that we can properly indicate
// that both sections are hit.
// Returns null if no containing item is found.
const findNearestContainer = (item, map) => {
// the B item is not an identified range in the A set, BUT
// it may be contained by an identified A range. If so, then
// any hit of that containing A range counts as a hit of this
// B range as well. We have to find the *narrowest* containing
// range to be accurate, since ranges can be hit and un-hit
// in a nested fashion.
let nearestContainingItem = null;
let containerDistance = null;
let containerKey = null;
for (const [i, mapItem] of Object.entries(map)) {
const mapLoc = mapItem.loc;
const itemLoc = item.loc;
// contained if all of line distances are > 0
// or line distance is 0 and col dist is >= 0
const distance = [
itemLoc.start.line - mapLoc.start.line,
itemLoc.start.column - mapLoc.start.column,
mapLoc.end.line - itemLoc.end.line,
mapLoc.end.column - itemLoc.end.column
];
if (
distance[0] < 0 ||
distance[2] < 0 ||
(distance[0] === 0 && distance[1] < 0) ||
(distance[2] === 0 && distance[3] < 0)
) {
continue;
}
if (nearestContainingItem === null) {
containerDistance = distance;
nearestContainingItem = mapItem;
containerKey = i;
continue;
}
// closer line more relevant than closer column
const closerBefore =
distance[0] < containerDistance[0] ||
(distance[0] === 0 && distance[1] < containerDistance[1]);
const closerAfter =
distance[2] < containerDistance[2] ||
(distance[2] === 0 && distance[3] < containerDistance[3]);
if (closerBefore || closerAfter) {
// closer
containerDistance = distance;
nearestContainingItem = mapItem;
containerKey = i;
}
}
return containerKey;
};

// either add two numbers, or all matching entries in a number[]
const addHits = (aHits, bHits) => {
if (typeof aHits === 'number' && typeof bHits === 'number') {
return aHits + bHits;
} else if (Array.isArray(aHits) && Array.isArray(bHits)) {
return aHits.map((a, i) => (a || 0) + (bHits[i] || 0));
}
return null;
};

const addNearestContainerHits = (item, itemHits, map, mapHits) => {
const container = findNearestContainer(item, map);
if (container) {
return addHits(itemHits, mapHits[container]);
} else {
return itemHits;
}
};

const mergeProp = (aHits, aMap, bHits, bMap, itemKey = keyFromLoc) => {
const aItems = {};
for (const [key, itemHits] of Object.entries(aHits)) {
const item = aMap[key];
aItems[itemKey(item)] = [itemHits, item];
}
for (const [key, bItemHits] of Object.entries(bHits)) {
const bItem = bMap[key];
const k = itemKey(bItem);

if (aItems[k]) {
const aPair = aItems[k];
if (bItemHits.forEach) {
// should this throw an exception if aPair[0] is not an array?
bItemHits.forEach((hits, h) => {
if (aPair[0][h] !== undefined) aPair[0][h] += hits;
else aPair[0][h] = hits;
});
} else {
aPair[0] += bItemHits;
}
const bItems = {};
for (const [key, itemHits] of Object.entries(bHits)) {
const item = bMap[key];
bItems[itemKey(item)] = [itemHits, item];
}
const mergedItems = {};
for (const [key, aValue] of Object.entries(aItems)) {
let aItemHits = aValue[0];
const aItem = aValue[1];
const bValue = bItems[key];
if (!bValue) {
// not an identified range in b, but might be contained by one
aItemHits = addNearestContainerHits(aItem, aItemHits, bMap, bHits);
} else {
aItems[k] = [bItemHits, bItem];
// is an identified range in b, so add the hits together
aItemHits = addHits(aItemHits, bValue[0]);
}
mergedItems[key] = [aItemHits, aItem];
}
// now find the items in b that are not in a. already added matches.
for (const [key, bValue] of Object.entries(bItems)) {
let bItemHits = bValue[0];
const bItem = bValue[1];
if (mergedItems[key]) continue;
// not an identified range in b, but might be contained by one
bItemHits = addNearestContainerHits(bItem, bItemHits, aMap, aHits);
mergedItems[key] = [bItemHits, bItem];
}

const hits = {};
const map = {};

Object.values(aItems).forEach(([itemHits, item], i) => {
Object.values(mergedItems).forEach(([itemHits, item], i) => {
hits[i] = itemHits;
map[i] = item;
});
Expand Down Expand Up @@ -320,7 +407,7 @@ class FileCoverage {
ret.branches = this.computeBranchTotals('b');
// Tracking additional information about branch truthiness
// can be optionally enabled:
if (this['bt']) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh, typo before?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that way!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this was intentional, then the code should have a test, because it wasn't being covered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

digging through #639 and linked efforts, seems like this was simply a typo

if (this.bT) {
ret.branchesTrue = this.computeBranchTotals('bT');
}
return new CoverageSummary(ret);
Expand All @@ -341,5 +428,9 @@ dataProperties(FileCoverage, [
]);

module.exports = {
FileCoverage
FileCoverage,
// exported for testing
findNearestContainer,
addHits,
addNearestContainerHits
};