From 52e2bb4a41bffe6904395a2f69f72e0b8c77da66 Mon Sep 17 00:00:00 2001
From: David Losert <davelosert@github.com>
Date: Sat, 2 Nov 2024 12:46:05 -0700
Subject: [PATCH] fix: Uncovered lines column now correctly show ranges when
 they are interrupted (e.g. by empty rows) (#439)

* fix: Uncovered lines column now correctly merges ranges when they are interrupted by empty rows

* fix: Fixes case where s property is greater than 1
---
 .../getUncoveredLinesFromStatements.test.ts   | 44 +++++++++++++++++++
 src/report/getUncoveredLinesFromStatements.ts | 35 ++++++++++-----
 2 files changed, 68 insertions(+), 11 deletions(-)

diff --git a/src/report/getUncoveredLinesFromStatements.test.ts b/src/report/getUncoveredLinesFromStatements.test.ts
index 05e738a..1ea6912 100644
--- a/src/report/getUncoveredLinesFromStatements.test.ts
+++ b/src/report/getUncoveredLinesFromStatements.test.ts
@@ -99,4 +99,48 @@ describe("getUncoveredLinesFromStatements()", () => {
 			{ start: 3, end: 4 },
 		]);
 	});
+
+	it("returns a single range if statement numbers are not sequential.", () => {
+		const statements: StatementCoverageReport = {
+			statementMap: {
+				"0": {
+					start: { line: 1, column: 0 },
+					end: { line: 1, column: 0 },
+				},
+				"5": {
+					start: { line: 6, column: 0 },
+					end: { line: 6, column: 0 },
+				},
+				"6": {
+					start: { line: 7, column: 0 },
+					end: { line: 7, column: 0 },
+				},
+			},
+			s: { "0": 0, "5": 0, "6": 0 },
+		};
+
+		const uncoveredLines = getUncoveredLinesFromStatements(statements);
+
+		expect(uncoveredLines).toEqual([{ start: 1, end: 7 }]);
+	});
+
+	it("handles the case where the property in 's' is greater than 1.", () => {
+		const statements: StatementCoverageReport = {
+			statementMap: {
+				"0": {
+					start: { line: 1, column: 0 },
+					end: { line: 1, column: 0 },
+				},
+				"1": {
+					start: { line: 2, column: 0 },
+					end: { line: 2, column: 0 },
+				},
+			},
+			s: { "0": 2, "1": 8 },
+		};
+
+		const uncoveredLines = getUncoveredLinesFromStatements(statements);
+
+		expect(uncoveredLines).toEqual([]);
+	});
 });
diff --git a/src/report/getUncoveredLinesFromStatements.ts b/src/report/getUncoveredLinesFromStatements.ts
index f845279..c52dde6 100644
--- a/src/report/getUncoveredLinesFromStatements.ts
+++ b/src/report/getUncoveredLinesFromStatements.ts
@@ -11,22 +11,35 @@ const getUncoveredLinesFromStatements = ({
 }: StatementCoverageReport): LineRange[] => {
 	const keys = Object.keys(statementMap);
 
-	const uncoveredLineRanges = keys.reduce<LineRange[]>((acc, key) => {
-		if (s[key] === 0) {
-			const lastRange = acc.at(-1);
-
-			if (lastRange && lastRange.end === statementMap[key].start.line - 1) {
-				lastRange.end = statementMap[key].end.line;
-				return acc;
+	const uncoveredLineRanges: LineRange[] = [];
+	let currentRange: LineRange | undefined = undefined;
+	for (const key of keys) {
+		if (s[key] > 0) {
+			// If the statement is covered, we need to close the current range.
+			if (currentRange) {
+				uncoveredLineRanges.push(currentRange);
+				currentRange = undefined;
 			}
+			// Besides that, we can just ignore covered lines
+			continue;
+		}
 
-			acc.push({
+		// Start a new range if we don't have one yet.
+		if (!currentRange) {
+			currentRange = {
 				start: statementMap[key].start.line,
 				end: statementMap[key].end.line,
-			});
+			};
+			continue;
 		}
-		return acc;
-	}, []);
+
+		currentRange.end = statementMap[key].end.line;
+	}
+
+	// If we still have a current range, we need to add it to the uncovered line ranges.
+	if (currentRange) {
+		uncoveredLineRanges.push(currentRange);
+	}
 
 	return uncoveredLineRanges;
 };