Adds assist for basic Flutter Builder

Fixes issues such as

* https://github.com/flutter/flutter-intellij/issues/814
* https://github.com/dart-lang/sdk/issues/33957 .

Essentially, in Flutter, it's often useful to create a new context in the UI/widget tree as a place to begin a search towards the trunk of the tree for various state structures attached to lower levels of the tree. The basic `Builder` widget is the official means for creating such a context/entry-point.

The patches in this PR are essentially a copy-paste-tweak from two types of existing, similar assists. The main origin was taken from `FlutterWrapStreamBuilder` which wraps client code with a constructor for the StreamBuilder sub-class. The new assist, provided here, is a little more basic/general than the StreamBuilder so it was able to also adopt assertion and test code from some of the other assists for basic widgets.

Closes https://github.com/dart-lang/sdk/pull/45656
https://github.com/dart-lang/sdk/pull/45656

GitOrigin-RevId: 00a5043e5dbd410f24f30f6503711413341dbe7a
Change-Id: I5f93837af571b4974e35da131112f82cd9359697
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/194941
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/assist.dart b/pkg/analysis_server/lib/src/services/correction/assist.dart
index 2929cc2..b85a733 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist.dart
@@ -138,6 +138,8 @@
   static const FLUTTER_WRAP_GENERIC =
       AssistKind('dart.assist.flutter.wrap.generic', 31, 'Wrap with widget...');
 
+  static const FLUTTER_WRAP_BUILDER = AssistKind(
+      'dart.assist.flutter.wrap.builder', 32, 'Wrap with Builder');
   static const FLUTTER_WRAP_CENTER =
       AssistKind('dart.assist.flutter.wrap.center', 32, 'Wrap with Center');
   static const FLUTTER_WRAP_COLUMN =
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 ef6a1ef..68c3a9b 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
@@ -51,6 +51,7 @@
 import 'package:analysis_server/src/services/correction/dart/flutter_swap_with_child.dart';
 import 'package:analysis_server/src/services/correction/dart/flutter_swap_with_parent.dart';
 import 'package:analysis_server/src/services/correction/dart/flutter_wrap.dart';
+import 'package:analysis_server/src/services/correction/dart/flutter_wrap_builder.dart';
 import 'package:analysis_server/src/services/correction/dart/flutter_wrap_generic.dart';
 import 'package:analysis_server/src/services/correction/dart/flutter_wrap_stream_builder.dart';
 import 'package:analysis_server/src/services/correction/dart/import_add_show.dart';
@@ -128,6 +129,7 @@
     FlutterRemoveWidget.newInstance,
     FlutterSwapWithChild.newInstance,
     FlutterSwapWithParent.newInstance,
+    FlutterWrapBuilder.newInstance,
     FlutterWrapGeneric.newInstance,
     FlutterWrapStreamBuilder.newInstance,
     ImportAddShow.newInstance,
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/flutter_wrap_builder.dart b/pkg/analysis_server/lib/src/services/correction/dart/flutter_wrap_builder.dart
new file mode 100644
index 0000000..a2b9351
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/flutter_wrap_builder.dart
@@ -0,0 +1,68 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// 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.
+
+// @dart = 2.9
+
+import 'package:analysis_server/src/services/correction/assist.dart';
+import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
+import 'package:analyzer_plugin/utilities/assist/assist.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+
+class FlutterWrapBuilder extends CorrectionProducer {
+  @override
+  AssistKind get assistKind => DartAssistKind.FLUTTER_WRAP_BUILDER;
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    var widgetExpr = flutter.identifyWidgetExpression(node);
+    if (widgetExpr == null) {
+      return;
+    }
+    if (flutter.isExactWidgetTypeBuilder(widgetExpr.staticType)) {
+      return;
+    }
+    var widgetSrc = utils.getNodeText(widgetExpr);
+
+    var builderElement = await sessionHelper.getClass(
+      flutter.widgetsUri,
+      'Builder',
+    );
+    if (builderElement == null) {
+      return;
+    }
+
+    await builder.addDartFileEdit(file, (builder) {
+      builder.addReplacement(range.node(widgetExpr), (builder) {
+        builder.writeReference(builderElement);
+
+        builder.writeln('(');
+
+        var indentOld = utils.getLinePrefix(widgetExpr.offset);
+        var indentNew1 = indentOld + utils.getIndent(1);
+        var indentNew2 = indentOld + utils.getIndent(2);
+
+        builder.write(indentNew1);
+        builder.writeln('builder: (context) {');
+
+        widgetSrc = widgetSrc.replaceAll(
+          RegExp('^$indentOld', multiLine: true),
+          indentNew2,
+        );
+        builder.write(indentNew2);
+        builder.write('return $widgetSrc');
+        builder.writeln(';');
+
+        builder.write(indentNew1);
+        builder.writeln('}');
+
+        builder.write(indentOld);
+        builder.write(')');
+      });
+    });
+  }
+
+  /// Return an instance of this class. Used as a tear-off in `AssistProcessor`.
+  static FlutterWrapBuilder newInstance() => FlutterWrapBuilder();
+}
diff --git a/pkg/analysis_server/lib/src/utilities/flutter.dart b/pkg/analysis_server/lib/src/utilities/flutter.dart
index 5e2d6fc..adaef36 100644
--- a/pkg/analysis_server/lib/src/utilities/flutter.dart
+++ b/pkg/analysis_server/lib/src/utilities/flutter.dart
@@ -13,6 +13,7 @@
   static final Flutter instance = Flutter();
 
   static const _nameAlign = 'Align';
+  static const _nameBuilder = 'Builder';
   static const _nameCenter = 'Center';
   static const _nameContainer = 'Container';
   static const _namePadding = 'Padding';
@@ -443,6 +444,12 @@
         _isExactWidget(type.element, _nameAlign, _uriBasic);
   }
 
+  /// Return `true` if the given [type] is the Flutter class `StreamBuilder`.
+  bool isExactWidgetTypeBuilder(DartType type) {
+    return type is InterfaceType &&
+        _isExactWidget(type.element, _nameBuilder, _uriBasic);
+  }
+
   /// Return `true` if the given [type] is the Flutter class `Center`.
   bool isExactWidgetTypeCenter(DartType type) {
     return type is InterfaceType &&
diff --git a/pkg/analysis_server/test/mock_packages/flutter/lib/src/widgets/basic.dart b/pkg/analysis_server/test/mock_packages/flutter/lib/src/widgets/basic.dart
index 68ecbc6..3a72bfd3 100644
--- a/pkg/analysis_server/test/mock_packages/flutter/lib/src/widgets/basic.dart
+++ b/pkg/analysis_server/test/mock_packages/flutter/lib/src/widgets/basic.dart
@@ -157,3 +157,11 @@
     Widget child,
   });
 }
+
+typedef WidgetBuilder = Widget Function(BuildContext context);
+
+class Builder {
+  final WidgetBuilder builder;
+  const Builder(
+      {Key key, @required this.builder});
+}
diff --git a/pkg/analysis_server/test/src/services/correction/assist/flutter_wrap_builder_test.dart b/pkg/analysis_server/test/src/services/correction/assist/flutter_wrap_builder_test.dart
new file mode 100644
index 0000000..1c8ecdc
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/assist/flutter_wrap_builder_test.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// 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.
+
+// @dart = 2.9
+
+import 'package:analysis_server/src/services/correction/assist.dart';
+import 'package:analyzer_plugin/utilities/assist/assist.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'assist_processor.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(FlutterWrapBuilderTest);
+  });
+}
+
+@reflectiveTest
+class FlutterWrapBuilderTest extends AssistProcessorTest {
+  @override
+  AssistKind get kind => DartAssistKind.FLUTTER_WRAP_BUILDER;
+
+  @override
+  void setUp() {
+    super.setUp();
+    writeTestPackageConfig(
+      flutter: true,
+    );
+  }
+
+  Future<void> test_aroundBuilder() async {
+    await resolveTestCode('''
+import 'package:flutter/widgets.dart';
+
+main() {
+  /*caret*/Builder(
+    builder: (context) => null,
+  );
+}
+''');
+    await assertNoAssist();
+  }
+
+  Future<void> test_aroundNamedConstructor() async {
+    await resolveTestCode('''
+import 'package:flutter/widgets.dart';
+
+class MyWidget extends StatelessWidget {
+  MyWidget.named();
+
+  Widget build(BuildContext context) => null;
+}
+
+main() {
+  return MyWidget./*caret*/named();
+}
+''');
+    await assertHasAssist('''
+import 'package:flutter/widgets.dart';
+
+class MyWidget extends StatelessWidget {
+  MyWidget.named();
+
+  Widget build(BuildContext context) => null;
+}
+
+main() {
+  return Builder(
+    builder: (context) {
+      return MyWidget.named();
+    }
+  );
+}
+''');
+  }
+
+  Future<void> test_aroundText() async {
+    await resolveTestCode('''
+import 'package:flutter/widgets.dart';
+
+main() {
+  /*caret*/Text('a');
+}
+''');
+    await assertHasAssist('''
+import 'package:flutter/widgets.dart';
+
+main() {
+  Builder(
+    builder: (context) {
+      return Text('a');
+    }
+  );
+}
+''');
+  }
+
+  Future<void> test_assignment() async {
+    await resolveTestCode('''
+import 'package:flutter/widgets.dart';
+
+main() {
+  Widget w;
+  w = /*caret*/Container();
+}
+''');
+    await assertHasAssist('''
+import 'package:flutter/widgets.dart';
+
+main() {
+  Widget w;
+  w = Builder(
+    builder: (context) {
+      return Container();
+    }
+  );
+}
+''');
+  }
+
+  Future<void> test_expressionFunctionBody() async {
+    await resolveTestCode('''
+import 'package:flutter/widgets.dart';
+class FakeFlutter {
+  main() => /*caret*/Container();
+}
+''');
+    await assertHasAssist('''
+import 'package:flutter/widgets.dart';
+class FakeFlutter {
+  main() => Builder(
+    builder: (context) {
+      return Container();
+    }
+  );
+}
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/assist/test_all.dart b/pkg/analysis_server/test/src/services/correction/assist/test_all.dart
index fef565f5..f39e638 100644
--- a/pkg/analysis_server/test/src/services/correction/assist/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/assist/test_all.dart
@@ -54,6 +54,7 @@
 import 'flutter_surround_with_set_state_test.dart' as surround_with_set_state;
 import 'flutter_swap_with_child_test.dart' as flutter_swap_with_child;
 import 'flutter_swap_with_parent_test.dart' as flutter_swap_with_parent;
+import 'flutter_wrap_builder_test.dart' as flutter_wrap_builder;
 import 'flutter_wrap_center_test.dart' as flutter_wrap_center;
 import 'flutter_wrap_column_test.dart' as flutter_wrap_column;
 import 'flutter_wrap_container_test.dart' as flutter_wrap_container;
@@ -139,6 +140,7 @@
     flutter_wrap_padding.main();
     flutter_wrap_row.main();
     flutter_wrap_sized_box.main();
+    flutter_wrap_builder.main();
     flutter_wrap_stream_builder.main();
     import_add_show.main();
     inline_invocation.main();