Merge pull request #350 from dart-lang/report_lines

Use the report lines flag
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aaf7b28..4c6b7bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@
   optional bool flag controlling whether function coverage is collected.
 * Ensure `createHitmap` returns a sorted hitmap. This fixes a potential issue with
   ignore line annotations.
+* Use the `reportLines` flag in `vm_service`'s `getSourceReport` RPC. This
+  typically halves the number of RPCs that the coverage collector needs to run.
 
 ## 1.0.3 - 2021-05-25
 
diff --git a/lib/src/collect.dart b/lib/src/collect.dart
index 7c481b0..841a7c5 100644
--- a/lib/src/collect.dart
+++ b/lib/src/collect.dart
@@ -97,6 +97,10 @@
   scopedOutput ??= <String>{};
   final vm = await service.getVM();
   final allCoverage = <Map<String, dynamic>>[];
+  final version = await service.getVersion();
+  final reportLines =
+      (version.major == 3 && version.minor != null && version.minor! >= 51) ||
+          (version.major != null && version.major! > 3);
 
   for (var isolateRef in vm.isolates!) {
     if (isolateIds != null && !isolateIds.contains(isolateRef.id)) continue;
@@ -110,9 +114,11 @@
         if (!scopedOutput.contains(scope)) continue;
         final scriptReport = await service.getSourceReport(
             isolateRef.id!, <String>[SourceReportKind.kCoverage],
-            forceCompile: true, scriptId: script.id);
-        final coverage = await _getCoverageJson(
-            service, isolateRef, scriptReport, includeDart, functionCoverage);
+            forceCompile: true,
+            scriptId: script.id,
+            reportLines: reportLines ? true : null);
+        final coverage = await _getCoverageJson(service, isolateRef,
+            scriptReport, includeDart, functionCoverage, reportLines);
         allCoverage.addAll(coverage);
       }
     } else {
@@ -120,9 +126,10 @@
         isolateRef.id!,
         <String>[SourceReportKind.kCoverage],
         forceCompile: true,
+        reportLines: reportLines ? true : null,
       );
-      final coverage = await _getCoverageJson(
-          service, isolateRef, isolateReport, includeDart, functionCoverage);
+      final coverage = await _getCoverageJson(service, isolateRef,
+          isolateReport, includeDart, functionCoverage, reportLines);
       allCoverage.addAll(coverage);
     }
   }
@@ -220,10 +227,12 @@
     IsolateRef isolateRef,
     SourceReport report,
     bool includeDart,
-    bool functionCoverage) async {
+    bool functionCoverage,
+    bool reportLines) async {
   final hitMaps = <Uri, HitMap>{};
   final scripts = <ScriptRef, Script>{};
   final libraries = <LibraryRef>{};
+  final needScripts = functionCoverage || !reportLines;
   for (var range in report.ranges!) {
     final scriptRef = report.scripts![range.scriptIndex!];
     final scriptUri = Uri.parse(report.scripts![range.scriptIndex!].uri!);
@@ -234,18 +243,22 @@
     // Skip scripts from dart:.
     if (!includeDart && scriptUri.scheme == 'dart') continue;
 
-    if (!scripts.containsKey(scriptRef)) {
-      scripts[scriptRef] =
-          await service.getObject(isolateRef.id!, scriptRef.id!) as Script;
-    }
-    final script = scripts[scriptRef];
-    if (script == null) continue;
-
     // Look up the hit maps for this script (shared across isolates).
     final hits = hitMaps.putIfAbsent(scriptUri, () => HitMap());
 
+    Script? script;
+    if (needScripts) {
+      if (!scripts.containsKey(scriptRef)) {
+        scripts[scriptRef] =
+            await service.getObject(isolateRef.id!, scriptRef.id!) as Script;
+      }
+      script = scripts[scriptRef];
+
+      if (script == null) continue;
+    }
+
     // If the script's library isn't loaded, load it then look up all its funcs.
-    final libRef = script.library;
+    final libRef = script?.library;
     if (functionCoverage && libRef != null && !libraries.contains(libRef)) {
       hits.funcHits ??= <int, int>{};
       hits.funcNames ??= <int, String>{};
@@ -254,7 +267,7 @@
           await service.getObject(isolateRef.id!, libRef.id!) as Library;
       if (library.functions != null) {
         for (var funcRef in library.functions!) {
-          await _processFunction(service, isolateRef, script, funcRef, hits);
+          await _processFunction(service, isolateRef, script!, funcRef, hits);
         }
       }
       if (library.classes != null) {
@@ -264,7 +277,7 @@
           if (clazz.functions != null) {
             for (var funcRef in clazz.functions!) {
               await _processFunction(
-                  service, isolateRef, script, funcRef, hits);
+                  service, isolateRef, script!, funcRef, hits);
             }
           }
         }
@@ -276,10 +289,10 @@
 
     if (coverage == null) continue;
 
-    for (final tokenPos in coverage.hits!) {
-      final line = _getLineFromTokenPos(script, tokenPos);
+    for (final pos in coverage.hits!) {
+      final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
       if (line == null) {
-        print('tokenPos $tokenPos has no line mapping for script $scriptUri');
+        print('tokenPos $pos has no line mapping for script $scriptUri');
         continue;
       }
       _incrementCountForKey(hits.lineHits, line);
@@ -287,10 +300,10 @@
         _incrementCountForKey(hits.funcHits!, line);
       }
     }
-    for (final tokenPos in coverage.misses!) {
-      final line = _getLineFromTokenPos(script, tokenPos);
+    for (final pos in coverage.misses!) {
+      final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
       if (line == null) {
-        print('tokenPos $tokenPos has no line mapping for script $scriptUri');
+        print('tokenPos $pos has no line mapping for script $scriptUri');
         continue;
       }
       hits.lineHits.putIfAbsent(line, () => 0);
diff --git a/pubspec.yaml b/pubspec.yaml
index c5ae05d..2b89721 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -13,7 +13,7 @@
   path: ^1.8.0
   source_maps: ^0.10.10
   stack_trace: ^1.10.0
-  vm_service: ">=6.1.0 <8.0.0"
+  vm_service: ">=7.3.0 <8.0.0"
 dev_dependencies:
   pedantic: ^1.10.0
   test: ^1.16.0
diff --git a/test/collect_coverage_api_test.dart b/test/collect_coverage_api_test.dart
index 5f9d1a3..3f82581 100644
--- a/test/collect_coverage_api_test.dart
+++ b/test/collect_coverage_api_test.dart
@@ -34,7 +34,7 @@
     });
 
     for (var sampleCoverageData in sources[_sampleAppFileUri]!) {
-      expect(sampleCoverageData['hits'], isNotNull);
+      expect(sampleCoverageData['hits'], isNotEmpty);
     }
 
     for (var sampleCoverageData in sources[_isolateLibFileUri]!) {
@@ -83,10 +83,38 @@
     _expectHitCount(hits, 11, 1);
     _expectHitCount(hits, 28, 1);
   });
+
+  test('collect_coverage_api with function coverage', () async {
+    final json = await _collectCoverage(functionCoverage: true);
+    expect(json.keys, unorderedEquals(<String>['type', 'coverage']));
+    expect(json, containsPair('type', 'CodeCoverage'));
+
+    final coverage = json['coverage'] as List;
+    expect(coverage, isNotEmpty);
+
+    final sources = coverage.cast<Map>().fold(<String, List<Map>>{},
+        (Map<String, List<Map>> map, value) {
+      final sourceUri = value['source'] as String;
+      map.putIfAbsent(sourceUri, () => <Map>[]).add(value);
+      return map;
+    });
+
+    for (var sampleCoverageData in sources[_sampleAppFileUri]!) {
+      expect(sampleCoverageData['funcNames'], isNotEmpty);
+      expect(sampleCoverageData['funcHits'], isNotEmpty);
+    }
+
+    for (var sampleCoverageData in sources[_isolateLibFileUri]!) {
+      expect(sampleCoverageData['funcNames'], isNotEmpty);
+      expect(sampleCoverageData['funcHits'], isNotEmpty);
+    }
+  });
 }
 
 Future<Map<String, dynamic>> _collectCoverage(
-    {Set<String> scopedOutput = const {}, bool isolateIds = false}) async {
+    {Set<String> scopedOutput = const {},
+    bool isolateIds = false,
+    bool functionCoverage = false}) async {
   final openPort = await getOpenPort();
 
   // run the sample app, with the right flags
@@ -115,7 +143,9 @@
   final isolateIdSet = isolateIds ? {isolateId} : null;
 
   return collect(serviceUri, true, true, false, scopedOutput,
-      timeout: timeout, isolateIds: isolateIdSet, functionCoverage: true);
+      timeout: timeout,
+      isolateIds: isolateIdSet,
+      functionCoverage: functionCoverage);
 }
 
 // Returns the first coverage hitmap for the script with with the specified
diff --git a/test/collect_coverage_test.dart b/test/collect_coverage_test.dart
index bc39bef..60484ae 100644
--- a/test/collect_coverage_test.dart
+++ b/test/collect_coverage_test.dart
@@ -81,7 +81,7 @@
   });
 
   test('createHitmap', () async {
-    final resultString = await _getCoverageResult();
+    final resultString = await _collectCoverage(true);
     final jsonResult = json.decode(resultString) as Map<String, dynamic>;
     final coverage = jsonResult['coverage'] as List;
     final hitMap = await createHitmap(
@@ -191,9 +191,9 @@
 String? _coverageData;
 
 Future<String> _getCoverageResult() async =>
-    _coverageData ??= await _collectCoverage();
+    _coverageData ??= await _collectCoverage(false);
 
-Future<String> _collectCoverage() async {
+Future<String> _collectCoverage(bool functionCoverage) async {
   expect(FileSystemEntity.isFileSync(testAppPath), isTrue);
 
   final openPort = await getOpenPort();
@@ -220,7 +220,7 @@
   // TODO: need to get all of this functionality in the lib
   final toolResult = await Process.run('dart', [
     _collectAppPath,
-    '--function-coverage',
+    if (functionCoverage) '--function-coverage',
     '--uri',
     '$serviceUri',
     '--resume-isolates',