allow LINT to reference a relative line (#2007)

diff --git a/pubspec.yaml b/pubspec.yaml
index cb16a87..c1c8c96 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,7 +9,7 @@
 documentation: https://dart-lang.github.io/linter/lints
 
 environment:
-  sdk: '>=2.2.2 <3.0.0'
+  sdk: '>=2.6.0 <3.0.0'
 
 dependencies:
   analyzer: ^0.39.3
diff --git a/test/rule_test.dart b/test/rule_test.dart
index e0ddffe..ff7bcb8 100644
--- a/test/rule_test.dart
+++ b/test/rule_test.dart
@@ -247,9 +247,8 @@
 
     var lineNumber = 1;
     for (var line in file.readAsLinesSync()) {
-      var annotation = extractAnnotation(line);
+      var annotation = extractAnnotation(lineNumber, line);
       if (annotation != null) {
-        annotation.lineNumber = lineNumber;
         expected.add(AnnotationMatcher(annotation));
       }
       ++lineNumber;
diff --git a/test/util/annotation.dart b/test/util/annotation.dart
index 396dc4a..dd2e521 100644
--- a/test/util/annotation.dart
+++ b/test/util/annotation.dart
@@ -6,49 +6,28 @@
 import 'package:analyzer/src/generated/source.dart';
 import 'package:linter/src/analyzer.dart';
 
-Annotation extractAnnotation(String line) {
-  final index = line.indexOf(RegExp(r'(//|#)[ ]?LINT'));
+Annotation extractAnnotation(int lineNumber, String line) {
+  final regexp =
+      RegExp(r'(//|#) ?LINT( \[([\-+]\d+)?(,?(\d+):(\d+))?\])?( (.*))?$');
+  final match = regexp.firstMatch(line);
+  if (match == null) return null;
 
-  if (index == -1) {
-    return null;
-  }
+  // ignore lints on commented out lines
+  final index = match.start;
+  final comment = match[1];
+  if (line.indexOf(comment) != index) return null;
 
-  // Grab the first comment to see if there's one preceding the annotation.
-  // Check for '#' first to allow for lints on dartdocs.
-  var comment = line.indexOf('#');
-  if (comment == -1) {
-    comment = line.indexOf('//');
-  }
+  final relativeLine = match[3].toInt() ?? 0;
+  final column = match[5].toInt();
+  final length = match[6].toInt();
+  final message = match[8].toNullIfBlank();
+  return Annotation.forLint(message, column, length)
+    ..lineNumber = lineNumber + relativeLine;
+}
 
-  // If the offset of the comment is not the offset of the annotation (for
-  // example, `"My phone #" # LINT`), do not proceed.
-  if (comment != index) {
-    return null;
-  }
-
-  int column;
-  int length;
-  var annotation = line.substring(index);
-  var leftBrace = annotation.indexOf('[');
-  if (leftBrace != -1) {
-    var sep = annotation.indexOf(':');
-    column = int.parse(annotation.substring(leftBrace + 1, sep));
-    var rightBrace = annotation.indexOf(']');
-    length = int.parse(annotation.substring(sep + 1, rightBrace));
-  }
-
-  var msgIndex = annotation.indexOf(']') + 1;
-  if (msgIndex < 1) {
-    msgIndex = annotation.indexOf('T') + 1;
-  }
-  String msg;
-  if (msgIndex < line.length) {
-    msg = line.substring(index + msgIndex).trim();
-    if (msg.isEmpty) {
-      msg = null;
-    }
-  }
-  return Annotation.forLint(msg, column, length);
+extension on String {
+  int toInt() => this == null ? null : int.parse(this);
+  String toNullIfBlank() => this == null || trim().isEmpty ? null : this;
 }
 
 /// Information about a 'LINT' annotation/comment.
diff --git a/test/util/annotation_test.dart b/test/util/annotation_test.dart
index 75feea2..ae1bd9a 100644
--- a/test/util/annotation_test.dart
+++ b/test/util/annotation_test.dart
@@ -10,22 +10,37 @@
 
 void main() {
   test('extraction', () {
-    expect(extractAnnotation('int x; // LINT [1:3]'), isNotNull);
-    expect(extractAnnotation('int x; //LINT'), isNotNull);
-    expect(extractAnnotation('int x; // OK'), isNull);
-    expect(extractAnnotation('int x;'), isNull);
-    expect(extractAnnotation('dynamic x; // LINT dynamic is bad').message,
+    expect(extractAnnotation(1, 'int x; // LINT [1:3]'), isNotNull);
+    expect(extractAnnotation(1, 'int x; //LINT'), isNotNull);
+    expect(extractAnnotation(1, 'int x; // OK'), isNull);
+    expect(extractAnnotation(1, 'int x;'), isNull);
+    expect(extractAnnotation(1, 'dynamic x; // LINT dynamic is bad').message,
         'dynamic is bad');
-    expect(extractAnnotation('dynamic x; // LINT [1:3] dynamic is bad').message,
+    expect(extractAnnotation(1, 'dynamic x; // LINT dynamic is bad').lineNumber,
+        1);
+    expect(
+        extractAnnotation(1, 'dynamic x; // LINT [1:3] dynamic is bad').message,
         'dynamic is bad');
     expect(
-        extractAnnotation('dynamic x; // LINT [1:3] dynamic is bad').column, 1);
+        extractAnnotation(1, 'dynamic x; // LINT [1:3] dynamic is bad').column,
+        1);
     expect(
-        extractAnnotation('dynamic x; // LINT [1:3] dynamic is bad').length, 3);
-    expect(extractAnnotation('dynamic x; //LINT').message, isNull);
-    expect(extractAnnotation('dynamic x; //LINT ').message, isNull);
+        extractAnnotation(1, 'dynamic x; // LINT [1:3] dynamic is bad').length,
+        3);
+    expect(extractAnnotation(1, 'dynamic x; //LINT').message, isNull);
+    expect(extractAnnotation(1, 'dynamic x; //LINT ').message, isNull);
     // Commented out lines shouldn't get linted.
-    expect(extractAnnotation('// dynamic x; //LINT '), isNull);
+    expect(extractAnnotation(1, '// dynamic x; //LINT '), isNull);
+    expect(extractAnnotation(1, 'int x; // LINT [2:3]').lineNumber, 1);
+    expect(extractAnnotation(1, 'int x; // LINT [2:3]').column, 2);
+    expect(extractAnnotation(1, 'int x; // LINT [2:3]').length, 3);
+    expect(extractAnnotation(1, 'int x; // LINT [+2]').lineNumber, 3);
+    expect(extractAnnotation(1, 'int x; // LINT [+2]').column, isNull);
+    expect(extractAnnotation(1, 'int x; // LINT [+2]').length, isNull);
+    expect(extractAnnotation(1, 'int x; // LINT [+2,4:5]').lineNumber, 3);
+    expect(extractAnnotation(1, 'int x; // LINT [+2,4:5]').column, 4);
+    expect(extractAnnotation(1, 'int x; // LINT [+2,4:5]').length, 5);
+    expect(extractAnnotation(10, 'int x; // LINT [-2]').lineNumber, 8);
   });
 
   test('equality', () {