Add an assist to convert from a class to a mixin

Change-Id: Id47de8ffe8c381973c5a4ca53f7459782e62d37f
Reviewed-on: https://dart-review.googlesource.com/74963
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@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 5e797f0..1b98a13 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist.dart
@@ -12,6 +12,8 @@
       'dart.assist.addTypeAnnotation', 30, "Add type annotation");
   static const ASSIGN_TO_LOCAL_VARIABLE = const AssistKind(
       'dart.assist.assignToVariable', 30, "Assign value to new local variable");
+  static const CONVERT_CLASS_TO_MIXIN = const AssistKind(
+      'dart.assist.convert.classToMixin', 30, "Convert class to a mixin");
   static const CONVERT_DOCUMENTATION_INTO_BLOCK = const AssistKind(
       'dart.assist.convert.blockComment',
       30,
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 4768ba4..2dad5df 100644
--- a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart
@@ -127,6 +127,7 @@
     await _addProposal_addTypeAnnotation_SimpleFormalParameter();
     await _addProposal_addTypeAnnotation_VariableDeclaration();
     await _addProposal_assignToLocalVariable();
+    await _addProposal_convertClassToMixin();
     await _addProposal_convertIntoFinalField();
     await _addProposal_convertIntoGetter();
     await _addProposal_convertDocumentationIntoBlock();
@@ -431,6 +432,60 @@
     }
   }
 
+  Future<void> _addProposal_convertClassToMixin() async {
+    ClassDeclaration classDeclaration =
+        node.getAncestor((n) => n is ClassDeclaration);
+    if (classDeclaration == null) {
+      return;
+    }
+    if (selectionOffset > classDeclaration.name.end ||
+        selectionEnd < classDeclaration.classKeyword.offset) {
+      return;
+    }
+    if (classDeclaration.members
+        .any((member) => member is ConstructorDeclaration)) {
+      return;
+    }
+    _SuperclassReferenceFinder finder = new _SuperclassReferenceFinder();
+    classDeclaration.accept(finder);
+    List<ClassElement> referencedClasses = finder.referencedClasses;
+    List<InterfaceType> superclassConstraints = <InterfaceType>[];
+    List<InterfaceType> interfaces = <InterfaceType>[];
+
+    ClassElement classElement = classDeclaration.declaredElement;
+    for (InterfaceType type in classElement.mixins) {
+      if (referencedClasses.contains(type.element)) {
+        superclassConstraints.add(type);
+      } else {
+        interfaces.add(type);
+      }
+    }
+    ExtendsClause extendsClause = classDeclaration.extendsClause;
+    if (extendsClause != null) {
+      if (referencedClasses.length > superclassConstraints.length) {
+        superclassConstraints.insert(0, classElement.supertype);
+      } else {
+        interfaces.insert(0, classElement.supertype);
+      }
+    }
+    interfaces.addAll(classElement.interfaces);
+
+    DartChangeBuilder changeBuilder = new DartChangeBuilder(session);
+    await changeBuilder.addFileEdit(file, (DartFileEditBuilder builder) {
+      builder.addReplacement(
+          range.startStart(
+              classDeclaration.classKeyword, classDeclaration.leftBracket),
+          (DartEditBuilder builder) {
+        builder.write('mixin ');
+        builder.write(classDeclaration.name.name);
+        builder.writeTypes(superclassConstraints, prefix: ' on ');
+        builder.writeTypes(interfaces, prefix: ' implements ');
+        builder.write(' ');
+      });
+    });
+    _addAssistFromBuilder(changeBuilder, DartAssistKind.CONVERT_CLASS_TO_MIXIN);
+  }
+
   Future<void> _addProposal_convertDocumentationIntoBlock() async {
     // TODO(brianwilkerson) Determine whether this await is necessary.
     await null;
@@ -3386,3 +3441,43 @@
     visitor(node);
   }
 }
+
+/**
+ * A visitor used to find all of the classes that define members referenced via
+ * `super`.
+ */
+class _SuperclassReferenceFinder extends RecursiveAstVisitor {
+  final List<ClassElement> referencedClasses = <ClassElement>[];
+
+  _SuperclassReferenceFinder();
+
+  @override
+  visitSuperExpression(SuperExpression node) {
+    AstNode parent = node.parent;
+    if (parent is BinaryExpression) {
+      _addElement(parent.staticElement);
+    } else if (parent is IndexExpression) {
+      _addElement(parent.staticElement);
+    } else if (parent is MethodInvocation) {
+      _addIdentifier(parent.methodName);
+    } else if (parent is PrefixedIdentifier) {
+      _addIdentifier(parent.identifier);
+    } else if (parent is PropertyAccess) {
+      _addIdentifier(parent.propertyName);
+    }
+    return super.visitSuperExpression(node);
+  }
+
+  void _addElement(Element element) {
+    if (element is ExecutableElement) {
+      Element enclosingElement = element.enclosingElement;
+      if (enclosingElement is ClassElement) {
+        referencedClasses.add(enclosingElement);
+      }
+    }
+  }
+
+  void _addIdentifier(SimpleIdentifier identifier) {
+    _addElement(identifier.staticElement);
+  }
+}
diff --git a/pkg/analysis_server/test/services/correction/assist_test.dart b/pkg/analysis_server/test/services/correction/assist_test.dart
index 5b2765e..25aa478 100644
--- a/pkg/analysis_server/test/services/correction/assist_test.dart
+++ b/pkg/analysis_server/test/services/correction/assist_test.dart
@@ -746,6 +746,388 @@
     await assertNoAssistAt('f();', DartAssistKind.ASSIGN_TO_LOCAL_VARIABLE);
   }
 
+  test_convertClassToMixin_extends_noSuper() async {
+    await resolveTestUnit('''
+class A {}
+class B extends A {}
+''');
+    await assertHasAssistAt('B', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {}
+mixin B implements A {}
+''');
+  }
+
+  test_convertClassToMixin_extends_super() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B extends A {
+  b() {
+    super.a();
+  }
+}
+''');
+    await assertHasAssistAt('B', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+mixin B on A {
+  b() {
+    super.a();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extends_superSuper() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B extends A {}
+class C extends B {
+  c() {
+    super.a();
+  }
+}
+''');
+    await assertHasAssistAt('C', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B extends A {}
+mixin C on B {
+  c() {
+    super.a();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extendsImplements_noSuper() async {
+    await resolveTestUnit('''
+class A {}
+class B {}
+class C extends A implements B {}
+''');
+    await assertHasAssistAt('C', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {}
+class B {}
+mixin C implements A, B {}
+''');
+  }
+
+  test_convertClassToMixin_extendsImplements_super_extends() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B {}
+class C extends A implements B {
+  c() {
+    super.a();
+  }
+}
+''');
+    await assertHasAssistAt('C', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B {}
+mixin C on A implements B {
+  c() {
+    super.a();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extendsWith_noSuper() async {
+    await resolveTestUnit('''
+class A {}
+class B {}
+class C extends A with B {}
+''');
+    await assertHasAssistAt('C', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {}
+class B {}
+mixin C implements A, B {}
+''');
+  }
+
+  test_convertClassToMixin_extendsWith_super_both() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C extends A with B {
+  c() {
+    super.a();
+    super.b();
+  }
+}
+''');
+    await assertHasAssistAt('C', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+mixin C on A, B {
+  c() {
+    super.a();
+    super.b();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extendsWith_super_extends() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C extends A with B {
+  c() {
+    super.a();
+  }
+}
+''');
+    await assertHasAssistAt('C', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+mixin C on A implements B {
+  c() {
+    super.a();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extendsWith_super_with() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C extends A with B {
+  c() {
+    super.b();
+  }
+}
+''');
+    await assertHasAssistAt('C', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+mixin C on B implements A {
+  c() {
+    super.b();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extendsWithImplements_noSuper() async {
+    await resolveTestUnit('''
+class A {}
+class B {}
+class C {}
+class D extends A with B implements C {}
+''');
+    await assertHasAssistAt('D', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {}
+class B {}
+class C {}
+mixin D implements A, B, C {}
+''');
+  }
+
+  test_convertClassToMixin_extendsWithImplements_super_both() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C {}
+class D extends A with B implements C {
+  d() {
+    super.a();
+    super.b();
+  }
+}
+''');
+    await assertHasAssistAt('D', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C {}
+mixin D on A, B implements C {
+  d() {
+    super.a();
+    super.b();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extendsWithImplements_super_extends() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C {}
+class D extends A with B implements C {
+  d() {
+    super.a();
+  }
+}
+''');
+    await assertHasAssistAt('D', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C {}
+mixin D on A implements B, C {
+  d() {
+    super.a();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_extendsWithImplements_super_with() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C {}
+class D extends A with B implements C {
+  d() {
+    super.b();
+  }
+}
+''');
+    await assertHasAssistAt('D', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+class B {
+  b() {}
+}
+class C {}
+mixin D on B implements A, C {
+  d() {
+    super.b();
+  }
+}
+''');
+  }
+
+  test_convertClassToMixin_implements() async {
+    await resolveTestUnit('''
+class A {}
+class B implements A {}
+''');
+    await assertHasAssistAt('B', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {}
+mixin B implements A {}
+''');
+  }
+
+  test_convertClassToMixin_noClauses_invalidSelection() async {
+    await resolveTestUnit('''
+class A {}
+''');
+    await assertNoAssistAt(
+      '{}',
+      DartAssistKind.CONVERT_CLASS_TO_MIXIN,
+    );
+  }
+
+  test_convertClassToMixin_noClauses_selectKeyword() async {
+    await resolveTestUnit('''
+class A {}
+''');
+    await assertHasAssistAt('class', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+mixin A {}
+''');
+  }
+
+  test_convertClassToMixin_noClauses_selectName() async {
+    await resolveTestUnit('''
+class A {}
+''');
+    await assertHasAssistAt('A', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+mixin A {}
+''');
+  }
+
+  test_convertClassToMixin_with_noSuper() async {
+    await resolveTestUnit('''
+class A {}
+class B with A {}
+''');
+    await assertHasAssistAt('B', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {}
+mixin B implements A {}
+''');
+  }
+
+  test_convertClassToMixin_with_super() async {
+    await resolveTestUnit('''
+class A {
+  a() {}
+}
+class B with A {
+  b() {
+    super.a();
+  }
+}
+''');
+    await assertHasAssistAt('B', DartAssistKind.CONVERT_CLASS_TO_MIXIN, '''
+class A {
+  a() {}
+}
+mixin B on A {
+  b() {
+    super.a();
+  }
+}
+''');
+  }
+
   test_convertDocumentationIntoBlock_BAD_alreadyBlock() async {
     await resolveTestUnit('''
 /**
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 991fe82..b78334b 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
@@ -673,11 +673,7 @@
     }
   }
 
-  /**
-   * Write the code for a comma-separated list of [types], optionally prefixed
-   * by a [prefix]. If the list of [types] is `null` or does not contain any
-   * types, then nothing will be written.
-   */
+  @override
   void writeTypes(Iterable<DartType> types, {String prefix}) {
     if (types == null || types.isEmpty) {
       return;
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 d6e0b8e..741df0e 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
@@ -288,6 +288,13 @@
    */
   void writeTypeParameters(List<TypeParameterElement> typeParameters,
       {ExecutableElement methodBeingCopied});
+
+  /**
+   * Write the code for a comma-separated list of [types], optionally prefixed
+   * by a [prefix]. If the list of [types] is `null` or does not contain any
+   * types, then nothing will be written.
+   */
+  void writeTypes(Iterable<DartType> types, {String prefix});
 }
 
 /**