c5fb640747
This updates the `showUserPrompt` method to accept a cancellation token so that if requests are cancelled (by the client, or something like a second refactor cancelling the first), the prompt is also cancelled (and when the response arrives, it can be ignored). Note: Unfortunately the protocol (and VS Code) don't actually allow for the prompt to be cancelled/hidden from the user, this is mainly to avoid us keeping the request "alive" on the server if the user ignores a prompt and we know the parent request was cancelled anyway. Fixes https://github.com/dart-lang/sdk/issues/63285 Change-Id: Ia77880a749b284a2ff31c8cf560307ad908d2175 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499360 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
357 lines
9.1 KiB
Dart
357 lines
9.1 KiB
Dart
// Copyright (c) 2018, 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 'dart:async';
|
|
|
|
import 'package:analysis_server/lsp_protocol/protocol.dart';
|
|
import 'package:analysis_server/src/lsp/constants.dart';
|
|
import 'package:analysis_server/src/services/correction/fix_internal.dart';
|
|
import 'package:linter/src/rules.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
|
|
import '../shared/shared_code_actions_source_tests.dart';
|
|
import 'code_actions_mixin.dart';
|
|
import 'server_abstract.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(SourceCodeActionsTest);
|
|
defineReflectiveTests(SortMembersSourceCodeActionsTest);
|
|
defineReflectiveTests(OrganizeImportsSourceCodeActionsTest);
|
|
defineReflectiveTests(FixAllSourceCodeActionsTest);
|
|
});
|
|
}
|
|
|
|
abstract class AbstractSourceCodeActionsTest
|
|
extends AbstractLspAnalysisServerTest
|
|
with
|
|
LspSharedTestMixin,
|
|
SharedSourceCodeActionsTestMixin,
|
|
CodeActionsTestMixin {}
|
|
|
|
@reflectiveTest
|
|
class FixAllSourceCodeActionsTest extends AbstractSourceCodeActionsTest {
|
|
@override
|
|
Future<void> setUp() async {
|
|
await super.setUp();
|
|
|
|
// Fix tests are likely to have diagnostics that need fixing.
|
|
failTestOnErrorDiagnostic = false;
|
|
|
|
registerBuiltInFixGenerators();
|
|
}
|
|
|
|
Future<void> test_appliesCorrectEdits() async {
|
|
const analysisOptionsContent = '''
|
|
linter:
|
|
rules:
|
|
- unnecessary_new
|
|
- prefer_collection_literals
|
|
''';
|
|
const content = '''
|
|
final a = new Object();
|
|
final b = new Set<String>();
|
|
''';
|
|
const expectedContent = '''
|
|
final a = Object();
|
|
final b = <String>{};
|
|
''';
|
|
|
|
registerLintRules();
|
|
newFile(analysisOptionsPath, analysisOptionsContent);
|
|
newFile(mainFilePath, content);
|
|
|
|
await verifyCodeActionLiteralEdits(
|
|
content,
|
|
expectedContent,
|
|
command: Commands.fixAll,
|
|
);
|
|
}
|
|
|
|
Future<void> test_multipleIterations_noOverlay() async {
|
|
const analysisOptionsContent = '''
|
|
linter:
|
|
rules:
|
|
- prefer_final_locals
|
|
- prefer_const_declarations
|
|
''';
|
|
const content = '''
|
|
void f() {
|
|
var a = 'test';
|
|
}
|
|
''';
|
|
const expectedContent = '''
|
|
void f() {
|
|
const a = 'test';
|
|
}
|
|
''';
|
|
|
|
registerLintRules();
|
|
newFile(analysisOptionsPath, analysisOptionsContent);
|
|
newFile(mainFilePath, content);
|
|
|
|
await verifyCodeActionLiteralEdits(
|
|
content,
|
|
expectedContent,
|
|
command: Commands.fixAll,
|
|
);
|
|
}
|
|
|
|
Future<void> test_multipleIterations_overlay() async {
|
|
const analysisOptionsContent = '''
|
|
linter:
|
|
rules:
|
|
- prefer_final_locals
|
|
- prefer_const_declarations
|
|
''';
|
|
const content = '''
|
|
void f() {
|
|
var a = 'test';
|
|
}
|
|
''';
|
|
const expectedContent = '''
|
|
void f() {
|
|
const a = 'test';
|
|
}
|
|
''';
|
|
|
|
registerLintRules();
|
|
newFile(analysisOptionsPath, analysisOptionsContent);
|
|
|
|
await verifyCodeActionLiteralEdits(
|
|
content,
|
|
expectedContent,
|
|
command: Commands.fixAll,
|
|
);
|
|
}
|
|
|
|
Future<void> test_multipleIterations_withClientModification() async {
|
|
const analysisOptionsContent = '''
|
|
linter:
|
|
rules:
|
|
- prefer_final_locals
|
|
- prefer_const_declarations
|
|
''';
|
|
const content = '''
|
|
void f() {
|
|
var a = 'test';
|
|
}
|
|
''';
|
|
registerLintRules();
|
|
newFile(analysisOptionsPath, analysisOptionsContent);
|
|
|
|
var codeAction = await expectCodeActionLiteral(
|
|
content,
|
|
command: Commands.fixAll,
|
|
);
|
|
var command = codeAction.command!;
|
|
|
|
// Files must be open to apply edits.
|
|
await openFile(testFileUri, content);
|
|
|
|
// Execute the command with a modification and capture the edit that is
|
|
// sent back to us.
|
|
ApplyWorkspaceEditParams? editParams;
|
|
await handleExpectedRequest<
|
|
Object?,
|
|
ApplyWorkspaceEditParams,
|
|
ApplyWorkspaceEditResult
|
|
>(
|
|
Method.workspace_applyEdit,
|
|
ApplyWorkspaceEditParams.fromJson,
|
|
() async {
|
|
// Apply the command and immediately modify a file afterwards without
|
|
// awaiting.
|
|
var commandFuture = executeCommand(command);
|
|
await replaceFile(12345, testFileUri, 'client-modified-content');
|
|
|
|
return commandFuture;
|
|
},
|
|
handler: (edit) async {
|
|
// When the server sends the edit back, just keep a copy and say we
|
|
// applied successfully (we'll verify the actual edit below).
|
|
editParams = edit;
|
|
return ApplyWorkspaceEditResult(applied: true);
|
|
},
|
|
);
|
|
|
|
// Extract the text edit from the 'workspace/applyEdit' params the server
|
|
// sent us.
|
|
var change = editParams?.edit.documentChanges!.single;
|
|
var edit = change!.map(
|
|
(create) => throw 'Expected edit, got create',
|
|
(delete) => throw 'Expected edit, got delete',
|
|
(rename) => throw 'Expected edit, got rename',
|
|
(edit) => edit,
|
|
);
|
|
// Ensure the edit says that it was based on version 1 (the original
|
|
// version) and not the updated 12345 version we sent.
|
|
expect(edit.textDocument.version, 1);
|
|
}
|
|
|
|
Future<void> test_part() async {
|
|
var containerFilePath = join(projectFolderPath, 'lib', 'container.dart');
|
|
var partFilePath = join(projectFolderPath, 'lib', 'part.dart');
|
|
const analysisOptionsContent = '''
|
|
linter:
|
|
rules:
|
|
- unnecessary_new
|
|
- prefer_collection_literals
|
|
''';
|
|
const containerFileContent = '''
|
|
part 'part.dart';
|
|
''';
|
|
const content = '''
|
|
part of 'container.dart';
|
|
|
|
final a = new Object();
|
|
final b = new Set<String>();
|
|
''';
|
|
const expectedContent = '''
|
|
part of 'container.dart';
|
|
|
|
final a = Object();
|
|
final b = <String>{};
|
|
''';
|
|
|
|
registerLintRules();
|
|
newFile(analysisOptionsPath, analysisOptionsContent);
|
|
newFile(containerFilePath, containerFileContent);
|
|
newFile(partFilePath, content);
|
|
|
|
await verifyCodeActionLiteralEdits(
|
|
filePath: partFilePath,
|
|
content,
|
|
expectedContent,
|
|
command: Commands.fixAll,
|
|
);
|
|
}
|
|
|
|
Future<void> test_privateUnusedParameters_notRemovedIfSave() async {
|
|
const content = '''
|
|
class _MyClass {
|
|
int? _param;
|
|
_MyClass({
|
|
this._param,
|
|
});
|
|
}
|
|
''';
|
|
|
|
var codeAction = await expectCodeActionLiteral(
|
|
content,
|
|
command: Commands.fixAll,
|
|
triggerKind: CodeActionTriggerKind.Automatic,
|
|
);
|
|
var command = codeAction.command!;
|
|
|
|
// We should not get an applyEdit call during the command execution because
|
|
// no edits should be produced.
|
|
var applyEditSubscription = requestsFromServer
|
|
.where((n) => n.method == Method.workspace_applyEdit)
|
|
.listen((_) => throw 'workspace/applyEdit was unexpectedly called');
|
|
var commandResponse = await executeCommand(command);
|
|
expect(commandResponse, isNull);
|
|
|
|
await pumpEventQueue();
|
|
await applyEditSubscription.cancel();
|
|
}
|
|
|
|
Future<void> test_privateUnusedParameters_removedByDefault() async {
|
|
const content = '''
|
|
class _MyClass {
|
|
int? param;
|
|
_MyClass({
|
|
this.param,
|
|
});
|
|
}
|
|
''';
|
|
const expectedContent = '''
|
|
class _MyClass {
|
|
int? param;
|
|
_MyClass() : param = null;
|
|
}
|
|
''';
|
|
|
|
await verifyCodeActionLiteralEdits(
|
|
content,
|
|
expectedContent,
|
|
command: Commands.fixAll,
|
|
);
|
|
}
|
|
|
|
Future<void> test_unavailable_outsideAnalysisRoot() async {
|
|
var otherFile = convertPath('/other/file.dart');
|
|
var content = '';
|
|
|
|
await expectNoAction(
|
|
filePath: otherFile,
|
|
content,
|
|
command: Commands.organizeImports,
|
|
);
|
|
}
|
|
|
|
Future<void> test_unusedUsings_notRemovedIfSave() async {
|
|
const content = '''
|
|
import 'dart:async';
|
|
int? a;
|
|
''';
|
|
|
|
var codeAction = await expectCodeActionLiteral(
|
|
content,
|
|
command: Commands.fixAll,
|
|
triggerKind: CodeActionTriggerKind.Automatic,
|
|
);
|
|
var command = codeAction.command!;
|
|
|
|
// We should not get an applyEdit call during the command execution because
|
|
// no edits should be produced.
|
|
var applyEditSubscription = requestsFromServer
|
|
.where((n) => n.method == Method.workspace_applyEdit)
|
|
.listen((_) => throw 'workspace/applyEdit was unexpectedly called');
|
|
var commandResponse = await executeCommand(command);
|
|
expect(commandResponse, isNull);
|
|
|
|
await pumpEventQueue();
|
|
await applyEditSubscription.cancel();
|
|
}
|
|
|
|
Future<void> test_unusedUsings_removedByDefault() async {
|
|
const content = '''
|
|
import 'dart:async';
|
|
int? a;
|
|
''';
|
|
const expectedContent = '''
|
|
int? a;
|
|
''';
|
|
|
|
await verifyCodeActionLiteralEdits(
|
|
content,
|
|
expectedContent,
|
|
command: Commands.fixAll,
|
|
);
|
|
}
|
|
}
|
|
|
|
@reflectiveTest
|
|
class OrganizeImportsSourceCodeActionsTest extends AbstractSourceCodeActionsTest
|
|
with
|
|
SharedSourceCodeActionsTestMixin,
|
|
// Tests are defined in a shared mixin.
|
|
SharedOrganizeImportsSourceCodeActionsTests {}
|
|
|
|
@reflectiveTest
|
|
class SortMembersSourceCodeActionsTest extends AbstractSourceCodeActionsTest
|
|
with
|
|
SharedSourceCodeActionsTestMixin,
|
|
// Tests are defined in a shared mixin.
|
|
SharedSortMembersSourceCodeActionsTests {}
|
|
|
|
@reflectiveTest
|
|
class SourceCodeActionsTest extends AbstractSourceCodeActionsTest
|
|
with
|
|
SharedSourceCodeActionsTestMixin,
|
|
// Tests are defined in a shared mixin.
|
|
SharedSourceCodeActionsTests {}
|