QuickFix. Issue 55805. Create extension setter.

Bug: https://github.com/dart-lang/sdk/issues/55805
Change-Id: I0972eee46a1b0ab8e14235e9a56fd08949f634f8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/369522
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/create_extension_member.dart b/pkg/analysis_server/lib/src/services/correction/dart/create_extension_member.dart
index 9a23022..cd24467 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/create_extension_member.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/create_extension_member.dart
@@ -171,6 +171,85 @@
   }
 }
 
+class CreateExtensionSetter extends _CreateExtensionMember {
+  String _setterName = '';
+
+  CreateExtensionSetter({
+    required super.context,
+  });
+
+  @override
+  List<String> get fixArguments => [_setterName];
+
+  @override
+  FixKind get fixKind => DartFixKind.CREATE_EXTENSION_SETTER;
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    var nameNode = node;
+    if (nameNode is! SimpleIdentifier) {
+      return;
+    }
+    if (!nameNode.inSetterContext()) {
+      return;
+    }
+
+    _setterName = 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.realTarget;
+        }
+    }
+    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 writeSetter(DartEditBuilder builder) {
+      builder.writeSetterDeclaration(
+        _setterName,
+        nameGroupName: 'NAME',
+        parameterType: fieldType,
+        parameterTypeGroupName: 'TYPE',
+      );
+    }
+
+    var updatedExisting = await _updateExistingExtension(
+      builder,
+      targetType,
+      (extension, builder) {
+        builder.insertGetter(extension, (builder) {
+          writeSetter(builder);
+        });
+      },
+    );
+    if (updatedExisting) {
+      return;
+    }
+
+    await _addNewExtension(builder, targetType, nameNode, writeSetter);
+  }
+}
+
 abstract class _CreateExtensionMember extends ResolvedCorrectionProducer {
   _CreateExtensionMember({
     required super.context,
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 036f2ce..86a551f 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -691,6 +691,11 @@
     DartFixKindPriority.DEFAULT - 20,
     "Create extension method '{0}'",
   );
+  static const CREATE_EXTENSION_SETTER = FixKind(
+    'dart.fix.create.extension.setter',
+    DartFixKindPriority.DEFAULT - 20,
+    "Create extension setter '{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 2f8b91a..08aa4a8 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -1297,6 +1297,7 @@
   ],
   CompileTimeErrorCode.UNDEFINED_SETTER: [
     ChangeTo.getterOrSetter,
+    CreateExtensionSetter.new,
     CreateField.new,
     CreateSetter.new,
   ],
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart
index 8798115..b53a26a 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart
@@ -12,6 +12,7 @@
   defineReflectiveSuite(() {
     defineReflectiveTests(CreateExtensionGetterTest);
     defineReflectiveTests(CreateExtensionMethodTest);
+    defineReflectiveTests(CreateExtensionSetterTest);
   });
 }
 
@@ -488,3 +489,193 @@
 ''');
   }
 }
+
+@reflectiveTest
+class CreateExtensionSetterTest extends FixProcessorTest {
+  @override
+  FixKind get kind => DartFixKind.CREATE_EXTENSION_SETTER;
+
+  Future<void> test_existingExtension() async {
+    await resolveTestCode('''
+void f() {
+  ''.test = 0;
+}
+
+extension on String {}
+''');
+    await assertHasFix('''
+void f() {
+  ''.test = 0;
+}
+
+extension on String {
+  set test(int test) {}
+}
+''');
+  }
+
+  Future<void> test_existingExtension_generic_matching() async {
+    await resolveTestCode('''
+void f(List<int> a) {
+  a.test = 0;
+}
+
+extension E<T> on Iterable<T> {}
+''');
+    await assertHasFix('''
+void f(List<int> a) {
+  a.test = 0;
+}
+
+extension E<T> on Iterable<T> {
+  set test(int test) {}
+}
+''');
+  }
+
+  Future<void> test_existingExtension_generic_notMatching() async {
+    await resolveTestCode('''
+void f(List<int> a) {
+  a.test = 0;
+}
+
+extension E<K, V> on Map<K, V> {}
+''');
+    await assertHasFix('''
+void f(List<int> a) {
+  a.test = 0;
+}
+
+extension on List<int> {
+  set test(int test) {}
+}
+
+extension E<K, V> on Map<K, V> {}
+''');
+  }
+
+  Future<void> test_existingExtension_hasMethod() async {
+    await resolveTestCode('''
+void f() {
+  ''.test = 0;
+}
+
+extension E on String {
+  // ignore:unused_element
+  void foo() {}
+}
+''');
+    await assertHasFix('''
+void f() {
+  ''.test = 0;
+}
+
+extension E on String {
+  set test(int test) {}
+
+  // ignore:unused_element
+  void foo() {}
+}
+''');
+  }
+
+  Future<void> test_existingExtension_notGeneric_matching() async {
+    await resolveTestCode('''
+void f() {
+  ''.test = 0;
+}
+
+extension on String {}
+''');
+    await assertHasFix('''
+void f() {
+  ''.test = 0;
+}
+
+extension on String {
+  set test(int test) {}
+}
+''');
+  }
+
+  Future<void> test_existingExtension_notGeneric_notMatching() async {
+    await resolveTestCode('''
+void f() {
+  ''.test = 0;
+}
+
+extension on int {}
+''');
+    await assertHasFix('''
+void f() {
+  ''.test = 0;
+}
+
+extension on String {
+  set test(int test) {}
+}
+
+extension on int {}
+''');
+  }
+
+  Future<void> test_parent_nothing() async {
+    await resolveTestCode('''
+void f() {
+  test = 0;
+}
+''');
+    await assertNoFix();
+  }
+
+  Future<void> test_parent_prefixedIdentifier() async {
+    await resolveTestCode('''
+void f(String a) {
+  a.test = 0;
+}
+''');
+    await assertHasFix('''
+void f(String a) {
+  a.test = 0;
+}
+
+extension on String {
+  set test(int test) {}
+}
+''');
+  }
+
+  Future<void> test_parent_propertyAccess_cascade() async {
+    await resolveTestCode('''
+void f(String a) {
+  a..test = 0;
+}
+''');
+    await assertHasFix('''
+void f(String a) {
+  a..test = 0;
+}
+
+extension on String {
+  set test(int test) {}
+}
+''');
+  }
+
+  Future<void> test_targetType_hasTypeArguments() async {
+    await resolveTestCode('''
+void f(List<int> a) {
+  a.test = 0;
+}
+''');
+    await assertHasFix('''
+void f(List<int> a) {
+  a.test = 0;
+}
+
+extension on List<int> {
+  set test(int test) {}
+}
+''');
+  }
+}