[DAS] Adds fix for expected 'on' keyword

R=srawlins@google.com

Fixes https://github.com/dart-lang/sdk/issues/47127

Change-Id: I316a2d29546c9edd3a808785efdbc148007c531b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/392540
Auto-Submit: Felipe Morschel <fmorschel.dev@gmail.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/insert_on_keyword.dart b/pkg/analysis_server/lib/src/services/correction/dart/insert_on_keyword.dart
new file mode 100644
index 0000000..ec31007
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/insert_on_keyword.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2024, 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:analysis_server/src/services/correction/fix.dart';
+import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+
+class InsertOnKeyword extends ResolvedCorrectionProducer {
+  InsertOnKeyword({required super.context});
+
+  @override
+  CorrectionApplicability get applicability =>
+          // Supports single instance and in file corrections
+          CorrectionApplicability
+          .acrossSingleFile;
+
+  @override
+  FixKind get fixKind => DartFixKind.INSERT_ON_KEYWORD;
+
+  @override
+  FixKind get multiFixKind => DartFixKind.INSERT_ON_KEYWORD_MULTI;
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    var node = this.node;
+    if (node is! ExtensionDeclaration) {
+      if (node.parent case ExtensionDeclaration parent) {
+        node = parent;
+      } else {
+        return;
+      }
+    }
+
+    var onClause = node.onClause;
+    if (onClause != null && onClause.onKeyword.isSynthetic) {
+      var onOffset = onClause.onKeyword.offset;
+      if (onClause.extendedType.length == 0) {
+        onOffset = node.name?.offset ?? onOffset;
+      }
+
+      await builder.addDartFileEdit(file, (builder) {
+        builder.addSimpleInsertion(onOffset, 'on ');
+      });
+    }
+  }
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 6d644d4..845b93b 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -899,6 +899,16 @@
     DartFixKindPriority.standard,
     'Insert body',
   );
+  static const INSERT_ON_KEYWORD = FixKind(
+    'dart.fix.insertOnKeyword',
+    DartFixKindPriority.standard,
+    "Insert 'on' keyword",
+  );
+  static const INSERT_ON_KEYWORD_MULTI = FixKind(
+    'dart.fix.insertOnKeyword.multi',
+    DartFixKindPriority.inFile,
+    "Insert 'on' keyword in file",
+  );
   static const INSERT_SEMICOLON = FixKind(
     'dart.fix.insertSemicolon',
     DartFixKindPriority.standard,
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index d0d15f0..597abbd 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -105,6 +105,7 @@
 import 'package:analysis_server/src/services/correction/dart/inline_invocation.dart';
 import 'package:analysis_server/src/services/correction/dart/inline_typedef.dart';
 import 'package:analysis_server/src/services/correction/dart/insert_body.dart';
+import 'package:analysis_server/src/services/correction/dart/insert_on_keyword.dart';
 import 'package:analysis_server/src/services/correction/dart/insert_semicolon.dart';
 import 'package:analysis_server/src/services/correction/dart/make_class_abstract.dart';
 import 'package:analysis_server/src/services/correction/dart/make_conditional_on_debug_mode.dart';
@@ -1134,7 +1135,11 @@
   ParserErrorCode.EXPECTED_SWITCH_EXPRESSION_BODY: [InsertBody.new],
   ParserErrorCode.EXPECTED_SWITCH_STATEMENT_BODY: [InsertBody.new],
   ParserErrorCode.EXPECTED_TRY_STATEMENT_BODY: [InsertBody.new],
-  ParserErrorCode.EXPECTED_TOKEN: [InsertSemicolon.new, ReplaceWithArrow.new],
+  ParserErrorCode.EXPECTED_TOKEN: [
+    InsertSemicolon.new,
+    ReplaceWithArrow.new,
+    InsertOnKeyword.new,
+  ],
   ParserErrorCode.EXTENSION_AUGMENTATION_HAS_ON_CLAUSE: [RemoveOnClause.new],
   ParserErrorCode.EXTENSION_DECLARES_CONSTRUCTOR: [RemoveConstructor.new],
   ParserErrorCode.EXTERNAL_CLASS: [RemoveLexeme.modifier],
diff --git a/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart b/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart
index 1869910..bb1dbc2 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart
@@ -217,6 +217,23 @@
     expect(resultCode, expected);
   }
 
+  Future<List<Fix>> getFixesForFirst(
+    bool Function(AnalysisError error) test,
+  ) async {
+    var errors = testAnalysisResult.errors.where(test);
+    expect(errors, isNotEmpty);
+    String? errorCode;
+    for (var error in errors) {
+      errorCode ??= error.errorCode.name;
+      if (errorCode != error.errorCode.name) {
+        fail('Expected only errors of one type but found: $errors');
+      }
+    }
+
+    var fixes = await _computeFixes(errors.first);
+    return fixes;
+  }
+
   Future<List<Fix>> getFixesForFirstError() async {
     var errors = testAnalysisResult.errors;
     expect(errors, isNotEmpty);
diff --git a/pkg/analysis_server/test/src/services/correction/fix/insert_on_keyword_test.dart b/pkg/analysis_server/test/src/services/correction/fix/insert_on_keyword_test.dart
new file mode 100644
index 0000000..f1609c1
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/insert_on_keyword_test.dart
@@ -0,0 +1,117 @@
+// Copyright (c) 2024, 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:analysis_server/src/services/correction/fix.dart';
+import 'package:analyzer/src/dart/error/syntactic_errors.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:test/expect.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(InsertOnKeywordMultiTest);
+    defineReflectiveTests(InsertOnKeywordTest);
+  });
+}
+
+@reflectiveTest
+class InsertOnKeywordMultiTest extends FixInFileProcessorTest {
+  Future<void> test_expected_onKeyword_multi() async {
+    await resolveTestCode('''
+extension String {}
+extension String {}
+''');
+    var fixes = await getFixesForFirst(
+      (e) => e.errorCode == ParserErrorCode.EXPECTED_TOKEN,
+    );
+    expect(fixes, hasLength(1));
+    assertProduces(fixes.first, r'''
+extension on String {}
+extension on String {}
+''');
+  }
+}
+
+@reflectiveTest
+class InsertOnKeywordTest extends FixProcessorTest {
+  @override
+  FixKind get kind => DartFixKind.INSERT_ON_KEYWORD;
+
+  Future<void> test_expected_onKeyword() async {
+    await resolveTestCode('''
+extension int {}
+''');
+    await assertHasFix(
+      '''
+extension on int {}
+''',
+      errorFilter: (error) {
+        return error.errorCode == ParserErrorCode.EXPECTED_TOKEN;
+      },
+    );
+  }
+
+  Future<void> test_expected_onKeyword_betweenNameAndType() async {
+    await resolveTestCode('''
+extension E int {}
+''');
+    await assertHasFix('''
+extension E on int {}
+''');
+  }
+
+  Future<void> test_expected_onKeyword_betweenTypeParameterAndType() async {
+    await resolveTestCode('''
+extension E<T> int {}
+''');
+    await assertHasFix('''
+extension E<T> on int {}
+''');
+  }
+
+  Future<void> test_expected_onKeyword_nonType() async {
+    await resolveTestCode('''
+extension UnresolvedType {}
+''');
+    await assertHasFix(
+      '''
+extension on UnresolvedType {}
+''',
+      errorFilter: (error) {
+        return error.errorCode == ParserErrorCode.EXPECTED_TOKEN;
+      },
+    );
+  }
+
+  Future<void> test_expected_onKeyword_nonTypeWithTypeArguments() async {
+    // We want to believe that the type parameter is from the undefined type.
+    await resolveTestCode('''
+extension UnresolvedType<T> {}
+''');
+    await assertHasFix(
+      '''
+extension on UnresolvedType<T> {}
+''',
+      errorFilter: (error) {
+        return error.errorCode == ParserErrorCode.EXPECTED_TOKEN;
+      },
+    );
+  }
+
+  Future<void> test_expected_onKeyword_typeWithTypeArguments() async {
+    await resolveTestCode('''
+extension List<int> {}
+''');
+    await assertHasFix(
+      '''
+extension on List<int> {}
+''',
+      errorFilter: (error) {
+        return error.errorCode == ParserErrorCode.EXPECTED_TOKEN;
+      },
+    );
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index c716d6e..ac46535 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -140,6 +140,7 @@
 import 'inline_invocation_test.dart' as inline_invocation;
 import 'inline_typedef_test.dart' as inline_typedef;
 import 'insert_body_test.dart' as insert_body;
+import 'insert_on_keyword_test.dart' as insert_on_keyword;
 import 'insert_semicolon_test.dart' as insert_semicolon;
 import 'make_class_abstract_test.dart' as make_class_abstract;
 import 'make_conditional_on_debug_mode_test.dart'
@@ -423,6 +424,7 @@
     inline_invocation.main();
     inline_typedef.main();
     insert_body.main();
+    insert_on_keyword.main();
     insert_semicolon.main();
     make_class_abstract.main();
     make_conditional_on_debug_mode.main();