d6cc4c3901
Currently the commands for Fix All are only supported for a real LSP server, so we shouldn't return a Source CodeAction that would try to run the command when running in the legacy server. Change-Id: I3256ef5e19c3a45e7fc3f3ec726a59da30d3a30c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/429642 Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Samuel Rawlins <srawlins@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
394 lines
12 KiB
Dart
394 lines
12 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 'package:analysis_server/lsp_protocol/protocol.dart';
|
|
import 'package:analysis_server/src/lsp/constants.dart';
|
|
import 'package:analysis_server/src/lsp/extensions/code_action.dart';
|
|
import 'package:analyzer/src/test_utilities/test_code_format.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../shared/shared_test_interface.dart';
|
|
import '../utils/lsp_protocol_extensions.dart';
|
|
import '../utils/test_code_extensions.dart';
|
|
import 'change_verifier.dart';
|
|
import 'request_helpers_mixin.dart';
|
|
|
|
mixin CodeActionsTestMixin
|
|
on
|
|
SharedTestInterface,
|
|
LspRequestHelpersMixin,
|
|
LspEditHelpersMixin,
|
|
LspVerifyEditHelpersMixin {
|
|
final String simplePubspecContent = 'name: my_project';
|
|
|
|
/// Whether the server supports the "Fix All" command (currently LSP-only).
|
|
bool get serverSupportsFixAll => true;
|
|
|
|
/// Initializes the server with some basic configuration and expects to find
|
|
/// a [CodeAction] with [kind]/[command]/[title].
|
|
Future<CodeAction> expectCodeAction(
|
|
TestCode code, {
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
String? title,
|
|
CodeActionTriggerKind? triggerKind,
|
|
String? filePath,
|
|
bool openTargetFile = false,
|
|
}) async {
|
|
filePath ??= testFilePath;
|
|
createFile(filePath, code.code);
|
|
|
|
await initializeServer();
|
|
|
|
var fileUri = uriConverter.toClientUri(filePath);
|
|
if (openTargetFile) {
|
|
await openFile(fileUri, code.code);
|
|
}
|
|
|
|
var codeActions = await getCodeActions(
|
|
fileUri,
|
|
position: code.positions.isNotEmpty ? code.position.position : null,
|
|
range: code.ranges.isNotEmpty ? code.range.range : null,
|
|
triggerKind: triggerKind,
|
|
);
|
|
|
|
var action = findCodeAction(
|
|
codeActions,
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
title: title,
|
|
);
|
|
if (action == null) {
|
|
fail('Failed to find a code action titled "$title".');
|
|
}
|
|
return action;
|
|
}
|
|
|
|
/// Initializes the server with some basic configuration and expects to find
|
|
/// a [CodeAction] with [kind]/[command]/[title].
|
|
Future<CodeActionLiteral> expectCodeActionLiteral(
|
|
String content, {
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
String? title,
|
|
CodeActionTriggerKind? triggerKind,
|
|
String? filePath,
|
|
bool openTargetFile = false,
|
|
}) async {
|
|
var action = await expectCodeAction(
|
|
TestCode.parse(content),
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
title: title,
|
|
triggerKind: triggerKind,
|
|
filePath: filePath,
|
|
openTargetFile: openTargetFile,
|
|
);
|
|
return action.asCodeActionLiteral;
|
|
}
|
|
|
|
/// Initializes the server with some basic configuration and expects to find
|
|
/// a [Command] code action (not a literal, even with a command) with
|
|
/// [command]/[title].
|
|
Future<Command> expectCommandCodeAction(
|
|
String content, {
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
String? title,
|
|
CodeActionTriggerKind? triggerKind,
|
|
String? filePath,
|
|
bool openTargetFile = false,
|
|
}) async {
|
|
var action = await expectCodeAction(
|
|
TestCode.parse(content),
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
title: title,
|
|
triggerKind: triggerKind,
|
|
filePath: filePath,
|
|
openTargetFile: openTargetFile,
|
|
);
|
|
return action.asCommand;
|
|
}
|
|
|
|
/// Verifies a command execution was logged to analytics.
|
|
///
|
|
/// Implementations are provided by the in-process test base classes. This
|
|
/// method will be a no-op for out-of-process tests because the analytics
|
|
/// manager will not be accessible.
|
|
void expectCommandLogged(String command);
|
|
|
|
/// Initializes the server with some basic configuration and expects not to
|
|
/// find a [CodeAction] with [kind]/[command]/[title].
|
|
Future<void> expectNoAction(
|
|
String content, {
|
|
String? filePath,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
String? title,
|
|
ProgressToken? workDoneToken,
|
|
}) async {
|
|
filePath ??= testFilePath;
|
|
var code = TestCode.parse(content);
|
|
createFile(filePath, code.code);
|
|
|
|
if (workDoneToken != null) {
|
|
setWorkDoneProgressSupport();
|
|
}
|
|
await initializeServer();
|
|
|
|
var codeActions = await getCodeActions(
|
|
uriConverter.toClientUri(filePath),
|
|
position: code.positions.isNotEmpty ? code.position.position : null,
|
|
range: code.ranges.isNotEmpty ? code.range.range : null,
|
|
workDoneToken: workDoneToken,
|
|
);
|
|
|
|
expect(
|
|
findCodeAction(codeActions, kind: kind, command: command, title: title),
|
|
isNull,
|
|
);
|
|
}
|
|
|
|
List<TextDocumentEdit> extractTextDocumentEdits(
|
|
DocumentChanges documentChanges,
|
|
) =>
|
|
// Extract TextDocumentEdits from union of resource changes
|
|
documentChanges
|
|
.map(
|
|
(change) => change.map(
|
|
(create) => null,
|
|
(delete) => null,
|
|
(rename) => null,
|
|
(textDocEdit) => textDocEdit,
|
|
),
|
|
)
|
|
.nonNulls
|
|
.toList();
|
|
|
|
/// Finds the single [CodeAction] matching [title], [kind] and [command].
|
|
///
|
|
/// If [command] and/or [commandArgs] are supplied, ensures the result is
|
|
/// either that command, or a literal code action with that command.
|
|
///
|
|
/// Returns `null` if there is not exactly one match.
|
|
CodeAction? findCodeAction(
|
|
List<CodeAction> actions, {
|
|
String? title,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
}) {
|
|
return findCodeActions(
|
|
actions,
|
|
title: title,
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
).singleOrNull;
|
|
}
|
|
|
|
/// Finds the single [CodeActionLiteral] matching [title], [kind] and [command].
|
|
///
|
|
/// If [command] and/or [commandArgs] are supplied, ensures the result is
|
|
/// either that command, or a literal code action with that command.
|
|
///
|
|
/// Returns `null` if there is not exactly one match.
|
|
CodeActionLiteral? findCodeActionLiteral(
|
|
List<CodeAction> actions, {
|
|
String? title,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
}) {
|
|
return findCodeAction(
|
|
actions,
|
|
title: title,
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
)?.asCodeActionLiteral;
|
|
}
|
|
|
|
List<CodeAction> findCodeActions(
|
|
List<CodeAction> actions, {
|
|
String? title,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
}) {
|
|
return actions.where((action) {
|
|
var actionLiteral = action.map((action) => action, (command) => null);
|
|
var actionCommand = action.map(
|
|
// Always expect a command (either to execute, or for logging)
|
|
(action) => action.command!,
|
|
(command) => command,
|
|
);
|
|
var actionTitle = actionLiteral?.title ?? actionCommand.title;
|
|
var actionKind = actionLiteral?.kind;
|
|
|
|
if (title != null && actionTitle != title) {
|
|
return false;
|
|
}
|
|
|
|
if (kind != null && actionKind != kind) {
|
|
return false;
|
|
}
|
|
|
|
// Some tests filter by only supplying a command, so if there is no
|
|
// title given, filter by the command. If a title was given, don't
|
|
// filter by the command and assert it below. This results in a better
|
|
// failure message if the action existed by title but without the correct
|
|
// command.
|
|
if (title == null &&
|
|
command != null &&
|
|
actionCommand.command != command) {
|
|
return false;
|
|
}
|
|
|
|
if (command != null) {
|
|
expect(actionCommand.command, command);
|
|
} else {
|
|
// Expect an edit if we weren't looking for a command-action.
|
|
expect(actionLiteral?.edit, isNotNull);
|
|
}
|
|
if (commandArgs != null) {
|
|
expect(actionCommand.arguments, equals(commandArgs));
|
|
}
|
|
|
|
return true;
|
|
}).toList();
|
|
}
|
|
|
|
CodeAction? findCommand(
|
|
List<CodeAction> actions,
|
|
String commandID, [
|
|
String? wantedTitle,
|
|
]) {
|
|
for (var codeAction in actions) {
|
|
var id = codeAction.command?.command;
|
|
var title = codeAction.title;
|
|
if (id == commandID && (wantedTitle == null || wantedTitle == title)) {
|
|
return codeAction;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Verifies that executing the given Code Action (either via a command or
|
|
/// an inline edit) results in the files matching the expected content.
|
|
Future<LspChangeVerifier> verifyCodeActionEdits(
|
|
CodeAction action,
|
|
String expectedContent, {
|
|
ProgressToken? workDoneToken,
|
|
}) async {
|
|
var command = action.command;
|
|
var edit = action.map((literal) => literal.edit, (_) => null);
|
|
|
|
// Verify the edits either by executing the command we expected, or
|
|
// the edits attached directly to the code action.
|
|
// Don't try to execute 'dart.logAction' because it will never produce
|
|
// edits.
|
|
if (command != null && command.command != Commands.logAction) {
|
|
assert(edit == null, 'Got a command but also a literal edit');
|
|
return await verifyCommandEdits(
|
|
command,
|
|
expectedContent,
|
|
workDoneToken: workDoneToken,
|
|
);
|
|
} else if (edit != null) {
|
|
return verifyEdit(edit, expectedContent);
|
|
} else {
|
|
throw 'CodeAction had neither a command or a literal edit';
|
|
}
|
|
}
|
|
|
|
/// Initializes the server with some basic configuration and expects to find
|
|
/// a [CodeActionLiteral] with [kind]/[title] that applies edits resulting in
|
|
/// [expected].
|
|
Future<LspChangeVerifier> verifyCodeActionLiteralEdits(
|
|
String content,
|
|
String expected, {
|
|
String? filePath,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
String? title,
|
|
ProgressToken? commandWorkDoneToken,
|
|
bool openTargetFile = false,
|
|
}) async {
|
|
filePath ??= testFilePath;
|
|
|
|
// For convenience, if a test doesn't provide an full set of edits
|
|
// we assume only a single edit of the file that was being modified.
|
|
if (!expected.startsWith(LspChangeVerifier.editMarkerStart)) {
|
|
expected = '''
|
|
${LspChangeVerifier.editMarkerStart} ${relativePath(filePath)}
|
|
$expected''';
|
|
}
|
|
|
|
var action = await expectCodeActionLiteral(
|
|
filePath: filePath,
|
|
content,
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
title: title,
|
|
openTargetFile: openTargetFile,
|
|
);
|
|
|
|
return await verifyCodeActionEdits(
|
|
CodeAction.t1(action),
|
|
expected,
|
|
workDoneToken: commandWorkDoneToken,
|
|
);
|
|
}
|
|
|
|
/// Initializes the server with some basic configuration and expects to find
|
|
/// a [Command] code action (and not a literal, even with a command) with
|
|
/// [title] that applies edits resulting in [expected].
|
|
Future<LspChangeVerifier> verifyCommandCodeActionEdits(
|
|
String content,
|
|
String expected, {
|
|
String? filePath,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
String? title,
|
|
ProgressToken? commandWorkDoneToken,
|
|
bool openTargetFile = false,
|
|
}) async {
|
|
filePath ??= testFilePath;
|
|
|
|
// For convenience, if a test doesn't provide an full set of edits
|
|
// we assume only a single edit of the file that was being modified.
|
|
if (!expected.startsWith(LspChangeVerifier.editMarkerStart)) {
|
|
expected = '''
|
|
${LspChangeVerifier.editMarkerStart} ${relativePath(filePath)}
|
|
$expected''';
|
|
}
|
|
|
|
var commandAction = await expectCommandCodeAction(
|
|
filePath: filePath,
|
|
content,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
title: title,
|
|
openTargetFile: openTargetFile,
|
|
);
|
|
|
|
return await verifyCodeActionEdits(
|
|
CodeAction.t2(commandAction),
|
|
expected,
|
|
workDoneToken: commandWorkDoneToken,
|
|
);
|
|
}
|
|
}
|