[analysis_server] Update remaining CodeActions tests to use new expectation format

This is a continuation of a previous change that stops tests from verifying only individual files modified via a `WorkspaceEdit`. All verifications of these edits are done via a single string that includes all changes, with annotations for creates/deletes/renames.

Additionally, it migrates some additional tests to use TestCode instead of the old markers, and removes some extra indenting these tests had.

It also starts changing how capabilities are set for tests from having to nest function calls in `initialize()` calls to instead calling simple helpers prior to initialization.

Change-Id: I35d16a296c2125ab830685437e14eb3b29ea4704
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/312841
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny 2023-07-07 16:15:24 +00:00 committed by Commit Queue
parent b05282a1e9
commit 201fb68e64
16 changed files with 1584 additions and 2116 deletions

View file

@ -62,6 +62,9 @@ Null _alwaysNull(_, [__]) => null;
bool _alwaysTrue(_, [__]) => true;
typedef DocumentChanges
= List<Either4<CreateFile, DeleteFile, RenameFile, TextDocumentEdit>>;
class Either2<T1, T2> implements ToJsonable {
final int _which;
final T1? _t1;

View file

@ -198,6 +198,12 @@ class DartCodeActionsProducer extends AbstractCodeActionsProducer {
@override
Future<List<Either2<CodeAction, Command>>> getRefactorActions() async {
// If the client does not support workspace/applyEdit, we won't be able to
// run any of these.
if (!supportsApplyEdit) {
return const [];
}
final refactorActions = <Either2<CodeAction, Command>>[];
try {

View file

@ -0,0 +1,215 @@
// Copyright (c) 2023, 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:collection/collection.dart';
import 'package:test/test.dart' hide expect;
import 'server_abstract.dart';
/// Applies LSP [WorkspaceEdit]s to produce a flattened string describing the
/// new file contents and any create/rename/deletes to use in test expectations.
class LspChangeVerifier {
/// Marks that signifies the start of an edit description.
static final editMarkerStart = '>>>>>>>>>>';
/// Marks the end of an edit description if the content did not end with a
/// newline.
static final editMarkerEnd = '<<<<<<<<<<';
/// Changes collected while applying the edit.
final _changes = <Uri, _Change>{};
/// A base test class used to obtain the current content of a file.
final LspAnalysisServerTestMixin _server;
/// The [WorkspaceEdit] being applied/verified.
final WorkspaceEdit edit;
LspChangeVerifier(this._server, this.edit) {
_applyEdit();
}
void verifyFiles(String expected, {Map<Uri, int>? expectedVersions}) {
_server.expect(_toChangeString(), equals(expected));
if (expectedVersions != null) {
_verifyDocumentVersions(expectedVersions);
}
}
void _applyChanges(Map<Uri, List<TextEdit>> changes) {
changes.forEach((fileUri, edits) {
final change = _change(fileUri);
change.content = _applyTextEdits(change.content!, edits);
});
}
void _applyDocumentChanges(DocumentChanges documentChanges) {
_applyResourceChanges(documentChanges);
}
void _applyEdit() {
final documentChanges = edit.documentChanges;
final changes = edit.changes;
if (documentChanges != null) {
_applyDocumentChanges(documentChanges);
}
if (changes != null) {
_applyChanges(changes);
}
}
void _applyResourceChanges(DocumentChanges changes) {
for (final change in changes) {
change.map(
_applyResourceCreate,
_applyResourceDelete,
_applyResourceRename,
_applyTextDocumentEdit,
);
}
}
void _applyResourceCreate(CreateFile create) {
final uri = create.uri;
final change = _change(uri);
if (change.content != null) {
throw 'Received create instruction for $uri which already exists';
}
_change(uri).content = '';
change.actions.add('created');
}
void _applyResourceDelete(DeleteFile delete) {
final uri = delete.uri;
final change = _change(uri);
if (change.content == null) {
throw 'Received delete instruction for $uri which does not exist';
}
change.content = null;
change.actions.add('deleted');
}
void _applyResourceRename(RenameFile rename) {
final oldUri = rename.oldUri;
final newUri = rename.newUri;
final oldChange = _change(oldUri);
final newChange = _change(newUri);
if (oldChange.content == null) {
throw 'Received rename instruction from $oldUri which did not exist';
} else if (newChange.content != null) {
throw 'Received rename instruction to $newUri which already exists';
}
newChange.content = oldChange.content;
newChange.actions.add('renamed from ${_relativeUri(oldUri)}');
oldChange.content = null;
oldChange.actions.add('renamed to ${_relativeUri(newUri)}');
}
void _applyTextDocumentEdit(TextDocumentEdit edit) {
final uri = edit.textDocument.uri;
final change = _change(uri);
if (change.content == null) {
throw 'Received edits for $uri which does not exist. '
'Perhaps a CreateFile change was missing from the edits?';
}
change.content = _applyTextDocumentEditEdit(change.content!, edit);
}
String _applyTextDocumentEditEdit(String content, TextDocumentEdit edit) {
// To simulate the behaviour we'll get from an LSP client, apply edits from
// the latest offset to the earliest, but with items at the same offset
// being reversed so that when applied sequentially they appear in the
// document in-order.
//
// This is essentially a stable sort over the offset (descending), but since
// List.sort() is not stable so we additionally sort by index).
final indexedEdits =
edit.edits.mapIndexed(TextEditWithIndex.fromUnion).toList();
indexedEdits.sort(TextEditWithIndex.compare);
return indexedEdits.map((e) => e.edit).fold(content, _server.applyTextEdit);
}
String _applyTextEdits(String content, List<TextEdit> changes) =>
_server.applyTextEdits(content, changes);
_Change _change(Uri fileUri) => _changes.putIfAbsent(
fileUri, () => _Change(_getCurrentFileContent(fileUri)));
void _expectDocumentVersion(
TextDocumentEdit edit,
Map<Uri, int> expectedVersions,
) {
final uri = edit.textDocument.uri;
final expectedVersion = expectedVersions[uri];
_server.expect(edit.textDocument.version, equals(expectedVersion));
}
String? _getCurrentFileContent(Uri uri) => _server.getCurrentFileContent(uri);
String _relativeUri(Uri uri) => _server.relativeUri(uri);
String _toChangeString() {
final buffer = StringBuffer();
for (final entry
in _changes.entries.sortedBy((entry) => _relativeUri(entry.key))) {
// Write the path in a common format for Windows/non-Windows.
final relativePath = _relativeUri(entry.key);
final change = entry.value;
final content = change.content;
// Write header/actions.
buffer.write('$editMarkerStart $relativePath');
for (final action in change.actions) {
buffer.write(' $action');
}
if (content?.isEmpty ?? false) {
buffer.write(' empty');
}
buffer.writeln();
// Write content.
if (content != null) {
buffer.write(content);
// If the content didn't end with a newline we need to add one, but
// add a marked so it's clear there was no trailing newline.
if (content.isNotEmpty && !content.endsWith('\n')) {
buffer.writeln(editMarkerEnd);
}
}
}
return buffer.toString();
}
/// Validates the document versions for a set of edits match the versions in
/// the supplied map.
void _verifyDocumentVersions(Map<Uri, int> expectedVersions) {
// For resource changes, we only need to validate changes since
// creates/renames/deletes do not supply versions.
for (var change in edit.documentChanges!) {
change.map(
(create) {},
(delete) {},
(rename) {},
(edit) => _expectDocumentVersion(edit, expectedVersions),
);
}
}
}
class _Change {
String? content;
final actions = <String>[];
_Change(this.content);
}

View file

@ -3,52 +3,48 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol.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 {
Future<void> checkCodeActionAvailable(
Uri uri,
String command,
String title, {
Range? range,
Position? position,
bool asCodeActionLiteral = false,
bool asCommand = false,
/// Initializes the server with some basic configuration and expects to find
/// a [CodeAction] with [kind]/[command]/[title].
Future<CodeAction> expectAction(
String content, {
CodeActionKind? kind,
String? command,
String? title,
CodeActionTriggerKind? triggerKind,
String? filePath,
bool openTargetFile = false,
bool failTestOnAnyErrorNotification = true,
}) async {
final codeActions =
await getCodeActions(uri, range: range, position: position);
final codeAction = findCommand(codeActions, command)!;
filePath ??= mainFilePath;
final fileUri = Uri.file(filePath);
final code = TestCode.parse(content);
newFile(filePath, code.code);
codeAction.map(
(command) {
if (!asCommand) {
throw 'Got Command but expected CodeAction literal';
}
expect(command.title, equals(title));
expect(
command.arguments,
equals([
{'path': uri.toFilePath()}
]),
);
},
(codeAction) {
if (!asCodeActionLiteral) {
throw 'Got CodeAction literal but expected Command';
}
expect(codeAction.title, equals(title));
expect(codeAction.command!.title, equals(title));
expect(
codeAction.command!.arguments,
equals([
{'path': uri.toFilePath()}
]),
);
},
await initialize(
failTestOnAnyErrorNotification: failTestOnAnyErrorNotification,
);
if (openTargetFile) {
await openFile(fileUri, code.code);
}
final codeActions = await getCodeActions(
fileUri,
position: code.positions.isNotEmpty ? code.position.position : null,
range: code.ranges.isNotEmpty ? code.range.range : null,
triggerKind: triggerKind,
);
return findAction(codeActions, kind: kind, command: command, title: title)!;
}
/// Expects that command [commandName] was logged to the analytics manager.
@ -62,14 +58,80 @@ abstract class AbstractCodeActionsTest extends AbstractLspAnalysisServerTest {
);
}
/// 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 ??= mainFilePath;
final code = TestCode.parse(content);
newFile(filePath, code.code);
if (workDoneToken != null) {
setWorkDoneProgressSupport();
}
await initialize();
final codeActions = await getCodeActions(
Uri.file(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].
///
/// Throws if zero or more than one actions match.
CodeAction? findAction(List<Either2<Command, CodeAction>> actions,
{String? title, CodeActionKind? kind, String? command}) {
return findActions(actions, title: title, kind: kind, command: command)
.singleOrNull;
}
List<CodeAction> findActions(List<Either2<Command, CodeAction>> actions,
{String? title, CodeActionKind? kind, String? command}) {
return actions
.map((action) => action.map((cmd) => null, (action) => action))
.where((action) => title == null || action?.title == title)
.where((action) => kind == null || action?.kind == kind)
.where(
(action) => command == null || action?.command?.command == command)
.map((action) {
// Always expect a command (either to execute, or for logging)
assert(action!.command != null);
// Expect an edit if we weren't looking for a command-action.
if (command == null) {
assert(action!.edit != null);
}
return action;
})
.whereNotNull()
.toList();
}
Either2<Command, CodeAction>? findCommand(
List<Either2<Command, CodeAction>> actions, String commandID,
[String? wantedTitle]) {
for (var codeAction in actions) {
final id = codeAction.map(
(cmd) => cmd.command, (action) => action.command?.command);
final title =
codeAction.map((cmd) => cmd.title, (action) => action.title);
(cmd) => cmd.command,
(action) => action.command?.command,
);
final title = codeAction.map(
(cmd) => cmd.title,
(action) => action.title,
);
if (id == commandID && (wantedTitle == null || wantedTitle == title)) {
return codeAction;
}
@ -77,40 +139,57 @@ abstract class AbstractCodeActionsTest extends AbstractLspAnalysisServerTest {
return null;
}
CodeAction? findEditAction(List<Either2<Command, CodeAction>> actions,
CodeActionKind actionKind, String title) {
return findEditActions(actions, actionKind, title).firstOrNull;
@override
void setUp() {
super.setUp();
// Some defaults that most tests use. Tests can opt-out by overwriting these
// before initializing.
setApplyEditSupport();
setDocumentChangesSupport();
}
List<CodeAction> findEditActions(List<Either2<Command, CodeAction>> actions,
CodeActionKind actionKind, String title) {
return actions
.map((action) => action.map((cmd) => null, (action) => action))
.where((action) => action?.kind == actionKind && action?.title == title)
.map((action) {
// Expect matching actions to contain an edit (and a log command).
assert(action!.command != null);
assert(action!.edit != null);
return action;
})
.whereNotNull()
.toList();
}
/// Initializes the server with some basic configuration and expects to find
/// a [CodeAction] with [kind]/[title] that applies edits resulting in
/// [expected].
Future<LspChangeVerifier> verifyActionEdits(
String content,
String expected, {
String? filePath,
CodeActionKind? kind,
String? command,
String? title,
ProgressToken? commandWorkDoneToken,
}) async {
filePath ??= mainFilePath;
/// Verifies that executing the given code actions command on the server
/// results in an edit being sent to the client that updates the file to match
/// the expected content.
Future<void> verifyCodeActionEdits(Either2<Command, CodeAction> codeAction,
String content, String expectedContent,
{bool expectDocumentChanges = false,
ProgressToken? workDoneToken}) async {
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
// 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''';
}
final action = await expectAction(
filePath: filePath,
content,
kind: kind,
command: command,
title: title,
);
await verifyCommandEdits(command, expectedContent,
expectDocumentChanges: expectDocumentChanges,
workDoneToken: workDoneToken);
// Verify the edits either by executing the command we expected, or
// the edits attached directly to the code action.
if (command != null) {
return await verifyCommandEdits(
action.command!,
expected,
workDoneToken: commandWorkDoneToken,
);
} else {
final edit = action.edit!;
return verifyEdit(edit, expected);
}
}
}

View file

@ -6,12 +6,13 @@ import 'dart:convert';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:collection/collection.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../utils/test_code_extensions.dart';
import 'code_actions_abstract.dart';
void main() {
@ -29,12 +30,13 @@ class AssistsCodeActionsTest extends AbstractCodeActionsTest {
projectFolderPath,
flutter: true,
);
setSupportedCodeActionKinds([CodeActionKind.Refactor]);
}
Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async {
// This code should get an assist to add a show combinator.
const content = '''
import '[[dart:async]]';
import '[!dart:async!]';
Future f;
''';
@ -44,38 +46,18 @@ import 'dart:async' show Future;
Future f;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('refactor.add.showCombinator'),
title: "Add explicit 'show' combinator",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final assist = findEditAction(
codeActions,
CodeActionKind('refactor.add.showCombinator'),
"Add explicit 'show' combinator")!;
// Ensure the edit came back, and using documentChanges.
final edit = assist.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
// This code should get an assist to add a show combinator.
const content = '''
import '[[dart:async]]';
import '[!dart:async!]';
Future f;
''';
@ -85,30 +67,14 @@ import 'dart:async' show Future;
Future f;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
setDocumentChangesSupport(false);
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('refactor.add.showCombinator'),
title: "Add explicit 'show' combinator",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final assistAction = findEditAction(
codeActions,
CodeActionKind('refactor.add.showCombinator'),
"Add explicit 'show' combinator")!;
// Ensure the edit came back, and using changes.
final edit = assistAction.edit!;
expect(edit.changes, isNotNull);
expect(edit.documentChanges, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_errorMessage_invalidIntegers() async {
@ -145,7 +111,7 @@ Future f;
}
}
}
'''),
'''),
);
final resp = await sendRequestToServer(request);
final error = resp.error!;
@ -164,7 +130,7 @@ import 'package:flutter/widgets.dart';
Widget build() {
return Te^xt('');
}
''';
''';
// For testing, the snippet will be inserted literally into the text, as
// this requires some magic on the client. The expected text should
@ -174,52 +140,31 @@ import 'package:flutter/widgets.dart';
Widget build() {
return Center($0child: Text(''));
}
''';
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
setSnippetTextEditSupport();
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('refactor.flutter.wrap.center'),
title: 'Wrap with Center',
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final assist = findEditAction(codeActions,
CodeActionKind('refactor.flutter.wrap.center'), 'Wrap with Center')!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, assist.edit!.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_logsExecution() async {
const content = '''
import '[[dart:async]]';
import '[!dart:async!]';
Future f;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
final action = await expectAction(
content,
kind: CodeActionKind('refactor.add.showCombinator'),
title: "Add explicit 'show' combinator",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final assistAction = findEditAction(
codeActions,
CodeActionKind('refactor.add.showCombinator'),
"Add explicit 'show' combinator")!;
await executeCommand(assistAction.command!);
await executeCommand(action.command!);
expectCommandLogged('dart.assist.add.showCombinator');
}
@ -238,8 +183,12 @@ Future f;
Future<void> test_plugin() async {
if (!AnalysisServer.supportsPlugins) return;
// This code should get an assist to replace 'foo' with 'bar'.'
const content = '[[foo]]';
const expectedContent = 'bar';
const content = '''
[!foo!]
''';
const expectedContent = '''
bar
''';
final pluginResult = plugin.EditGetAssistsResult([
plugin.PrioritizedSourceChange(
@ -259,33 +208,19 @@ Future f;
request is plugin.EditGetAssistsParams ? pluginResult : null,
);
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('refactor.fooToBar'),
title: "Change 'foo' to 'bar'",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final assist = findEditAction(codeActions,
CodeActionKind('refactor.fooToBar'), "Change 'foo' to 'bar'")!;
final edit = assist.edit!;
expect(edit.changes, isNotNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_plugin_sortsWithServer() async {
if (!AnalysisServer.supportsPlugins) return;
// Produces a server assist of "Convert to single quoted string" (with a
// priority of 30).
const content = 'import "[[dart:async]]";';
final code = TestCode.parse('import "[!dart:async!]";');
// Provide two plugin results that should sort either side of the server assist.
final pluginResult = plugin.EditGetAssistsResult([
@ -297,14 +232,14 @@ Future f;
request is plugin.EditGetAssistsParams ? pluginResult : null,
);
newFile(mainFilePath, withoutMarkers(content));
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
await getCodeActions(mainFileUri, range: code.range.range);
final codeActionTitles = codeActions.map((action) =>
action.map((command) => command.title, (action) => action.title));
@ -354,31 +289,13 @@ build() {
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
setSnippetTextEditSupport();
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('refactor.flutter.wrap.generic'),
title: 'Wrap with widget...',
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final assist = findEditAction(
codeActions,
CodeActionKind('refactor.flutter.wrap.generic'),
'Wrap with widget...')!;
// Ensure applying the changes will give us the expected content.
final edit = assist.edit!;
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_snippetTextEdits_singleEditGroup() async {
@ -426,39 +343,17 @@ build() {
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
setSnippetTextEditSupport();
final verifier = await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('refactor.flutter.wrap.generic'),
title: 'Wrap with widget...',
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final assist = findEditAction(
codeActions,
CodeActionKind('refactor.flutter.wrap.generic'),
'Wrap with widget...')!;
// Ensure the edit came back, and using documentChanges.
final edit = assist.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
// Also ensure there was a single edit that was correctly marked
// as a SnippetTextEdit.
final textEdits = _extractTextDocumentEdits(edit.documentChanges!)
final textEdits = extractTextDocumentEdits(verifier.edit.documentChanges!)
.expand((tde) => tde.edits)
.map((edit) => edit.map(
(e) => throw 'Expected SnippetTextEdit, got AnnotatedTextEdit',
@ -490,28 +385,15 @@ build() {
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
final assist = await expectAction(
content,
kind: CodeActionKind('refactor.flutter.wrap.generic'),
title: 'Wrap with widget...',
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final assist = findEditAction(
codeActions,
CodeActionKind('refactor.flutter.wrap.generic'),
'Wrap with widget...')!;
// Ensure the edit came back, and using documentChanges.
final edit = assist.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Extract just TextDocumentEdits, create/rename/delete are not relevant.
final textDocumentEdits = _extractTextDocumentEdits(edit.documentChanges!);
final edit = assist.edit!;
final textDocumentEdits = extractTextDocumentEdits(edit.documentChanges!);
final textEdits = textDocumentEdits
.expand((tde) => tde.edits)
.map((edit) => edit.map((e) => e, (e) => e, (e) => e))
@ -561,7 +443,7 @@ build() => Contai^ner(child: Container());
Future<void> test_surround_editGroupsAndSelection() async {
const content = '''
void f() {
[[print(0);]]
[!print(0);!]
}
''';
@ -573,37 +455,17 @@ void f() {
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
setSnippetTextEditSupport();
final verifier = await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('refactor.surround.if'),
title: "Surround with 'if'",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final assist = findEditAction(codeActions,
CodeActionKind('refactor.surround.if'), "Surround with 'if'")!;
// Ensure the edit came back, and using documentChanges.
final edit = assist.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
// Also ensure there was a single edit that was correctly marked
// as a SnippetTextEdit.
final textEdits = _extractTextDocumentEdits(edit.documentChanges!)
final textEdits = extractTextDocumentEdits(verifier.edit.documentChanges!)
.expand((tde) => tde.edits)
.map((edit) => edit.map(
(e) => throw 'Expected SnippetTextEdit, got AnnotatedTextEdit',
@ -614,22 +476,6 @@ void f() {
expect(textEdits, hasLength(1));
expect(textEdits.first.insertTextFormat, equals(InsertTextFormat.Snippet));
}
List<TextDocumentEdit> _extractTextDocumentEdits(
List<Either4<CreateFile, DeleteFile, RenameFile, TextDocumentEdit>>
documentChanges) =>
// Extract TextDocumentEdits from union of resource changes
documentChanges
.map(
(change) => change.map(
(create) => null,
(delete) => null,
(rename) => null,
(textDocEdit) => textDocEdit,
),
)
.whereNotNull()
.toList();
}
class _RawParams extends ToJsonable {

View file

@ -10,7 +10,6 @@ import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:linter/src/rules.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@ -41,11 +40,13 @@ class FixesCodeActionsTest extends AbstractCodeActionsTest {
///
/// Used to ensure that both Dart and non-Dart files fixes are returned.
Future<void> checkPluginResults(String filePath) async {
final fileUri = Uri.file(filePath);
// This code should get a fix to replace 'foo' with 'bar'.'
const content = '[[foo]]';
const expectedContent = 'bar';
const content = '''
[!foo!]
''';
const expectedContent = '''
bar
''';
final pluginResult = plugin.EditGetFixesResult([
plugin.AnalysisErrorFixes(
@ -76,26 +77,19 @@ class FixesCodeActionsTest extends AbstractCodeActionsTest {
request is plugin.EditGetFixesParams ? pluginResult : null,
);
newFile(filePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
await verifyActionEdits(
filePath: filePath,
content,
expectedContent,
kind: CodeActionKind('quickfix.fooToBar'),
title: "Change 'foo' to 'bar'",
);
}
final codeActions =
await getCodeActions(fileUri, range: rangeFromMarkers(content));
final assist = findEditAction(codeActions,
CodeActionKind('quickfix.fooToBar'), "Change 'foo' to 'bar'")!;
final edit = assist.edit!;
expect(edit.changes, isNotNull);
// Ensure applying the changes will give us the expected content.
final contents = {
filePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[filePath], equals(expectedContent));
@override
void setUp() {
super.setUp();
setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
}
Future<void> test_addImport_noPreference() async {
@ -109,10 +103,7 @@ MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
await initialize();
final codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
@ -142,10 +133,7 @@ MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
await initialize();
final codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
@ -174,10 +162,7 @@ MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
await initialize();
final codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
@ -212,7 +197,7 @@ MyCla^ss? a;
linter:
rules:
- prefer_is_empty
- [[camel_case_types]]
- [!camel_case_types!]
- lines_longer_than_80_chars
''';
@ -223,25 +208,13 @@ linter:
- lines_longer_than_80_chars
''';
newFile(analysisOptionsPath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
await verifyActionEdits(
filePath: analysisOptionsPath,
content,
expectedContent,
kind: CodeActionKind('quickfix.removeLint'),
title: "Remove 'camel_case_types'",
);
// Expect a fix.
final codeActions = await getCodeActions(analysisOptionsUri,
range: rangeFromMarkers(content));
final fix = findEditAction(codeActions,
CodeActionKind('quickfix.removeLint'), "Remove 'camel_case_types'")!;
// Ensure it makes the correct edits.
final edit = fix.edit!;
final contents = {
analysisOptionsPath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[analysisOptionsPath], equals(expectedContent));
} finally {
// Restore the "real" `camel_case_types`.
Registry.ruleRegistry.register(camelCaseTypes);
@ -251,125 +224,77 @@ linter:
Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async {
// This code should get a fix to remove the unused import.
const content = '''
import 'dart:async';
[[import]] 'dart:convert';
import 'dart:async';
[!import!] 'dart:convert';
Future foo;
''';
Future foo;
''';
const expectedContent = '''
import 'dart:async';
import 'dart:async';
Future foo;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
Future foo;
''';
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.remove.unusedImport'),
title: 'Remove unused import',
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.unusedImport'),
'Remove unused import')!;
// Ensure the edit came back, and using documentChanges.
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
// This code should get a fix to remove the unused import.
const content = '''
import 'dart:async';
[[import]] 'dart:convert';
import 'dart:async';
[!import!] 'dart:convert';
Future foo;
''';
Future foo;
''';
const expectedContent = '''
import 'dart:async';
import 'dart:async';
Future foo;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
Future foo;
''';
setDocumentChangesSupport(false);
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.remove.unusedImport'),
title: 'Remove unused import',
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.unusedImport'),
'Remove unused import')!;
// Ensure the edit came back, and using changes.
final edit = fixAction.edit!;
expect(edit.changes, isNotNull);
expect(edit.documentChanges, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_createFile() async {
const content = '''
import '[[newfile.dart]]';
''';
import '[!newfile.dart!]';
''';
final expectedCreatedFile =
path.join(path.dirname(mainFilePath), 'newfile.dart');
const expectedContent = '''
>>>>>>>>>> lib/newfile.dart created
// TODO Implement this library.<<<<<<<<<<
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities: withResourceOperationKinds(
emptyWorkspaceClientCapabilities, [ResourceOperationKind.Create]),
setFileCreateSupport();
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.create.file'),
title: "Create file 'newfile.dart'",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(codeActions,
CodeActionKind('quickfix.create.file'), "Create file 'newfile.dart'")!;
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
// Ensure applying the changes creates the file and with the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[expectedCreatedFile], isNotEmpty);
}
Future<void> test_filtersCorrectly() async {
const content = '''
import 'dart:async';
[[import]] 'dart:convert';
final code = TestCode.parse('''
import 'dart:async';
[!import!] 'dart:convert';
Future foo;
''';
newFile(mainFilePath, withoutMarkers(content));
Future foo;
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities,
@ -379,7 +304,7 @@ linter:
ofKind(CodeActionKind kind) => getCodeActions(
mainFileUri,
range: rangeFromMarkers(content),
range: code.range.range,
kinds: [kind],
);
@ -393,25 +318,18 @@ linter:
Future<void> test_fixAll_logsExecution() async {
const content = '''
void f(String a) {
[[print(a!!)]];
[!print(a!!)!];
print(a!!);
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
''';
final action = await expectAction(
content,
kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
title: "Remove '!'s in file",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
"Remove '!'s in file",
)!;
await executeCommand(fixAction.command!);
await executeCommand(action.command!);
expectCommandLogged('dart.fix.remove.nonNullAssertion.multi');
}
@ -419,19 +337,15 @@ void f(String a) {
// Some fixes (for example 'create function foo') are not available in the
// batch processor, so should not generate fix-all-in-file fixes even if there
// are multiple instances.
const content = '''
var a = [[foo]]();
final code = TestCode.parse('''
var a = [!foo!]();
var b = bar();
''';
''');
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
newFile(mainFilePath, code.code);
await initialize();
final allFixes =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final allFixes = await getCodeActions(mainFileUri, range: code.range.range);
// Expect only the single-fix, there should be no apply-all.
expect(allFixes, hasLength(1));
@ -442,75 +356,51 @@ var b = bar();
Future<void> test_fixAll_notWhenSingle() async {
const content = '''
void f(String a) {
[[print(a!)]];
[!print(a!)!];
}
''';
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
await expectNoAction(
content,
kind: CodeActionKind('quickfix'),
title: "Remove '!'s in file",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions, CodeActionKind('quickfix'), "Remove '!'s in file");
// Should not appear if there was only a single error.
expect(fixAction, isNull);
}
Future<void> test_fixAll_whenMultiple() async {
const content = '''
void f(String a) {
[[print(a!!)]];
[!print(a!!)!];
print(a!!);
}
''';
''';
const expectedContent = '''
void f(String a) {
print(a);
print(a);
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
''';
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
title: "Remove '!'s in file",
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
"Remove '!'s in file",
)!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, fixAction.edit!.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_ignoreDiagnostic_afterOtherFixes() async {
const content = '''
final code = TestCode.parse('''
void main() {
Uint8List inputBytes = Uin^t8List.fromList(List.filled(100000000, 0));
}
''';
''');
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
newFile(mainFilePath, code.code);
await initialize();
final position = positionFromMarker(content);
final position = code.position.position;
final range = Range(start: position, end: position);
final codeActions = await getCodeActions(mainFileUri, range: range);
final codeActionKinds = codeActions.map(
@ -544,9 +434,10 @@ void main() {
// This comment is attached to the below import
import 'dart:async';
[[import]] 'dart:convert';
[!import!] 'dart:convert';
Future foo;''';
Future foo;
''';
const expectedContent = '''
// Header comment
@ -559,80 +450,53 @@ Future foo;''';
import 'dart:async';
import 'dart:convert';
Future foo;''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
Future foo;
''';
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.ignore.file'),
title: "Ignore 'unused_import' for the whole file",
);
// Find the ignore action.
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.ignore.file'),
"Ignore 'unused_import' for the whole file")!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, fixAction.edit!.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_ignoreDiagnosticForLine() async {
const content = '''
import 'dart:async';
[[import]] 'dart:convert';
[!import!] 'dart:convert';
Future foo;''';
Future foo;
''';
const expectedContent = '''
import 'dart:async';
// ignore: unused_import
import 'dart:convert';
Future foo;''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
Future foo;
''';
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.ignore.line'),
title: "Ignore 'unused_import' for this line",
);
// Find the ignore action.
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.ignore.line'),
"Ignore 'unused_import' for this line")!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, fixAction.edit!.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_logsExecution() async {
const content = '''
[[import]] 'dart:convert';
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final code = TestCode.parse('''
[!import!] 'dart:convert';
''');
newFile(mainFilePath, code.code);
await initialize();
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.unusedImport'),
'Remove unused import')!;
await getCodeActions(mainFileUri, range: code.range.range);
final fixAction = findAction(codeActions,
title: 'Remove unused import',
kind: CodeActionKind('quickfix.remove.unusedImport'))!;
await executeCommand(fixAction.command!);
expectCommandLogged('dart.fix.remove.unusedImport');
@ -657,17 +521,13 @@ int foo() {
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
await initialize();
final codeActions =
await getCodeActions(mainFileUri, range: code.range.range);
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.convert.toExpressionBody'),
'Convert to expression body');
final fixAction = findAction(codeActions,
title: 'Convert to expression body',
kind: CodeActionKind('quickfix.convert.toExpressionBody'));
expect(fixAction, isNotNull);
}
@ -677,62 +537,58 @@ int foo() {
// diagnostics have their own fixes of the same type.
//
// Expect only the only one nearest to the start of the range to be returned.
const content = '''
final code = TestCode.parse('''
void f() {
var a = [];
print(a!!);^
}
''';
''');
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
newFile(mainFilePath, code.code);
await initialize();
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final removeNnaAction = findEditActions(codeActions,
CodeActionKind('quickfix.remove.nonNullAssertion'), "Remove the '!'");
final codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
final removeNnaAction = findAction(codeActions,
title: "Remove the '!'",
kind: CodeActionKind('quickfix.remove.nonNullAssertion'));
// Expect only one of the fixes.
expect(removeNnaAction, hasLength(1));
expect(removeNnaAction, isNotNull);
// Ensure the action is for the diagnostic on the second bang which was
// closest to the range requested.
final secondBangPos =
positionFromOffset(withoutMarkers(content).indexOf('!);'), content);
expect(removeNnaAction.first.diagnostics, hasLength(1));
final diagStart = removeNnaAction.first.diagnostics!.first.range.start;
positionFromOffset(code.code.indexOf('!);'), code.code);
expect(removeNnaAction!.diagnostics, hasLength(1));
final diagStart = removeNnaAction.diagnostics!.first.range.start;
expect(diagStart, equals(secondBangPos));
}
Future<void> test_noDuplicates_sameFix() async {
const content = '''
var a = [Test, Test, Te[[]]st];
''';
final code = TestCode.parse('''
var a = [Test, Test, Te[!!]st];
''');
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
newFile(mainFilePath, code.code);
await initialize();
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final createClassActions = findEditActions(codeActions,
CodeActionKind('quickfix.create.class'), "Create class 'Test'");
await getCodeActions(mainFileUri, range: code.range.range);
final createClassActions = findAction(codeActions,
title: "Create class 'Test'",
kind: CodeActionKind('quickfix.create.class'));
expect(createClassActions, hasLength(1));
expect(createClassActions.first.diagnostics, hasLength(3));
expect(createClassActions, isNotNull);
expect(createClassActions!.diagnostics, hasLength(3));
}
Future<void> test_noDuplicates_withDocumentChangesSupport() async {
const content = '''
var a = [Test, Test, Te[[]]st];
''';
final code = TestCode.parse('''
var a = [Test, Test, Te[!!]st];
''');
newFile(mainFilePath, withoutMarkers(content));
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
@ -740,12 +596,13 @@ void f() {
withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final createClassActions = findEditActions(codeActions,
CodeActionKind('quickfix.create.class'), "Create class 'Test'");
await getCodeActions(mainFileUri, range: code.range.range);
final createClassActions = findAction(codeActions,
title: "Create class 'Test'",
kind: CodeActionKind('quickfix.create.class'));
expect(createClassActions, hasLength(1));
expect(createClassActions.first.diagnostics, hasLength(3));
expect(createClassActions, isNotNull);
expect(createClassActions!.diagnostics, hasLength(3));
}
Future<void> test_organizeImportsFix_namedOrganizeImports() async {
@ -759,11 +616,11 @@ linter:
// This code should get a fix to sort the imports.
const content = '''
import 'dart:io';
[[import 'dart:async']];
[!import 'dart:async'!];
Completer a;
ProcessInfo b;
''';
''';
const expectedContent = '''
import 'dart:async';
@ -771,39 +628,21 @@ import 'dart:io';
Completer a;
ProcessInfo b;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
''';
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.organize.imports'),
title: 'Organize Imports',
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(codeActions,
CodeActionKind('quickfix.organize.imports'), 'Organize Imports')!;
// Ensure the edit came back, and using changes.
final edit = fixAction.edit!;
expect(edit.changes, isNotNull);
expect(edit.documentChanges, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_outsideRoot() async {
final otherFilePath = convertPath('/home/otherProject/foo.dart');
final otherFileUri = Uri.file(otherFilePath);
newFile(otherFilePath, 'bad code to create error');
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
await initialize();
final codeActions = await getCodeActions(
otherFileUri,
@ -826,9 +665,9 @@ ProcessInfo b;
if (!AnalysisServer.supportsPlugins) return;
// Produces a server fix for removing unused import with a default
// priority of 50.
const content = '''
[[import]] 'dart:convert';
''';
final code = TestCode.parse('''
[!import!] 'dart:convert';
''');
// Provide two plugin results that should sort either side of the server fix.
final pluginResult = plugin.EditGetFixesResult([
@ -851,14 +690,11 @@ ProcessInfo b;
request is plugin.EditGetFixesParams ? pluginResult : null,
);
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
newFile(mainFilePath, code.code);
await initialize();
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
await getCodeActions(mainFileUri, range: code.range.range);
final codeActionTitles = codeActions.map((action) =>
action.map((command) => command.title, (action) => action.title));
@ -873,31 +709,19 @@ ProcessInfo b;
}
Future<void> test_pubspec() async {
const content = '';
const content = '^';
const expectedContent = r'''
name: my_project
''';
newFile(pubspecFilePath, content);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
await verifyActionEdits(
filePath: pubspecFilePath,
content,
expectedContent,
kind: CodeActionKind('quickfix.add.name'),
title: "Add 'name' key",
);
// Expect a fix.
final codeActions =
await getCodeActions(pubspecFileUri, range: startOfDocRange);
final fix = findEditAction(
codeActions, CodeActionKind('quickfix.add.name'), "Add 'name' key")!;
// Ensure it makes the correct edits.
final edit = fix.edit!;
final contents = {
pubspecFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[pubspecFilePath], equals(expectedContent));
}
Future<void> test_snippets_createMethod_functionTypeNestedParameters() async {
@ -917,33 +741,13 @@ class A {
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
setSnippetTextEditSupport();
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.create.method'),
title: "Create method 'c'",
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final fixAction = findEditAction(codeActions,
CodeActionKind('quickfix.create.method'), "Create method 'c'")!;
// Ensure the edit came back, and using documentChanges.
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void>
@ -965,35 +769,13 @@ void f() {
useFunction(int g(a, b)) {}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
setSnippetTextEditSupport();
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.create.localVariable'),
title: "Create local variable 'test'",
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.create.localVariable'),
"Create local variable 'test'")!;
// Ensure the edit came back, and using documentChanges.
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
void _enableLints(List<String> lintNames) {

File diff suppressed because it is too large Load diff

View file

@ -22,39 +22,32 @@ void main() {
}
abstract class AbstractSourceCodeActionsTest extends AbstractCodeActionsTest {
/// Wrapper around [checkCodeActionAvailable] for Source actions where
/// position/range is irrelevant (so uses [startOfDocPos]).
Future<void> checkSourceCodeActionAvailable(
Uri uri,
String command,
String title, {
bool asCodeActionLiteral = false,
bool asCommand = false,
}) async {
return checkCodeActionAvailable(
uri,
command,
title,
position: startOfDocPos,
asCodeActionLiteral: asCodeActionLiteral,
asCommand: asCommand,
);
}
/// Wrapper around [getCodeActions] for Source actions where position/range is
/// irrelevant (so uses [startOfDocPos]).
Future<List<Either2<Command, CodeAction>>> getSourceCodeActions(
/// For convenience since source code actions do not rely on a position (but
/// one must be provided), uses [startOfDocPos] to avoid every test needing
/// to include a '^' marker.
@override
Future<List<Either2<Command, CodeAction>>> getCodeActions(
Uri fileUri, {
Range? range,
Position? position,
List<CodeActionKind>? kinds,
CodeActionTriggerKind? triggerKind,
ProgressToken? workDoneToken,
}) {
return getCodeActions(
return super.getCodeActions(
fileUri,
position: startOfDocPos,
kinds: kinds,
triggerKind: triggerKind,
workDoneToken: workDoneToken,
);
}
@override
void setUp() {
super.setUp();
setSupportedCodeActionKinds([CodeActionKind.Source]);
}
}
@reflectiveTest
@ -71,7 +64,6 @@ final a = new Object();
final b = new Set<String>();
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
final a = Object();
final b = <String>{};
''';
@ -80,14 +72,11 @@ final b = <String>{};
newFile(analysisOptionsPath, analysisOptionsContent);
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.fixAll)!;
await verifyCodeActionEdits(codeAction, content, expectedContent);
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_multipleIterations_noOverlay() async {
@ -103,7 +92,6 @@ void f() {
}
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
void f() {
const a = 'test';
}
@ -113,14 +101,11 @@ void f() {
newFile(analysisOptionsPath, analysisOptionsContent);
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.fixAll)!;
await verifyCodeActionEdits(codeAction, content, expectedContent);
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_multipleIterations_overlay() async {
@ -136,7 +121,6 @@ void f() {
}
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
void f() {
const a = 'test';
}
@ -145,15 +129,11 @@ void f() {
registerLintRules();
newFile(analysisOptionsPath, analysisOptionsContent);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
await openFile(mainFileUri, content);
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.fixAll)!;
await verifyCodeActionEdits(codeAction, content, expectedContent);
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_multipleIterations_withClientModification() async {
@ -170,20 +150,15 @@ void f() {
''';
registerLintRules();
newFile(analysisOptionsPath, analysisOptionsContent);
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities: withApplyEditSupport(
withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
await openFile(mainFileUri, content);
// Find the "Fix All" action command.
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.fixAll)!;
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
final codeAction = await expectAction(
content,
command: Commands.fixAll,
);
final command = codeAction.command!;
// Files must be open to apply edits.
await openFile(mainFileUri, content);
// Execute the command with a modification and capture the edit that is
// sent back to us.
@ -222,14 +197,13 @@ void f() {
Future<void> test_unavailable_outsideAnalysisRoot() async {
final otherFile = convertPath('/other/file.dart');
newFile(otherFile, '');
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final content = '';
final codeActions = await getSourceCodeActions(Uri.file(otherFile));
final codeAction = findCommand(codeActions, Commands.fixAll);
expect(codeAction, isNull);
await expectNoAction(
filePath: otherFile,
content,
command: Commands.organizeImports,
);
}
Future<void> test_unusedUsings_notRemovedIfSave() async {
@ -238,15 +212,12 @@ import 'dart:async';
int? a;
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri,
triggerKind: CodeActionTriggerKind.Automatic);
final command = findCommand(codeActions, Commands.fixAll)!
.map((command) => command, (action) => action.command)!;
final codeAction = await expectAction(
content,
command: Commands.fixAll,
triggerKind: CodeActionTriggerKind.Automatic,
);
final command = codeAction.command!;
// We should not get an applyEdit call during the command execution because
// no edits should be produced.
@ -266,20 +237,14 @@ import 'dart:async';
int? a;
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
int? a;
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.fixAll)!;
await verifyCodeActionEdits(codeAction, content, expectedContent);
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
}
@ -296,23 +261,18 @@ Completer foo;
int minified(int x, int y) => min(x, y);
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
import 'dart:async';
import 'dart:math';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities: withApplyEditSupport(
withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.organizeImports)!;
await verifyCodeActionEdits(codeAction, content, expectedContent,
expectDocumentChanges: true);
await verifyActionEdits(
content,
expectedContent,
command: Commands.organizeImports,
);
}
Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
@ -325,71 +285,52 @@ Completer foo;
int minified(int x, int y) => min(x, y);
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
import 'dart:async';
import 'dart:math';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.organizeImports)!;
await verifyCodeActionEdits(codeAction, content, expectedContent);
setDocumentChangesSupport(false);
await verifyActionEdits(
content,
expectedContent,
command: Commands.organizeImports,
);
}
Future<void> test_availableAsCodeActionLiteral() async {
newFile(mainFilePath, '');
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Source]),
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
const content = '';
await checkSourceCodeActionAvailable(
mainFileUri,
Commands.organizeImports,
'Organize Imports',
asCodeActionLiteral: true,
await expectAction(
content,
command: Commands.organizeImports,
);
}
Future<void> test_availableAsCommand() async {
newFile(mainFilePath, '');
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
setSupportedCodeActionKinds(null); // no codeActionLiteralSupport
await initialize();
await checkSourceCodeActionAvailable(
mainFileUri,
Commands.organizeImports,
'Organize Imports',
asCommand: true,
final actions = await getCodeActions(mainFileUri);
final action = findCommand(actions, Commands.organizeImports)!;
action.map(
(command) {},
(codeActionLiteral) => throw 'Expected command, got codeActionLiteral',
);
}
Future<void> test_fileHasErrors_failsSilentlyForAutomatic() async {
final content = 'invalid dart code';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(
mainFileUri,
final codeAction = await expectAction(
content,
command: Commands.organizeImports,
triggerKind: CodeActionTriggerKind.Automatic,
);
final codeAction = findCommand(codeActions, Commands.organizeImports)!;
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
);
final command = codeAction.command!;
// Expect a valid null result.
final response = await executeCommand(command);
@ -398,18 +339,12 @@ int minified(int x, int y) => min(x, y);
Future<void> test_fileHasErrors_failsWithErrorForManual() async {
final content = 'invalid dart code';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.organizeImports)!;
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
final codeAction = await expectAction(
content,
command: Commands.organizeImports,
);
final command = codeAction.command!;
// Ensure the request returned an error (error responses are thrown by
// the test helper to make consuming success results simpler).
@ -419,11 +354,9 @@ int minified(int x, int y) => min(x, y);
Future<void> test_filtersCorrectly() async {
newFile(mainFilePath, '');
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
await initialize();
ofKind(CodeActionKind kind) => getSourceCodeActions(
ofKind(CodeActionKind kind) => getCodeActions(
mainFileUri,
kinds: [kind],
);
@ -444,18 +377,12 @@ import 'dart:math';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.organizeImports)!;
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
final codeAction = await expectAction(
content,
command: Commands.organizeImports,
);
final command = codeAction.command!;
// Execute the command and it should return without needing us to process
// a workspace/applyEdit command because there were no edits.
@ -465,25 +392,23 @@ int minified(int x, int y) => min(x, y);
}
Future<void> test_unavailableWhenNotRequested() async {
newFile(mainFilePath, '');
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final content = '';
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.organizeImports);
expect(codeAction, isNull);
setSupportedCodeActionKinds([CodeActionKind.Refactor]); // not Source
await expectNoAction(
content,
command: Commands.organizeImports,
);
}
Future<void> test_unavailableWithoutApplyEditSupport() async {
newFile(mainFilePath, '');
await initialize();
final content = '';
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.organizeImports);
expect(codeAction, isNull);
setApplyEditSupport(false);
await expectNoAction(
content,
command: Commands.organizeImports,
);
}
}
@ -495,20 +420,15 @@ String b;
String a;
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
String a;
String b;
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities: withApplyEditSupport(
withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.sortMembers)!;
await verifyCodeActionEdits(codeAction, content, expectedContent,
expectDocumentChanges: true);
await verifyActionEdits(
content,
expectedContent,
command: Commands.sortMembers,
);
}
Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
@ -517,48 +437,37 @@ String b;
String a;
''';
const expectedContent = '''
>>>>>>>>>> lib/main.dart
String a;
String b;
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.sortMembers)!;
await verifyCodeActionEdits(codeAction, content, expectedContent);
setDocumentChangesSupport(false);
await verifyActionEdits(
content,
expectedContent,
command: Commands.sortMembers,
);
}
Future<void> test_availableAsCodeActionLiteral() async {
newFile(mainFilePath, '');
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Source]),
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
const content = '';
await checkSourceCodeActionAvailable(
mainFileUri,
Commands.sortMembers,
'Sort Members',
asCodeActionLiteral: true,
await expectAction(
content,
command: Commands.sortMembers,
);
}
Future<void> test_availableAsCommand() async {
newFile(mainFilePath, '');
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
setSupportedCodeActionKinds(null); // no codeActionLiteralSupport
await initialize();
await checkSourceCodeActionAvailable(
mainFileUri,
Commands.sortMembers,
'Sort Members',
asCommand: true,
final actions = await getCodeActions(mainFileUri);
final action = findCommand(actions, Commands.sortMembers)!;
action.map(
(command) {},
(codeActionLiteral) => throw 'Expected command, got codeActionLiteral',
);
}
@ -567,18 +476,12 @@ String b;
String b;
String a;
''';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.sortMembers)!;
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
final codeAction = await expectAction(
content,
command: Commands.sortMembers,
);
final command = codeAction.command!;
final commandResponse = handleExpectedRequest<Object?,
ApplyWorkspaceEditParams, ApplyWorkspaceEditResult>(
@ -600,21 +503,13 @@ String a;
Future<void> test_fileHasErrors_failsSilentlyForAutomatic() async {
final content = 'invalid dart code';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(
mainFileUri,
final codeAction = await expectAction(
content,
command: Commands.sortMembers,
triggerKind: CodeActionTriggerKind.Automatic,
);
final codeAction = findCommand(codeActions, Commands.sortMembers)!;
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
);
final command = codeAction.command!;
// Expect a valid null result.
final response = await executeCommand(command);
@ -623,18 +518,12 @@ String a;
Future<void> test_fileHasErrors_failsWithErrorForManual() async {
final content = 'invalid dart code';
newFile(mainFilePath, content);
await initialize(
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.sortMembers)!;
final command = codeAction.map(
(command) => command,
(codeAction) => codeAction.command!,
final codeAction = await expectAction(
content,
command: Commands.sortMembers,
);
final command = codeAction.command!;
// Ensure the request returned an error (error responses are thrown by
// the test helper to make consuming success results simpler).
@ -643,36 +532,30 @@ String a;
}
Future<void> test_nonDartFile() async {
newFile(pubspecFilePath, simplePubspecContent);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Source]),
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final codeActions = await getSourceCodeActions(pubspecFileUri);
expect(codeActions, isEmpty);
await expectNoAction(
filePath: pubspecFilePath,
simplePubspecContent,
command: Commands.sortMembers,
);
}
Future<void> test_unavailableWhenNotRequested() async {
newFile(mainFilePath, '');
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withApplyEditSupport(emptyWorkspaceClientCapabilities));
final content = '';
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.sortMembers);
expect(codeAction, isNull);
setSupportedCodeActionKinds([CodeActionKind.Refactor]); // not Source
await expectNoAction(
content,
command: Commands.sortMembers,
);
}
Future<void> test_unavailableWithoutApplyEditSupport() async {
newFile(mainFilePath, '');
await initialize();
final content = '';
final codeActions = await getSourceCodeActions(mainFileUri);
final codeAction = findCommand(codeActions, Commands.sortMembers);
expect(codeAction, isNull);
setApplyEditSupport(false);
await expectNoAction(
content,
command: Commands.sortMembers,
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,7 @@ import 'package:test/test.dart' as test show expect;
import '../mocks.dart';
import '../mocks_lsp.dart';
import '../src/utilities/mock_packages.dart';
import 'change_verifier.dart';
const dartLanguageId = 'dart';
@ -104,11 +105,9 @@ abstract class AbstractLspAnalysisServerTest
/// Executes [command] which is expected to call back to the client to apply
/// a [WorkspaceEdit].
///
/// Changes are applied to [contents] to be verified by the caller.
Future<void> executeCommandForEdits(
Command command,
Map<String, String?> contents, {
bool expectDocumentChanges = false,
/// Returns a [LspChangeVerifier] that can be used to verify changes.
Future<LspChangeVerifier> executeCommandForEdits(
Command command, {
ProgressToken? workDoneToken,
}) async {
ApplyWorkspaceEditParams? editParams;
@ -131,69 +130,19 @@ abstract class AbstractLspAnalysisServerTest
// Ensure the edit came back, and using the expected change type.
expect(editParams, isNotNull);
final edit = editParams!.edit;
if (expectDocumentChanges) {
expect(edit.changes, isNull);
expect(edit.documentChanges, isNotNull);
applyDocumentChanges(contents, edit.documentChanges!);
} else {
expect(edit.changes, isNotNull);
expect(edit.documentChanges, isNull);
applyChanges(contents, edit.changes!);
}
}
void expectChanges(Map<Uri, List<TextEdit>> changes, String expected) {
final editedContents = <String, String?>{};
applyChanges(editedContents, changes);
expectEditedContent(editedContents, expected);
final expectDocumentChanges =
workspaceCapabilities.workspaceEdit?.documentChanges ?? false;
expect(edit.documentChanges, expectDocumentChanges ? isNotNull : isNull);
expect(edit.changes, expectDocumentChanges ? isNull : isNotNull);
return LspChangeVerifier(this, edit);
}
void expectContextBuilds() =>
expect(server.contextBuilds - _previousContextBuilds, greaterThan(0),
reason: 'Contexts should have been rebuilt');
Map<String, String?> expectDocumentChanges(
List<Either4<CreateFile, DeleteFile, RenameFile, TextDocumentEdit>> changes,
String expected,
) {
final editedContents = <String, String?>{};
applyDocumentChanges(editedContents, changes);
expectEditedContent(editedContents, expected);
return editedContents;
}
void expectEditedContent(
Map<String, String?> editedContents, String expected) {
final buffer = StringBuffer();
for (final entry in editedContents.entries.sortedBy((entry) => entry.key)) {
// Write the path in a common format for Windows/non-Windows.
final relativePath = path
.relative(
entry.key,
from: projectFolderPath,
)
.replaceAll(r'\', '/');
final content = entry.value;
// TODO(dantup): Extract this (and the applying of edits) to a class
// that can also update the test expectations, and record renames better
// than just a delete/create.
if (content == null) {
buffer.write('>>>>>>>>>> $relativePath deleted\n');
} else if (content.trim().isEmpty) {
buffer.write('>>>>>>>>>> $relativePath empty\n');
} else {
buffer.write('>>>>>>>>>> $relativePath\n$content');
// If the content didn't end with a newline we need to add one, but
// add a marked so it's clear there was no trailing newline.
if (!content.endsWith('\n')) {
buffer.write('<<<<<<<<<<\n');
}
}
}
expect(buffer.toString().trim(), equals(expected.trim()));
}
void expectNoContextBuilds() =>
expect(server.contextBuilds - _previousContextBuilds, equals(0),
reason: 'Contexts should not have been rebuilt');
@ -213,6 +162,21 @@ abstract class AbstractLspAnalysisServerTest
}
}
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,
),
)
.whereNotNull()
.toList();
@override
String? getCurrentFileContent(Uri uri) {
try {
@ -342,22 +306,33 @@ analyzer:
/// Verifies that executing the given command on the server results in an edit
/// being sent in the client that updates the files to match the expected
/// content.
Future<void> verifyCommandEdits(
Future<LspChangeVerifier> verifyCommandEdits(
Command command,
String expectedContent, {
bool expectDocumentChanges = false,
ProgressToken? workDoneToken,
}) async {
final contents = <String, String?>{};
await executeCommandForEdits(
final verifier = await executeCommandForEdits(
command,
contents,
expectDocumentChanges: expectDocumentChanges,
workDoneToken: workDoneToken,
);
expectEditedContent(contents, expectedContent);
verifier.verifyFiles(expectedContent);
return verifier;
}
LspChangeVerifier verifyEdit(
WorkspaceEdit edit,
String expected, {
Map<Uri, int>? expectedVersions,
}) {
final expectDocumentChanges =
workspaceCapabilities.workspaceEdit?.documentChanges ?? false;
expect(edit.documentChanges, expectDocumentChanges ? isNotNull : isNull);
expect(edit.changes, expectDocumentChanges ? isNull : isNotNull);
final verifier = LspChangeVerifier(this, edit);
verifier.verifyFiles(expected, expectedVersions: expectedVersions);
return verifier;
}
/// Adds a trailing slash (direction based on path context) to [path].
@ -385,6 +360,22 @@ mixin ClientCapabilitiesHelperMixin {
final emptyWindowClientCapabilities = WindowClientCapabilities();
/// The set of TextDocument capabilities used if no explicit instance is
/// passed to [initialize].
var textDocumentCapabilities = TextDocumentClientCapabilities();
/// The set of Workspace capabilities used if no explicit instance is
/// passed to [initialize].
var workspaceCapabilities = WorkspaceClientCapabilities();
/// The set of Window capabilities used if no explicit instance is
/// passed to [initialize].
var windowCapabilities = WindowClientCapabilities();
/// The set of experimental capabilities used if no explicit instance is
/// passed to [initialize].
var experimentalCapabilities = <String, Object?>{};
TextDocumentClientCapabilities extendTextDocumentCapabilities(
TextDocumentClientCapabilities source,
Map<String, dynamic> textDocumentCapabilities,
@ -425,6 +416,61 @@ mixin ClientCapabilitiesHelperMixin {
}
}
void setApplyEditSupport([bool supported = true]) {
workspaceCapabilities =
withApplyEditSupport(workspaceCapabilities, supported);
}
void setConfigurationSupport() {
workspaceCapabilities = withConfigurationSupport(workspaceCapabilities);
}
void setDocumentChangesSupport([bool supported = true]) {
workspaceCapabilities =
withDocumentChangesSupport(workspaceCapabilities, supported);
}
void setFileCreateSupport([bool supported = true]) {
if (supported) {
workspaceCapabilities = withDocumentChangesSupport(
withResourceOperationKinds(
workspaceCapabilities, [ResourceOperationKind.Create]));
} else {
workspaceCapabilities.workspaceEdit?.resourceOperations
?.remove(ResourceOperationKind.Create);
}
}
void setFileRenameSupport([bool supported = true]) {
if (supported) {
workspaceCapabilities = withDocumentChangesSupport(
withResourceOperationKinds(
workspaceCapabilities, [ResourceOperationKind.Rename]));
} else {
workspaceCapabilities.workspaceEdit?.resourceOperations
?.remove(ResourceOperationKind.Rename);
}
}
void setSnippetTextEditSupport([bool supported = true]) {
experimentalCapabilities['snippetTextEdit'] = supported;
}
void setSupportedCodeActionKinds(List<CodeActionKind>? kinds) {
textDocumentCapabilities =
withCodeActionKinds(textDocumentCapabilities, kinds);
}
void setSupportedCommandParameterKinds(Set<String>? kinds) {
experimentalCapabilities['dartCodeAction'] = {
'commandParameterSupport': {'supportedKinds': kinds?.toList()},
};
}
void setWorkDoneProgressSupport() {
windowCapabilities = withWorkDoneProgressSupport(windowCapabilities);
}
TextDocumentClientCapabilities
withAllSupportedTextDocumentDynamicRegistrations(
TextDocumentClientCapabilities source,
@ -474,20 +520,24 @@ mixin ClientCapabilitiesHelperMixin {
}
WorkspaceClientCapabilities withApplyEditSupport(
WorkspaceClientCapabilities source,
) {
return extendWorkspaceCapabilities(source, {'applyEdit': true});
WorkspaceClientCapabilities source,
[bool supported = true]) {
return extendWorkspaceCapabilities(source, {'applyEdit': supported});
}
TextDocumentClientCapabilities withCodeActionKinds(
TextDocumentClientCapabilities source,
List<CodeActionKind> kinds,
List<CodeActionKind>? kinds,
) {
return extendTextDocumentCapabilities(source, {
'codeAction': {
'codeActionLiteralSupport': {
'codeActionKind': {'valueSet': kinds.map((k) => k.toJson()).toList()}
}
'codeActionLiteralSupport': kinds != null
? {
'codeActionKind': {
'valueSet': kinds.map((k) => k.toJson()).toList()
}
}
: null,
}
});
}
@ -613,10 +663,11 @@ mixin ClientCapabilitiesHelperMixin {
}
WorkspaceClientCapabilities withDocumentChangesSupport(
WorkspaceClientCapabilities source,
) {
WorkspaceClientCapabilities source, [
bool supported = true,
]) {
return extendWorkspaceCapabilities(source, {
'workspaceEdit': {'documentChanges': true}
'workspaceEdit': {'documentChanges': supported}
});
}
@ -891,129 +942,6 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
Stream<Message> get serverToClient;
void applyChanges(
Map<String, String?> editedContents,
Map<Uri, List<TextEdit>> changes,
) {
changes.forEach((fileUri, edits) {
final filePath = fileUri.toFilePath();
final currentContent = editedContents.containsKey(filePath)
? editedContents[filePath]
: getCurrentFileContent(fileUri);
editedContents[filePath] = applyTextEdits(currentContent!, edits);
});
}
void applyDocumentChanges(
Map<String, String?> editedContent,
List<Either4<CreateFile, DeleteFile, RenameFile, TextDocumentEdit>>
documentChanges, {
Map<String, int>? expectedVersions,
}) {
// If we were supplied with expected versions, ensure that all returned
// edits match the versions.
if (expectedVersions != null) {
expectDocumentVersions(documentChanges, expectedVersions);
}
applyResourceChanges(editedContent, documentChanges);
}
void applyResourceChanges(
Map<String, String?> editedContent,
List<Either4<CreateFile, DeleteFile, RenameFile, TextDocumentEdit>> changes,
) {
for (final change in changes) {
change.map(
(create) => applyResourceCreate(editedContent, create),
(delete) => applyResourceDelete(editedContent, delete),
(rename) => applyResourceRename(editedContent, rename),
(textDocEdit) => applyTextDocumentEdits(editedContent, [textDocEdit]),
);
}
}
void applyResourceCreate(
Map<String, String?> editedContent, CreateFile create) {
final uri = create.uri;
final path = uri.toFilePath();
final currentContent = editedContent.containsKey(path)
? editedContent[path]
: getCurrentFileContent(uri);
if (currentContent != null) {
throw 'Received create instruction for $path which already exists';
}
editedContent[path] = '';
}
void applyResourceDelete(
Map<String, String?> editedContent, DeleteFile delete) {
final uri = delete.uri;
final path = uri.toFilePath();
final currentContent = editedContent.containsKey(path)
? editedContent[path]
: getCurrentFileContent(uri);
if (currentContent == null) {
throw 'Received delete instruction for $path which does not exist';
}
editedContent[path] = null;
}
void applyResourceRename(
Map<String, String?> editedContent, RenameFile rename) {
final oldUri = rename.oldUri;
final newUri = rename.newUri;
final oldPath = oldUri.toFilePath();
final newPath = newUri.toFilePath();
final oldContent = editedContent.containsKey(oldPath)
? editedContent[oldPath]
: getCurrentFileContent(oldUri);
final newContent = editedContent.containsKey(newPath)
? editedContent[newPath]
: getCurrentFileContent(newUri);
if (oldContent == null) {
throw 'Received rename instruction from $oldPath which did not exist';
} else if (newContent != null) {
throw 'Received rename instruction to $newPath which already exists';
}
editedContent[newPath] = oldContent;
editedContent[oldPath] = null;
}
String applyTextDocumentEdit(String content, TextDocumentEdit edit) {
// To simulate the behaviour we'll get from an LSP client, apply edits from
// the latest offset to the earliest, but with items at the same offset
// being reversed so that when applied sequentially they appear in the
// document in-order.
//
// This is essentially a stable sort over the offset (descending), but since
// List.sort() is not stable so we additionally sort by index).
final indexedEdits =
edit.edits.mapIndexed(_TextEditWithIndex.fromUnion).toList();
indexedEdits.sort(_TextEditWithIndex.compare);
return indexedEdits.map((e) => e.edit).fold(content, applyTextEdit);
}
void applyTextDocumentEdits(
Map<String, String?> editedContent, List<TextDocumentEdit> edits) {
for (var edit in edits) {
final uri = edit.textDocument.uri;
final path = uri.toFilePath();
final currentContent = editedContent.containsKey(path)
? editedContent[path]
: getCurrentFileContent(uri);
if (currentContent == null) {
throw 'Received edits for $path which does not exist. '
'Perhaps a CreateFile change was missing from the edits?';
}
editedContent[path] = applyTextDocumentEdit(currentContent, edit);
}
}
String applyTextEdit(String content, TextEdit edit) {
final startPos = edit.range.start;
final endPos = edit.range.end;
@ -1068,8 +996,8 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
validateChangesCanBeApplied();
final indexedEdits = changes.mapIndexed(_TextEditWithIndex.new).toList();
indexedEdits.sort(_TextEditWithIndex.compare);
final indexedEdits = changes.mapIndexed(TextEditWithIndex.new).toList();
indexedEdits.sort(TextEditWithIndex.compare);
return indexedEdits.map((e) => e.edit).fold(content, applyTextEdit);
}
@ -1173,35 +1101,6 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
void expect(Object? actual, Matcher matcher, {String? reason}) =>
test.expect(actual, matcher, reason: reason);
void expectDocumentVersion(
TextDocumentEdit edit,
Map<String, int> expectedVersions,
) {
final path = edit.textDocument.uri.toFilePath();
final expectedVersion = expectedVersions[path];
expect(edit.textDocument.version, equals(expectedVersion));
}
/// Validates the document versions for a set of edits match the versions in
/// the supplied map.
void expectDocumentVersions(
List<Either4<CreateFile, DeleteFile, RenameFile, TextDocumentEdit>>
documentChanges,
Map<String, int> expectedVersions,
) {
// For resource changes, we only need to validate changes since
// creates/renames/deletes do not supply versions.
for (var change in documentChanges) {
change.map(
(create) {},
(delete) {},
(rename) {},
(edit) => expectDocumentVersion(edit, expectedVersions),
);
}
}
Future<ShowMessageParams> expectErrorNotification(
FutureOr<void> Function() f, {
Duration timeout = const Duration(seconds: 5),
@ -1301,6 +1200,7 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
Position? position,
List<CodeActionKind>? kinds,
CodeActionTriggerKind? triggerKind,
ProgressToken? workDoneToken,
}) {
range ??= position != null
? Range(start: position, end: position)
@ -1318,6 +1218,7 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
only: kinds,
triggerKind: triggerKind,
),
workDoneToken: workDoneToken,
),
);
return expectSuccessfulResponseTo(
@ -1726,6 +1627,9 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
String? rootPath,
Uri? rootUri,
List<Uri>? workspaceFolders,
// TODO(dantup): Remove these capabilities fields in favour of methods like
// [setApplyEditSupport] which allows extracting initialization in tests
// without needing to pass capabilities these all the way through.
TextDocumentClientCapabilities? textDocumentCapabilities,
WorkspaceClientCapabilities? workspaceCapabilities,
WindowClientCapabilities? windowCapabilities,
@ -1745,10 +1649,10 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
}
final clientCapabilities = ClientCapabilities(
workspace: workspaceCapabilities,
textDocument: textDocumentCapabilities,
window: windowCapabilities,
experimental: experimentalCapabilities,
workspace: workspaceCapabilities ?? this.workspaceCapabilities,
textDocument: textDocumentCapabilities ?? this.textDocumentCapabilities,
window: windowCapabilities ?? this.windowCapabilities,
experimental: experimentalCapabilities ?? this.experimentalCapabilities,
);
_clientCapabilities = clientCapabilities;
@ -1968,6 +1872,10 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
FutureOr<Map<String, Object?>> globalConfig, {
FutureOr<Map<String, Map<String, Object?>>>? folderConfig,
}) {
final self = this;
if (self is AbstractLspAnalysisServerTest) {
self.setConfigurationSupport();
}
return handleExpectedRequest<T, ConfigurationParams,
List<Map<String, Object?>>>(
Method.workspace_configuration,
@ -2079,6 +1987,17 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
);
}
/// Formats a path relative to the project root always using forward slashes.
///
/// This is used in the text format for comparing edits.
String relativePath(String filePath) =>
path.relative(filePath, from: projectFolderPath).replaceAll(r'\', '/');
/// Formats a path relative to the project root always using forward slashes.
///
/// This is used in the text format for comparing edits.
String relativeUri(Uri uri) => relativePath(uri.toFilePath());
Future<WorkspaceEdit?> rename(
Uri uri,
int? version,
@ -2413,20 +2332,20 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
}
}
class _TextEditWithIndex {
class TextEditWithIndex {
final int index;
final TextEdit edit;
_TextEditWithIndex(this.index, this.edit);
TextEditWithIndex(this.index, this.edit);
_TextEditWithIndex.fromUnion(
TextEditWithIndex.fromUnion(
this.index, Either3<AnnotatedTextEdit, SnippetTextEdit, TextEdit> edit)
: edit = edit.map((e) => e, (e) => e, (e) => e);
/// Compares two [_TextEditWithIndex] to sort them by the order in which they
/// Compares two [TextEditWithIndex] to sort them by the order in which they
/// can be sequentially applied to a String to match the behaviour of an LSP
/// client.
static int compare(_TextEditWithIndex edit1, _TextEditWithIndex edit2) {
static int compare(TextEditWithIndex edit1, TextEditWithIndex edit2) {
final end1 = edit1.edit.range.end;
final end2 = edit2.edit.range.end;

View file

@ -102,7 +102,8 @@ final a = A();
class A {}
''';
final expectedMainContent = '''
final expectedContent = '''
>>>>>>>>>> lib/main.dart
import 'other_new.dart';
final a = A();
@ -118,12 +119,7 @@ final a = A();
),
]);
// Ensure applying the edit will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(mainContent),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedMainContent));
verifyEdit(edit, expectedContent);
}
Future<void> test_renameFolder_updatesImports() async {
@ -143,6 +139,7 @@ class A {}
''';
final expectedMainContent = '''
>>>>>>>>>> lib/main.dart
import 'folder_new/other.dart';
final a = A();
@ -158,11 +155,6 @@ final a = A();
),
]);
// Ensure applying the edit will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(mainContent),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedMainContent));
verifyEdit(edit, expectedMainContent);
}
}

View file

@ -194,8 +194,7 @@ void f() {
ConvertAllFormalParametersToNamed.constTitle,
);
await verifyCommandEdits(codeAction.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(codeAction.command!, expected);
}
Future<void> _assertNoRefactoring() async {

View file

@ -286,8 +286,7 @@ void f() {
ConvertSelectedFormalParametersToNamed.constTitle,
);
await verifyCommandEdits(codeAction.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(codeAction.command!, expected);
}
Future<void> _assertNoRefactoring() async {

View file

@ -370,8 +370,7 @@ void f() {
MoveSelectedFormalParametersLeft.constTitle,
);
await verifyCommandEdits(codeAction.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(codeAction.command!, expected);
}
Future<void> _assertNoRefactoring() async {

View file

@ -36,6 +36,13 @@ class ^A {}
arguments[0] = newFileUri.toString();
}
@override
void setUp() {
super.setUp();
setFileCreateSupport();
}
/// Test that references to getter/setters in different libraries used in
/// a compound assignment are both imported into the destination file.
Future<void> test_compoundAssignment_multipleLibraries() async {
@ -57,7 +64,7 @@ void function^ToMove() {
var declarationName = 'functionToMove';
var expected = '''
>>>>>>>>>> lib/function_to_move.dart
>>>>>>>>>> lib/function_to_move.dart created
import 'package:test/getter.dart';
import 'package:test/setter.dart';
@ -67,7 +74,6 @@ void functionToMove() {
>>>>>>>>>> lib/main.dart
import 'package:test/getter.dart';
import 'package:test/setter.dart';
''';
await _singleDeclaration(
originalSource: originalSource,
@ -89,7 +95,7 @@ class B {}
var declarationName = 'ClassToMove';
var expected = '''
>>>>>>>>>> lib/class_to_move.dart
>>>>>>>>>> lib/class_to_move.dart created
// File header.
class ClassToMove {}
@ -127,8 +133,7 @@ class A {}
await initializeServer();
final action = await expectCodeAction(simpleClassRefactorTitle);
await verifyCommandEdits(action.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(action.command!, expected);
}
Future<void> test_existingFile_withHeader() async {
@ -155,8 +160,7 @@ class A {}
await initializeServer();
final action = await expectCodeAction(simpleClassRefactorTitle);
await verifyCommandEdits(action.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(action.command!, expected);
}
Future<void> test_existingFile_withImports() async {
@ -183,8 +187,7 @@ class A {}
await initializeServer();
final action = await expectCodeAction(simpleClassRefactorTitle);
await verifyCommandEdits(action.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(action.command!, expected);
}
Future<void> test_imports_declarationInSrc() async {
@ -205,11 +208,10 @@ A? mov^ing;
import 'package:test/a.dart';
A? staying;
>>>>>>>>>> lib/moving.dart
>>>>>>>>>> lib/moving.dart created
import 'package:test/a.dart';
A? moving;
''';
await _singleDeclaration(
originalSource: originalSource,
@ -240,7 +242,7 @@ void ^f() {
var declarationName = 'f';
var expected = '''
>>>>>>>>>> lib/f.dart
>>>>>>>>>> lib/f.dart created
import 'package:test/extensions.dart';
import 'package:test/main.dart';
@ -283,7 +285,7 @@ void ^f() {
var declarationName = 'f';
var expected = '''
>>>>>>>>>> lib/f.dart
>>>>>>>>>> lib/f.dart created
import 'package:test/extensions.dart';
import 'package:test/main.dart';
@ -461,14 +463,13 @@ void ^moving() {
import 'package:test/extensions.dart' as other;
class A {}
>>>>>>>>>> lib/moving.dart
>>>>>>>>>> lib/moving.dart created
import 'package:test/extensions.dart' as other;
import 'package:test/main.dart';
void moving() {
A().extensionMethod();
}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -504,14 +505,13 @@ void ^moving() {
import 'package:test/extensions.dart' as other;
class A {}
>>>>>>>>>> lib/moving.dart
>>>>>>>>>> lib/moving.dart created
import 'package:test/extensions.dart' as other;
import 'package:test/main.dart';
void moving() {
A() + A();
}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -755,7 +755,7 @@ class B^ {
var declarationName = 'B';
var expected = '''
>>>>>>>>>> lib/b.dart
>>>>>>>>>> lib/b.dart created
import 'dart:io';
class B {
@ -765,8 +765,6 @@ class B {
import 'dart:io';
class A {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -784,14 +782,12 @@ class ClassToMove^ extends A {}
var declarationName = 'ClassToMove';
var expected = '''
>>>>>>>>>> lib/class_to_move.dart
>>>>>>>>>> lib/class_to_move.dart created
import 'package:test/main.dart';
class ClassToMove extends A {}
>>>>>>>>>> lib/main.dart
class A {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -809,13 +805,12 @@ class B^ {}
var declarationName = 'B';
var expected = '''
>>>>>>>>>> lib/b.dart
>>>>>>>>>> lib/b.dart created
class B {}
>>>>>>>>>> lib/main.dart
import 'package:test/b.dart';
class A extends B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -839,7 +834,7 @@ B? b;
''';
var expected = '''
>>>>>>>>>> lib/b.dart
>>>>>>>>>> lib/b.dart created
class B {}
>>>>>>>>>> lib/c.dart
import 'package:test/b.dart';
@ -848,8 +843,6 @@ import 'package:test/main.dart';
B? b;
>>>>>>>>>> lib/main.dart
class A {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -877,7 +870,7 @@ void f(p.B b, q.B b, B b) {}
''';
var expected = '''
>>>>>>>>>> lib/b.dart
>>>>>>>>>> lib/b.dart created
class B {}
>>>>>>>>>> lib/c.dart
import 'package:test/b.dart';
@ -890,8 +883,6 @@ import 'package:test/main.dart' as q;
void f(p.B b, q.B b, B b) {}
>>>>>>>>>> lib/main.dart
class A {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -917,7 +908,7 @@ p.B? b;
''';
var expected = '''
>>>>>>>>>> lib/b.dart
>>>>>>>>>> lib/b.dart created
class B {}
>>>>>>>>>> lib/c.dart
import 'package:test/b.dart' as p;
@ -926,8 +917,6 @@ import 'package:test/main.dart' as p;
p.B? b;
>>>>>>>>>> lib/main.dart
class A {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -969,7 +958,6 @@ import 'package:test/a.dart' hide A;
import 'package:test/a.dart';
A? moving;
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1009,7 +997,6 @@ import 'package:test/a.dart' hide A;
import 'package:test/a.dart' show A;
A? moving;
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1038,11 +1025,10 @@ A? mov^ing;
import 'package:test/a.dart' show A;
A? staying;
>>>>>>>>>> lib/moving.dart
>>>>>>>>>> lib/moving.dart created
import 'package:test/a.dart' show A;
A? moving;
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1062,13 +1048,12 @@ class B {}
var declarationName = 'ClassToMove';
var expected = '''
>>>>>>>>>> lib/class_to_move.dart
>>>>>>>>>> lib/class_to_move.dart created
class ClassToMove {}
>>>>>>>>>> lib/main.dart
class A {}
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1098,7 +1083,7 @@ class B {}
''';
var expected = '''
>>>>>>>>>> lib/class_to_move1.dart
>>>>>>>>>> lib/class_to_move1.dart created
class ClassToMove1 {}
class ClassToMove2 {}
@ -1106,7 +1091,6 @@ class ClassToMove2 {}
class A {}
class B {}
''';
await _multipleDeclarations(
originalSource: originalSource,
@ -1122,7 +1106,7 @@ class B {}
class A {}
''');
await initializeServer(experimentalOptInFlag: false);
await initializeServer();
await expectNoCodeAction(null);
}
@ -1133,7 +1117,7 @@ imp^ort 'dart:core';
class A {}
''');
await initializeServer(experimentalOptInFlag: false);
await initializeServer();
await expectNoCodeAction(null);
}
@ -1158,7 +1142,7 @@ void function^ToMove() {
var declarationName = 'functionToMove';
var expected = '''
>>>>>>>>>> lib/function_to_move.dart
>>>>>>>>>> lib/function_to_move.dart created
import 'package:test/getter.dart';
import 'package:test/setter.dart';
@ -1168,7 +1152,6 @@ void functionToMove() {
>>>>>>>>>> lib/main.dart
import 'package:test/getter.dart';
import 'package:test/setter.dart';
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1187,7 +1170,7 @@ import 'package:test/setter.dart';
Future<void>
test_protocol_available_withoutClientCommandParameterSupport() async {
addTestSource(simpleClassContent);
await initializeServer(commandParameterSupportedKinds: null);
await initializeServer();
// This refactor is available without command parameter support because
// it has defaults.
await expectCodeAction(simpleClassRefactorTitle);
@ -1209,7 +1192,7 @@ import 'package:test/setter.dart';
/// Expected new file content.
const expected = '''
>>>>>>>>>> lib/main.dart empty
>>>>>>>>>> lib/my_new_class.dart
>>>>>>>>>> lib/my_new_class.dart created
class A {}
''';
@ -1217,13 +1200,13 @@ class A {}
final action = await expectCodeAction(simpleClassRefactorTitle);
// Replace the file URI argument with our custom path.
replaceSaveUriArgument(action, newFileUri);
await verifyCommandEdits(action.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(action.command!, expected);
}
Future<void> test_protocol_unavailable_withoutFileCreateSupport() async {
addTestSource(simpleClassContent);
await initializeServer(fileCreateSupport: false);
setFileCreateSupport(false);
await initializeServer();
await expectNoCodeAction(simpleClassRefactorTitle);
}
@ -1238,7 +1221,7 @@ class Neither {}
''';
var expected = '''
>>>>>>>>>> lib/either.dart
>>>>>>>>>> lib/either.dart created
sealed class Either {}
class Left extends Either {}
@ -1246,7 +1229,6 @@ class Right extends Either {}
>>>>>>>>>> lib/main.dart
class Neither {}
''';
await _multipleDeclarations(
originalSource: originalSource,
@ -1304,7 +1286,7 @@ class Neither {}
// TODO(dantup): Track down where this extra newline is coming from.
var expected = '''
>>>>>>>>>> lib/either.dart
>>>>>>>>>> lib/either.dart created
sealed class Either {}
class Left extends Either {}
@ -1316,7 +1298,6 @@ import 'package:test/either.dart';
class LeftSub extends Left {}
class Neither {}
''';
await _multipleDeclarations(
originalSource: originalSource,
@ -1336,7 +1317,7 @@ class Neither {}
''';
var expected = '''
>>>>>>>>>> lib/either.dart
>>>>>>>>>> lib/either.dart created
sealed class Either {}
class Left extends Either {}
@ -1344,7 +1325,6 @@ class Right extends Either {}
>>>>>>>>>> lib/main.dart
class Neither {}
''';
await _multipleDeclarations(
originalSource: originalSource,
@ -1364,7 +1344,7 @@ class Neither {}
''';
var expected = '''
>>>>>>>>>> lib/either.dart
>>>>>>>>>> lib/either.dart created
sealed class Either {}
class Left implements Either {}
@ -1372,7 +1352,6 @@ class Right implements Either {}
>>>>>>>>>> lib/main.dart
class Neither {}
''';
await _multipleDeclarations(
originalSource: originalSource,
@ -1399,14 +1378,13 @@ import 'package:test/sealed_root.dart';
class SubSubSubclass extends SubSubclass {}
>>>>>>>>>> lib/sealed_root.dart
>>>>>>>>>> lib/sealed_root.dart created
sealed class SealedRoot {}
class Subclass extends SealedRoot {}
sealed class SealedSubclass extends SealedRoot {}
class SubSubclass extends SealedSubclass {}
''';
await _multipleDeclarations(
originalSource: originalSource,
@ -1426,13 +1404,12 @@ class B {}
var declarationName = 'ClassToMove';
var expected = '''
>>>>>>>>>> lib/class_to_move.dart
>>>>>>>>>> lib/class_to_move.dart created
class ClassToMove<T> {}
>>>>>>>>>> lib/main.dart
class A {}
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1452,13 +1429,12 @@ class B {}
var declarationName = 'EnumToMove';
var expected = '''
>>>>>>>>>> lib/enum_to_move.dart
>>>>>>>>>> lib/enum_to_move.dart created
enum EnumToMove { a, b }
>>>>>>>>>> lib/main.dart
class A {}
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1478,13 +1454,12 @@ class B {}
var declarationName = 'ExtensionToMove';
var expected = '''
>>>>>>>>>> lib/extension_to_move.dart
>>>>>>>>>> lib/extension_to_move.dart created
extension ExtensionToMove on int { }
>>>>>>>>>> lib/main.dart
class A {}
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1504,13 +1479,12 @@ class B {}
var declarationName = 'functionToMove';
var expected = '''
>>>>>>>>>> lib/function_to_move.dart
>>>>>>>>>> lib/function_to_move.dart created
void functionToMove() { }
>>>>>>>>>> lib/main.dart
class A {}
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1530,13 +1504,12 @@ class B {}
var declarationName = 'functionToMove';
var expected = '''
>>>>>>>>>> lib/function_to_move.dart
>>>>>>>>>> lib/function_to_move.dart created
void functionToMove() { }
>>>>>>>>>> lib/main.dart
class A {}
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1560,9 +1533,8 @@ class B {}
class A {}
class B {}
>>>>>>>>>> lib/mixin_to_move.dart
>>>>>>>>>> lib/mixin_to_move.dart created
mixin MixinToMove { }
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1592,7 +1564,6 @@ part of 'main.dart';
class ClassToMove {}
>>>>>>>>>> lib/main.dart
part 'class_to_move.dart';
''';
await _singleDeclaration(
@ -1623,7 +1594,6 @@ part 'main.dart';
class ClassToMove {}
>>>>>>>>>> lib/main.dart
part of 'class_to_move.dart';
''';
await _singleDeclaration(
@ -1660,7 +1630,6 @@ part of 'containing_library.dart';
class ClassToMove {}
>>>>>>>>>> lib/main.dart
part of 'containing_library.dart';
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1685,9 +1654,8 @@ class B {}
class A {}
class B {}
>>>>>>>>>> lib/type_to_move.dart
>>>>>>>>>> lib/type_to_move.dart created
typedef TypeToMove = void Function();
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1712,9 +1680,8 @@ class B {}
class A {}
class B {}
>>>>>>>>>> lib/variable_to_move.dart
>>>>>>>>>> lib/variable_to_move.dart created
int variableToMove = 3;
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1733,13 +1700,12 @@ class B {}
var declarationName = 'A';
var expected = '''
>>>>>>>>>> lib/a.dart
>>>>>>>>>> lib/a.dart created
///
class A {}
>>>>>>>>>> lib/main.dart
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
@ -1782,8 +1748,7 @@ class B {}
await initializeServer();
final action = await expectCodeAction(actionTitle);
await verifyCommandEdits(action.command!, expected,
expectDocumentChanges: true);
await verifyCommandEdits(action.command!, expected);
}
Future<void> _singleDeclaration({
@ -1824,7 +1789,7 @@ ${code.rawCode}
var expected = '''
>>>>>>>>>> lib/main.dart
import 'package:test/other.dart' as other;
>>>>>>>>>> lib/moving.dart
>>>>>>>>>> lib/moving.dart created
import 'package:test/other.dart' as other;
${code.code}

View file

@ -45,11 +45,7 @@ abstract class RefactoringTest extends AbstractCodeActionsTest {
/// Executes the refactor in [action].
Future<void> executeRefactor(CodeAction action) async {
await executeCommandForEdits(
action.command!,
{},
expectDocumentChanges: true,
);
await executeCommandForEdits(action.command!);
}
/// Expects to find a refactor [CodeAction] in [mainFileUri] at the offset of
@ -107,39 +103,11 @@ abstract class RefactoringTest extends AbstractCodeActionsTest {
/// corresponding flags are set to `false`.
Future<void> initializeServer({
bool experimentalOptInFlag = true,
Set<String>? commandParameterSupportedKinds,
bool fileCreateSupport = true,
bool applyEditSupport = true,
}) async {
final config = {
if (experimentalOptInFlag) 'experimentalRefactors': true,
};
final experimentalCapabilities = {
if (commandParameterSupportedKinds != null)
'dartCodeAction': {
'commandParameterSupport': {
'supportedKinds': commandParameterSupportedKinds.toList()
},
}
};
var workspaceCapabilities =
withConfigurationSupport(emptyWorkspaceClientCapabilities);
if (applyEditSupport) {
workspaceCapabilities = withApplyEditSupport(workspaceCapabilities);
}
if (fileCreateSupport) {
workspaceCapabilities = withDocumentChangesSupport(
withResourceOperationKinds(
workspaceCapabilities, [ResourceOperationKind.Create]));
}
await provideConfig(
() => initialize(
workspaceCapabilities: workspaceCapabilities,
experimentalCapabilities: experimentalCapabilities,
),
config,
);
await provideConfig(super.initialize, config);
}
}