56e5655353
Change-Id: Iaeb2152d47df05434ab470901900da56cae7e792 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/196125 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
576 lines
17 KiB
Dart
576 lines
17 KiB
Dart
// 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_classNewKeyword() async {
|
|
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);
|
|
|
|
// 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),
|
|
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_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<void> _test_rename_withDocumentChanges(
|
|
String content,
|
|
String newName,
|
|
String? expectedContent, {
|
|
sendRenameVersion = true,
|
|
}) async {
|
|
// The specific number doesn't matter here, it's just a placeholder to confirm
|
|
// the values match.
|
|
final documentVersion = 222;
|
|
await initialize(
|
|
workspaceCapabilities:
|
|
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
|
|
);
|
|
await openFile(mainFileUri, withoutMarkers(content),
|
|
version: documentVersion);
|
|
|
|
final result = await rename(
|
|
mainFileUri,
|
|
sendRenameVersion ? documentVersion : null,
|
|
positionFromMarker(content),
|
|
newName,
|
|
);
|
|
if (expectedContent == null) {
|
|
expect(result, isNull);
|
|
} else {
|
|
// Ensure applying the changes will give us the expected content.
|
|
final contents = {
|
|
mainFilePath: withoutMarkers(content),
|
|
};
|
|
final documentVersions = {
|
|
mainFilePath: documentVersion,
|
|
};
|
|
applyDocumentChanges(
|
|
contents,
|
|
result!.documentChanges!,
|
|
expectedVersions: documentVersions,
|
|
);
|
|
expect(contents[mainFilePath], equals(expectedContent));
|
|
}
|
|
}
|
|
}
|