[DAS] Fixes multiple commas by `Add missing required arguments`

This also refactors the correction producer so that it is a bit easier to read and maintain.

Fixes: https://github.com/dart-lang/sdk/issues/61531
Change-Id: I54656f3e917764881bc4817f7b9692bb9fd973b5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/450360
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Auto-Submit: Felipe Morschel <git@fmorschel.dev>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/add_missing_required_argument.dart b/pkg/analysis_server/lib/src/services/correction/dart/add_missing_required_argument.dart
index 6704592..6622984 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/add_missing_required_argument.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/add_missing_required_argument.dart
@@ -2,12 +2,12 @@
 // 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:_fe_analyzer_shared/src/scanner/token.dart';
 import 'package:analysis_server/src/services/completion/dart/utilities.dart';
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
 import 'package:analysis_server_plugin/src/correction/fix_generators.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/diagnostic/diagnostic.dart';
 import 'package:analyzer/error/error.dart';
@@ -68,7 +68,7 @@
     }
 
     var diagnostic = this.diagnostic;
-    if (diagnostic is! Diagnostic) {
+    if (diagnostic == null) {
       return;
     }
 
@@ -80,6 +80,7 @@
     // Should not happen since the current diagnostic is in the list of errors
     // where this fix is valid.
     if (diagnostics.isEmpty) {
+      assert(false, 'No valid diagnostics found, but expected at least one.');
       diagnostics = [diagnostic];
     }
 
@@ -91,92 +92,135 @@
 
     _missingParameters = diagnostics.length;
 
-    for (var (index, diagnostic) in diagnostics.indexed) {
-      if (targetElement is ExecutableElement && argumentList != null) {
-        // Format: "Missing required argument 'foo'."
-        var messageParts = diagnostic.problemMessage
-            .messageText(includeUrl: false)
-            .split("'");
-        if (messageParts.length < 2) {
-          return;
-        }
+    if (argumentList == null || targetElement is! ExecutableElement) {
+      return;
+    }
+    var arguments = argumentList.arguments;
 
-        var parameterName = messageParts[1];
-
-        var missingParameter = targetElement.formalParameters.firstWhereOrNull(
-          (p) => p.name == parameterName,
-        );
-        if (missingParameter == null) {
-          return;
-        }
-
-        int offset;
-        var hasTrailingComma = false;
-        var insertBetweenParams = false;
-        var arguments = argumentList.arguments;
-        if (arguments.isEmpty) {
-          offset = argumentList.leftParenthesis.end;
-        } else {
-          var lastArgument = arguments.last;
-          offset = lastArgument.end;
-          hasTrailingComma =
-              lastArgument.endToken.next!.type == TokenType.COMMA;
-
-          if (lastArgument is NamedExpression && creation.isWidgetExpression) {
-            if (lastArgument.isChildArgument ||
-                lastArgument.isChildrenArgument) {
-              offset = lastArgument.offset;
-              hasTrailingComma = true;
-              insertBetweenParams = true;
-            }
+    int offset;
+    var insertLeadingComma = false;
+    var insertFlutterTrailingComma = true;
+    var insertBetweenFlutterParams = false;
+    if (arguments.isEmpty) {
+      offset = argumentList.leftParenthesis.end;
+    } else {
+      var lastArgument = arguments.last;
+      offset = lastArgument.end;
+      insertLeadingComma = true;
+      if (creation.isWidgetExpression) {
+        // If the last argument is a `child:` or `children:` parameter in a
+        // Flutter widget, insert the new parameter(s) before it.
+        if (lastArgument is NamedExpression) {
+          if (lastArgument.isChildArgument || lastArgument.isChildrenArgument) {
+            offset = lastArgument.offset;
+            insertBetweenFlutterParams = true;
+            // Insert a leading comma if we are have a case like:
+            // ```dart
+            // MyWidget(other: 1, child: ...,)
+            // ```
+            // so that we get:
+            // ```dart
+            // MyWidget(other: 1, newParam: ..., child: ...,)
+            // ```
+            // since the offset is at the end of `other: 1`.
+            insertLeadingComma = arguments.length > 1;
           }
         }
-
-        var codeStyleOptions = getCodeStyleOptions(unitResult.file);
-        var defaultValue = getDefaultStringParameterValue(
-          missingParameter,
-          codeStyleOptions.preferredQuoteForStrings,
-        );
-
-        await builder.addDartFileEdit(file, (builder) {
-          builder.addInsertion(offset, (builder) {
-            if ((arguments.isNotEmpty || index > 0) && !insertBetweenParams) {
-              builder.write(', ');
-            }
-
-            builder.write('$parameterName: ');
-
-            // Use defaultValue.cursorPosition if it's not null.
-            if (defaultValue != null) {
-              var text = defaultValue.text;
-              var cursorPosition = defaultValue.cursorPosition;
-              if (cursorPosition != null) {
-                builder.write(text.substring(0, cursorPosition));
-                builder.selectHere();
-                builder.write(text.substring(cursorPosition));
-              } else {
-                builder.addSimpleLinkedEdit('VALUE', text);
-              }
-            } else {
-              builder.addSimpleLinkedEdit('VALUE', 'null');
-            }
-
-            if (creation.isWidgetExpression) {
-              // Insert a trailing comma after Flutter instance creation params.
-              if (!hasTrailingComma) {
-                builder.write(',');
-              } else if (insertBetweenParams) {
-                builder.writeln(',');
-
-                // Insert indent before the child: or children: param.
-                var indent = utils.getLinePrefix(offset);
-                builder.write(indent);
-              }
-            }
-          });
-        });
+        if (lastArgument.endToken.next case Token(
+          type: TokenType.COMMA,
+        ) when !insertBetweenFlutterParams) {
+          // If there is a trailing comma after the last argument, don't add
+          // another one.
+          insertFlutterTrailingComma = false;
+        }
       }
     }
+
+    Map<String, DefaultArgument?> parameters = {};
+    for (var diagnostic in diagnostics) {
+      // Format: "Missing required argument 'foo'."
+      var messageParts = diagnostic.problemMessage
+          .messageText(includeUrl: false)
+          .split("'");
+      if (messageParts.length < 2) {
+        return;
+      }
+
+      var parameterName = messageParts[1];
+
+      var missingParameter = targetElement.formalParameters.firstWhereOrNull(
+        (p) => p.name == parameterName,
+      );
+      if (missingParameter == null) {
+        assert(
+          false,
+          'Could not find parameter $parameterName, but expected since it was '
+          'reported as missing.',
+        );
+        continue;
+      }
+
+      var codeStyleOptions = getCodeStyleOptions(unitResult.file);
+      var defaultValue = getDefaultStringParameterValue(
+        missingParameter,
+        codeStyleOptions.preferredQuoteForStrings,
+      );
+
+      parameters[parameterName] = defaultValue;
+    }
+
+    var parametersLength = parameters.length;
+
+    await builder.addDartFileEdit(file, (builder) {
+      builder.addInsertion(offset, (builder) {
+        if (insertLeadingComma) {
+          builder.write(', ');
+        }
+        var entries = parameters.entries.toList();
+        var childOrChildren = entries.firstWhereOrNull(
+          (e) => const {'child', 'children'}.contains(e.key),
+        );
+        if (childOrChildren != null) {
+          // If there is a child: or children: parameter, insert it last.
+          entries.remove(childOrChildren);
+          entries.add(childOrChildren);
+        }
+        entries.forEachIndexed((index, entry) {
+          var MapEntry(key: parameterName, value: defaultValue) = entry;
+
+          builder.write('$parameterName: ');
+
+          // Use defaultValue.cursorPosition if it's not null.
+          if (defaultValue != null) {
+            var text = defaultValue.text;
+            var cursorPosition = defaultValue.cursorPosition;
+            if (cursorPosition != null) {
+              builder.write(text.substring(0, cursorPosition));
+              builder.selectHere();
+              builder.write(text.substring(cursorPosition));
+            } else {
+              builder.addSimpleLinkedEdit('VALUE', text);
+            }
+          } else {
+            builder.addSimpleLinkedEdit('VALUE', 'null');
+          }
+
+          if (index < parametersLength - 1) {
+            builder.write(', ');
+          } else if (insertBetweenFlutterParams) {
+            // Insert a trailing comma after Flutter instance creation params.
+            builder.writeln(',');
+            // Insert indent before the child: or children: param.
+            var indent = utils.getLinePrefix(offset);
+            builder.write(indent);
+          } else if (creation.isWidgetExpression &&
+              insertFlutterTrailingComma) {
+            // If this is a Flutter widget, format with a trailing comma.
+            builder.write(',');
+          }
+        });
+      });
+    });
   }
 }
 
diff --git a/pkg/analysis_server/test/src/services/correction/fix/add_missing_required_argument_test.dart b/pkg/analysis_server/test/src/services/correction/fix/add_missing_required_argument_test.dart
index 048536a..2808fd1 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/add_missing_required_argument_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/add_missing_required_argument_test.dart
@@ -319,6 +319,31 @@
 ''');
   }
 
+  Future<void> test_flutter_closure_children() async {
+    await resolveTestCode('''
+import 'package:flutter/widgets.dart';
+
+class MyWidget extends Widget {
+  MyWidget({required List<Widget> children, required void Function()? fn});
+}
+
+build() {
+  return new MyWidget();
+}
+''');
+    await assertHasFix('''
+import 'package:flutter/widgets.dart';
+
+class MyWidget extends Widget {
+  MyWidget({required List<Widget> children, required void Function()? fn});
+}
+
+build() {
+  return new MyWidget(fn: () {  }, children: [],);
+}
+''', errorFilter: (diagnostic) => diagnostic.message.contains("'children'"));
+  }
+
   Future<void> test_functionType_noParameterName() async {
     await resolveTestCode('''
 void foo({required void Function(int) f}) {}