Add assist to convert child: to children: in Flutter new-exprs

R=brianwilkerson@google.com, scheglov@google.com

Review-Url: https://codereview.chromium.org/2749283004 .
diff --git a/pkg/analysis_server/lib/src/services/correction/assist.dart b/pkg/analysis_server/lib/src/services/correction/assist.dart
index 3b8ea1c..7158950 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist.dart
@@ -83,6 +83,8 @@
       'CONVERT_DOCUMENTATION_INTO_LINE',
       30,
       "Convert into line documentation comment");
+  static const CONVERT_FLUTTER_CHILD =
+      const AssistKind('CONVERT_FLUTTER_CHILD', 30, "Convert to children:");
   static const CONVERT_INTO_BLOCK_BODY = const AssistKind(
       'CONVERT_INTO_BLOCK_BODY', 30, "Convert into block body");
   static const CONVERT_INTO_EXPRESSION_BODY = const AssistKind(
@@ -119,10 +121,10 @@
       "Join 'if' statement with outer 'if' statement");
   static const JOIN_VARIABLE_DECLARATION = const AssistKind(
       'JOIN_VARIABLE_DECLARATION', 30, "Join variable declaration");
-  static const MOVE_FLUTTER_WIDGET_DOWN = const AssistKind(
-      "MOVE_FLUTTER_WIDGET_DOWN", 30, "Move widget down");
-  static const MOVE_FLUTTER_WIDGET_UP = const AssistKind(
-      "MOVE_FLUTTER_WIDGET_UP", 30, "Move widget up");
+  static const MOVE_FLUTTER_WIDGET_DOWN =
+      const AssistKind("MOVE_FLUTTER_WIDGET_DOWN", 30, "Move widget down");
+  static const MOVE_FLUTTER_WIDGET_UP =
+      const AssistKind("MOVE_FLUTTER_WIDGET_UP", 30, "Move widget up");
   static const REPARENT_FLUTTER_LIST = const AssistKind(
       "REPARENT_FLUTTER_LIST", 30, "Wrap widget list with new widget");
   static const REPARENT_FLUTTER_WIDGET = const AssistKind(
diff --git a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
index c343b17..18d32b1 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
@@ -121,6 +121,7 @@
     _addProposal_convertDocumentationIntoLine();
     _addProposal_convertToBlockFunctionBody();
     _addProposal_convertToExpressionFunctionBody();
+    _addProposal_convertFlutterChild();
     _addProposal_convertToForIndexLoop();
     _addProposal_convertToIsNot_onIs();
     _addProposal_convertToIsNot_onNot();
@@ -505,6 +506,73 @@
     _addAssist(DartAssistKind.CONVERT_DOCUMENTATION_INTO_LINE, []);
   }
 
+  void _addProposal_convertFlutterChild() {
+    NamedExpression namedExp;
+    // Allow assist to activate from either the new-expr or the child: arg.
+    if (node is SimpleIdentifier &&
+        node.parent is Label &&
+        node.parent.parent is NamedExpression) {
+      namedExp = node.parent.parent as NamedExpression;
+      if ((node as SimpleIdentifier).name != 'child' ||
+          namedExp.expression == null) {
+        return;
+      }
+      if (namedExp.parent?.parent is! InstanceCreationExpression) {
+        return;
+      }
+      InstanceCreationExpression newExpr = namedExp.parent.parent;
+      if (newExpr == null || !_isFlutterInstanceCreationExpression(newExpr)) {
+        return;
+      }
+    } else {
+      InstanceCreationExpression newExpr = _identifyNewExpression();
+      if (newExpr == null || !_isFlutterInstanceCreationExpression(newExpr)) {
+        _coverageMarker();
+        return;
+      }
+      namedExp = _findChildArgument(newExpr);
+      if (namedExp == null || namedExp.expression == null) {
+        _coverageMarker();
+        return;
+      }
+    }
+    InstanceCreationExpression childArg = _getChildWidget(namedExp, false);
+    if (childArg == null) {
+      _coverageMarker();
+      return;
+    }
+    int childLoc = namedExp.offset + 'child'.length;
+    _addInsertEdit(childLoc, 'ren');
+    int listLoc = childArg.offset;
+    String childArgSrc = utils.getNodeText(childArg);
+    if (!childArgSrc.contains(eol)) {
+      _addInsertEdit(listLoc, '<Widget>[');
+      _addInsertEdit(listLoc + childArg.length, ']');
+    } else {
+      int newlineLoc = childArgSrc.lastIndexOf(eol);
+      if (newlineLoc == childArgSrc.length) {
+        newlineLoc -= 1;
+      }
+      String indentOld = utils.getLinePrefix(childArg.offset + 1 + newlineLoc);
+      String indentNew = '$indentOld${utils.getIndent(1)}';
+      // The separator includes 'child:' but that has no newlines.
+      String separator =
+          utils.getText(namedExp.offset, childArg.offset - namedExp.offset);
+      String prefix = separator.contains(eol) ? "" : "$eol$indentNew";
+      if (prefix.isEmpty) {
+        _addInsertEdit(namedExp.offset + 'child:'.length, ' <Widget>[');
+        _addRemoveEdit(rangeStartLength(childArg.offset - 2, 2));
+      } else {
+        _addInsertEdit(listLoc, '<Widget>[');
+      }
+      String newChildArgSrc = childArgSrc.replaceAll(
+          new RegExp("^$indentOld", multiLine: true), "$indentNew");
+      newChildArgSrc = "$prefix$newChildArgSrc,$eol$indentOld]";
+      _addReplaceEdit(rangeNode(childArg), newChildArgSrc);
+    }
+    _addAssist(DartAssistKind.CONVERT_FLUTTER_CHILD, []);
+  }
+
   void _addProposal_convertIntoFinalField() {
     // Find the enclosing getter.
     MethodDeclaration getter;
@@ -2311,11 +2379,12 @@
     return _getChildWidget(child);
   }
 
-  InstanceCreationExpression _getChildWidget(NamedExpression child) {
+  InstanceCreationExpression _getChildWidget(NamedExpression child,
+      [bool strict = false]) {
     if (child?.expression is InstanceCreationExpression) {
       InstanceCreationExpression childNewExpr = child.expression;
       if (_isFlutterInstanceCreationExpression(childNewExpr)) {
-        if (_findChildArgument(childNewExpr) != null) {
+        if (!strict || (_findChildArgument(childNewExpr) != null)) {
           return childNewExpr;
         }
       }
diff --git a/pkg/analysis_server/test/services/correction/assist_test.dart b/pkg/analysis_server/test/services/correction/assist_test.dart
index b52c25d..29b3920 100644
--- a/pkg/analysis_server/test/services/correction/assist_test.dart
+++ b/pkg/analysis_server/test/services/correction/assist_test.dart
@@ -1036,6 +1036,129 @@
 ''');
   }
 
+  test_convertFlutterChild_OK_multiLine() async {
+    _configureFlutterPkg({
+      'src/widgets/framework.dart': _flutter_framework_code,
+    });
+    await resolveTestUnit('''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      /*caret*/child: new Container(
+        width: 200.0,
+        height: 300.0,
+      ),
+      key: null,
+    ),
+// end
+  );
+}
+''');
+    _setCaretLocation();
+    await assertHasAssist(
+        DartAssistKind.CONVERT_FLUTTER_CHILD,
+        '''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      /*caret*/children: <Widget>[
+        new Container(
+          width: 200.0,
+          height: 300.0,
+        ),
+      ],
+      key: null,
+    ),
+// end
+  );
+}
+''');
+  }
+
+  test_convertFlutterChild_OK_newlineChild() async {
+    // This case could occur with deeply nested constructors, common in Flutter.
+    _configureFlutterPkg({
+      'src/widgets/framework.dart': _flutter_framework_code,
+    });
+    await resolveTestUnit('''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      /*caret*/child:
+          new Container(
+        width: 200.0,
+        height: 300.0,
+      ),
+      key: null,
+    ),
+// end
+  );
+}
+''');
+    _setCaretLocation();
+    await assertHasAssist(
+        DartAssistKind.CONVERT_FLUTTER_CHILD,
+        '''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      /*caret*/children: <Widget>[
+        new Container(
+          width: 200.0,
+          height: 300.0,
+        ),
+      ],
+      key: null,
+    ),
+// end
+  );
+}
+''');
+  }
+
+  test_convertFlutterChild_OK_singleLine() async {
+    _configureFlutterPkg({
+      'src/widgets/framework.dart': _flutter_framework_code,
+    });
+    await resolveTestUnit('''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      /*caret*/child: new GestureDetector(),
+      key: null,
+    ),
+// end
+  );
+}
+''');
+    _setCaretLocation();
+    await assertHasAssist(
+        DartAssistKind.CONVERT_FLUTTER_CHILD,
+        '''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      /*caret*/children: <Widget>[new GestureDetector()],
+      key: null,
+    ),
+// end
+  );
+}
+''');
+  }
+
   test_convertToBlockBody_BAD_noEnclosingFunction() async {
     await resolveTestUnit('''
 var v = 123;
@@ -3502,6 +3625,104 @@
 ''');
   }
 
+  test_moveFlutterWidgetDown_OK() async {
+    _configureFlutterPkg({
+      'src/widgets/framework.dart': _flutter_framework_code,
+    });
+    await resolveTestUnit('''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new /*caret*/GestureDetector(
+      onTap: () => startResize(),
+      child: new Center(
+        child: new Container(
+          width: 200.0,
+          height: 300.0,
+        ),
+        key: null,
+      ),
+    ),
+// end
+  );
+}
+startResize() {}
+''');
+    _setCaretLocation();
+    await assertHasAssist(
+        DartAssistKind.MOVE_FLUTTER_WIDGET_DOWN,
+        '''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      child: new /*caret*/GestureDetector(
+        onTap: () => startResize(),
+        child: new Container(
+          width: 200.0,
+          height: 300.0,
+        ),
+      ),
+      key: null,
+    ),
+// end
+  );
+}
+startResize() {}
+''');
+  }
+
+  test_moveFlutterWidgetUp_OK() async {
+    _configureFlutterPkg({
+      'src/widgets/framework.dart': _flutter_framework_code,
+    });
+    await resolveTestUnit('''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new Center(
+      child: new /*caret*/GestureDetector(
+        onTap: () => startResize(),
+        child: new Container(
+          width: 200.0,
+          height: 300.0,
+        ),
+      ),
+      key: null,
+    ),
+// end
+  );
+}
+startResize() {}
+''');
+    _setCaretLocation();
+    await assertHasAssist(
+        DartAssistKind.MOVE_FLUTTER_WIDGET_UP,
+        '''
+import 'package:flutter/src/widgets/framework.dart';
+build() {
+  return new Scaffold(
+// start
+    body: new /*caret*/GestureDetector(
+      onTap: () => startResize(),
+      child: new Center(
+        child: new Container(
+          width: 200.0,
+          height: 300.0,
+        ),
+        key: null,
+      ),
+    ),
+// end
+  );
+}
+startResize() {}
+''');
+  }
+
   test_removeTypeAnnotation_classField_OK() async {
     await resolveTestUnit('''
 class A {
@@ -3666,104 +3887,6 @@
     await assertNoAssist(DartAssistKind.REPARENT_FLUTTER_LIST);
   }
 
-  test_moveFlutterWidgetDown_OK() async {
-    _configureFlutterPkg({
-      'src/widgets/framework.dart': _flutter_framework_code,
-    });
-    await resolveTestUnit('''
-import 'package:flutter/src/widgets/framework.dart';
-build() {
-  return new Scaffold(
-// start
-    body: new /*caret*/GestureDetector(
-      onTap: () => startResize(),
-      child: new Center(
-        child: new Container(
-          width: 200.0,
-          height: 300.0,
-        ),
-        key: null,
-      ),
-    ),
-// end
-  );
-}
-startResize() {}
-''');
-    _setCaretLocation();
-    await assertHasAssist(
-        DartAssistKind.MOVE_FLUTTER_WIDGET_DOWN,
-        '''
-import 'package:flutter/src/widgets/framework.dart';
-build() {
-  return new Scaffold(
-// start
-    body: new Center(
-      child: new /*caret*/GestureDetector(
-        onTap: () => startResize(),
-        child: new Container(
-          width: 200.0,
-          height: 300.0,
-        ),
-      ),
-      key: null,
-    ),
-// end
-  );
-}
-startResize() {}
-''');
-  }
-
-  test_moveFlutterWidgetUp_OK() async {
-    _configureFlutterPkg({
-      'src/widgets/framework.dart': _flutter_framework_code,
-    });
-    await resolveTestUnit('''
-import 'package:flutter/src/widgets/framework.dart';
-build() {
-  return new Scaffold(
-// start
-    body: new Center(
-      child: new /*caret*/GestureDetector(
-        onTap: () => startResize(),
-        child: new Container(
-          width: 200.0,
-          height: 300.0,
-        ),
-      ),
-      key: null,
-    ),
-// end
-  );
-}
-startResize() {}
-''');
-    _setCaretLocation();
-    await assertHasAssist(
-        DartAssistKind.MOVE_FLUTTER_WIDGET_UP,
-        '''
-import 'package:flutter/src/widgets/framework.dart';
-build() {
-  return new Scaffold(
-// start
-    body: new /*caret*/GestureDetector(
-      onTap: () => startResize(),
-      child: new Center(
-        child: new Container(
-          width: 200.0,
-          height: 300.0,
-        ),
-        key: null,
-      ),
-    ),
-// end
-  );
-}
-startResize() {}
-''');
-  }
-
   test_reparentFlutterList_OK_multiLine() async {
     _configureFlutterPkg({
       'src/widgets/framework.dart': _flutter_framework_code,