Revert the 0.13.11 revert and properly model it as a breaking change (#305)

* Revert "Revert breaking change in 13.10 (#304)"

This reverts commit 49219fc713fa1ad6f64278151d5c6a673b8c4525.

* update version and changelog

* pin test

* remove overrides
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f932ddf..5351d7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,13 @@
-## 0.13.11 - 2020-03-09
+## 0.14.0 - 2020-06-04
+
+* Add flag `--check-ignore` that is used to ignore lines from coverage
+  depending on the comments.
+
+  Use // coverage:ignore-line to ignore one line.
+  Use // coverage:ignore-start and // coverage:ignore-end to ignore range of lines inclusive.
+  Use // coverage:ignore-file to ignore the whole file.
+
+## 0.13.11 - 2020-06-04
 
 * Revert breaking change in 13.10
 
diff --git a/README.md b/README.md
index 0791043..b321342 100644
--- a/README.md
+++ b/README.md
@@ -55,3 +55,9 @@
 
 where `app_package` is the path to the package whose coverage is being
 collected. If `--sdk-root` is set, Dart SDK coverage will also be output.
+
+#### Ignore lines from coverage
+
+- `// coverage:ignore-line` to ignore one line.
+- `// coverage:ignore-start` and `// coverage:ignore-end` to ignore range of lines inclusive.
+- `// coverage:ignore-file` to ignore the whole file.
diff --git a/bin/format_coverage.dart b/bin/format_coverage.dart
index 3e2b659..978b061 100644
--- a/bin/format_coverage.dart
+++ b/bin/format_coverage.dart
@@ -24,6 +24,7 @@
   bool prettyPrint;
   bool lcov;
   bool expectMarkers;
+  bool checkIgnore;
   bool verbose;
 }
 
@@ -39,10 +40,15 @@
     print('  package-root: ${env.pkgRoot}');
     print('  package-spec: ${env.packagesPath}');
     print('  report-on: ${env.reportOn}');
+    print('  check-ignore: ${env.checkIgnore}');
   }
 
   final clock = Stopwatch()..start();
-  final hitmap = await parseCoverage(files, env.workers);
+  final hitmap = await parseCoverage(
+    files,
+    env.workers,
+    checkIgnoredLines: env.checkIgnore,
+  );
 
   // All workers are done. Process the data.
   if (env.verbose) {
@@ -125,6 +131,13 @@
       help: 'convert coverage data to lcov format');
   parser.addFlag('verbose',
       abbr: 'v', negatable: false, help: 'verbose output');
+  parser.addFlag(
+    'check-ignore',
+    abbr: 'c',
+    negatable: false,
+    help: 'check for coverage ignore comments.'
+        ' Not supported in web coverage.',
+  );
   parser.addFlag('help', abbr: 'h', negatable: false, help: 'show this help');
 
   final args = parser.parse(arguments);
@@ -214,6 +227,7 @@
     fail('Invalid worker count: $e');
   }
 
+  env.checkIgnore = args['check-ignore'] as bool;
   env.verbose = args['verbose'] as bool;
   return env;
 }
diff --git a/lib/src/hitmap.dart b/lib/src/hitmap.dart
index 5d6bca0..a562abc 100644
--- a/lib/src/hitmap.dart
+++ b/lib/src/hitmap.dart
@@ -6,11 +6,21 @@
 import 'dart:convert' show json;
 import 'dart:io';
 
+import 'package:coverage/src/resolver.dart';
+import 'package:coverage/src/util.dart';
+
 /// Creates a single hitmap from a raw json object. Throws away all entries that
 /// are not resolvable.
 ///
 /// `jsonResult` is expected to be a List<Map<String, dynamic>>.
-Map<String, Map<int, int>> createHitmap(List<Map<String, dynamic>> jsonResult) {
+Future<Map<String, Map<int, int>>> createHitmap(
+  List<Map<String, dynamic>> jsonResult, {
+  bool checkIgnoredLines = false,
+  String packagesPath,
+}) async {
+  final resolver = Resolver(packagesPath: packagesPath);
+  final loader = Loader();
+
   // Map of source file to map of line to hit count for that line.
   final globalHitMap = <String, Map<int, int>>{};
 
@@ -26,6 +36,24 @@
       continue;
     }
 
+    var ignoredLinesList = <List<int>>[];
+
+    if (checkIgnoredLines) {
+      final lines = await loader.load(resolver.resolve(source));
+      ignoredLinesList = getIgnoredLines(lines);
+
+      // Ignore the whole file.
+      if (ignoredLinesList.length == 1 &&
+          ignoredLinesList[0][0] == 0 &&
+          ignoredLinesList[0][1] == lines.length) {
+        continue;
+      }
+    }
+
+    // Move to the first ignore range.
+    final ignoredLines = ignoredLinesList.iterator;
+    ignoredLines.moveNext();
+
     final sourceHitMap = globalHitMap.putIfAbsent(source, () => <int, int>{});
     final hits = e['hits'] as List;
     // hits is a flat array of the following format:
@@ -36,6 +64,8 @@
       final k = hits[i];
       if (k is int) {
         // Single line.
+        if (_shouldIgnoreLine(ignoredLines, k)) continue;
+
         addToMap(sourceHitMap, k, hits[i + 1] as int);
       } else if (k is String) {
         // Linerange. We expand line ranges to actual lines at this point.
@@ -43,6 +73,8 @@
         final start = int.parse(k.substring(0, splitPos));
         final end = int.parse(k.substring(splitPos + 1));
         for (var j = start; j <= end; j++) {
+          if (_shouldIgnoreLine(ignoredLines, j)) continue;
+
           addToMap(sourceHitMap, j, hits[i + 1] as int);
         }
       } else {
@@ -53,6 +85,29 @@
   return globalHitMap;
 }
 
+bool _shouldIgnoreLine(Iterator<List<int>> ignoredRanges, int line) {
+  if (ignoredRanges.current == null || ignoredRanges.current.isEmpty) {
+    return false;
+  }
+
+  if (line < ignoredRanges.current[0]) return false;
+
+  while (ignoredRanges.current != null &&
+      ignoredRanges.current.isNotEmpty &&
+      ignoredRanges.current[1] < line) {
+    ignoredRanges.moveNext();
+  }
+
+  if (ignoredRanges.current != null &&
+      ignoredRanges.current.isNotEmpty &&
+      ignoredRanges.current[0] <= line &&
+      line <= ignoredRanges.current[1]) {
+    return true;
+  }
+
+  return false;
+}
+
 /// Merges [newMap] into [result].
 void mergeHitmaps(
     Map<String, Map<int, int>> newMap, Map<String, Map<int, int>> result) {
@@ -73,7 +128,10 @@
 
 /// Generates a merged hitmap from a set of coverage JSON files.
 Future<Map<String, Map<int, int>>> parseCoverage(
-    Iterable<File> files, int _) async {
+  Iterable<File> files,
+  int _, {
+  bool checkIgnoredLines = false,
+}) async {
   final globalHitmap = <String, Map<int, int>>{};
   for (var file in files) {
     final contents = file.readAsStringSync();
@@ -81,7 +139,10 @@
     if (jsonMap.containsKey('coverage')) {
       final jsonResult = jsonMap['coverage'] as List;
       mergeHitmaps(
-        createHitmap(jsonResult.cast<Map<String, dynamic>>()),
+        await createHitmap(
+          jsonResult.cast<Map<String, dynamic>>(),
+          checkIgnoredLines: checkIgnoredLines,
+        ),
         globalHitmap,
       );
     }
diff --git a/lib/src/util.dart b/lib/src/util.dart
index f0a3d7e..62cf870 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -109,3 +109,69 @@
   hash = hash ^ (hash >> 11);
   return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
 }
+
+const muliLineIgnoreStart = '// coverage:ignore-start';
+const muliLineIgnoreEnd = '// coverage:ignore-end';
+const singleLineIgnore = '// coverage:ignore-line';
+const ignoreFile = '// coverage:ignore-file';
+
+/// Return list containing inclusive range of lines to be ignored by coverage.
+/// If there is a error in balancing the statements it will ignore nothing,
+/// unless `coverage:ignore-file` is found.
+/// Return [0, lines.length] if the whole file is ignored.
+///
+/// ```
+/// 1.  final str = ''; // coverage:ignore-line
+/// 2.  final str = '';
+/// 3.  final str = ''; // coverage:ignore-start
+/// 4.  final str = '';
+/// 5.  final str = ''; // coverage:ignore-end
+/// ```
+///
+/// Returns
+/// ```
+/// [
+///   [1,1],
+///   [3,5],
+/// ]
+/// ```
+///
+List<List<int>> getIgnoredLines(List<String> lines) {
+  final ignoredLines = <List<int>>[];
+  if (lines == null) return ignoredLines;
+
+  final allLines = [
+    [0, lines.length]
+  ];
+
+  var isError = false;
+  var i = 0;
+  while (i < lines.length) {
+    if (lines[i].contains(ignoreFile)) return allLines;
+
+    if (lines[i].contains(muliLineIgnoreEnd)) isError = true;
+
+    if (lines[i].contains(singleLineIgnore)) ignoredLines.add([i + 1, i + 1]);
+
+    if (lines[i].contains(muliLineIgnoreStart)) {
+      final start = i;
+      ++i;
+      while (i < lines.length) {
+        if (lines[i].contains(ignoreFile)) return allLines;
+        if (lines[i].contains(muliLineIgnoreStart)) {
+          isError = true;
+          break;
+        }
+
+        if (lines[i].contains(muliLineIgnoreEnd)) {
+          ignoredLines.add([start + 1, i + 1]);
+          break;
+        }
+        ++i;
+      }
+    }
+    ++i;
+  }
+
+  return isError ? [] : ignoredLines;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index b3ed99c..3f6a77f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: coverage
-version: 0.13.11
+version: 0.14.0
 description: Coverage data manipulation and formatting
 homepage: https://github.com/dart-lang/coverage
 
@@ -23,3 +23,4 @@
 executables:
   collect_coverage:
   format_coverage:
+
diff --git a/test/collect_coverage_test.dart b/test/collect_coverage_test.dart
index 0ce58da..bd2b234 100644
--- a/test/collect_coverage_test.dart
+++ b/test/collect_coverage_test.dart
@@ -53,7 +53,10 @@
     final resultString = await _getCoverageResult();
     final jsonResult = json.decode(resultString) as Map<String, dynamic>;
     final coverage = jsonResult['coverage'] as List;
-    final hitMap = createHitmap(coverage.cast<Map<String, dynamic>>());
+    final hitMap = await createHitmap(
+      coverage.cast<Map<String, dynamic>>(),
+      checkIgnoredLines: true,
+    );
     expect(hitMap, contains(_sampleAppFileUri));
 
     final isolateFile = hitMap[_isolateLibFileUri];
@@ -70,6 +73,8 @@
       33: 1,
       34: 3,
       35: 1,
+      46: 1,
+      47: 1,
     };
     if (Platform.version.startsWith('1.')) {
       // Dart VMs prior to 2.0.0-dev.5.0 contain a bug that emits coverage on the
diff --git a/test/run_and_collect_test.dart b/test/run_and_collect_test.dart
index 54596d6..aa0fe2a 100644
--- a/test/run_and_collect_test.dart
+++ b/test/run_and_collect_test.dart
@@ -40,7 +40,10 @@
       expect(sampleCoverageData['hits'], isNotEmpty);
     }
 
-    final hitMap = createHitmap(coverage);
+    final hitMap = await createHitmap(
+      coverage,
+      checkIgnoredLines: true,
+    );
     expect(hitMap, contains(_sampleAppFileUri));
 
     final actualHits = hitMap[_isolateLibFileUri];
@@ -57,6 +60,8 @@
       33: 1,
       34: 3,
       35: 1,
+      46: 1,
+      47: 1,
     };
     // Dart VMs prior to 2.0.0-dev.5.0 contain a bug that emits coverage on the
     // closing brace of async function blocks.
diff --git a/test/test_files/test_app_isolate.dart b/test/test_files/test_app_isolate.dart
index 0f72d27..eac9c2d 100644
--- a/test/test_files/test_app_isolate.dart
+++ b/test/test_files/test_app_isolate.dart
@@ -34,4 +34,24 @@
     final sum = (threeThings[1] + threeThings[2]) as int;
     port.send(sum);
   });
+
+  print('678'); // coverage:ignore-line
+
+  // coverage:ignore-start
+  print('1');
+  print('2');
+  print('3');
+  // coverage:ignore-end
+
+  print('4');
+  print('5');
+
+  print('6'); // coverage:ignore-start
+  print('7');
+  print('8');
+  // coverage:ignore-end
+  print('9'); // coverage:ignore-start
+  print('10');
+  print('11'); // coverage:ignore-line
+  // coverage:ignore-end
 }
diff --git a/test/util_test.dart b/test/util_test.dart
index b98ffe2..b8bf823 100644
--- a/test/util_test.dart
+++ b/test/util_test.dart
@@ -129,4 +129,106 @@
           Uri.parse('http://foo.bar:9999/cG90YXRv/'));
     });
   });
+
+  group('getIgnoredLines', () {
+    const invalidSources = [
+      '''final str = ''; // coverage:ignore-start
+        final str = '';
+        final str = ''; // coverage:ignore-start
+        ''',
+      '''final str = ''; // coverage:ignore-start
+        final str = '';
+        final str = ''; // coverage:ignore-start
+        final str = ''; // coverage:ignore-end
+        final str = '';
+        final str = ''; // coverage:ignore-end
+        ''',
+      '''final str = ''; // coverage:ignore-start
+        final str = '';
+        final str = ''; // coverage:ignore-end
+        final str = '';
+        final str = ''; // coverage:ignore-end
+        ''',
+      '''final str = ''; // coverage:ignore-end
+        final str = '';
+        final str = ''; // coverage:ignore-start
+        final str = '';
+        final str = ''; // coverage:ignore-end
+        ''',
+      '''final str = ''; // coverage:ignore-end
+        final str = '';
+        final str = ''; // coverage:ignore-end
+        ''',
+      '''final str = ''; // coverage:ignore-end
+        final str = '';
+        final str = ''; // coverage:ignore-start
+        ''',
+      '''final str = ''; // coverage:ignore-end
+        ''',
+      '''final str = ''; // coverage:ignore-start
+        ''',
+    ];
+
+    test('returns empty when the annotations are not balanced', () {
+      for (final content in invalidSources) {
+        expect(getIgnoredLines(content.split('\n')), isEmpty);
+      }
+    });
+
+    test(
+        'returns [[0,lines.length]] when the annotations are not '
+        'balanced but the whole file is ignored', () {
+      for (final content in invalidSources) {
+        final lines = content.split('\n');
+        lines.add(' // coverage:ignore-file');
+        expect(getIgnoredLines(lines), [
+          [0, lines.length]
+        ]);
+      }
+    });
+
+    test('Returns [[0,lines.length]] when the whole file is ignored', () {
+      final lines = '''final str = ''; // coverage:ignore-start
+      final str = ''; // coverage:ignore-end
+      final str = ''; // coverage:ignore-file
+      '''
+          .split('\n');
+
+      expect(getIgnoredLines(lines), [
+        [0, lines.length]
+      ]);
+    });
+
+    test('return the correct range of lines ignored', () {
+      final lines = '''
+      final str = ''; // coverage:ignore-start
+      final str = ''; // coverage:ignore-line
+      final str = ''; // coverage:ignore-end
+      final str = ''; // coverage:ignore-start
+      final str = ''; // coverage:ignore-line
+      final str = ''; // coverage:ignore-end
+      '''
+          .split('\n');
+
+      expect(getIgnoredLines(lines), [
+        [1, 3],
+        [4, 6],
+      ]);
+    });
+
+    test('return the correct list of lines ignored', () {
+      final lines = '''
+      final str = ''; // coverage:ignore-line
+      final str = ''; // coverage:ignore-line
+      final str = ''; // coverage:ignore-line
+      '''
+          .split('\n');
+
+      expect(getIgnoredLines(lines), [
+        [1, 1],
+        [2, 2],
+        [3, 3],
+      ]);
+    });
+  });
 }
diff --git a/tool/travis.sh b/tool/travis.sh
index 1037a88..b0358e3 100755
--- a/tool/travis.sh
+++ b/tool/travis.sh
@@ -31,5 +31,6 @@
     --in=var/coverage.json \
     --out=var/lcov.info \
     --packages=.packages \
-    --report-on=lib
+    --report-on=lib \
+    --check-ignore
 fi