[CFE/Analyzer/parser] Fix crash upon double with separators and missing number after e

E.g. the user typing "1_234e" (and having yet to type a number) would
crash the analyzer.

Change-Id: Iff71c274a5e4719b3c8877ddbc9775d8d091c42f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/437240
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/scanner/abstract_scanner.dart b/pkg/_fe_analyzer_shared/lib/src/scanner/abstract_scanner.dart
index b17ddd2..df46757 100644
--- a/pkg/_fe_analyzer_shared/lib/src/scanner/abstract_scanner.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/scanner/abstract_scanner.dart
@@ -1696,7 +1696,9 @@
           } else {
             if (!hasExponentDigits) {
               appendSyntheticSubstringToken(
-                TokenType.DOUBLE,
+                hasSeparators
+                    ? TokenType.DOUBLE_WITH_SEPARATORS
+                    : TokenType.DOUBLE,
                 start,
                 /* asciiOnly = */ true,
                 '0',
diff --git a/pkg/analyzer/test/src/diagnostics/number_literals_with_separators_test.dart b/pkg/analyzer/test/src/diagnostics/number_literals_with_separators_test.dart
new file mode 100644
index 0000000..d4d8709
--- /dev/null
+++ b/pkg/analyzer/test/src/diagnostics/number_literals_with_separators_test.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/src/dart/error/syntactic_errors.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../dart/resolution/context_collection_resolution.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(NumberLiteralsWithSeparatorsTest);
+  });
+}
+
+@reflectiveTest
+class NumberLiteralsWithSeparatorsTest extends PubPackageResolutionTest {
+  Future<void> assertHasErrors(String code) async {
+    addTestFile(code);
+    await resolveTestFile();
+    expect(result.diagnostics, isNotEmpty);
+  }
+
+  Future<void> test_double_with_separators_and_e() async {
+    await assertNoErrorsInCode('double x = 1.234_567e2;');
+  }
+
+  Future<void> test_missing_number_after_e_1() async {
+    await assertErrorsInCode('dynamic x = 1_234_567e;', [
+      error(ScannerErrorCode.MISSING_DIGIT, 21, 1),
+    ]);
+  }
+
+  Future<void> test_missing_number_after_e_2() async {
+    await assertErrorsInCode('dynamic x = 1.234_567e;', [
+      error(ScannerErrorCode.MISSING_DIGIT, 21, 1),
+    ]);
+  }
+
+  Future<void> test_other_erroneous_1() async {
+    await assertHasErrors('dynamic x = 1_1e;');
+  }
+
+  Future<void> test_other_erroneous_2() async {
+    await assertHasErrors('dynamic x = 1_e;');
+  }
+
+  Future<void> test_other_erroneous_3() async {
+    await assertHasErrors('dynamic x = 1e_;');
+  }
+
+  Future<void> test_other_erroneous_4() async {
+    await assertHasErrors('dynamic x = 1e-_;');
+  }
+
+  Future<void> test_other_erroneous_5() async {
+    await assertHasErrors('dynamic x = 1e-_1;');
+  }
+
+  Future<void> test_other_erroneous_6() async {
+    await assertHasErrors('dynamic x = 1e+_;');
+  }
+
+  Future<void> test_other_erroneous_7() async {
+    await assertHasErrors('dynamic x = 1e+_1;');
+  }
+
+  Future<void> test_simple_double_with_separators() async {
+    await assertNoErrorsInCode('double x = 1.234_567;');
+  }
+
+  Future<void> test_simple_int_with_separators() async {
+    await assertNoErrorsInCode('int x = 1_234_567;');
+  }
+}
diff --git a/pkg/analyzer/test/src/diagnostics/test_all.dart b/pkg/analyzer/test/src/diagnostics/test_all.dart
index 58159d3..f4e687b 100644
--- a/pkg/analyzer/test/src/diagnostics/test_all.dart
+++ b/pkg/analyzer/test/src/diagnostics/test_all.dart
@@ -692,6 +692,7 @@
     as nullable_type_in_implements_clause;
 import 'nullable_type_in_on_clause_test.dart' as nullable_type_in_on_clause;
 import 'nullable_type_in_with_clause_test.dart' as nullable_type_in_with_clause;
+import 'number_literals_with_separators_test.dart' as number_literals_with_separators;
 import 'object_cannot_extend_another_class_test.dart'
     as object_cannot_extend_another_class;
 import 'obsolete_colon_for_default_value_test.dart'
@@ -1385,6 +1386,7 @@
     nullable_type_in_implements_clause.main();
     nullable_type_in_on_clause.main();
     nullable_type_in_with_clause.main();
+    number_literals_with_separators.main();
     object_cannot_extend_another_class.main();
     obsolete_colon_for_default_value.main();
     on_repeated.main();
diff --git a/pkg/front_end/test/precedence_info_test.dart b/pkg/front_end/test/precedence_info_test.dart
index 8ef6bc3..8d22392 100644
--- a/pkg/front_end/test/precedence_info_test.dart
+++ b/pkg/front_end/test/precedence_info_test.dart
@@ -398,8 +398,14 @@
   }
 
   void test_type() {
-    void assertLexeme(String source, TokenType tt) {
+    void assertLexeme(String source, TokenType tt,
+        {bool skipErrorTokens = false}) {
       var token = scanString(source, includeComments: true).tokens;
+      if (skipErrorTokens) {
+        while (token is ErrorToken) {
+          token = token.next!;
+        }
+      }
       expect(token.type, same(tt), reason: source);
     }
 
@@ -413,5 +419,9 @@
     assertLexeme('#!/', TokenType.SCRIPT_TAG);
     assertLexeme('foo', TokenType.IDENTIFIER);
     assertLexeme('"foo"', TokenType.STRING);
+
+    // Invalid but recovers nicely.
+    assertLexeme('1_e', TokenType.DOUBLE_WITH_SEPARATORS,
+        skipErrorTokens: true);
   }
 }
diff --git a/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart
new file mode 100644
index 0000000..872e6ea
--- /dev/null
+++ b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+void foo() {
+  print(123_456e);
+  print(1.234_567e);
+}
\ No newline at end of file
diff --git a/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.expect b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.expect
new file mode 100644
index 0000000..faf37dd
--- /dev/null
+++ b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.expect
@@ -0,0 +1,21 @@
+library;
+//
+// Problems in library:
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:6:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(123_456e);
+//         ^
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:7:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(1.234_567e);
+//         ^
+//
+import self as self;
+import "dart:core" as core;
+
+static method foo() → void {
+  core::print(123456.0);
+  core::print(1.234567);
+}
diff --git a/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.modular.expect b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.modular.expect
new file mode 100644
index 0000000..faf37dd
--- /dev/null
+++ b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.modular.expect
@@ -0,0 +1,21 @@
+library;
+//
+// Problems in library:
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:6:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(123_456e);
+//         ^
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:7:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(1.234_567e);
+//         ^
+//
+import self as self;
+import "dart:core" as core;
+
+static method foo() → void {
+  core::print(123456.0);
+  core::print(1.234567);
+}
diff --git a/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.outline.expect b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.outline.expect
new file mode 100644
index 0000000..0125757
--- /dev/null
+++ b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.outline.expect
@@ -0,0 +1,18 @@
+library;
+//
+// Problems in library:
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:6:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(123_456e);
+//         ^
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:7:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(1.234_567e);
+//         ^
+//
+import self as self;
+
+static method foo() → void
+  ;
diff --git a/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.transformed.expect b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.transformed.expect
new file mode 100644
index 0000000..faf37dd
--- /dev/null
+++ b/pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart.strong.transformed.expect
@@ -0,0 +1,21 @@
+library;
+//
+// Problems in library:
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:6:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(123_456e);
+//         ^
+//
+// pkg/front_end/testcases/coverage/digit-separators/separators_errors_test.dart:7:9: Error: Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+// Make sure there is an exponent, and remove any whitespace before it.
+//   print(1.234_567e);
+//         ^
+//
+import self as self;
+import "dart:core" as core;
+
+static method foo() → void {
+  core::print(123456.0);
+  core::print(1.234567);
+}
diff --git a/pkg/front_end/testcases/textual_outline.status b/pkg/front_end/testcases/textual_outline.status
index d117142..0d70e93 100644
--- a/pkg/front_end/testcases/textual_outline.status
+++ b/pkg/front_end/testcases/textual_outline.status
@@ -28,6 +28,7 @@
 super_parameters/var_before_super: FormatterCrash
 triple_shift/invalid_operator: FormatterCrash
 
+coverage/digit-separators/separators_errors_test: EmptyOutput
 general/error_recovery/issue_38415.crash: EmptyOutput
 general/error_recovery/issue_39024.crash: EmptyOutput
 general/error_recovery/issue_39058.crash: EmptyOutput
diff --git a/tests/language/number/separators_error_test.dart b/tests/language/number/separators_error_test.dart
index b32cb4f..5663422 100644
--- a/tests/language/number/separators_error_test.dart
+++ b/tests/language/number/separators_error_test.dart
@@ -148,4 +148,10 @@
   //    ^^^
   // [analyzer] COMPILE_TIME_ERROR.UNDEFINED_GETTER
   // [cfe] The getter '_0e' isn't defined for the type 'int'.
+
+  x = 1.234_456e;
+  //  ^
+  // [cfe] Numbers in exponential notation should always contain an exponent (an integer number with an optional sign).
+  //           ^
+  // [analyzer] SYNTACTIC_ERROR.MISSING_DIGIT
 }