Issue 2287. Move all, but constructors and fields set in constructors, from StatelessWidget into State.
R=brianwilkerson@google.com
Bug: https://github.com/flutter/flutter-intellij/issues/2287
Change-Id: I313973bfd74642d04b35b793c7f463c8050ee9a2
Reviewed-on: https://dart-review.googlesource.com/60221
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
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 8fd931b..52473f1 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
@@ -1404,8 +1404,6 @@
}
Future<Null> _addProposal_flutterConvertToStatefulWidget() async {
- // TODO(brianwilkerson) Determine whether this await is necessary.
- await null;
ClassDeclaration widgetClass =
node.getAncestor((n) => n is ClassDeclaration);
TypeName superclass = widgetClass?.extendsClause?.superclass;
@@ -1447,27 +1445,93 @@
String widgetName = widgetClassElement.displayName;
String stateName = widgetName + 'State';
- var buildLinesRange = utils.getLinesRange(range.node(buildMethod));
- var buildText = utils.getRangeText(buildLinesRange);
+ // Find fields assigned in constructors.
+ var fieldsAssignedInConstructors = new Set<FieldElement>();
+ for (var member in widgetClass.members) {
+ if (member is ConstructorDeclaration) {
+ member.accept(new _SimpleIdentifierRecursiveAstVisitor((node) {
+ if (node.parent is FieldFormalParameter) {
+ Element element = node.staticElement;
+ if (element is FieldFormalParameterElement) {
+ fieldsAssignedInConstructors.add(element.field);
+ }
+ }
+ if (node.parent is ConstructorFieldInitializer) {
+ Element element = node.staticElement;
+ if (element is FieldElement) {
+ fieldsAssignedInConstructors.add(element);
+ }
+ }
+ if (node.inSetterContext()) {
+ Element element = node.staticElement;
+ if (element is PropertyAccessorElement) {
+ PropertyInducingElement field = element.variable;
+ if (field is FieldElement) {
+ fieldsAssignedInConstructors.add(field);
+ }
+ }
+ }
+ }));
+ }
+ }
- // Update the build() text to insert `widget.` before references to
- // the widget class members.
- final List<SourceEdit> buildTextEdits = [];
- buildMethod.body.accept(new _SimpleIdentifierRecursiveAstVisitor((node) {
- if (node.staticElement?.enclosingElement == widgetClassElement) {
- var offset = node.offset - buildLinesRange.offset;
- AstNode parent = node.parent;
- if (parent is InterpolationExpression &&
- parent.leftBracket.type ==
- TokenType.STRING_INTERPOLATION_IDENTIFIER) {
- buildTextEdits.add(new SourceEdit(offset, 0, '{widget.'));
- buildTextEdits.add(new SourceEdit(offset + node.length, 0, '}'));
- } else {
- buildTextEdits.add(new SourceEdit(offset, 0, 'widget.'));
+ // Prepare nodes to move.
+ var nodesToMove = new Set<ClassMember>();
+ var elementsToMove = new Set<Element>();
+ for (var member in widgetClass.members) {
+ if (member is FieldDeclaration && !member.isStatic) {
+ for (VariableDeclaration fieldNode in member.fields.variables) {
+ FieldElement fieldElement = fieldNode.element;
+ if (!fieldsAssignedInConstructors.contains(fieldElement)) {
+ nodesToMove.add(member);
+ elementsToMove.add(fieldElement);
+ elementsToMove.add(fieldElement.getter);
+ if (fieldElement.setter != null) {
+ elementsToMove.add(fieldElement.setter);
+ }
+ }
}
}
- }));
- buildText = SourceEdit.applySequence(buildText, buildTextEdits.reversed);
+ if (member is MethodDeclaration && !member.isStatic) {
+ nodesToMove.add(member);
+ elementsToMove.add(member.element);
+ }
+ }
+
+ /// Return the code for the [movedNode] which is suitable to be used
+ /// inside the `State` class, so that references to the widget fields and
+ /// methods, that are not moved, are qualified with the corresponding
+ /// instance `widget.`, or static `MyWidgetClass.` qualifier.
+ String rewriteWidgetMemberReferences(AstNode movedNode) {
+ var linesRange = utils.getLinesRange(range.node(movedNode));
+ var text = utils.getRangeText(linesRange);
+
+ // Insert `widget.` before references to the widget instance members.
+ final List<SourceEdit> edits = [];
+ movedNode.accept(new _SimpleIdentifierRecursiveAstVisitor((node) {
+ if (node.inDeclarationContext()) {
+ return;
+ }
+ var element = node.staticElement;
+ if (element is ExecutableElement &&
+ element?.enclosingElement == widgetClassElement &&
+ !elementsToMove.contains(element)) {
+ var offset = node.offset - linesRange.offset;
+ var qualifier = element.isStatic ? widgetName : 'widget';
+
+ AstNode parent = node.parent;
+ if (parent is InterpolationExpression &&
+ parent.leftBracket.type ==
+ TokenType.STRING_INTERPOLATION_IDENTIFIER) {
+ edits.add(new SourceEdit(offset, 0, '{$qualifier.'));
+ edits.add(new SourceEdit(offset + node.length, 0, '}'));
+ } else {
+ edits.add(new SourceEdit(offset, 0, '$qualifier.'));
+ }
+ }
+ }));
+ return SourceEdit.applySequence(text, edits.reversed);
+ }
var statefulWidgetClass = await sessionHelper.getClass(
flutter.WIDGETS_LIBRARY_URI, 'StatefulWidget');
@@ -1480,23 +1544,94 @@
DartChangeBuilder changeBuilder = new DartChangeBuilder(session);
await changeBuilder.addFileEdit(file, (DartFileEditBuilder builder) async {
- // TODO(brianwilkerson) Determine whether this await is necessary.
- await null;
builder.addReplacement(range.node(superclass), (builder) {
builder.writeType(statefulWidgetClass.type);
});
- builder.addReplacement(buildLinesRange, (builder) {
- builder.writeln(' @override');
- builder.writeln(' $stateName createState() {');
- builder.writeln(' return new $stateName();');
- builder.writeln(' }');
- });
+
+ int replaceOffset = 0;
+ bool hasBuildMethod = false;
+
+ /// Replace code between [replaceOffset] and [replaceEnd] with
+ /// `createState()`, empty line, or nothing.
+ void replaceInterval(int replaceEnd,
+ {bool replaceWithEmptyLine: false,
+ bool hasEmptyLineBeforeCreateState: false,
+ bool hasEmptyLineAfterCreateState: true}) {
+ int replaceLength = replaceEnd - replaceOffset;
+ builder.addReplacement(
+ new SourceRange(replaceOffset, replaceLength),
+ (builder) {
+ if (hasBuildMethod) {
+ if (hasEmptyLineBeforeCreateState) {
+ builder.writeln();
+ }
+ builder.writeln(' @override');
+ builder.writeln(' $stateName createState() {');
+ builder.writeln(' return new $stateName();');
+ builder.writeln(' }');
+ if (hasEmptyLineAfterCreateState) {
+ builder.writeln();
+ }
+ hasBuildMethod = false;
+ } else if (replaceWithEmptyLine) {
+ builder.writeln();
+ }
+ },
+ );
+ replaceOffset = 0;
+ }
+
+ // Remove continuous ranges of lines of nodes being moved.
+ bool lastToRemoveIsField = false;
+ int endOfLastNodeToKeep = 0;
+ for (var node in widgetClass.members) {
+ if (nodesToMove.contains(node)) {
+ if (replaceOffset == 0) {
+ var linesRange = utils.getLinesRange(range.node(node));
+ replaceOffset = linesRange.offset;
+ }
+ if (node == buildMethod) {
+ hasBuildMethod = true;
+ }
+ lastToRemoveIsField = node is FieldDeclaration;
+ } else {
+ var linesRange = utils.getLinesRange(range.node(node));
+ endOfLastNodeToKeep = linesRange.end;
+ if (replaceOffset != 0) {
+ replaceInterval(linesRange.offset,
+ replaceWithEmptyLine:
+ lastToRemoveIsField && node is! FieldDeclaration);
+ }
+ }
+ }
+
+ // Remove nodes at the end of the widget class.
+ if (replaceOffset != 0) {
+ // Remove from the last node to keep, so remove empty lines.
+ if (endOfLastNodeToKeep != 0) {
+ replaceOffset = endOfLastNodeToKeep;
+ }
+ replaceInterval(widgetClass.rightBracket.offset,
+ hasEmptyLineBeforeCreateState: endOfLastNodeToKeep != 0,
+ hasEmptyLineAfterCreateState: false);
+ }
+
+ // Create the State subclass.
builder.addInsertion(widgetClass.end, (builder) {
builder.writeln();
builder.writeln();
builder.writeClassDeclaration(stateName, superclass: stateType,
membersWriter: () {
- builder.write(buildText);
+ bool writeEmptyLine = false;
+ for (var member in nodesToMove) {
+ if (writeEmptyLine) {
+ builder.writeln();
+ }
+ String text = rewriteWidgetMemberReferences(member);
+ builder.write(text);
+ // Write empty lines between members, but not before the first.
+ writeEmptyLine = true;
+ }
});
});
});
diff --git a/pkg/analysis_server/test/services/correction/assist_test.dart b/pkg/analysis_server/test/services/correction/assist_test.dart
index 986c6ae..e165000 100644
--- a/pkg/analysis_server/test/services/correction/assist_test.dart
+++ b/pkg/analysis_server/test/services/correction/assist_test.dart
@@ -3058,6 +3058,314 @@
''');
}
+ test_flutterConvertToStatefulWidget_OK_empty() async {
+ addFlutterPackage();
+ await resolveTestUnit(r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new Container();
+ }
+}
+''');
+ _setCaretLocation();
+ await assertHasAssist(
+ DartAssistKind.FLUTTER_CONVERT_TO_STATEFUL_WIDGET, r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatefulWidget {
+ @override
+ MyWidgetState createState() {
+ return new MyWidgetState();
+ }
+}
+
+class MyWidgetState extends State<MyWidget> {
+ @override
+ Widget build(BuildContext context) {
+ return new Container();
+ }
+}
+''');
+ }
+
+ test_flutterConvertToStatefulWidget_OK_fields() async {
+ addFlutterPackage();
+ await resolveTestUnit(r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatelessWidget {
+ static String staticField1;
+ final String instanceField1;
+ final String instanceField2;
+ String instanceField3;
+ static String staticField2;
+ String instanceField4;
+ String instanceField5;
+ static String staticField3;
+
+ MyWidget(this.instanceField1) : instanceField2 = '' {
+ instanceField3 = '';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ instanceField4 = instanceField1;
+ return new Row(
+ children: [
+ new Text(instanceField1),
+ new Text(instanceField2),
+ new Text(instanceField3),
+ new Text(instanceField4),
+ new Text(instanceField5),
+ new Text(staticField1),
+ new Text(staticField2),
+ new Text(staticField3),
+ ],
+ );
+ }
+}
+''');
+ _setCaretLocation();
+ await assertHasAssist(
+ DartAssistKind.FLUTTER_CONVERT_TO_STATEFUL_WIDGET, r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatefulWidget {
+ static String staticField1;
+ final String instanceField1;
+ final String instanceField2;
+ String instanceField3;
+ static String staticField2;
+ static String staticField3;
+
+ MyWidget(this.instanceField1) : instanceField2 = '' {
+ instanceField3 = '';
+ }
+
+ @override
+ MyWidgetState createState() {
+ return new MyWidgetState();
+ }
+}
+
+class MyWidgetState extends State<MyWidget> {
+ String instanceField4;
+
+ String instanceField5;
+
+ @override
+ Widget build(BuildContext context) {
+ instanceField4 = widget.instanceField1;
+ return new Row(
+ children: [
+ new Text(widget.instanceField1),
+ new Text(widget.instanceField2),
+ new Text(widget.instanceField3),
+ new Text(instanceField4),
+ new Text(instanceField5),
+ new Text(MyWidget.staticField1),
+ new Text(MyWidget.staticField2),
+ new Text(MyWidget.staticField3),
+ ],
+ );
+ }
+}
+''');
+ }
+
+ test_flutterConvertToStatefulWidget_OK_getters() async {
+ addFlutterPackage();
+ await resolveTestUnit(r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new Row(
+ children: [
+ new Text(staticGetter1),
+ new Text(staticGetter2),
+ new Text(instanceGetter1),
+ new Text(instanceGetter2),
+ ],
+ );
+ }
+
+ static String get staticGetter1 => '';
+
+ String get instanceGetter1 => '';
+
+ static String get staticGetter2 => '';
+
+ String get instanceGetter2 => '';
+}
+''');
+ _setCaretLocation();
+ await assertHasAssist(
+ DartAssistKind.FLUTTER_CONVERT_TO_STATEFUL_WIDGET, r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatefulWidget {
+ @override
+ MyWidgetState createState() {
+ return new MyWidgetState();
+ }
+
+ static String get staticGetter1 => '';
+
+ static String get staticGetter2 => '';
+}
+
+class MyWidgetState extends State<MyWidget> {
+ @override
+ Widget build(BuildContext context) {
+ return new Row(
+ children: [
+ new Text(MyWidget.staticGetter1),
+ new Text(MyWidget.staticGetter2),
+ new Text(instanceGetter1),
+ new Text(instanceGetter2),
+ ],
+ );
+ }
+
+ String get instanceGetter1 => '';
+
+ String get instanceGetter2 => '';
+}
+''');
+ }
+
+ test_flutterConvertToStatefulWidget_OK_methods() async {
+ addFlutterPackage();
+ await resolveTestUnit(r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatelessWidget {
+ static String staticField;
+ final String instanceField1;
+ String instanceField2;
+
+ MyWidget(this.instanceField1);
+
+ @override
+ Widget build(BuildContext context) {
+ return new Row(
+ children: [
+ new Text(instanceField1),
+ new Text(instanceField2),
+ new Text(staticField),
+ ],
+ );
+ }
+
+ void instanceMethod1() {
+ instanceMethod1();
+ instanceMethod2();
+ staticMethod1();
+ }
+
+ static void staticMethod1() {
+ print('static 1');
+ }
+
+ void instanceMethod2() {
+ print('instance 2');
+ }
+
+ static void staticMethod2() {
+ print('static 2');
+ }
+}
+''');
+ _setCaretLocation();
+ await assertHasAssist(
+ DartAssistKind.FLUTTER_CONVERT_TO_STATEFUL_WIDGET, r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatefulWidget {
+ static String staticField;
+ final String instanceField1;
+
+ MyWidget(this.instanceField1);
+
+ @override
+ MyWidgetState createState() {
+ return new MyWidgetState();
+ }
+
+ static void staticMethod1() {
+ print('static 1');
+ }
+
+ static void staticMethod2() {
+ print('static 2');
+ }
+}
+
+class MyWidgetState extends State<MyWidget> {
+ String instanceField2;
+
+ @override
+ Widget build(BuildContext context) {
+ return new Row(
+ children: [
+ new Text(widget.instanceField1),
+ new Text(instanceField2),
+ new Text(MyWidget.staticField),
+ ],
+ );
+ }
+
+ void instanceMethod1() {
+ instanceMethod1();
+ instanceMethod2();
+ MyWidget.staticMethod1();
+ }
+
+ void instanceMethod2() {
+ print('instance 2');
+ }
+}
+''');
+ }
+
+ test_flutterConvertToStatefulWidget_OK_tail() async {
+ addFlutterPackage();
+ await resolveTestUnit(r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new Container();
+ }
+}
+''');
+ _setCaretLocation();
+ await assertHasAssist(
+ DartAssistKind.FLUTTER_CONVERT_TO_STATEFUL_WIDGET, r'''
+import 'package:flutter/material.dart';
+
+class /*caret*/MyWidget extends StatefulWidget {
+ @override
+ MyWidgetState createState() {
+ return new MyWidgetState();
+ }
+}
+
+class MyWidgetState extends State<MyWidget> {
+ @override
+ Widget build(BuildContext context) {
+ return new Container();
+ }
+}
+''');
+ }
+
test_flutterMoveWidgetDown_BAD_last() async {
addFlutterPackage();
await resolveTestUnit('''