| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analyzer/file_system/memory_file_system.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; |
| import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/conflicting_edit_exception.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import 'mocks.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(ChangeBuilderImplTest); |
| defineReflectiveTests(EditBuilderImplTest); |
| defineReflectiveTests(FileEditBuilderImplTest); |
| defineReflectiveTests(LinkedEditBuilderImplTest); |
| }); |
| } |
| |
| abstract class AbstractChangeBuilderTest { |
| MemoryResourceProvider resourceProvider = MemoryResourceProvider(); |
| |
| late ChangeBuilderImpl builder; |
| |
| void setUp() { |
| builder = ChangeBuilderImpl(session: MockAnalysisSession(resourceProvider)); |
| } |
| } |
| |
| @reflectiveTest |
| class ChangeBuilderImplTest extends AbstractChangeBuilderTest { |
| void test_copy_empty() { |
| var copy = builder.copy() as ChangeBuilderImpl; |
| expect(identical(copy, builder), isFalse); |
| expect(copy.workspace, builder.workspace); |
| expect(copy.eol, builder.eol); |
| } |
| |
| Future<void> test_copy_newEdit() async { |
| await builder.addGenericFileEdit('/test.dart', (builder) { |
| builder.addSimpleInsertion(0, 'x'); |
| }); |
| var copy = builder.copy() as ChangeBuilderImpl; |
| await copy.addGenericFileEdit('/test.dart', (builder) { |
| builder.addSimpleInsertion(10, 'x'); |
| }); |
| var change = builder.sourceChange; |
| expect(change.edits[0].edits, hasLength(1)); |
| } |
| |
| Future<void> test_copy_newFile() async { |
| await builder.addGenericFileEdit('/test1.dart', (builder) { |
| builder.addSimpleInsertion(0, 'x'); |
| }); |
| var copy = builder.copy() as ChangeBuilderImpl; |
| await copy.addGenericFileEdit('/test2.dart', (builder) { |
| builder.addSimpleInsertion(0, 'x'); |
| }); |
| var change = builder.sourceChange; |
| expect(change.edits, hasLength(1)); |
| } |
| |
| Future<void> test_copy_newLinkedEditGroup() async { |
| await builder.addGenericFileEdit('/test.dart', (builder) { |
| builder.addLinkedPosition(SourceRange(1, 2), 'a'); |
| }); |
| var copy = builder.copy() as ChangeBuilderImpl; |
| await copy.addGenericFileEdit('/test.dart', (builder) { |
| builder.addLinkedPosition(SourceRange(3, 4), 'b'); |
| }); |
| var change = builder.sourceChange; |
| expect(change.linkedEditGroups, hasLength(1)); |
| } |
| |
| Future<void> test_copy_newLinkedPosition() async { |
| await builder.addGenericFileEdit('/test.dart', (builder) { |
| builder.addLinkedPosition(SourceRange(1, 2), 'a'); |
| }); |
| var copy = builder.copy() as ChangeBuilderImpl; |
| await copy.addGenericFileEdit('/test.dart', (builder) { |
| builder.addLinkedPosition(SourceRange(3, 4), 'a'); |
| }); |
| var change = builder.sourceChange; |
| expect(change.linkedEditGroups[0].positions, hasLength(1)); |
| } |
| |
| Future<void> test_copy_selection() async { |
| builder.setSelection(Position('/test.dart', 5)); |
| var copy = builder.copy() as ChangeBuilderImpl; |
| copy.setSelection(Position('/test.dart', 10)); |
| var change = builder.sourceChange; |
| expect(change.selection!.offset, 5); |
| } |
| |
| void test_getLinkedEditGroup() { |
| var group = builder.getLinkedEditGroup('a'); |
| expect(identical(builder.getLinkedEditGroup('b'), group), isFalse); |
| expect(identical(builder.getLinkedEditGroup('a'), group), isTrue); |
| } |
| |
| void test_setSelection() { |
| var position = Position('test.dart', 3); |
| builder.setSelection(position); |
| expect(builder.sourceChange.selection, position); |
| } |
| |
| void test_sourceChange_emptyEdit() async { |
| var path = '/test.dart'; |
| await builder.addGenericFileEdit(path, (builder) {}); |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| expect(sourceChange.edits, isEmpty); |
| expect(sourceChange.linkedEditGroups, isEmpty); |
| expect(sourceChange.message, isEmpty); |
| expect(sourceChange.selection, isNull); |
| } |
| |
| void test_sourceChange_noEdits() { |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| expect(sourceChange.edits, isEmpty); |
| expect(sourceChange.linkedEditGroups, isEmpty); |
| expect(sourceChange.message, isEmpty); |
| expect(sourceChange.selection, isNull); |
| } |
| |
| Future<void> test_sourceChange_oneChange() async { |
| var path = '/test.dart'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addSimpleInsertion(0, '_'); |
| }); |
| builder.getLinkedEditGroup('a'); |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| expect(sourceChange.edits, hasLength(1)); |
| expect(sourceChange.linkedEditGroups, hasLength(1)); |
| expect(sourceChange.message, isEmpty); |
| expect(sourceChange.selection, isNull); |
| } |
| } |
| |
| @reflectiveTest |
| class EditBuilderImplTest extends AbstractChangeBuilderTest { |
| String path = '/test.dart'; |
| |
| Future<void> test_addLinkedEdit() async { |
| var offset = 10; |
| var text = 'content'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (builder) { |
| builder.addLinkedEdit('a', (builder) { |
| builder.write(text); |
| }); |
| var sourceEdit = (builder as EditBuilderImpl).sourceEdit; |
| expect(sourceEdit.replacement, text); |
| }); |
| }); |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| var groups = sourceChange.linkedEditGroups; |
| expect(groups, hasLength(1)); |
| var group = groups[0]; |
| expect(group, isNotNull); |
| expect(group.length, text.length); |
| var positions = group.positions; |
| expect(positions, hasLength(1)); |
| expect(positions[0].offset, offset); |
| } |
| |
| Future<void> test_addSimpleLinkedEdit() async { |
| var offset = 10; |
| var text = 'content'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (builder) { |
| builder.addSimpleLinkedEdit('a', text); |
| var sourceEdit = (builder as EditBuilderImpl).sourceEdit; |
| expect(sourceEdit.replacement, text); |
| }); |
| }); |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| var groups = sourceChange.linkedEditGroups; |
| expect(groups, hasLength(1)); |
| var group = groups[0]; |
| expect(group, isNotNull); |
| expect(group.length, text.length); |
| var positions = group.positions; |
| expect(positions, hasLength(1)); |
| expect(positions[0].offset, offset); |
| } |
| |
| Future<void> test_createLinkedEditBuilder() async { |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (builder) { |
| var linkBuilder = |
| (builder as EditBuilderImpl).createLinkedEditBuilder(); |
| expect(linkBuilder, const TypeMatcher<LinkedEditBuilder>()); |
| }); |
| }); |
| } |
| |
| Future<void> test_selectHere() async { |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (EditBuilder builder) { |
| builder.selectHere(); |
| }); |
| }); |
| expect(builder.sourceChange.selection!.offset, 10); |
| } |
| |
| Future<void> test_write() async { |
| var offset = 10; |
| var text = 'write'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(offset, (builder) { |
| builder.write(text); |
| }); |
| }); |
| |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| |
| var fileEdits = sourceChange.edits; |
| expect(fileEdits, hasLength(1)); |
| var fileEdit = fileEdits[0]; |
| expect(fileEdit, isNotNull); |
| expect(fileEdit.file, path); |
| |
| var edits = fileEdit.edits; |
| expect(edits, hasLength(1)); |
| var edit = edits[0]; |
| expect(edit, isNotNull); |
| expect(edit.offset, offset); |
| expect(edit.length, 0); |
| expect(edit.replacement, text); |
| } |
| |
| Future<void> test_writeln_withoutText() async { |
| var offset = 52; |
| var length = 12; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addReplacement(SourceRange(offset, length), (builder) { |
| builder.writeln(); |
| }); |
| }); |
| |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| |
| var fileEdits = sourceChange.edits; |
| expect(fileEdits, hasLength(1)); |
| var fileEdit = fileEdits[0]; |
| expect(fileEdit, isNotNull); |
| expect(fileEdit.file, path); |
| |
| var edits = fileEdit.edits; |
| expect(edits, hasLength(1)); |
| var edit = edits[0]; |
| expect(edit, isNotNull); |
| expect(edit.offset, offset); |
| expect(edit.length, length); |
| expect(edit.replacement == '\n' || edit.replacement == '\r\n', isTrue); |
| } |
| |
| Future<void> test_writeln_withText() async { |
| var offset = 52; |
| var length = 12; |
| var text = 'writeln'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addReplacement(SourceRange(offset, length), (builder) { |
| builder.writeln(text); |
| }); |
| }); |
| |
| var sourceChange = builder.sourceChange; |
| expect(sourceChange, isNotNull); |
| |
| var fileEdits = sourceChange.edits; |
| expect(fileEdits, hasLength(1)); |
| var fileEdit = fileEdits[0]; |
| expect(fileEdit, isNotNull); |
| expect(fileEdit.file, path); |
| |
| var edits = fileEdit.edits; |
| expect(edits, hasLength(1)); |
| var edit = edits[0]; |
| expect(edit, isNotNull); |
| expect(edit.offset, offset); |
| expect(edit.length, length); |
| expect(edit.replacement == '$text\n' || edit.replacement == '$text\r\n', |
| isTrue); |
| } |
| } |
| |
| @reflectiveTest |
| class FileEditBuilderImplTest extends AbstractChangeBuilderTest { |
| String path = '/test.dart'; |
| |
| Future<void> test_addDeletion() async { |
| var offset = 23; |
| var length = 7; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addDeletion(SourceRange(offset, length)); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(1)); |
| expect(edits[0].offset, offset); |
| expect(edits[0].length, length); |
| expect(edits[0].replacement, isEmpty); |
| } |
| |
| Future<void> test_addDeletion_adjacent_lowerOffsetFirst() async { |
| // TODO(brianwilkerson) This should also merge the deletions, but is written |
| // to ensure that existing uses of FileEditBuilder continue to work even |
| // without that change. |
| var firstOffset = 23; |
| var firstLength = 7; |
| var secondOffset = 30; |
| var secondLength = 5; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addDeletion(SourceRange(firstOffset, firstLength)); |
| builder.addDeletion(SourceRange(secondOffset, secondLength)); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(2)); |
| expect(edits[0].offset, secondOffset); |
| expect(edits[0].length, secondLength); |
| expect(edits[0].replacement, isEmpty); |
| expect(edits[1].offset, firstOffset); |
| expect(edits[1].length, firstLength); |
| expect(edits[1].replacement, isEmpty); |
| } |
| |
| Future<void> test_addDeletion_adjacent_lowerOffsetSecond() async { |
| // TODO(brianwilkerson) This should also merge the deletions, but is written |
| // to ensure that existing uses of FileEditBuilder continue to work even |
| // without that change. |
| var firstOffset = 23; |
| var firstLength = 7; |
| var secondOffset = 30; |
| var secondLength = 5; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addDeletion(SourceRange(secondOffset, secondLength)); |
| builder.addDeletion(SourceRange(firstOffset, firstLength)); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(2)); |
| expect(edits[0].offset, secondOffset); |
| expect(edits[0].length, secondLength); |
| expect(edits[0].replacement, isEmpty); |
| expect(edits[1].offset, firstOffset); |
| expect(edits[1].length, firstLength); |
| expect(edits[1].replacement, isEmpty); |
| } |
| |
| @failingTest |
| Future<void> test_addDeletion_overlapping() async { |
| // This support is not yet implemented. |
| var firstOffset = 23; |
| var firstLength = 7; |
| var secondOffset = 27; |
| var secondLength = 8; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addDeletion(SourceRange(firstOffset, firstLength)); |
| builder.addDeletion(SourceRange(secondOffset, secondLength)); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(1)); |
| expect(edits[0].offset, firstOffset); |
| expect(edits[0].length, secondOffset + secondLength - firstOffset); |
| expect(edits[0].replacement, isEmpty); |
| } |
| |
| Future<void> test_addInsertion() async { |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (builder) { |
| expect(builder, isNotNull); |
| }); |
| }); |
| } |
| |
| Future<void> test_addInsertion_updatesLinkedPositions() async { |
| var groupName = 'a'; |
| var range = SourceRange(3, 6); |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addLinkedPosition(range, groupName); |
| // Insert 50 characters before the linked position. |
| builder.addInsertion(0, (builder) => builder.write('// ${'a' * 46}\n')); |
| }); |
| |
| var group = builder.getLinkedEditGroup(groupName); |
| var positions = group.positions; |
| expect(positions, hasLength(1)); |
| var position = positions[0]; |
| expect(position.file, path); |
| // Expect the linked position was moved along by the edit. |
| expect(position.offset, range.offset + 50); |
| expect(group.length, range.length); |
| } |
| |
| Future<void> test_addLinkedPosition() async { |
| var groupName = 'a'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addLinkedPosition(SourceRange(3, 6), groupName); |
| }); |
| |
| var group = builder.getLinkedEditGroup(groupName); |
| var positions = group.positions; |
| expect(positions, hasLength(1)); |
| var position = positions[0]; |
| expect(position.file, path); |
| expect(position.offset, 3); |
| expect(group.length, 6); |
| } |
| |
| Future<void> test_addReplacement() async { |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addReplacement(SourceRange(4, 5), (builder) { |
| expect(builder, isNotNull); |
| }); |
| }); |
| } |
| |
| Future<void> test_addSimpleInsertion() async { |
| var offset = 23; |
| var text = 'xyz'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addSimpleInsertion(offset, text); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(1)); |
| expect(edits[0].offset, offset); |
| expect(edits[0].length, 0); |
| expect(edits[0].replacement, text); |
| } |
| |
| Future<void> test_addSimpleInsertion_sameOffset() async { |
| var offset = 23; |
| var text = 'xyz'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addSimpleInsertion(offset, text); |
| builder.addSimpleInsertion(offset, 'abc'); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(2)); |
| expect(edits[0].offset, offset); |
| expect(edits[0].length, 0); |
| expect(edits[0].replacement, 'abc'); |
| expect(edits[1].offset, offset); |
| expect(edits[1].length, 0); |
| expect(edits[1].replacement, text); |
| } |
| |
| Future<void> test_addSimpleReplacement() async { |
| var offset = 23; |
| var length = 7; |
| var text = 'xyz'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addSimpleReplacement(SourceRange(offset, length), text); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(1)); |
| expect(edits[0].offset, offset); |
| expect(edits[0].length, length); |
| expect(edits[0].replacement, text); |
| } |
| |
| Future<void> test_addSimpleReplacement_adjacent() async { |
| var firstOffset = 23; |
| var firstLength = 7; |
| var secondOffset = firstOffset + firstLength; |
| var secondLength = 5; |
| var text = 'xyz'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addSimpleReplacement(SourceRange(firstOffset, firstLength), text); |
| builder.addSimpleReplacement( |
| SourceRange(secondOffset, secondLength), text); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(2)); |
| expect(edits[0].offset, secondOffset); |
| expect(edits[0].length, secondLength); |
| expect(edits[0].replacement, text); |
| expect(edits[1].offset, firstOffset); |
| expect(edits[1].length, firstLength); |
| expect(edits[1].replacement, text); |
| } |
| |
| Future<void> test_addSimpleReplacement_overlapsHead() async { |
| var offset = 23; |
| var length = 7; |
| var text = 'xyz'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addSimpleReplacement(SourceRange(offset, length), text); |
| expect(() { |
| builder.addSimpleReplacement(SourceRange(offset - 2, length), text); |
| }, throwsA(isA<ConflictingEditException>())); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(1)); |
| expect(edits[0].offset, offset); |
| expect(edits[0].length, length); |
| expect(edits[0].replacement, text); |
| } |
| |
| Future<void> test_addSimpleReplacement_overlapsTail() async { |
| var offset = 23; |
| var length = 7; |
| var text = 'xyz'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addSimpleReplacement(SourceRange(offset, length), text); |
| expect(() { |
| builder.addSimpleReplacement(SourceRange(offset + 2, length), text); |
| }, throwsA(isA<ConflictingEditException>())); |
| }); |
| var edits = builder.sourceChange.edits[0].edits; |
| expect(edits, hasLength(1)); |
| expect(edits[0].offset, offset); |
| expect(edits[0].length, length); |
| expect(edits[0].replacement, text); |
| } |
| |
| Future<void> test_createEditBuilder() async { |
| await builder.addGenericFileEdit(path, (builder) { |
| var offset = 4; |
| var length = 5; |
| var editBuilder = |
| (builder as FileEditBuilderImpl).createEditBuilder(offset, length); |
| expect(editBuilder, const TypeMatcher<EditBuilder>()); |
| var sourceEdit = editBuilder.sourceEdit; |
| expect(sourceEdit.length, length); |
| expect(sourceEdit.offset, offset); |
| expect(sourceEdit.replacement, isEmpty); |
| }); |
| } |
| } |
| |
| @reflectiveTest |
| class LinkedEditBuilderImplTest extends AbstractChangeBuilderTest { |
| String path = '/test.dart'; |
| |
| Future<void> test_addSuggestion() async { |
| var groupName = 'a'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (builder) { |
| builder.addLinkedEdit(groupName, (builder) { |
| builder.write('A'); |
| builder.addSuggestion(LinkedEditSuggestionKind.TYPE, 'B'); |
| }); |
| }); |
| }); |
| |
| var group = builder.getLinkedEditGroup(groupName); |
| expect(group.suggestions, hasLength(1)); |
| } |
| |
| Future<void> test_addSuggestion_zeroLength() async { |
| var groupName = 'a'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (builder) { |
| builder.addLinkedEdit(groupName, (builder) { |
| builder.addSuggestion(LinkedEditSuggestionKind.TYPE, 'A'); |
| }); |
| }); |
| }); |
| |
| expect(builder.sourceChange.linkedEditGroups, isEmpty); |
| } |
| |
| Future<void> test_addSuggestions() async { |
| var groupName = 'a'; |
| await builder.addGenericFileEdit(path, (builder) { |
| builder.addInsertion(10, (builder) { |
| builder.addLinkedEdit(groupName, (builder) { |
| builder.write('A'); |
| builder.addSuggestions(LinkedEditSuggestionKind.TYPE, ['B', 'C']); |
| }); |
| }); |
| }); |
| |
| var group = builder.getLinkedEditGroup(groupName); |
| expect(group.suggestions, hasLength(2)); |
| } |
| } |