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 {