[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();