blob: edf75544af35276e94a8a43fb96f3338999f32cc [file] [log] [blame]
// 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/platform.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 {
content = normalizeNewlinesForPlatform(content);
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''';
}
content = normalizeNewlinesForPlatform(content);
expected = normalizeNewlinesForPlatform(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''';
}
content = normalizeNewlinesForPlatform(content);
expected = normalizeNewlinesForPlatform(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,
);
}
}