Skip to content

Commit 1500539

Browse files
authored
[coverage] Expose filterIgnored function (#2123)
1 parent 9963447 commit 1500539

File tree

6 files changed

+211
-91
lines changed

6 files changed

+211
-91
lines changed

pkgs/coverage/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 1.15.0
2+
3+
- Expose `filterIgnored` function, which filters the coverage data according to
4+
`// coverage:ignore-...` comments. Previously this filtering functionality
5+
was only available when loading coverage data from json using `parseJson` etc.
6+
17
## 1.14.1
28

39
- Remove dependency on `package:pubspec_parse`.

pkgs/coverage/lib/src/hitmap.dart

Lines changed: 39 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ class HitMap {
5959
required Map<String, List<List<int>>?> ignoredLinesInFilesCache,
6060
required Resolver resolver,
6161
}) {
62-
final loader = Loader();
63-
6462
// Map of source file to map of line to hit count for that line.
6563
final globalHitMap = <String, HitMap>{};
6664

@@ -71,66 +69,6 @@ class HitMap {
7169
continue;
7270
}
7371

74-
var ignoredLinesList = <List<int>>[];
75-
76-
if (checkIgnoredLines) {
77-
if (ignoredLinesInFilesCache.containsKey(source)) {
78-
final cacheHit = ignoredLinesInFilesCache[source];
79-
if (cacheHit == null) {
80-
// Null-entry indicates that the whole file was ignored.
81-
continue;
82-
}
83-
ignoredLinesList = cacheHit;
84-
} else {
85-
final path = resolver.resolve(source);
86-
if (path != null) {
87-
final lines = loader.loadSync(path) ?? [];
88-
ignoredLinesList = getIgnoredLines(path, lines);
89-
90-
// Ignore the whole file.
91-
if (ignoredLinesList.length == 1 &&
92-
ignoredLinesList[0][0] == 0 &&
93-
ignoredLinesList[0][1] == lines.length) {
94-
// Null-entry indicates that the whole file was ignored.
95-
ignoredLinesInFilesCache[source] = null;
96-
continue;
97-
}
98-
ignoredLinesInFilesCache[source] = ignoredLinesList;
99-
} else {
100-
// Couldn't resolve source. Allow cache to answer next time
101-
// anyway.
102-
ignoredLinesInFilesCache[source] = ignoredLinesList;
103-
}
104-
}
105-
}
106-
107-
// Move to the first ignore range.
108-
final ignoredLines = ignoredLinesList.iterator;
109-
var hasCurrent = ignoredLines.moveNext();
110-
111-
bool shouldIgnoreLine(Iterator<List<int>> ignoredRanges, int line) {
112-
if (!hasCurrent || ignoredRanges.current.isEmpty) {
113-
return false;
114-
}
115-
116-
if (line < ignoredRanges.current[0]) return false;
117-
118-
while (hasCurrent &&
119-
ignoredRanges.current.isNotEmpty &&
120-
ignoredRanges.current[1] < line) {
121-
hasCurrent = ignoredRanges.moveNext();
122-
}
123-
124-
if (hasCurrent &&
125-
ignoredRanges.current.isNotEmpty &&
126-
ignoredRanges.current[0] <= line &&
127-
line <= ignoredRanges.current[1]) {
128-
return true;
129-
}
130-
131-
return false;
132-
}
133-
13472
void addToMap(Map<int, int> map, int line, int count) {
13573
final oldCount = map.putIfAbsent(line, () => 0);
13674
map[line] = count + oldCount;
@@ -147,17 +85,13 @@ class HitMap {
14785
final k = hits[i];
14886
if (k is int) {
14987
// Single line.
150-
if (shouldIgnoreLine(ignoredLines, k)) continue;
151-
15288
addToMap(hitMap, k, hits[i + 1] as int);
15389
} else if (k is String) {
15490
// Linerange. We expand line ranges to actual lines at this point.
15591
final splitPos = k.indexOf('-');
15692
final start = int.parse(k.substring(0, splitPos));
15793
final end = int.parse(k.substring(splitPos + 1));
15894
for (var j = start; j <= end; j++) {
159-
if (shouldIgnoreLine(ignoredLines, j)) continue;
160-
16195
addToMap(hitMap, j, hits[i + 1] as int);
16296
}
16397
} else {
@@ -185,7 +119,12 @@ class HitMap {
185119
fillHitMap(e['branchHits'] as List, sourceHitMap.branchHits!);
186120
}
187121
}
188-
return globalHitMap;
122+
return checkIgnoredLines
123+
? globalHitMap.filterIgnored(
124+
ignoredLinesInFilesCache: ignoredLinesInFilesCache,
125+
resolver: resolver,
126+
)
127+
: globalHitMap;
189128
}
190129

191130
/// Creates a single hitmap from a raw json object.
@@ -268,6 +207,39 @@ extension FileHitMaps on Map<String, HitMap> {
268207
}
269208
});
270209
}
210+
211+
/// Filters out lines that are ignored by ignore comments.
212+
Map<String, HitMap> filterIgnored({
213+
required Map<String, List<List<int>>?> ignoredLinesInFilesCache,
214+
required Resolver resolver,
215+
}) {
216+
final loader = Loader();
217+
final newHitMaps = <String, HitMap>{};
218+
for (final MapEntry(key: source, value: hitMap) in entries) {
219+
final ignoredLinesList = ignoredLinesInFilesCache.putIfAbsent(source, () {
220+
final path = resolver.resolve(source);
221+
if (path == null) return <List<int>>[];
222+
return getIgnoredLines(path, loader.loadSync(path));
223+
});
224+
// Null here means that the whole file is ignored.
225+
if (ignoredLinesList == null) continue;
226+
227+
Map<int, int>? filterHits(Map<int, int>? hits) => hits == null
228+
? null
229+
: {
230+
for (final MapEntry(key: line, value: count) in hits.entries)
231+
if (!ignoredLinesList.ignoredContains(line)) line: count,
232+
};
233+
234+
newHitMaps[source] = HitMap(
235+
filterHits(hitMap.lineHits),
236+
filterHits(hitMap.funcHits),
237+
hitMap.funcNames,
238+
filterHits(hitMap.branchHits),
239+
);
240+
}
241+
return newHitMaps;
242+
}
271243
}
272244

273245
/// Class containing information about a coverage hit.

pkgs/coverage/lib/src/util.dart

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,16 @@ Uri? extractVMServiceUri(String str) {
5959
return null;
6060
}
6161

62-
final muliLineIgnoreStart = RegExp(r'//\s*coverage:ignore-start[\w\d\s]*$');
63-
final muliLineIgnoreEnd = RegExp(r'//\s*coverage:ignore-end[\w\d\s]*$');
62+
final multiLineIgnoreStart = RegExp(r'//\s*coverage:ignore-start[\w\d\s]*$');
63+
final multiLineIgnoreEnd = RegExp(r'//\s*coverage:ignore-end[\w\d\s]*$');
6464
final singleLineIgnore = RegExp(r'//\s*coverage:ignore-line[\w\d\s]*$');
6565
final ignoreFile = RegExp(r'//\s*coverage:ignore-file[\w\d\s]*$');
6666

67-
/// Return list containing inclusive range of lines to be ignored by coverage.
67+
/// Return list containing inclusive ranges of lines to be ignored by coverage.
6868
/// If there is a error in balancing the statements it will throw a
6969
/// [FormatException],
7070
/// unless `coverage:ignore-file` is found.
71-
/// Return [0, lines.length] if the whole file is ignored.
71+
/// Return null if the whole file is ignored.
7272
///
7373
/// ```
7474
/// 1. final str = ''; // coverage:ignore-line
@@ -86,42 +86,39 @@ final ignoreFile = RegExp(r'//\s*coverage:ignore-file[\w\d\s]*$');
8686
/// ]
8787
/// ```
8888
///
89-
List<List<int>> getIgnoredLines(String filePath, List<String>? lines) {
89+
/// The returned ranges are in sorted order, and never overlap.
90+
List<List<int>>? getIgnoredLines(String filePath, List<String>? lines) {
9091
final ignoredLines = <List<int>>[];
9192
if (lines == null) return ignoredLines;
9293

93-
final allLines = [
94-
[0, lines.length]
95-
];
96-
9794
FormatException? err;
9895
var i = 0;
9996
while (i < lines.length) {
100-
if (lines[i].contains(ignoreFile)) return allLines;
97+
if (lines[i].contains(ignoreFile)) return null;
10198

102-
if (lines[i].contains(muliLineIgnoreEnd)) {
99+
if (lines[i].contains(multiLineIgnoreEnd)) {
103100
err ??= FormatException(
104101
'unmatched coverage:ignore-end found at $filePath:${i + 1}',
105102
);
106103
}
107104

108105
if (lines[i].contains(singleLineIgnore)) ignoredLines.add([i + 1, i + 1]);
109106

110-
if (lines[i].contains(muliLineIgnoreStart)) {
107+
if (lines[i].contains(multiLineIgnoreStart)) {
111108
final start = i;
112109
var isUnmatched = true;
113110
++i;
114111
while (i < lines.length) {
115-
if (lines[i].contains(ignoreFile)) return allLines;
116-
if (lines[i].contains(muliLineIgnoreStart)) {
112+
if (lines[i].contains(ignoreFile)) return null;
113+
if (lines[i].contains(multiLineIgnoreStart)) {
117114
err ??= FormatException(
118115
'coverage:ignore-start found at $filePath:${i + 1}'
119116
' before previous coverage:ignore-start ended',
120117
);
121118
break;
122119
}
123120

124-
if (lines[i].contains(muliLineIgnoreEnd)) {
121+
if (lines[i].contains(multiLineIgnoreEnd)) {
125122
ignoredLines.add([start + 1, i + 1]);
126123
isUnmatched = false;
127124
break;
@@ -146,6 +143,26 @@ List<List<int>> getIgnoredLines(String filePath, List<String>? lines) {
146143
throw err;
147144
}
148145

146+
extension IgnoredLinesContains on List<List<int>> {
147+
/// Returns whether this list of line ranges contains the given line.
148+
bool ignoredContains(int line) {
149+
if (length == 0 || this[0][0] > line) return false;
150+
151+
// Binary search for the range with the largest start value that is <= line.
152+
var lo = 0;
153+
var hi = length;
154+
while (lo < hi - 1) {
155+
final mid = lo + (hi - lo) ~/ 2;
156+
if (this[mid][0] <= line) {
157+
lo = mid;
158+
} else {
159+
hi = mid;
160+
}
161+
}
162+
return this[lo][1] >= line;
163+
}
164+
}
165+
149166
extension StandardOutExtension on Stream<List<int>> {
150167
Stream<String> lines() =>
151168
transform(const SystemEncoding().decoder).transform(const LineSplitter());

pkgs/coverage/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: coverage
2-
version: 1.14.1
2+
version: 1.15.0
33
description: Coverage data manipulation and formatting
44
repository: https://github.com/dart-lang/tools/tree/main/pkgs/coverage
55
issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Acoverage
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:coverage/coverage.dart' show Resolver;
8+
import 'package:coverage/src/hitmap.dart';
9+
import 'package:test/test.dart';
10+
11+
String fileUri(String relativePath) =>
12+
Uri.file(File(relativePath).absolute.path).toString();
13+
14+
void main() {
15+
test('filter ignored', () async {
16+
// The ignored lines come from the comments in the test dart files. But the
17+
// hitmaps are fake, and don't have to correspond to real coverage data.
18+
final hitmaps = {
19+
fileUri('nonexistent_file.dart'): HitMap(
20+
{1: 1, 2: 2, 3: 3},
21+
{1: 1, 2: 2, 3: 3},
22+
{1: 'abc', 2: 'def'},
23+
{1: 1, 2: 2, 3: 3},
24+
),
25+
fileUri('another_nonexistent_file.dart'): HitMap(
26+
{1: 1, 2: 2, 3: 3},
27+
),
28+
fileUri('test/test_files/test_app.dart'): HitMap(
29+
{1: 1, 2: 2, 3: 3},
30+
{1: 1, 2: 2, 3: 3},
31+
{1: 'abc', 2: 'def'},
32+
{1: 1, 2: 2, 3: 3},
33+
),
34+
fileUri('test/test_files/test_app_isolate.dart'): HitMap(
35+
{for (var i = 50; i < 100; ++i) i: i},
36+
{for (var i = 50; i < 100; ++i) i: i},
37+
{for (var i = 50; i < 100; ++i) i: '$i'},
38+
{for (var i = 50; i < 100; ++i) i: i},
39+
),
40+
};
41+
42+
// Lines ignored in test/test_files/test_app_isolate.dart.
43+
const ignores = [
44+
52,
45+
54,
46+
55,
47+
56,
48+
57,
49+
58,
50+
63,
51+
64,
52+
65,
53+
66,
54+
67,
55+
68,
56+
69,
57+
70,
58+
71,
59+
72,
60+
73,
61+
];
62+
63+
final expected = {
64+
fileUri('nonexistent_file.dart'): HitMap(
65+
{1: 1, 2: 2, 3: 3},
66+
{1: 1, 2: 2, 3: 3},
67+
{1: 'abc', 2: 'def'},
68+
{1: 1, 2: 2, 3: 3},
69+
),
70+
fileUri('another_nonexistent_file.dart'): HitMap(
71+
{1: 1, 2: 2, 3: 3},
72+
),
73+
fileUri('test/test_files/test_app_isolate.dart'): HitMap(
74+
{
75+
for (var i = 50; i < 100; ++i)
76+
if (!ignores.contains(i)) i: i
77+
},
78+
{
79+
for (var i = 50; i < 100; ++i)
80+
if (!ignores.contains(i)) i: i
81+
},
82+
{for (var i = 50; i < 100; ++i) i: '$i'},
83+
{
84+
for (var i = 50; i < 100; ++i)
85+
if (!ignores.contains(i)) i: i
86+
},
87+
),
88+
};
89+
90+
final resolver = await Resolver.create(packagePath: '.');
91+
92+
final actual =
93+
hitmaps.filterIgnored(ignoredLinesInFilesCache: {}, resolver: resolver);
94+
95+
expect(actual.keys.toList(), expected.keys.toList());
96+
for (final source in expected.keys) {
97+
expect(actual[source]!.lineHits, expected[source]!.lineHits);
98+
expect(actual[source]!.funcHits, expected[source]!.funcHits);
99+
expect(actual[source]!.funcNames, expected[source]!.funcNames);
100+
expect(actual[source]!.branchHits, expected[source]!.branchHits);
101+
}
102+
});
103+
}

0 commit comments

Comments
 (0)