[kernel/DDC] DDC shouldn't crash when trying to translate line/column to offset for expresion compilation

Follow-up to https://dart-review.googlesource.com/c/sdk/+/446042.

Change-Id: Icfbaa763a6d6089a74f62088602c07c478520741
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/446420
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
diff --git a/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart b/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
index f02e26f..b17aff6 100644
--- a/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
@@ -353,7 +353,14 @@
 
     // TODO(jensj): Eventually make the scriptUri required.
     if (scriptFileUri != null) {
-      final offset = _component.getOffset(scriptFileUri, line, column);
+      final offset = _component.getOffsetNoThrow(scriptFileUri, line, column);
+      if (offset < 0) {
+        _log(
+          'Got offset negative offset ($offset) for '
+          '$scriptFileUri:$line:$column',
+        );
+        return DartScope(library, null, null, null, null, {}, []);
+      }
       final scope = DartScopeBuilder2.findScopeFromOffset(
         library,
         scriptFileUri,
@@ -388,11 +395,19 @@
 
     final foundScopes = <DartScope>[];
     for (final fileUri in fileUris) {
-      final offset = _component.getOffset(fileUri, line, column);
+      final offset = _component.getOffsetNoThrow(fileUri, line, column);
+      if (offset < 0) continue;
       foundScopes.add(
         DartScopeBuilder2.findScopeFromOffset(library, fileUri, offset),
       );
     }
+    if (foundScopes.isEmpty) {
+      _log(
+        'Got only negative offsets for library/parts ${library.fileUri} '
+        'at $line:$column.',
+      );
+      return DartScope(library, null, null, null, null, {}, []);
+    }
     if (foundScopes.length == 1) {
       return foundScopes[0];
     }
diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_shared.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_shared.dart
index baa3c9b..56d4c53 100644
--- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_shared.dart
+++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_shared.dart
@@ -217,6 +217,88 @@
       );
     });
 
+    test('does not crash on line 0 in a library', () {
+      driver.requestController.add({
+        'command': 'UpdateDeps',
+        'inputs': driver.inputs,
+      });
+
+      driver.requestController.add({
+        'command': 'CompileExpression',
+        'expression': '1 + 1',
+        'line': 0,
+        'column': 0,
+        'jsModules': {},
+        'jsScope': {},
+        'libraryUri': driver.config.getModule('testModule').libraryUris.first,
+        'moduleName': driver.config.getModule('testModule').moduleName,
+      });
+
+      expect(
+        driver.responseController.stream,
+        emitsInOrder([
+          equals({'succeeded': true}),
+          equals({
+            'succeeded': true,
+            'errors': isEmpty,
+            'warnings': isEmpty,
+            'infos': isEmpty,
+            'compiledProcedure': contains('1 + 1;'),
+          }),
+        ]),
+      );
+    });
+
+    test('does not crash on line thats too high in a library', () {
+      driver.requestController.add({
+        'command': 'UpdateDeps',
+        'inputs': driver.inputs,
+      });
+
+      driver.requestController.add({
+        'command': 'CompileExpression',
+        'expression': '1 + 1',
+        'line': 10000000,
+        'column': 1,
+        'jsModules': {},
+        'jsScope': {},
+        'libraryUri': driver.config.getModule('testModule').libraryUris.first,
+        'moduleName': driver.config.getModule('testModule').moduleName,
+      });
+
+      driver.requestController.add({
+        'command': 'CompileExpression',
+        'expression': '2 + 2',
+        'line': 1,
+        'column': 10000000,
+        'jsModules': {},
+        'jsScope': {},
+        'libraryUri': driver.config.getModule('testModule').libraryUris.first,
+        'moduleName': driver.config.getModule('testModule').moduleName,
+      });
+
+      expect(
+        driver.responseController.stream,
+        emitsInOrder([
+          equals({'succeeded': true}),
+          equals({
+            'succeeded': true,
+            'errors': isEmpty,
+            'warnings': isEmpty,
+            'infos': isEmpty,
+            'compiledProcedure': contains('1 + 1;'),
+          }),
+          equals({
+            'succeeded': true,
+            'errors': isEmpty,
+            'warnings': isEmpty,
+            'infos': isEmpty,
+            'compiledProcedure': contains('2 + 2;'),
+          }),
+        ]),
+      );
+    });
+
     test('can compile expressions in a library', () {
       driver.requestController.add({
         'command': 'UpdateDeps',
diff --git a/pkg/kernel/lib/src/ast/components.dart b/pkg/kernel/lib/src/ast/components.dart
index 17df623..770b677 100644
--- a/pkg/kernel/lib/src/ast/components.dart
+++ b/pkg/kernel/lib/src/ast/components.dart
@@ -159,6 +159,14 @@
     return uriToSource[file]?.getOffset(line, column) ?? -1;
   }
 
+  /// Translates line and column numbers to an offset in the given file.
+  ///
+  /// Returns offset of the line and column in the file, or -1 if the
+  /// source is not available, has no lines, or the line is out of range.
+  int getOffsetNoThrow(Uri file, int line, int column) {
+    return uriToSource[file]?.getOffsetNoThrow(line, column) ?? -1;
+  }
+
   void addMetadataRepository(MetadataRepository repository) {
     metadata[repository.tag] = repository;
   }
@@ -291,6 +299,25 @@
     RangeError.checkValueInInterval(offset, 0, lineStarts.last, 'offset');
     return offset;
   }
+
+  /// Translates 1-based line and column numbers to an offset in the given file
+  ///
+  /// Returns offset of the line and column in the file, or -1 if the source
+  /// has no lines or the input is out of range.
+  int getOffsetNoThrow(int line, int column) {
+    List<int>? lineStarts = this.lineStarts;
+    if (lineStarts == null || lineStarts.isEmpty) {
+      return -1;
+    }
+    if (line < 1 || line > lineStarts.length) {
+      return -1;
+    }
+    int offset = lineStarts[line - 1] + column - 1;
+    if (offset < 0 || offset > lineStarts.last) {
+      return -1;
+    }
+    return offset;
+  }
 }
 
 abstract class MetadataRepository<T> {