Add quick fix to remove unnecessary null-aware assignments

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

Change-Id: I29398feae3ad3a83ced722a876c6f6416ef46da8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/169581
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/remove_dead_if_null.dart b/pkg/analysis_server/lib/src/services/correction/dart/remove_dead_if_null.dart
index bf576ec..17e9a2d 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/remove_dead_if_null.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/remove_dead_if_null.dart
@@ -6,6 +6,8 @@
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/source/source_range.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
 import 'package:analyzer_plugin/utilities/range_factory.dart';
@@ -16,41 +18,44 @@
 
   @override
   Future<void> compute(ChangeBuilder builder) async {
-    //
-    // Find the dead if-null expression.
-    //
-    BinaryExpression findIfNull() {
-      var child = node;
-      var parent = node.parent;
-      while (parent != null) {
-        if (parent is BinaryExpression &&
-            parent.operator.type == TokenType.QUESTION_QUESTION &&
-            parent.rightOperand == child) {
-          return parent;
-        }
-        child = parent;
-        parent = parent.parent;
-      }
-      return null;
-    }
-
-    var expression = findIfNull();
-    if (expression == null) {
+    var sourceRange = findIfNull();
+    if (sourceRange == null) {
       return;
     }
-    //
-    // Extract the information needed to build the edit.
-    //
-    var sourceRange =
-        range.endEnd(expression.leftOperand, expression.rightOperand);
-    //
-    // Build the edit.
-    //
+
     await builder.addDartFileEdit(file, (builder) {
       builder.addDeletion(sourceRange);
     });
   }
 
-  /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
+  /// Finds the dead if-null expression above [node].
+  SourceRange findIfNull() {
+    var child = node;
+    var parent = node.parent;
+    while (parent != null) {
+      if (parent is BinaryExpression &&
+          parent.operator.type == TokenType.QUESTION_QUESTION &&
+          parent.rightOperand == child) {
+        return range.endEnd(parent.leftOperand, parent.rightOperand);
+      }
+      if (parent is AssignmentExpression &&
+          parent.operator.type == TokenType.QUESTION_QUESTION_EQ &&
+          parent.rightHandSide == child) {
+        var assignee = parent.leftHandSide;
+        if (parent.parent is ExpressionStatement &&
+            assignee is SimpleIdentifier &&
+            assignee.staticElement is PromotableElement) {
+          return utils.getLinesRange(range.node(parent.parent));
+        } else {
+          return range.endEnd(parent.leftHandSide, parent.rightHandSide);
+        }
+      }
+      child = parent;
+      parent = parent.parent;
+    }
+    return null;
+  }
+
+  /// Returns an instance of this class. Used as a tear-off in `FixProcessor`.
   static RemoveDeadIfNull newInstance() => RemoveDeadIfNull();
 }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/remove_if_null_operator_test.dart b/pkg/analysis_server/test/src/services/correction/fix/remove_if_null_operator_test.dart
index 3797ce9..3f86e17 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/remove_if_null_operator_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/remove_if_null_operator_test.dart
@@ -12,12 +12,78 @@
 
 void main() {
   defineReflectiveSuite(() {
+    defineReflectiveTests(DeadNullAwareAssignmentExpressionTest);
     defineReflectiveTests(DeadNullAwareExpressionTest);
     defineReflectiveTests(UnnecessaryNullInIfNullOperatorsTest);
   });
 }
 
 @reflectiveTest
+class DeadNullAwareAssignmentExpressionTest extends FixProcessorTest
+    with WithNullSafetyMixin {
+  @override
+  FixKind get kind => DartFixKind.REMOVE_IF_NULL_OPERATOR;
+
+  Future<void> test_assignmentExpression_nonPromotable() async {
+    await resolveTestCode('''
+class C {
+  int a = 1;
+  void f(int b) {
+    a ??= b;
+  }
+}
+''');
+    await assertHasFix('''
+class C {
+  int a = 1;
+  void f(int b) {
+    a;
+  }
+}
+''');
+  }
+
+  Future<void> test_assignmentExpression_promotable() async {
+    await resolveTestCode('''
+void f(int a, int b) {
+  a ??= b;
+}
+''');
+    await assertHasFix('''
+void f(int a, int b) {
+}
+''');
+  }
+
+  Future<void> test_immediateChild() async {
+    await resolveTestCode('''
+void f(int a, int b) => a ??= b;
+''');
+    await assertHasFix('''
+void f(int a, int b) => a;
+''');
+  }
+
+  Future<void> test_nestedChild() async {
+    await resolveTestCode('''
+void f(int a, int b) => a ??= b * 2 + 1;
+''');
+    await assertHasFix('''
+void f(int a, int b) => a;
+''');
+  }
+
+  Future<void> test_nestedChild_onRight() async {
+    await resolveTestCode('''
+void f(int a, int b, int c) => a = b ??= c;
+''');
+    await assertHasFix('''
+void f(int a, int b, int c) => a = b;
+''');
+  }
+}
+
+@reflectiveTest
 class DeadNullAwareExpressionTest extends FixProcessorTest
     with WithNullSafetyMixin {
   @override