Ensure unterminated string error is reported on visible characters

Change-Id: Ie6321062b71a45293a91a3a50fc4d24b2633235c
Reviewed-on: https://dart-review.googlesource.com/c/80502
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Dan Rubel <danrubel@google.com>
diff --git a/pkg/front_end/lib/src/fasta/scanner/abstract_scanner.dart b/pkg/front_end/lib/src/fasta/scanner/abstract_scanner.dart
index 643a546..91812bb 100644
--- a/pkg/front_end/lib/src/fasta/scanner/abstract_scanner.dart
+++ b/pkg/front_end/lib/src/fasta/scanner/abstract_scanner.dart
@@ -1017,7 +1017,7 @@
 
   /**
    * [next] is the first character after the quote.
-   * [start] is the scanOffset of the quote.
+   * [quoteStart] is the scanOffset of the quote.
    *
    * The token contains a substring of the source file, including the
    * string quotes, backslashes for escaping. For interpolated strings,
@@ -1027,7 +1027,8 @@
    *
    * gives StringToken("a $), StringToken(b) and StringToken( c").
    */
-  int tokenizeSingleLineString(int next, int quoteChar, int start) {
+  int tokenizeSingleLineString(int next, int quoteChar, int quoteStart) {
+    int start = quoteStart;
     bool asciiOnly = true;
     while (!identical(next, quoteChar)) {
       if (identical(next, $BACKSLASH)) {
@@ -1044,7 +1045,7 @@
               identical(next, $CR) ||
               identical(next, $EOF))) {
         if (!asciiOnly) handleUnicode(start);
-        unterminatedString(quoteChar, start,
+        unterminatedString(quoteChar, quoteStart, start,
             asciiOnly: asciiOnly, isMultiLine: false, isRaw: false);
         return next;
       }
@@ -1103,17 +1104,17 @@
     return next;
   }
 
-  int tokenizeSingleLineRawString(int next, int quoteChar, int start) {
+  int tokenizeSingleLineRawString(int next, int quoteChar, int quoteStart) {
     bool asciiOnly = true;
     while (next != $EOF) {
       if (identical(next, quoteChar)) {
-        if (!asciiOnly) handleUnicode(start);
+        if (!asciiOnly) handleUnicode(quoteStart);
         next = advance();
-        appendSubstringToken(TokenType.STRING, start, asciiOnly);
+        appendSubstringToken(TokenType.STRING, quoteStart, asciiOnly);
         return next;
       } else if (identical(next, $LF) || identical(next, $CR)) {
-        if (!asciiOnly) handleUnicode(start);
-        unterminatedString(quoteChar, start,
+        if (!asciiOnly) handleUnicode(quoteStart);
+        unterminatedString(quoteChar, quoteStart, quoteStart,
             asciiOnly: asciiOnly, isMultiLine: false, isRaw: true);
         return next;
       } else if (next > 127) {
@@ -1121,16 +1122,16 @@
       }
       next = advance();
     }
-    if (!asciiOnly) handleUnicode(start);
-    unterminatedString(quoteChar, start,
+    if (!asciiOnly) handleUnicode(quoteStart);
+    unterminatedString(quoteChar, quoteStart, quoteStart,
         asciiOnly: asciiOnly, isMultiLine: false, isRaw: true);
     return next;
   }
 
-  int tokenizeMultiLineRawString(int quoteChar, int start) {
+  int tokenizeMultiLineRawString(int quoteChar, int quoteStart) {
     bool asciiOnlyString = true;
     bool asciiOnlyLine = true;
-    int unicodeStart = start;
+    int unicodeStart = quoteStart;
     int next = advance(); // Advance past the (last) quote (of three).
     outer:
     while (!identical(next, $EOF)) {
@@ -1156,19 +1157,20 @@
         if (identical(next, quoteChar)) {
           if (!asciiOnlyLine) handleUnicode(unicodeStart);
           next = advance();
-          appendSubstringToken(TokenType.STRING, start, asciiOnlyString);
+          appendSubstringToken(TokenType.STRING, quoteStart, asciiOnlyString);
           return next;
         }
       }
     }
     if (!asciiOnlyLine) handleUnicode(unicodeStart);
-    unterminatedString(quoteChar, start,
+    unterminatedString(quoteChar, quoteStart, quoteStart,
         asciiOnly: asciiOnlyLine, isMultiLine: true, isRaw: true);
     return next;
   }
 
-  int tokenizeMultiLineString(int quoteChar, int start, bool raw) {
-    if (raw) return tokenizeMultiLineRawString(quoteChar, start);
+  int tokenizeMultiLineString(int quoteChar, int quoteStart, bool raw) {
+    if (raw) return tokenizeMultiLineRawString(quoteChar, quoteStart);
+    int start = quoteStart;
     bool asciiOnlyString = true;
     bool asciiOnlyLine = true;
     int unicodeStart = start;
@@ -1215,7 +1217,7 @@
       next = advance();
     }
     if (!asciiOnlyLine) handleUnicode(unicodeStart);
-    unterminatedString(quoteChar, start,
+    unterminatedString(quoteChar, quoteStart, start,
         asciiOnly: asciiOnlyString, isMultiLine: true, isRaw: false);
     return next;
   }
@@ -1230,14 +1232,16 @@
     return advanceAfterError(shouldAdvance);
   }
 
-  void unterminatedString(int quoteChar, int start,
+  void unterminatedString(int quoteChar, int quoteStart, int start,
       {bool asciiOnly, bool isMultiLine, bool isRaw}) {
     String suffix = new String.fromCharCodes(
         isMultiLine ? [quoteChar, quoteChar, quoteChar] : [quoteChar]);
     String prefix = isRaw ? 'r$suffix' : suffix;
 
     appendSyntheticSubstringToken(TokenType.STRING, start, asciiOnly, suffix);
-    appendErrorToken(new UnterminatedString(prefix, tokenStart, stringOffset));
+    // Ensure that the error is reported on a visible token
+    int errorStart = tokenStart < stringOffset ? tokenStart : quoteStart;
+    appendErrorToken(new UnterminatedString(prefix, errorStart, stringOffset));
   }
 
   int advanceAfterError(bool shouldAdvance) {
diff --git a/pkg/front_end/lib/src/scanner/errors.dart b/pkg/front_end/lib/src/scanner/errors.dart
index 8822805..dc45f52 100644
--- a/pkg/front_end/lib/src/scanner/errors.dart
+++ b/pkg/front_end/lib/src/scanner/errors.dart
@@ -118,8 +118,9 @@
     case "UNTERMINATED_STRING_LITERAL":
       // TODO(paulberry,ahe): Fasta reports the error location as the entire
       // string; analyzer expects the end of the string.
-      charOffset = endOffset - 1;
-      return _makeError(ScannerErrorCode.UNTERMINATED_STRING_LITERAL, null);
+      reportError(
+          ScannerErrorCode.UNTERMINATED_STRING_LITERAL, endOffset - 1, null);
+      return;
 
     case "UNTERMINATED_MULTI_LINE_COMMENT":
       // TODO(paulberry,ahe): Fasta reports the error location as the entire
diff --git a/pkg/front_end/testcases/regress/issue_29976.dart.direct.expect b/pkg/front_end/testcases/regress/issue_29976.dart.direct.expect
index b2fd9b3..e64910f 100644
--- a/pkg/front_end/testcases/regress/issue_29976.dart.direct.expect
+++ b/pkg/front_end/testcases/regress/issue_29976.dart.direct.expect
@@ -13,7 +13,9 @@
 //     "x${x*"'"é'}x
 //       ^
 //
-// pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: String starting with " must end with ".
+// pkg/front_end/testcases/regress/issue_29976.dart:9:5: Error: String starting with " must end with ".
+//     "x${x*"'"é'}x
+//     ^
 //
 // pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: Expected a declaration, but got ''.
 //
@@ -48,7 +50,9 @@
 //     "x${x*"'"é'}x
 //       ^
 //
-// pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: String starting with " must end with ".
+// pkg/front_end/testcases/regress/issue_29976.dart:9:5: Error: String starting with " must end with ".
+//     "x${x*"'"é'}x
+//     ^
 //
 // pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: Expected a declaration, but got ''.
 //
diff --git a/pkg/front_end/testcases/regress/issue_29976.dart.direct.transformed.expect b/pkg/front_end/testcases/regress/issue_29976.dart.direct.transformed.expect
index d89579f..286bb14 100644
--- a/pkg/front_end/testcases/regress/issue_29976.dart.direct.transformed.expect
+++ b/pkg/front_end/testcases/regress/issue_29976.dart.direct.transformed.expect
@@ -13,7 +13,9 @@
 //     "x${x*"'"é'}x
 //       ^
 //
-// pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: String starting with " must end with ".
+// pkg/front_end/testcases/regress/issue_29976.dart:9:5: Error: String starting with " must end with ".
+//     "x${x*"'"é'}x
+//     ^
 //
 // pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: Expected a declaration, but got ''.
 //
diff --git a/pkg/front_end/testcases/regress/issue_29976.dart.outline.expect b/pkg/front_end/testcases/regress/issue_29976.dart.outline.expect
index eb72274..7f8efff 100644
--- a/pkg/front_end/testcases/regress/issue_29976.dart.outline.expect
+++ b/pkg/front_end/testcases/regress/issue_29976.dart.outline.expect
@@ -13,7 +13,9 @@
 //     "x${x*"'"é'}x
 //       ^
 //
-// pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: String starting with " must end with ".
+// pkg/front_end/testcases/regress/issue_29976.dart:9:5: Error: String starting with " must end with ".
+//     "x${x*"'"é'}x
+//     ^
 //
 // pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: Expected a declaration, but got ''.
 
diff --git a/pkg/front_end/testcases/regress/issue_29976.dart.strong.expect b/pkg/front_end/testcases/regress/issue_29976.dart.strong.expect
index e6fed7c..032bd88 100644
--- a/pkg/front_end/testcases/regress/issue_29976.dart.strong.expect
+++ b/pkg/front_end/testcases/regress/issue_29976.dart.strong.expect
@@ -13,7 +13,9 @@
 //     "x${x*"'"é'}x
 //       ^
 //
-// pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: String starting with " must end with ".
+// pkg/front_end/testcases/regress/issue_29976.dart:9:5: Error: String starting with " must end with ".
+//     "x${x*"'"é'}x
+//     ^
 //
 // pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: Expected a declaration, but got ''.
 //
@@ -48,7 +50,9 @@
 //     "x${x*"'"é'}x
 //       ^
 //
-// pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: String starting with " must end with ".
+// pkg/front_end/testcases/regress/issue_29976.dart:9:5: Error: String starting with " must end with ".
+//     "x${x*"'"é'}x
+//     ^
 //
 // pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: Expected a declaration, but got ''.
 //
diff --git a/pkg/front_end/testcases/regress/issue_29976.dart.strong.transformed.expect b/pkg/front_end/testcases/regress/issue_29976.dart.strong.transformed.expect
index 2b9588c..7b89520 100644
--- a/pkg/front_end/testcases/regress/issue_29976.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/regress/issue_29976.dart.strong.transformed.expect
@@ -13,7 +13,9 @@
 //     "x${x*"'"é'}x
 //       ^
 //
-// pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: String starting with " must end with ".
+// pkg/front_end/testcases/regress/issue_29976.dart:9:5: Error: String starting with " must end with ".
+//     "x${x*"'"é'}x
+//     ^
 //
 // pkg/front_end/testcases/regress/issue_29976.dart:12:1: Error: Expected a declaration, but got ''.
 //
diff --git a/pkg/front_end/testcases/regress/issue_29982.dart.direct.expect b/pkg/front_end/testcases/regress/issue_29982.dart.direct.expect
index 2a74ae4..861561d 100644
--- a/pkg/front_end/testcases/regress/issue_29982.dart.direct.expect
+++ b/pkg/front_end/testcases/regress/issue_29982.dart.direct.expect
@@ -13,7 +13,9 @@
 //   print('${eh[éh']}');
 //          ^
 //
-// pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: String starting with ' must end with '.
+// pkg/front_end/testcases/regress/issue_29982.dart:7:11: Error: String starting with ' must end with '.
+//   print('${eh[éh']}');
+//           ^
 //
 // pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: Expected a declaration, but got ''.
 //
@@ -48,7 +50,9 @@
 //   print('${eh[éh']}');
 //          ^
 //
-// pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: String starting with ' must end with '.
+// pkg/front_end/testcases/regress/issue_29982.dart:7:11: Error: String starting with ' must end with '.
+//   print('${eh[éh']}');
+//           ^
 //
 // pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: Expected a declaration, but got ''.
 //
diff --git a/pkg/front_end/testcases/regress/issue_29982.dart.direct.transformed.expect b/pkg/front_end/testcases/regress/issue_29982.dart.direct.transformed.expect
index e608ced..3c85c4a 100644
--- a/pkg/front_end/testcases/regress/issue_29982.dart.direct.transformed.expect
+++ b/pkg/front_end/testcases/regress/issue_29982.dart.direct.transformed.expect
@@ -13,7 +13,9 @@
 //   print('${eh[éh']}');
 //          ^
 //
-// pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: String starting with ' must end with '.
+// pkg/front_end/testcases/regress/issue_29982.dart:7:11: Error: String starting with ' must end with '.
+//   print('${eh[éh']}');
+//           ^
 //
 // pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: Expected a declaration, but got ''.
 //
diff --git a/pkg/front_end/testcases/regress/issue_29982.dart.outline.expect b/pkg/front_end/testcases/regress/issue_29982.dart.outline.expect
index 8e9ae53..60e4f5b 100644
--- a/pkg/front_end/testcases/regress/issue_29982.dart.outline.expect
+++ b/pkg/front_end/testcases/regress/issue_29982.dart.outline.expect
@@ -13,7 +13,9 @@
 //   print('${eh[éh']}');
 //          ^
 //
-// pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: String starting with ' must end with '.
+// pkg/front_end/testcases/regress/issue_29982.dart:7:11: Error: String starting with ' must end with '.
+//   print('${eh[éh']}');
+//           ^
 //
 // pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: Expected a declaration, but got ''.
 
diff --git a/pkg/front_end/testcases/regress/issue_29982.dart.strong.expect b/pkg/front_end/testcases/regress/issue_29982.dart.strong.expect
index 7c7d19a..96c2b57 100644
--- a/pkg/front_end/testcases/regress/issue_29982.dart.strong.expect
+++ b/pkg/front_end/testcases/regress/issue_29982.dart.strong.expect
@@ -13,7 +13,9 @@
 //   print('${eh[éh']}');
 //          ^
 //
-// pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: String starting with ' must end with '.
+// pkg/front_end/testcases/regress/issue_29982.dart:7:11: Error: String starting with ' must end with '.
+//   print('${eh[éh']}');
+//           ^
 //
 // pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: Expected a declaration, but got ''.
 //
@@ -48,7 +50,9 @@
 //   print('${eh[éh']}');
 //          ^
 //
-// pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: String starting with ' must end with '.
+// pkg/front_end/testcases/regress/issue_29982.dart:7:11: Error: String starting with ' must end with '.
+//   print('${eh[éh']}');
+//           ^
 //
 // pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: Expected a declaration, but got ''.
 //
diff --git a/pkg/front_end/testcases/regress/issue_29982.dart.strong.transformed.expect b/pkg/front_end/testcases/regress/issue_29982.dart.strong.transformed.expect
index 039a91c..9a1c7cd 100644
--- a/pkg/front_end/testcases/regress/issue_29982.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/regress/issue_29982.dart.strong.transformed.expect
@@ -13,7 +13,9 @@
 //   print('${eh[éh']}');
 //          ^
 //
-// pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: String starting with ' must end with '.
+// pkg/front_end/testcases/regress/issue_29982.dart:7:11: Error: String starting with ' must end with '.
+//   print('${eh[éh']}');
+//           ^
 //
 // pkg/front_end/testcases/regress/issue_29982.dart:9:1: Error: Expected a declaration, but got ''.
 //