blob: f62944d98c764979e3f2000a2a7c5043ffc41059 [file] [log] [blame]
// Copyright (c) 2019, 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:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../tool/lsp_spec/matchers.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(RenameTest);
});
}
@reflectiveTest
class RenameTest extends AbstractLspAnalysisServerTest {
Future<void> test_prepare_class() {
const content = '''
class MyClass {}
final a = new [[My^Class]]();
''';
return _test_prepare(content, 'MyClass');
}
Future<void> test_prepare_classNewKeyword() async {
const content = '''
class MyClass {}
final a = n^ew [[MyClass]]();
''';
return _test_prepare(content, 'MyClass');
}
Future<void> test_prepare_importPrefix() async {
const content = '''
import 'dart:async' as [[myPr^efix]];
''';
return _test_prepare(content, 'myPrefix');
}
Future<void> test_prepare_importWithoutPrefix() async {
const content = '''
imp[[^]]ort 'dart:async';
''';
return _test_prepare(content, '');
}
Future<void> test_prepare_importWithPrefix() async {
const content = '''
imp^ort 'dart:async' as [[myPrefix]];
''';
return _test_prepare(content, 'myPrefix');
}
Future<void> test_prepare_invalidRenameLocation() async {
const content = '''
main() {
// comm^ent
}
''';
return _test_prepare(content, null);
}
Future<void> test_prepare_sdkClass() async {
const content = '''
final a = new [[Ob^ject]]();
''';
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final request = makeRequest(
Method.textDocument_prepareRename,
TextDocumentPositionParams(
textDocument: TextDocumentIdentifier(uri: mainFileUri.toString()),
position: positionFromMarker(content),
),
);
final response = await channel.sendRequestToServer(request);
expect(response.id, equals(request.id));
expect(response.result, isNull);
expect(response.error, isNotNull);
expect(response.error!.code, ServerErrorCodes.RenameNotValid);
expect(response.error!.message, contains('is defined in the SDK'));
}
Future<void> test_prepare_variable() async {
const content = '''
main() {
var variable = 0;
print([[vari^able]]);
}
''';
return _test_prepare(content, 'variable');
}
Future<void> test_rename_class() {
const content = '''
class MyClass {}
final a = new [[My^Class]]();
''';
const expectedContent = '''
class MyNewClass {}
final a = new MyNewClass();
''';
return _test_rename_withDocumentChanges(
content, 'MyNewClass', expectedContent);
}
Future<void> test_rename_class_doesNotRenameFileIfDisabled() async {
const content = '''
class Main {}
final a = new [[Ma^in]]();
''';
const expectedContent = '''
class MyNewMain {}
final a = new MyNewMain();
''';
final contents = await _test_rename_withDocumentChanges(
content, 'MyNewMain', expectedContent);
// Ensure the only file is still called main.dart.
expect(contents.keys.single, equals(mainFilePath));
}
Future<void> test_rename_class_doesRenameFileAfterPrompt() async {
const content = '''
class Main {}
final a = new [[Ma^in]]();
''';
const expectedContent = '''
class MyNewMain {}
final a = new MyNewMain();
''';
final newMainFilePath = join(projectFolderPath, 'lib', 'my_new_main.dart');
/// Helper that performs the rename and tests the results. Passed below to
/// provideConfig() so we can wrap this with handlers to provide the
/// required config.
Future<void> testRename() async {
final contents = await _test_rename_withDocumentChanges(
content,
'MyNewMain',
expectedContent,
expectedFilePath: newMainFilePath,
workspaceCapabilities: withConfigurationSupport(
withResourceOperationKinds(emptyWorkspaceClientCapabilities,
[ResourceOperationKind.Rename])),
);
// Ensure we now only have the newly-renamed file and not the old one
// (its contents will have been checked by the function above).
expect(contents.keys.single, equals(newMainFilePath));
}
/// Helper that will respond to the window/showMessageRequest request from
/// the server when prompted about renaming the file.
Future<MessageActionItem> promptHandler(
ShowMessageRequestParams params) async {
// Ensure the prompt is as expected.
expect(params.type, equals(MessageType.Info));
expect(
params.message, equals("Rename 'main.dart' to 'my_new_main.dart'?"));
expect(params.actions, hasLength(2));
expect(params.actions![0],
equals(MessageActionItem(title: UserPromptActions.yes)));
expect(params.actions![1],
equals(MessageActionItem(title: UserPromptActions.no)));
// Respond to the request with the required action.
return params.actions!.first;
}
// Run the test and provide the config + prompt handling function.
return handleExpectedRequest(
Method.window_showMessageRequest,
ShowMessageRequestParams.fromJson,
() => provideConfig(
testRename,
{'renameFilesWithClasses': 'prompt'},
),
handler: promptHandler,
);
}
Future<void> test_rename_class_doesRenameFileIfAlwaysEnabled() async {
const content = '''
class Main {}
final a = new [[Ma^in]]();
''';
const expectedContent = '''
class MyNewMain {}
final a = new MyNewMain();
''';
final newMainFilePath = join(projectFolderPath, 'lib', 'my_new_main.dart');
await provideConfig(
() async {
final contents = await _test_rename_withDocumentChanges(
content,
'MyNewMain',
expectedContent,
expectedFilePath: newMainFilePath,
workspaceCapabilities: withConfigurationSupport(
withResourceOperationKinds(emptyWorkspaceClientCapabilities,
[ResourceOperationKind.Rename])),
);
// Ensure we now only have the newly-renamed file and not the old one
// (its contents will have been checked by the function above).
expect(contents.keys.single, equals(newMainFilePath));
},
{'renameFilesWithClasses': 'always'},
);
}
Future<void> test_rename_class_doesRenameFileIfRenamedFromAnother() async {
const mainContent = '''
class Main {}
''';
const otherContent = '''
import 'main.dart';
final a = Ma^in();
''';
const expectedContent = '''
class MyNewMain {}
''';
// Since we don't actually perform the file rename (we only include an
// instruction for the client to do so), the import will not be updated
// by us. Instead, the client will send the rename event back to the server
// and it would be handled normally as if the user had done it locally.
const expectedOtherContent = '''
import 'main.dart';
final a = MyNewMain();
''';
final otherFilePath = join(projectFolderPath, 'lib', 'other.dart');
final newMainFilePath = join(projectFolderPath, 'lib', 'my_new_main.dart');
newFile(mainFilePath, content: withoutMarkers(mainContent));
await pumpEventQueue(times: 5000);
await provideConfig(
() async {
final contents = await _test_rename_withDocumentChanges(
otherContent,
'MyNewMain',
expectedOtherContent,
filePath: otherFilePath,
contents: {
otherFilePath: otherContent,
mainFilePath: mainContent,
},
workspaceCapabilities: withConfigurationSupport(
withResourceOperationKinds(emptyWorkspaceClientCapabilities,
[ResourceOperationKind.Rename])),
);
// Expect that main was renamed to my_new_main and the other file was
// updated.
expect(contents.containsKey(mainFilePath), isFalse);
expect(contents.containsKey(newMainFilePath), isTrue);
expect(contents.containsKey(otherFilePath), isTrue);
expect(contents[newMainFilePath], expectedContent);
expect(contents[otherFilePath], expectedOtherContent);
},
{'renameFilesWithClasses': 'always'},
);
}
Future<void> test_rename_classNewKeyword() {
const content = '''
class MyClass {}
final a = n^ew MyClass();
''';
const expectedContent = '''
class MyNewClass {}
final a = new MyNewClass();
''';
return _test_rename_withDocumentChanges(
content, 'MyNewClass', expectedContent);
}
Future<void> test_rename_duplicateName_applyAfterDocumentChanges() async {
// Perform a refactor that results in a prompt to the user, but then modify
// the document before accepting/rejecting to make the rename invalid.
const content = '''
class MyOtherClass {}
class MyClass {}
final a = n^ew MyClass();
''';
final result = await _test_rename_prompt(
content,
'MyOtherClass',
expectedMessage:
'Library already declares class with name \'MyOtherClass\'.',
action: UserPromptActions.renameAnyway,
beforeResponding: () => replaceFile(999, mainFileUri, 'Updated content'),
);
expect(result.result, isNull);
expect(result.error, isNotNull);
expect(result.error, isResponseError(ErrorCodes.ContentModified));
}
Future<void> test_rename_duplicateName_applyAnyway() async {
const content = '''
class MyOtherClass {}
class MyClass {}
final a = n^ew MyClass();
''';
const expectedContent = '''
class MyOtherClass {}
class MyOtherClass {}
final a = new MyOtherClass();
''';
final response = await _test_rename_prompt(
content,
'MyOtherClass',
expectedMessage:
'Library already declares class with name \'MyOtherClass\'.',
action: UserPromptActions.renameAnyway,
);
final error = response.error;
if (error != null) {
throw error;
}
final result =
WorkspaceEdit.fromJson(response.result as Map<String, Object?>);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(
contents,
result.documentChanges!,
);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_rename_duplicateName_reject() async {
const content = '''
class MyOtherClass {}
class MyClass {}
final a = n^ew MyClass();
''';
final response = await _test_rename_prompt(
content,
'MyOtherClass',
expectedMessage:
'Library already declares class with name \'MyOtherClass\'.',
action: UserPromptActions.cancel,
);
// Expect a successful empty response if cancelled.
expect(response.error, isNull);
expect(
WorkspaceEdit.fromJson(response.result as Map<String, Object?>),
equals(emptyWorkspaceEdit),
);
}
Future<void> test_rename_importPrefix() {
const content = '''
import 'dart:async' as myPr^efix;
''';
const expectedContent = '''
import 'dart:async' as myNewPrefix;
''';
return _test_rename_withDocumentChanges(
content, 'myNewPrefix', expectedContent);
}
Future<void> test_rename_importWithoutPrefix() {
const content = '''
imp^ort 'dart:async';
''';
const expectedContent = '''
import 'dart:async' as myAddedPrefix;
''';
return _test_rename_withDocumentChanges(
content, 'myAddedPrefix', expectedContent);
}
Future<void> test_rename_importWithPrefix() {
const content = '''
imp^ort 'dart:async' as myPrefix;
''';
const expectedContent = '''
import 'dart:async' as myNewPrefix;
''';
return _test_rename_withDocumentChanges(
content, 'myNewPrefix', expectedContent);
}
Future<void> test_rename_invalidRenameLocation() {
const content = '''
main() {
// comm^ent
}
''';
return _test_rename_withDocumentChanges(content, 'MyNewClass', null);
}
Future<void> test_rename_multipleFiles() async {
final referencedFilePath =
join(projectFolderPath, 'lib', 'referenced.dart');
final referencedFileUri = Uri.file(referencedFilePath);
const mainContent = '''
import 'referenced.dart';
final a = new My^Class();
''';
const referencedContent = '''
class MyClass {}
''';
const expectedMainContent = '''
import 'referenced.dart';
final a = new MyNewClass();
''';
const expectedReferencedContent = '''
class MyNewClass {}
''';
const mainVersion = 111;
const referencedVersion = 222;
await initialize(
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
);
await openFile(mainFileUri, withoutMarkers(mainContent),
version: mainVersion);
await openFile(referencedFileUri, withoutMarkers(referencedContent),
version: referencedVersion);
final result = (await rename(
mainFileUri,
mainVersion,
positionFromMarker(mainContent),
'MyNewClass',
))!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(mainContent),
referencedFilePath: withoutMarkers(referencedContent),
};
final documentVersions = {
mainFilePath: mainVersion,
referencedFilePath: referencedVersion,
};
applyDocumentChanges(
contents,
result.documentChanges!,
expectedVersions: documentVersions,
);
expect(contents[mainFilePath], equals(expectedMainContent));
expect(contents[referencedFilePath], equals(expectedReferencedContent));
}
Future<void> test_rename_nonClass_doesNotRenameFile() async {
const content = '''
final Ma^in = 'test';
''';
const expectedContent = '''
final MyNewMain = 'test';
''';
await provideConfig(
() async {
final contents = await _test_rename_withDocumentChanges(
content,
'MyNewMain',
expectedContent,
workspaceCapabilities: withConfigurationSupport(
withResourceOperationKinds(emptyWorkspaceClientCapabilities,
[ResourceOperationKind.Rename])),
);
// Ensure the only file is still called main.dart.
expect(contents.keys.single, equals(mainFilePath));
},
{'renameFilesWithClasses': 'always'},
);
}
Future<void> test_rename_rejectedForBadName() async {
const content = '''
class MyClass {}
final a = n^ew MyClass();
''';
final error = await _test_rename_failure(content, 'not a valid class name');
expect(error.code, equals(ServerErrorCodes.RenameNotValid));
expect(error.message, contains('name must not contain'));
}
Future<void> test_rename_rejectedForSameName() async {
const content = '''
class My^Class {}
''';
final error = await _test_rename_failure(content, 'MyClass');
expect(error.code, equals(ServerErrorCodes.RenameNotValid));
expect(error.message,
contains('new name must be different than the current name'));
}
Future<void> test_rename_rejectedForStaleDocument() async {
const content = '''
class MyClass {}
final a = n^ew MyClass();
''';
final error =
await _test_rename_failure(content, 'MyNewClass', openFileVersion: 111);
expect(error.code, equals(ErrorCodes.ContentModified));
expect(error.message, contains('Document was modified'));
}
Future<void> test_rename_rejectionsDoNotCrashServer() async {
// Checks that a rename failure does not stop the server from responding
// as was previously the case in https://github.com/dart-lang/sdk/issues/42573
// because the error code was duplicated/reused for ClientServerInconsistentState.
const content = '''
/// Test Class
class My^Class {}
''';
final error = await _test_rename_failure(content, 'MyClass');
expect(error.code, isNotNull);
// Send any other request to ensure the server is still responsive.
final hover = await getHover(mainFileUri, positionFromMarker(content));
expect(hover?.contents, isNotNull);
}
Future<void> test_rename_sdkClass() async {
const content = '''
final a = new [[Ob^ject]]();
''';
newFile(mainFilePath, content: withoutMarkers(content));
await initialize();
final request = makeRequest(
Method.textDocument_rename,
RenameParams(
newName: 'Object2',
textDocument: TextDocumentIdentifier(uri: mainFileUri.toString()),
position: positionFromMarker(content),
),
);
final response = await channel.sendRequestToServer(request);
expect(response.id, equals(request.id));
expect(response.result, isNull);
expect(response.error, isNotNull);
expect(response.error!.code, ServerErrorCodes.RenameNotValid);
expect(response.error!.message, contains('is defined in the SDK'));
}
Future<void> test_rename_usingLegacyChangeInterface() async {
// This test initializes without support for DocumentChanges (versioning)
// whereas the other tests all use DocumentChanges support (preferred).
const content = '''
class MyClass {}
final a = new My^Class();
''';
const expectedContent = '''
class MyNewClass {}
final a = new MyNewClass();
''';
await initialize();
await openFile(mainFileUri, withoutMarkers(content), version: 222);
final result = (await rename(
mainFileUri,
222,
positionFromMarker(content),
'MyNewClass',
))!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, result.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_rename_variable() {
const content = '''
main() {
var variable = 0;
print([[vari^able]]);
}
''';
const expectedContent = '''
main() {
var foo = 0;
print(foo);
}
''';
return _test_rename_withDocumentChanges(content, 'foo', expectedContent);
}
Future<void> test_rename_withoutVersionedIdentifier() {
// Without sending a document version, the rename should still work because
// the server should use the version it had at the start of the rename
// operation.
const content = '''
class MyClass {}
final a = new [[My^Class]]();
''';
const expectedContent = '''
class MyNewClass {}
final a = new MyNewClass();
''';
return _test_rename_withDocumentChanges(
content, 'MyNewClass', expectedContent,
sendRenameVersion: false);
}
Future<void> _test_prepare(
String content, String? expectedPlaceholder) async {
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final result =
await prepareRename(mainFileUri, positionFromMarker(content));
if (expectedPlaceholder == null) {
expect(result, isNull);
} else {
expect(result!.range, equals(rangeFromMarkers(content)));
expect(result.placeholder, equals(expectedPlaceholder));
}
}
Future<ResponseError> _test_rename_failure(
String content,
String newName, {
int openFileVersion = 222,
int renameRequestFileVersion = 222,
}) async {
await initialize(
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
);
await openFile(mainFileUri, withoutMarkers(content),
version: openFileVersion);
final result = await renameRaw(
mainFileUri,
renameRequestFileVersion,
positionFromMarker(content),
newName,
);
expect(result.result, isNull);
expect(result.error, isNotNull);
return result.error!;
}
/// Tests a rename that is expected to cause an error, which will trigger
/// a ShowMessageRequest from the server to the client to allow the refactor
/// to be continued or rejected.
Future<ResponseMessage> _test_rename_prompt(
String content,
String newName, {
required String expectedMessage,
Future<void> Function()? beforeResponding,
required String action,
int openFileVersion = 222,
int renameRequestFileVersion = 222,
}) async {
await initialize(
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
);
await openFile(mainFileUri, withoutMarkers(content),
version: openFileVersion);
// Expect the server to call us back with a ShowMessageRequest prompt about
// the errors for us to accept/reject.
return handleExpectedRequest(
Method.window_showMessageRequest,
ShowMessageRequestParams.fromJson,
() => renameRaw(
mainFileUri,
renameRequestFileVersion,
positionFromMarker(content),
newName,
),
handler: (ShowMessageRequestParams params) async {
// Ensure the warning prompt is as expected.
expect(params.type, equals(MessageType.Warning));
expect(params.message, equals(expectedMessage));
expect(params.actions, hasLength(2));
expect(params.actions![0],
equals(MessageActionItem(title: UserPromptActions.renameAnyway)));
expect(params.actions![1],
equals(MessageActionItem(title: UserPromptActions.cancel)));
// Allow the test to run some code before we send the response.
await beforeResponding?.call();
// Respond to the request with the required action.
return MessageActionItem(title: action);
},
);
}
Future<Map<String, String>> _test_rename_withDocumentChanges(
String content,
String newName,
String? expectedContent, {
String? filePath,
String? expectedFilePath,
bool sendRenameVersion = true,
ClientCapabilitiesWorkspace? workspaceCapabilities,
Map<String, String>? contents,
}) async {
contents ??= {};
filePath ??= mainFilePath;
expectedFilePath ??= filePath;
final fileUri = Uri.file(filePath);
// The specific number doesn't matter here, it's just a placeholder to confirm
// the values match.
final documentVersion = 222;
contents[filePath] = withoutMarkers(content);
final documentVersions = {
filePath: documentVersion,
};
final initialAnalysis = waitForAnalysisComplete();
await initialize(
workspaceCapabilities: withDocumentChangesSupport(
workspaceCapabilities ?? emptyWorkspaceClientCapabilities),
);
await openFile(fileUri, withoutMarkers(content), version: documentVersion);
await initialAnalysis;
final result = await rename(
fileUri,
sendRenameVersion ? documentVersion : null,
positionFromMarker(content),
newName,
);
if (expectedContent == null) {
expect(result, isNull);
} else {
// Ensure applying the changes will give us the expected content.
applyDocumentChanges(
contents,
result!.documentChanges!,
expectedVersions: documentVersions,
);
expect(contents[expectedFilePath], equals(expectedContent));
}
return contents;
}
}