QuickFix. Issue 55805. Create extension getter.
Bug: https://github.com/dart-lang/sdk/issues/55805
Change-Id: I52f1cdf036e17dda4e91da98beef19184f68a965
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/368502
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart b/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart
index 131be1b..73ee0f5 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/create_getter.dart
@@ -10,10 +10,123 @@
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
+import 'package:analyzer/src/dart/resolver/applicable_extensions.dart';
+import 'package:analyzer/src/utilities/extensions/ast.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:meta/meta.dart';
+class CreateExtensionGetter extends ResolvedCorrectionProducer {
+ String _getterName = '';
+
+ @override
+ CorrectionApplicability get applicability {
+ // Not predictably the correct action.
+ return CorrectionApplicability.singleLocation;
+ }
+
+ @override
+ List<String> get fixArguments => [_getterName];
+
+ @override
+ FixKind get fixKind => DartFixKind.CREATE_EXTENSION_GETTER;
+
+ @override
+ Future<void> compute(ChangeBuilder builder) async {
+ var nameNode = node;
+ if (nameNode is! SimpleIdentifier) {
+ return;
+ }
+ if (!nameNode.inGetterContext()) {
+ return;
+ }
+
+ _getterName = nameNode.name;
+
+ // prepare target
+ Expression? target;
+ switch (nameNode.parent) {
+ case PrefixedIdentifier prefixedIdentifier:
+ if (prefixedIdentifier.identifier == nameNode) {
+ target = prefixedIdentifier.prefix;
+ }
+ case PropertyAccess propertyAccess:
+ if (propertyAccess.propertyName == nameNode) {
+ target = propertyAccess.target;
+ }
+ }
+ if (target == null) {
+ return;
+ }
+
+ // We need the type for the extension.
+ var targetType = target.staticType;
+ if (targetType == null ||
+ targetType is DynamicType ||
+ targetType is InvalidType) {
+ return;
+ }
+
+ // Try to find the type of the field.
+ var fieldTypeNode = climbPropertyAccess(nameNode);
+ var fieldType = inferUndefinedExpressionType(fieldTypeNode);
+
+ void writeGetter(DartEditBuilder builder) {
+ if (fieldType != null) {
+ builder.writeType(fieldType);
+ builder.write(' ');
+ }
+ builder.write('get $_getterName => ');
+ builder.addLinkedEdit('VALUE', (builder) {
+ builder.write('null');
+ });
+ builder.write(';');
+ }
+
+ // Try to add to an existing extension.
+ for (var existingExtension in unitResult.unit.declarations) {
+ if (existingExtension is ExtensionDeclaration) {
+ var element = existingExtension.declaredElement!;
+ var instantiated = [element].applicableTo(
+ targetLibrary: libraryElement,
+ targetType: targetType,
+ strictCasts: true,
+ );
+ if (instantiated.isNotEmpty) {
+ await builder.addDartFileEdit(file, (builder) {
+ builder.insertGetter(existingExtension, (builder) {
+ writeGetter(builder);
+ });
+ });
+ return;
+ }
+ }
+ }
+
+ // The new extension should be added after it.
+ var enclosingUnitChild = nameNode.enclosingUnitChild;
+ if (enclosingUnitChild == null) {
+ return;
+ }
+
+ // Add a new extension.
+ await builder.addDartFileEdit(file, (builder) {
+ builder.addInsertion(enclosingUnitChild.end, (builder) {
+ builder.writeln();
+ builder.writeln();
+ builder.write('extension on ');
+ builder.writeType(targetType);
+ builder.writeln(' {');
+ builder.write(' ');
+ writeGetter(builder);
+ builder.writeln();
+ builder.write('}');
+ });
+ });
+ }
+}
+
/// Shared implementation that identifies what getter should be added,
/// but delegates to the subtypes to produce the fix code.
abstract class CreateFieldOrGetter extends ResolvedCorrectionProducer {
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 33069c6..3282a63 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -681,6 +681,11 @@
DartFixKindPriority.DEFAULT,
'Create constructor to call {0}',
);
+ static const CREATE_EXTENSION_GETTER = FixKind(
+ 'dart.fix.create.extension.getter',
+ DartFixKindPriority.DEFAULT - 20,
+ "Create extension getter '{0}'",
+ );
static const CREATE_FIELD = FixKind(
'dart.fix.create.field',
49,
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 9f0581d..25c2220 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -1261,6 +1261,7 @@
CompileTimeErrorCode.UNDEFINED_GETTER: [
ChangeTo.getterOrSetter,
CreateClass.new,
+ CreateExtensionGetter.new,
CreateField.new,
CreateGetter.new,
CreateLocalVariable.new,
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart
index a2786b4..3034088 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart
@@ -10,12 +10,225 @@
void main() {
defineReflectiveSuite(() {
+ defineReflectiveTests(CreateExtensionGetterTest);
defineReflectiveTests(CreateGetterTest);
defineReflectiveTests(CreateGetterMixinTest);
});
}
@reflectiveTest
+class CreateExtensionGetterTest extends FixProcessorTest {
+ @override
+ FixKind get kind => DartFixKind.CREATE_EXTENSION_GETTER;
+
+ Future<void> test_contextType() async {
+ await resolveTestCode('''
+void f() {
+ // ignore:unused_local_variable
+ int v = ''.test;
+}
+''');
+ await assertHasFix('''
+void f() {
+ // ignore:unused_local_variable
+ int v = ''.test;
+}
+
+extension on String {
+ int get test => null;
+}
+''');
+ }
+
+ Future<void> test_contextType_no() async {
+ await resolveTestCode('''
+void f() {
+ ''.test;
+}
+''');
+ await assertHasFix('''
+void f() {
+ ''.test;
+}
+
+extension on String {
+ get test => null;
+}
+''');
+ assertLinkedGroup(change.linkedEditGroups[0], ['null']);
+ }
+
+ Future<void> test_existingExtension_contextType() async {
+ await resolveTestCode('''
+void f() {
+ // ignore:unused_local_variable
+ int v = ''.test;
+}
+
+extension on String {}
+''');
+ await assertHasFix('''
+void f() {
+ // ignore:unused_local_variable
+ int v = ''.test;
+}
+
+extension on String {
+ int get test => null;
+}
+''');
+ }
+
+ Future<void> test_existingExtension_generic_matching() async {
+ await resolveTestCode('''
+void f(List<int> a) {
+ a.test;
+}
+
+extension E<T> on Iterable<T> {}
+''');
+ await assertHasFix('''
+void f(List<int> a) {
+ a.test;
+}
+
+extension E<T> on Iterable<T> {
+ get test => null;
+}
+''');
+ }
+
+ Future<void> test_existingExtension_generic_notMatching() async {
+ await resolveTestCode('''
+void f(List<int> a) {
+ a.test;
+}
+
+extension E<K, V> on Map<K, V> {}
+''');
+ await assertHasFix('''
+void f(List<int> a) {
+ a.test;
+}
+
+extension on List<int> {
+ get test => null;
+}
+
+extension E<K, V> on Map<K, V> {}
+''');
+ }
+
+ Future<void> test_existingExtension_hasMethod() async {
+ await resolveTestCode('''
+void f() {
+ ''.test;
+}
+
+extension E on String {
+ // ignore:unused_element
+ void foo() {}
+}
+''');
+ await assertHasFix('''
+void f() {
+ ''.test;
+}
+
+extension E on String {
+ get test => null;
+
+ // ignore:unused_element
+ void foo() {}
+}
+''');
+ }
+
+ Future<void> test_existingExtension_notGeneric_matching() async {
+ await resolveTestCode('''
+void f() {
+ ''.test;
+}
+
+extension on String {}
+''');
+ await assertHasFix('''
+void f() {
+ ''.test;
+}
+
+extension on String {
+ get test => null;
+}
+''');
+ }
+
+ Future<void> test_existingExtension_notGeneric_notMatching() async {
+ await resolveTestCode('''
+void f() {
+ ''.test;
+}
+
+extension on int {}
+''');
+ await assertHasFix('''
+void f() {
+ ''.test;
+}
+
+extension on String {
+ get test => null;
+}
+
+extension on int {}
+''');
+ }
+
+ Future<void> test_parent_nothing() async {
+ await resolveTestCode('''
+void f() {
+ test;
+}
+''');
+ await assertNoFix();
+ }
+
+ Future<void> test_parent_prefixedIdentifier() async {
+ await resolveTestCode('''
+void f(String a) {
+ a.test;
+}
+''');
+ await assertHasFix('''
+void f(String a) {
+ a.test;
+}
+
+extension on String {
+ get test => null;
+}
+''');
+ }
+
+ Future<void> test_targetType_hasTypeArguments() async {
+ await resolveTestCode('''
+void f(List<int> a) {
+ a.test;
+}
+''');
+ await assertHasFix('''
+void f(List<int> a) {
+ a.test;
+}
+
+extension on List<int> {
+ get test => null;
+}
+''');
+ }
+}
+
+@reflectiveTest
class CreateGetterMixinTest extends FixProcessorTest {
@override
FixKind get kind => DartFixKind.CREATE_GETTER;
diff --git a/pkg/analyzer/lib/src/utilities/extensions/ast.dart b/pkg/analyzer/lib/src/utilities/extensions/ast.dart
index 522ab76..5631790 100644
--- a/pkg/analyzer/lib/src/utilities/extensions/ast.dart
+++ b/pkg/analyzer/lib/src/utilities/extensions/ast.dart
@@ -41,6 +41,15 @@
return null;
}
+ AstNode? get enclosingUnitChild {
+ for (var node in withParents) {
+ if (node.parent is CompilationUnit) {
+ return node;
+ }
+ }
+ return null;
+ }
+
/// This node and all its parents.
Iterable<AstNode> get withParents sync* {
var current = this;