Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit ece17f7

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
[Analyzer] Ask user whether to proceed with renames that produce errors
Fixes Dart-Code/Dart-Code#2745. Change-Id: Ia2770f18ebca410bce93bd2f3042e294083dc435 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/167621 Commit-Queue: Danny Tuppeny <danny@tuppeny.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
1 parent 7d994c1 commit ece17f7

File tree

4 files changed

+171
-13
lines changed

4 files changed

+171
-13
lines changed

pkg/analysis_server/lib/src/lsp/constants.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const dartTypeFormattingCharacters = ['}', ';'];
5252
/// A [ProgressToken] used for reporting progress when the server is analyzing.
5353
final analyzingProgressToken = Either2<num, String>.t2('ANALYZING');
5454

55+
final emptyWorkspaceEdit = WorkspaceEdit();
56+
5557
/// Constants for command IDs that are exchanged between LSP client/server.
5658
abstract class Commands {
5759
/// A list of all commands IDs that can be sent to the client to inform which
@@ -127,3 +129,9 @@ abstract class ServerErrorCodes {
127129
/// if it crashes 5 times in the last 180 seconds."
128130
static const ClientServerInconsistentState = ErrorCodes(-32099);
129131
}
132+
133+
/// Strings used in user prompts (window/showMessageRequest).
134+
abstract class UserPromptActions {
135+
static const String cancel = 'Cancel';
136+
static const String renameAnyway = 'Rename Anyway';
137+
}

pkg/analysis_server/lib/src/lsp/handlers/handler_rename.dart

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class RenameHandler extends MessageHandler<RenameParams, WorkspaceEdit> {
126126
if (token.isCancellationRequested) {
127127
return cancelled();
128128
}
129-
if (initStatus.hasError) {
129+
if (initStatus.hasFatalError) {
130130
return error(
131131
ServerErrorCodes.RenameNotValid, initStatus.problem.message, null);
132132
}
@@ -144,9 +144,30 @@ class RenameHandler extends MessageHandler<RenameParams, WorkspaceEdit> {
144144
if (token.isCancellationRequested) {
145145
return cancelled();
146146
}
147-
if (finalStatus.hasError) {
147+
if (finalStatus.hasFatalError) {
148148
return error(
149149
ServerErrorCodes.RenameNotValid, finalStatus.problem.message, null);
150+
} else if (finalStatus.hasError || finalStatus.hasWarning) {
151+
// Ask the user whether to proceed with the rename.
152+
final userChoice = await server.showUserPrompt(
153+
MessageType.Warning,
154+
finalStatus.message,
155+
[
156+
MessageActionItem(title: UserPromptActions.renameAnyway),
157+
MessageActionItem(title: UserPromptActions.cancel),
158+
],
159+
);
160+
161+
if (token.isCancellationRequested) {
162+
return cancelled();
163+
}
164+
165+
if (userChoice.title != UserPromptActions.renameAnyway) {
166+
// Return an empty workspace edit response so we do not perform any
167+
// rename, but also so we do not cause the client to show the user an
168+
// error after they clicked cancel.
169+
return success(emptyWorkspaceEdit);
170+
}
150171
}
151172

152173
// Compute the actual change.

pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,17 @@ class LspAnalysisServer extends AbstractAnalysisServer {
599599
));
600600
}
601601

602+
/// Shows the user a prompt with some actions to select using ShowMessageRequest.
603+
Future<MessageActionItem> showUserPrompt(
604+
MessageType type, String message, List<MessageActionItem> actions) async {
605+
final response = await sendRequest(
606+
Method.window_showMessageRequest,
607+
ShowMessageRequestParams(type: type, message: message, actions: actions),
608+
);
609+
610+
return MessageActionItem.fromJson(response.result);
611+
}
612+
602613
Future<void> shutdown() {
603614
// Defer closing the channel so that the shutdown response can be sent and
604615
// logged.

pkg/analysis_server/test/lsp/rename_test.dart

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
66
import 'package:analysis_server/src/lsp/constants.dart';
7+
import 'package:meta/meta.dart';
78
import 'package:test/test.dart';
89
import 'package:test_reflective_loader/test_reflective_loader.dart';
910

11+
import '../tool/lsp_spec/matchers.dart';
1012
import 'server_abstract.dart';
1113

1214
void main() {
@@ -130,6 +132,84 @@ class RenameTest extends AbstractLspAnalysisServerTest {
130132
content, 'MyNewClass', expectedContent);
131133
}
132134

135+
Future<void> test_rename_duplicateName_applyAfterDocumentChanges() async {
136+
// Perform a refactor that results in a prompt to the user, but then modify
137+
// the document before accepting/rejecting to make the rename invalid.
138+
const content = '''
139+
class MyOtherClass {}
140+
class MyClass {}
141+
final a = n^ew MyClass();
142+
''';
143+
final result = await _test_rename_prompt(
144+
content,
145+
'MyOtherClass',
146+
expectedMessage:
147+
'Library already declares class with name \'MyOtherClass\'.',
148+
action: UserPromptActions.renameAnyway,
149+
beforeResponding: () => replaceFile(999, mainFileUri, 'Updated content'),
150+
);
151+
expect(result.result, isNull);
152+
expect(result.error, isNotNull);
153+
expect(result.error, isResponseError(ErrorCodes.ContentModified));
154+
}
155+
156+
Future<void> test_rename_duplicateName_applyAnyway() async {
157+
const content = '''
158+
class MyOtherClass {}
159+
class MyClass {}
160+
final a = n^ew MyClass();
161+
''';
162+
const expectedContent = '''
163+
class MyOtherClass {}
164+
class MyOtherClass {}
165+
final a = new MyOtherClass();
166+
''';
167+
final response = await _test_rename_prompt(
168+
content,
169+
'MyOtherClass',
170+
expectedMessage:
171+
'Library already declares class with name \'MyOtherClass\'.',
172+
action: UserPromptActions.renameAnyway,
173+
);
174+
175+
if (response.error != null) {
176+
throw response.error;
177+
}
178+
179+
final result = WorkspaceEdit.fromJson(response.result);
180+
181+
// Ensure applying the changes will give us the expected content.
182+
final contents = {
183+
mainFilePath: withoutMarkers(content),
184+
};
185+
applyDocumentChanges(
186+
contents,
187+
result.documentChanges,
188+
);
189+
expect(contents[mainFilePath], equals(expectedContent));
190+
}
191+
192+
Future<void> test_rename_duplicateName_reject() async {
193+
const content = '''
194+
class MyOtherClass {}
195+
class MyClass {}
196+
final a = n^ew MyClass();
197+
''';
198+
final response = await _test_rename_prompt(
199+
content,
200+
'MyOtherClass',
201+
expectedMessage:
202+
'Library already declares class with name \'MyOtherClass\'.',
203+
action: UserPromptActions.cancel,
204+
);
205+
// Expect a successful empty response if cancelled.
206+
expect(response.error, isNull);
207+
expect(
208+
WorkspaceEdit.fromJson(response.result),
209+
equals(emptyWorkspaceEdit),
210+
);
211+
}
212+
133213
Future<void> test_rename_importPrefix() {
134214
const content = '''
135215
import 'dart:async' as myPr^efix;
@@ -237,17 +317,6 @@ class RenameTest extends AbstractLspAnalysisServerTest {
237317
expect(error.message, contains('name must not contain'));
238318
}
239319

240-
Future<void> test_rename_rejectedForDuplicateName() async {
241-
const content = '''
242-
class MyOtherClass {}
243-
class MyClass {}
244-
final a = n^ew MyClass();
245-
''';
246-
final error = await _test_rename_failure(content, 'MyOtherClass');
247-
expect(error.code, equals(ServerErrorCodes.RenameNotValid));
248-
expect(error.message, contains('already declares class with name'));
249-
}
250-
251320
Future<void> test_rename_rejectedForSameName() async {
252321
const content = '''
253322
class My^Class {}
@@ -413,6 +482,55 @@ class RenameTest extends AbstractLspAnalysisServerTest {
413482
return result.error;
414483
}
415484

485+
/// Tests a rename that is expected to cause an error, which will trigger
486+
/// a ShowMessageRequest from the server to the client to allow the refactor
487+
/// to be continued or rejected.
488+
Future<ResponseMessage> _test_rename_prompt(
489+
String content,
490+
String newName, {
491+
@required String expectedMessage,
492+
Future<void> Function() beforeResponding,
493+
@required String action,
494+
int openFileVersion = 222,
495+
int renameRequestFileVersion = 222,
496+
}) async {
497+
await initialize(
498+
workspaceCapabilities:
499+
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
500+
);
501+
await openFile(mainFileUri, withoutMarkers(content),
502+
version: openFileVersion);
503+
504+
// Expect the server to call us back with a ShowMessageRequest prompt about
505+
// the errors for us to accept/reject.
506+
return handleExpectedRequest(
507+
Method.window_showMessageRequest,
508+
ShowMessageRequestParams.fromJson,
509+
() => renameRaw(
510+
mainFileUri,
511+
renameRequestFileVersion,
512+
positionFromMarker(content),
513+
newName,
514+
),
515+
handler: (ShowMessageRequestParams params) async {
516+
// Ensure the warning prompt is as expected.
517+
expect(params.type, equals(MessageType.Warning));
518+
expect(params.message, equals(expectedMessage));
519+
expect(params.actions, hasLength(2));
520+
expect(params.actions[0],
521+
equals(MessageActionItem(title: UserPromptActions.renameAnyway)));
522+
expect(params.actions[1],
523+
equals(MessageActionItem(title: UserPromptActions.cancel)));
524+
525+
// Allow the test to run some code before we send the response.
526+
await beforeResponding?.call();
527+
528+
// Respond to the request with the required action.
529+
return MessageActionItem(title: action);
530+
},
531+
);
532+
}
533+
416534
Future<void> _test_rename_withDocumentChanges(
417535
String content,
418536
String newName,

0 commit comments

Comments
 (0)