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;