Files
sdk/pkg/analysis_server/test/lsp/code_actions_abstract.dart
T
Danny Tuppeny 9831fc4e98 [analysis_server] Rename "CodeAction" to CodeActionLiteral"
The term "CodeAction" is a bit overloaded. It could mean both an
individual result from the `textDocument/codeAction` request (which is a
`Command` or a `CodeAction`), or the `CodeAction` type defined in
the spec (which the spec refers to as a "Code Action literal").

To reduce confusion where we have similar APIs that operate on
"Code Actions" (CodeAction|Command), this renames the `CodeAction` class to
`CodeActionLiteral` and we will use the term `CodeAction` to mean either of
those types.

To make things simpler to review, this change _only_ renames the class, and also swaps the order of the types in some places that used `Either2<Command, CodeAction>` (which is opposite to the spec and some other code). Some further clean up will be done in a separate change.

Change-Id: Idcd8265f9229c3450004e68334e98a7b530330a4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/425300
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
2025-04-29 20:54:58 -07:00

251 lines
7.6 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/services/correction/assist_internal.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:collection/collection.dart';
import 'package:test/test.dart';
import '../utils/test_code_extensions.dart';
import 'change_verifier.dart';
import 'server_abstract.dart';
abstract class AbstractCodeActionsTest extends AbstractLspAnalysisServerTest {
/// Initializes the server with some basic configuration and expects to find
/// a [CodeActionLiteral] with [kind]/[command]/[title].
Future<CodeActionLiteral> expectAction(
String content, {
CodeActionKind? kind,
String? command,
List<Object>? commandArgs,
String? title,
CodeActionTriggerKind? triggerKind,
String? filePath,
bool openTargetFile = false,
}) async {
filePath ??= mainFilePath;
var code = TestCode.parse(content);
newFile(filePath, code.code);
await initialize();
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 = findAction(
codeActions,
kind: kind,
command: command,
commandArgs: commandArgs,
title: title,
);
if (action == null) {
fail('Failed to find a code action titled "$title".');
}
return action;
}
/// Expects that command [commandName] was logged to the analytics manager.
void expectCommandLogged(String commandName) {
expect(
server.analyticsManager
.getRequestData(Method.workspace_executeCommand.toString())
.additionalEnumCounts['command']!
.keys,
contains(commandName),
);
}
/// Initializes the server with some basic configuration and expects not to
/// find a [CodeActionLiteral] with [kind]/[command]/[title].
Future<void> expectNoAction(
String content, {
String? filePath,
CodeActionKind? kind,
String? command,
String? title,
ProgressToken? workDoneToken,
}) async {
filePath ??= mainFilePath;
var code = TestCode.parse(content);
newFile(filePath, code.code);
if (workDoneToken != null) {
setWorkDoneProgressSupport();
}
await initialize();
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(
findAction(codeActions, kind: kind, command: command, title: title),
isNull,
);
}
/// Finds the single action matching [title], [kind] and [command].
///
/// If [command] and/or [commandArgs] are supplied, ensures the action has
/// a matching command/args.
///
/// Throws if zero or more than one actions match.
CodeActionLiteral? findAction(
List<Either2<CodeActionLiteral, Command>> actions, {
String? title,
CodeActionKind? kind,
String? command,
List<Object>? commandArgs,
}) {
return findActions(
actions,
title: title,
kind: kind,
command: command,
commandArgs: commandArgs,
).singleOrNull;
}
List<CodeActionLiteral> findActions(
List<Either2<CodeActionLiteral, Command>> actions, {
String? title,
CodeActionKind? kind,
String? command,
List<Object>? commandArgs,
}) {
return actions
.map((action) => action.map((action) => action, (cmd) => null))
.where((action) => title == null || action?.title == title)
.where((action) => kind == null || action?.kind == kind)
// 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.
.where(
(action) =>
title != null ||
command == null ||
action?.command?.command == command,
)
.map((action) {
// Always expect a command (either to execute, or for logging)
expect(action!.command, isNotNull);
if (command != null) {
expect(action.command!.command, command);
} else {
// Expect an edit if we weren't looking for a command-action.
expect(action.edit, isNotNull);
}
if (commandArgs != null) {
expect(action.command!.arguments, equals(commandArgs));
}
return action;
})
.nonNulls
.toList();
}
Either2<CodeActionLiteral, Command>? findCommand(
List<Either2<CodeActionLiteral, Command>> actions,
String commandID, [
String? wantedTitle,
]) {
for (var codeAction in actions) {
var id = codeAction.map(
(action) => action.command?.command,
(cmd) => cmd.command,
);
var title = codeAction.map((cmd) => cmd.title, (action) => action.title);
if (id == commandID && (wantedTitle == null || wantedTitle == title)) {
return codeAction;
}
}
return null;
}
@override
void setUp() {
super.setUp();
// Fix tests are likely to have diagnostics that need fixing.
failTestOnErrorDiagnostic = false;
// Some defaults that most tests use. Tests can opt-out by overwriting these
// before initializing.
setApplyEditSupport();
setDocumentChangesSupport();
registerBuiltInAssistGenerators();
registerBuiltInFixGenerators();
}
/// Initializes the server with some basic configuration and expects to find
/// a [CodeActionLiteral] with [kind]/[title] that applies edits resulting in
/// [expected].
Future<LspChangeVerifier> verifyActionEdits(
String content,
String expected, {
String? filePath,
CodeActionKind? kind,
String? command,
List<Object>? commandArgs,
String? title,
ProgressToken? commandWorkDoneToken,
bool openTargetFile = false,
}) async {
filePath ??= mainFilePath;
// 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 expectAction(
filePath: filePath,
content,
kind: kind,
command: command,
commandArgs: commandArgs,
title: title,
openTargetFile: openTargetFile,
);
// 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 != 'dart.logAction') {
return await verifyCommandEdits(
action.command!,
expected,
workDoneToken: commandWorkDoneToken,
);
} else {
var edit = action.edit!;
return verifyEdit(edit, expected);
}
}
}