Add a fix to create a mixin

Change-Id: Id0f31949a5639a00e884abc61b2cfcb5bc552b44
Reviewed-on: https://dart-review.googlesource.com/75130
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 769d2ca..ca5c026 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -148,6 +148,8 @@
       const FixKind('CREATE_METHOD', 50, "Create method '{0}'");
   static const CREATE_MISSING_OVERRIDES = const FixKind(
       'CREATE_MISSING_OVERRIDES', 49, "Create {0} missing override(s)");
+  static const CREATE_MIXIN =
+      const FixKind('CREATE_MIXIN', 50, "Create mixin '{0}'");
   static const CREATE_NO_SUCH_METHOD = const FixKind(
       'CREATE_NO_SUCH_METHOD', 51, "Create 'noSuchMethod' method");
   static const CONVERT_TO_NAMED_ARGUMENTS = const FixKind(
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index cb2d1a6..7a3a2ef 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -438,6 +438,7 @@
         errorCode == StaticWarningCode.UNDEFINED_CLASS) {
       await _addFix_importLibrary_withType();
       await _addFix_createClass();
+      await _addFix_createMixin();
       await _addFix_undefinedClass_useSimilar();
     }
     if (errorCode ==
@@ -461,6 +462,7 @@
       await _addFix_createField();
       await _addFix_createGetter();
       await _addFix_createFunction_forFunctionType();
+      await _addFix_createMixin();
       await _addFix_importLibrary_withType();
       await _addFix_importLibrary_withTopLevelVariable();
       await _addFix_createLocalVariable();
@@ -492,6 +494,7 @@
     if (errorCode == StaticTypeWarningCode.NON_TYPE_AS_TYPE_ARGUMENT) {
       await _addFix_importLibrary_withType();
       await _addFix_createClass();
+      await _addFix_createMixin();
     }
     if (errorCode == StaticTypeWarningCode.UNDEFINED_FUNCTION) {
       await _addFix_createClass();
@@ -508,6 +511,7 @@
       // TODO(brianwilkerson) The following were added because fasta produces
       // UNDEFINED_GETTER in places where analyzer produced UNDEFINED_IDENTIFIER
       await _addFix_createClass();
+      await _addFix_createMixin();
       await _addFix_createLocalVariable();
       await _addFix_importLibrary_withTopLevelVariable();
       await _addFix_importLibrary_withType();
@@ -2069,6 +2073,92 @@
     utils.targetExecutableElement = null;
   }
 
+  Future<void> _addFix_createMixin() async {
+    Element prefixElement = null;
+    String name = null;
+    SimpleIdentifier nameNode;
+    if (node is SimpleIdentifier) {
+      AstNode parent = node.parent;
+      if (parent is PrefixedIdentifier) {
+        if (parent.parent is InstanceCreationExpression) {
+          return;
+        }
+        PrefixedIdentifier prefixedIdentifier = parent;
+        prefixElement = prefixedIdentifier.prefix.staticElement;
+        if (prefixElement == null) {
+          return;
+        }
+        parent = prefixedIdentifier.parent;
+        nameNode = prefixedIdentifier.identifier;
+        name = prefixedIdentifier.identifier.name;
+      } else if (parent is TypeName &&
+          parent.parent is ConstructorName &&
+          parent.parent.parent is InstanceCreationExpression) {
+        return;
+      } else {
+        nameNode = node;
+        name = nameNode.name;
+      }
+      if (!_mayBeTypeIdentifier(nameNode)) {
+        return;
+      }
+    } else {
+      return;
+    }
+    // prepare environment
+    Element targetUnit;
+    String prefix = '';
+    String suffix = '';
+    int offset = -1;
+    String filePath;
+    if (prefixElement == null) {
+      targetUnit = unitElement;
+      CompilationUnitMember enclosingMember = node.getAncestor((node) =>
+          node is CompilationUnitMember && node.parent is CompilationUnit);
+      if (enclosingMember == null) {
+        return;
+      }
+      offset = enclosingMember.end;
+      filePath = file;
+      prefix = '$eol$eol';
+    } else {
+      for (ImportElement import in unitLibraryElement.imports) {
+        if (prefixElement is PrefixElement && import.prefix == prefixElement) {
+          LibraryElement library = import.importedLibrary;
+          if (library != null) {
+            targetUnit = library.definingCompilationUnit;
+            Source targetSource = targetUnit.source;
+            try {
+              offset = targetSource.contents.data.length;
+              filePath = targetSource.fullName;
+              prefix = '$eol';
+              suffix = '$eol';
+            } on FileSystemException {
+              // If we can't read the file to get the offset, then we can't
+              // create a fix.
+            }
+            break;
+          }
+        }
+      }
+    }
+    if (offset < 0) {
+      return;
+    }
+    DartChangeBuilder changeBuilder = new DartChangeBuilder(session);
+    await changeBuilder.addFileEdit(filePath, (DartFileEditBuilder builder) {
+      builder.addInsertion(offset, (DartEditBuilder builder) {
+        builder.write(prefix);
+        builder.writeMixinDeclaration(name, nameGroupName: 'NAME');
+        builder.write(suffix);
+      });
+      if (prefixElement == null) {
+        builder.addLinkedPosition(range.node(node), 'NAME');
+      }
+    });
+    _addFixFromBuilder(changeBuilder, DartFixKind.CREATE_MIXIN, args: [name]);
+  }
+
   Future<void> _addFix_createNoSuchMethod() async {
     // TODO(brianwilkerson) Determine whether this await is necessary.
     await null;
diff --git a/pkg/analysis_server/test/edit/fixes_test.dart b/pkg/analysis_server/test/edit/fixes_test.dart
index 1be66ae..4d07618 100644
--- a/pkg/analysis_server/test/edit/fixes_test.dart
+++ b/pkg/analysis_server/test/edit/fixes_test.dart
@@ -45,9 +45,10 @@
     expect(error.severity, AnalysisErrorSeverity.WARNING);
     expect(error.type, AnalysisErrorType.STATIC_WARNING);
     List<SourceChange> fixes = errorFixes[0].fixes;
-    expect(fixes, hasLength(2));
+    expect(fixes, hasLength(3));
     expect(fixes[0].message, matches('Import library'));
     expect(fixes[1].message, matches('Create class'));
+    expect(fixes[2].message, matches('Create mixin'));
   }
 
   test_fromPlugins() async {
diff --git a/pkg/analysis_server/test/services/correction/fix_test.dart b/pkg/analysis_server/test/services/correction/fix_test.dart
index 63b5a1c..34350b4 100644
--- a/pkg/analysis_server/test/services/correction/fix_test.dart
+++ b/pkg/analysis_server/test/services/correction/fix_test.dart
@@ -4129,6 +4129,146 @@
 ''');
   }
 
+  test_createMixin() async {
+    await resolveTestUnit('''
+main() {
+  Test v = null;
+}
+''');
+    await assertHasFix(DartFixKind.CREATE_MIXIN, '''
+main() {
+  Test v = null;
+}
+
+mixin Test {
+}
+''');
+    _assertLinkedGroup(change.linkedEditGroups[0], ['Test v =', 'Test {']);
+  }
+
+  test_createMixin_BAD_hasUnresolvedPrefix() async {
+    await resolveTestUnit('''
+main() {
+  prefix.Test v = null;
+}
+''');
+    await assertNoFix(DartFixKind.CREATE_MIXIN);
+  }
+
+  test_createMixin_BAD_instanceCreation_withNew() async {
+    await resolveTestUnit('''
+main() {
+  new Test();
+}
+''');
+    await assertNoFix(DartFixKind.CREATE_MIXIN);
+  }
+
+  test_createMixin_BAD_instanceCreation_withoutNew() async {
+    await resolveTestUnit('''
+main() {
+  Test();
+}
+''');
+    await assertNoFix(DartFixKind.CREATE_MIXIN);
+  }
+
+  test_createMixin_inLibraryOfPrefix() async {
+    String libCode = r'''
+library my.lib;
+
+class A {}
+''';
+    addSource('/project/lib.dart', libCode);
+    await resolveTestUnit('''
+import 'lib.dart' as lib;
+
+main() {
+  lib.A a = null;
+  lib.Test t = null;
+}
+''');
+    AnalysisError error = await _findErrorToFix();
+    fix = await _assertHasFix(DartFixKind.CREATE_MIXIN, error);
+    change = fix.change;
+    // apply to "lib.dart"
+    List<SourceFileEdit> fileEdits = change.edits;
+    expect(fileEdits, hasLength(1));
+    SourceFileEdit fileEdit = change.edits[0];
+    expect(fileEdit.file, convertPath('/project/lib.dart'));
+    expect(SourceEdit.applySequence(libCode, fileEdit.edits), r'''
+library my.lib;
+
+class A {}
+
+mixin Test {
+}
+''');
+    expect(change.linkedEditGroups, hasLength(1));
+  }
+
+  test_createMixin_innerLocalFunction() async {
+    await resolveTestUnit('''
+f() {
+  g() {
+    Test v = null;
+  }
+}
+''');
+    await assertHasFix(DartFixKind.CREATE_MIXIN, '''
+f() {
+  g() {
+    Test v = null;
+  }
+}
+
+mixin Test {
+}
+''');
+    _assertLinkedGroup(change.linkedEditGroups[0], ['Test v =', 'Test {']);
+  }
+
+  test_createMixin_itemOfList() async {
+    await resolveTestUnit('''
+main() {
+  var a = [Test];
+}
+''');
+    await assertHasFix(DartFixKind.CREATE_MIXIN, '''
+main() {
+  var a = [Test];
+}
+
+mixin Test {
+}
+''');
+    _assertLinkedGroup(change.linkedEditGroups[0], ['Test];', 'Test {']);
+  }
+
+  test_createMixin_itemOfList_inAnnotation() async {
+    errorFilter = (AnalysisError error) {
+      return error.errorCode == StaticWarningCode.UNDEFINED_IDENTIFIER;
+    };
+    await resolveTestUnit('''
+class MyAnnotation {
+  const MyAnnotation(a, b);
+}
+@MyAnnotation(int, const [Test])
+main() {}
+''');
+    await assertHasFix(DartFixKind.CREATE_MIXIN, '''
+class MyAnnotation {
+  const MyAnnotation(a, b);
+}
+@MyAnnotation(int, const [Test])
+main() {}
+
+mixin Test {
+}
+''');
+    _assertLinkedGroup(change.linkedEditGroups[0], ['Test])', 'Test {']);
+  }
+
   test_createNoSuchMethod_BAD_classTypeAlias() async {
     await resolveTestUnit('''
 abstract class A {
diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
index b78334b..682aac9 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
@@ -150,6 +150,8 @@
       write(' extends ');
       writeType(superclass, groupName: superclassGroupName);
     } else if (mixins != null && mixins.isNotEmpty) {
+      // TODO(brianwilkerson) Remove this branch when 2.1 semantics are
+      // supported everywhere.
       write(' extends Object ');
     }
     writeTypes(mixins, prefix: ' with ');
@@ -363,6 +365,29 @@
   }
 
   @override
+  void writeMixinDeclaration(String name,
+      {Iterable<DartType> interfaces,
+      void membersWriter(),
+      String nameGroupName,
+      Iterable<DartType> superclassConstraints}) {
+    // TODO(brianwilkerson) Add support for type parameters, probably as a
+    // parameterWriter parameter.
+    write('mixin ');
+    if (nameGroupName == null) {
+      write(name);
+    } else {
+      addSimpleLinkedEdit(nameGroupName, name);
+    }
+    writeTypes(superclassConstraints, prefix: ' on ');
+    writeTypes(interfaces, prefix: ' implements ');
+    writeln(' {');
+    if (membersWriter != null) {
+      membersWriter();
+    }
+    write('}');
+  }
+
+  @override
   void writeOverrideOfInheritedMember(ExecutableElement member,
       {StringBuffer displayTextBuffer, String returnTypeGroupName}) {
     void withCarbonCopyBuffer(f()) {
diff --git a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
index 741df0e..25b5c44 100644
--- a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
+++ b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
@@ -190,6 +190,19 @@
       String typeGroupName});
 
   /**
+   * Write the code for a declaration of a mixin with the given [name]. If a
+   * list of [interfaces] is provided, then the mixin will implement those
+   * interfaces. If a [membersWriter] is provided, then it will be invoked to
+   * allow members to be generated. If a [nameGroupName] is provided, then the
+   * name of the class will be included in the linked edit group with that name.
+   */
+  void writeMixinDeclaration(String name,
+      {Iterable<DartType> interfaces,
+      void membersWriter(),
+      String nameGroupName,
+      Iterable<DartType> superclassConstraints});
+
+  /**
    * Append a placeholder for an override of the specified inherited [member].
    * If provided, write a string value suitable for display (e.g., in a
    * completion popup) in the given [displayTextBuffer].
diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
index 15f5b60..7f0f6e9 100644
--- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
@@ -1088,6 +1088,96 @@
     expect(group.positions, hasLength(1));
   }
 
+  test_writeMixinDeclaration_interfaces() async {
+    String path = provider.convertPath('/test.dart');
+    addSource(path, 'class A {}');
+    DartType typeA = await _getType(path, 'A');
+
+    DartChangeBuilderImpl builder = new DartChangeBuilder(session);
+    await builder.addFileEdit(path, (FileEditBuilder builder) {
+      builder.addInsertion(0, (EditBuilder builder) {
+        (builder as DartEditBuilder)
+            .writeMixinDeclaration('M', interfaces: [typeA]);
+      });
+    });
+    SourceEdit edit = getEdit(builder);
+    expect(
+        edit.replacement, equalsIgnoringWhitespace('mixin M implements A { }'));
+  }
+
+  test_writeMixinDeclaration_interfacesAndSuperclassConstraints() async {
+    String path = provider.convertPath('/test.dart');
+    addSource(path, 'class A {} class B {}');
+    DartType typeA = await _getType(path, 'A');
+    DartType typeB = await _getType(path, 'B');
+
+    DartChangeBuilderImpl builder = new DartChangeBuilder(session);
+    await builder.addFileEdit(path, (FileEditBuilder builder) {
+      builder.addInsertion(0, (EditBuilder builder) {
+        (builder as DartEditBuilder).writeMixinDeclaration('M',
+            interfaces: [typeA], superclassConstraints: [typeB]);
+      });
+    });
+    SourceEdit edit = getEdit(builder);
+    expect(edit.replacement,
+        equalsIgnoringWhitespace('mixin M on B implements A { }'));
+  }
+
+  test_writeMixinDeclaration_memberWriter() async {
+    String path = provider.convertPath('/test.dart');
+    addSource(path, '');
+
+    DartChangeBuilderImpl builder = new DartChangeBuilder(session);
+    await builder.addFileEdit(path, (FileEditBuilder builder) {
+      builder.addInsertion(0, (EditBuilder builder) {
+        (builder as DartEditBuilder).writeMixinDeclaration('M',
+            membersWriter: () {
+          builder.write('/**/');
+        });
+      });
+    });
+    SourceEdit edit = getEdit(builder);
+    expect(edit.replacement, equalsIgnoringWhitespace('mixin M { /**/}'));
+  }
+
+  test_writeMixinDeclaration_nameGroupName() async {
+    String path = provider.convertPath('/test.dart');
+    addSource(path, '');
+
+    DartChangeBuilderImpl builder = new DartChangeBuilder(session);
+    await builder.addFileEdit(path, (FileEditBuilder builder) {
+      builder.addInsertion(0, (EditBuilder builder) {
+        (builder as DartEditBuilder)
+            .writeMixinDeclaration('M', nameGroupName: 'name');
+      });
+    });
+    SourceEdit edit = getEdit(builder);
+    expect(edit.replacement, equalsIgnoringWhitespace('mixin M { }'));
+
+    List<LinkedEditGroup> linkedEditGroups =
+        builder.sourceChange.linkedEditGroups;
+    expect(linkedEditGroups, hasLength(1));
+    LinkedEditGroup group = linkedEditGroups[0];
+    expect(group.length, 1);
+    expect(group.positions, hasLength(1));
+  }
+
+  test_writeMixinDeclaration_superclassConstraints() async {
+    String path = provider.convertPath('/test.dart');
+    addSource(path, 'class A {}');
+    DartType typeA = await _getType(path, 'A');
+
+    DartChangeBuilderImpl builder = new DartChangeBuilder(session);
+    await builder.addFileEdit(path, (FileEditBuilder builder) {
+      builder.addInsertion(0, (EditBuilder builder) {
+        (builder as DartEditBuilder)
+            .writeMixinDeclaration('M', superclassConstraints: [typeA]);
+      });
+    });
+    SourceEdit edit = getEdit(builder);
+    expect(edit.replacement, equalsIgnoringWhitespace('mixin M on A { }'));
+  }
+
   test_writeOverrideOfInheritedMember_getter_abstract() async {
     await _assertWriteOverrideOfInheritedAccessor('''
 abstract class A {