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});
}
/**