blob: 72aecbb794c73e443056549a5e23b410031575d4 [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:linter/src/rules.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../tool/lsp_spec/matchers.dart';
import 'code_actions_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(SortMembersSourceCodeActionsTest);
defineReflectiveTests(OrganizeImportsSourceCodeActionsTest);
defineReflectiveTests(FixAllSourceCodeActionsTest);
});
}
abstract class AbstractSourceCodeActionsTest extends AbstractCodeActionsTest {
/// 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 super.getCodeActions(
fileUri,
position: startOfDocPos,
kinds: kinds,
triggerKind: triggerKind,
workDoneToken: workDoneToken,
);
}
@override
void setUp() {
super.setUp();
setSupportedCodeActionKinds([CodeActionKind.Source]);
}
}
@reflectiveTest
class FixAllSourceCodeActionsTest extends AbstractSourceCodeActionsTest {
Future<void> test_appliesCorrectEdits() async {
const analysisOptionsContent = '''
linter:
rules:
- unnecessary_new
- prefer_collection_literals
''';
const content = '''
final a = new Object();
final b = new Set<String>();
''';
const expectedContent = '''
final a = Object();
final b = <String>{};
''';
registerLintRules();
newFile(analysisOptionsPath, analysisOptionsContent);
newFile(mainFilePath, content);
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_multipleIterations_noOverlay() async {
const analysisOptionsContent = '''
linter:
rules:
- prefer_final_locals
- prefer_const_declarations
''';
const content = '''
void f() {
var a = 'test';
}
''';
const expectedContent = '''
void f() {
const a = 'test';
}
''';
registerLintRules();
newFile(analysisOptionsPath, analysisOptionsContent);
newFile(mainFilePath, content);
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_multipleIterations_overlay() async {
const analysisOptionsContent = '''
linter:
rules:
- prefer_final_locals
- prefer_const_declarations
''';
const content = '''
void f() {
var a = 'test';
}
''';
const expectedContent = '''
void f() {
const a = 'test';
}
''';
registerLintRules();
newFile(analysisOptionsPath, analysisOptionsContent);
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_multipleIterations_withClientModification() async {
const analysisOptionsContent = '''
linter:
rules:
- prefer_final_locals
- prefer_const_declarations
''';
const content = '''
void f() {
var a = 'test';
}
''';
registerLintRules();
newFile(analysisOptionsPath, analysisOptionsContent);
var codeAction = await expectAction(
content,
command: Commands.fixAll,
);
var 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.
ApplyWorkspaceEditParams? editParams;
await handleExpectedRequest<Object?, ApplyWorkspaceEditParams,
ApplyWorkspaceEditResult>(
Method.workspace_applyEdit,
ApplyWorkspaceEditParams.fromJson,
() async {
// Apply the command and immediately modify a file afterwards.
var commandFuture = executeCommand(command);
await replaceFile(12345, mainFileUri, 'client-modified-content');
return commandFuture;
},
handler: (edit) {
// When the server sends the edit back, just keep a copy and say we
// applied successfully (we'll verify the actual edit below).
editParams = edit;
return ApplyWorkspaceEditResult(applied: true);
},
);
// Extract the text edit from the 'workspace/applyEdit' params the server
// sent us.
var change = editParams?.edit.documentChanges!.single;
var edit = change!.map(
(create) => throw 'Expected edit, got create',
(delete) => throw 'Expected edit, got delete',
(rename) => throw 'Expected edit, got rename',
(edit) => edit,
);
// Ensure the edit says that it was based on version 1 (the original
// version) and not the updated 12345 version we sent.
expect(edit.textDocument.version, 1);
}
Future<void> test_part() async {
var containerFilePath = join(projectFolderPath, 'lib', 'container.dart');
var partFilePath = join(projectFolderPath, 'lib', 'part.dart');
const analysisOptionsContent = '''
linter:
rules:
- unnecessary_new
- prefer_collection_literals
''';
const containerFileContent = '''
part 'part.dart';
''';
const content = '''
part of 'container.dart';
final a = new Object();
final b = new Set<String>();
''';
const expectedContent = '''
part of 'container.dart';
final a = Object();
final b = <String>{};
''';
registerLintRules();
newFile(analysisOptionsPath, analysisOptionsContent);
newFile(containerFilePath, containerFileContent);
newFile(partFilePath, content);
await verifyActionEdits(
filePath: partFilePath,
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_privateUnusedParameters_notRemovedIfSave() async {
const content = '''
class _MyClass {
int? _param;
_MyClass({
this._param,
});
}
''';
var codeAction = await expectAction(
content,
command: Commands.fixAll,
triggerKind: CodeActionTriggerKind.Automatic,
);
var command = codeAction.command!;
// We should not get an applyEdit call during the command execution because
// no edits should be produced.
var applyEditSubscription = requestsFromServer
.where((n) => n.method == Method.workspace_applyEdit)
.listen((_) => throw 'workspace/applyEdit was unexpectedly called');
var commandResponse = await executeCommand(command);
expect(commandResponse, isNull);
await pumpEventQueue();
await applyEditSubscription.cancel();
}
Future<void> test_privateUnusedParameters_removedByDefault() async {
const content = '''
class _MyClass {
int? param;
_MyClass({
this.param,
});
}
''';
const expectedContent = '''
class _MyClass {
int? param;
_MyClass();
}
''';
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
Future<void> test_unavailable_outsideAnalysisRoot() async {
var otherFile = convertPath('/other/file.dart');
var content = '';
await expectNoAction(
filePath: otherFile,
content,
command: Commands.organizeImports,
);
}
Future<void> test_unusedUsings_notRemovedIfSave() async {
const content = '''
import 'dart:async';
int? a;
''';
var codeAction = await expectAction(
content,
command: Commands.fixAll,
triggerKind: CodeActionTriggerKind.Automatic,
);
var command = codeAction.command!;
// We should not get an applyEdit call during the command execution because
// no edits should be produced.
var applyEditSubscription = requestsFromServer
.where((n) => n.method == Method.workspace_applyEdit)
.listen((_) => throw 'workspace/applyEdit was unexpectedly called');
var commandResponse = await executeCommand(command);
expect(commandResponse, isNull);
await pumpEventQueue();
await applyEditSubscription.cancel();
}
Future<void> test_unusedUsings_removedByDefault() async {
const content = '''
import 'dart:async';
int? a;
''';
const expectedContent = '''
int? a;
''';
await verifyActionEdits(
content,
expectedContent,
command: Commands.fixAll,
);
}
}
@reflectiveTest
class OrganizeImportsSourceCodeActionsTest
extends AbstractSourceCodeActionsTest {
Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async {
const content = '''
import 'dart:math';
import 'dart:async';
import 'dart:convert';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
const expectedContent = '''
import 'dart:async';
import 'dart:math';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
await verifyActionEdits(
content,
expectedContent,
command: Commands.organizeImports,
);
}
Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
const content = '''
import 'dart:math';
import 'dart:async';
import 'dart:convert';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
const expectedContent = '''
import 'dart:async';
import 'dart:math';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
setDocumentChangesSupport(false);
await verifyActionEdits(
content,
expectedContent,
command: Commands.organizeImports,
);
}
Future<void> test_availableAsCodeActionLiteral() async {
const content = '';
await expectAction(
content,
command: Commands.organizeImports,
);
}
Future<void> test_availableAsCommand() async {
newFile(mainFilePath, '');
setSupportedCodeActionKinds(null); // no codeActionLiteralSupport
await initialize();
var actions = await getCodeActions(mainFileUri);
var action = findCommand(actions, Commands.organizeImports)!;
action.map(
(command) {},
(codeActionLiteral) => throw 'Expected command, got codeActionLiteral',
);
}
Future<void> test_fileHasErrors_failsSilentlyForAutomatic() async {
var content = 'invalid dart code';
var codeAction = await expectAction(
content,
command: Commands.organizeImports,
triggerKind: CodeActionTriggerKind.Automatic,
);
var command = codeAction.command!;
// Expect a valid null result.
var response = await executeCommand(command);
expect(response, isNull);
}
Future<void> test_fileHasErrors_failsWithErrorForManual() async {
var content = 'invalid dart code';
var codeAction = await expectAction(
content,
command: Commands.organizeImports,
);
var command = codeAction.command!;
// Ensure the request returned an error (error responses are thrown by
// the test helper to make consuming success results simpler).
await expectLater(executeCommand(command),
throwsA(isResponseError(ServerErrorCodes.FileHasErrors)));
}
Future<void> test_filtersCorrectly() async {
newFile(mainFilePath, '');
await initialize();
ofKind(CodeActionKind kind) => getCodeActions(
mainFileUri,
kinds: [kind],
);
expect(await ofKind(CodeActionKind.Source), hasLength(3));
expect(await ofKind(CodeActionKind.SourceOrganizeImports), hasLength(1));
expect(await ofKind(DartCodeActionKind.SortMembers), hasLength(1));
expect(await ofKind(DartCodeActionKind.FixAll), hasLength(1));
expect(await ofKind(CodeActionKind('source.foo')), isEmpty);
expect(await ofKind(CodeActionKind.Refactor), isEmpty);
}
Future<void> test_noEdits() async {
const content = '''
import 'dart:async';
import 'dart:math';
Completer foo;
int minified(int x, int y) => min(x, y);
''';
var codeAction = await expectAction(
content,
command: Commands.organizeImports,
);
var command = codeAction.command!;
// Execute the command and it should return without needing us to process
// a workspace/applyEdit command because there were no edits.
var commandResponse = await executeCommand(command);
// Successful edits return an empty success() response.
expect(commandResponse, isNull);
}
Future<void> test_unavailableWhenNotRequested() async {
var content = '';
setSupportedCodeActionKinds([CodeActionKind.Refactor]); // not Source
await expectNoAction(
content,
command: Commands.organizeImports,
);
}
Future<void> test_unavailableWithoutApplyEditSupport() async {
var content = '';
setApplyEditSupport(false);
await expectNoAction(
content,
command: Commands.organizeImports,
);
}
}
@reflectiveTest
class SortMembersSourceCodeActionsTest extends AbstractSourceCodeActionsTest {
Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async {
const content = '''
String b;
String a;
''';
const expectedContent = '''
String a;
String b;
''';
await verifyActionEdits(
content,
expectedContent,
command: Commands.sortMembers,
);
}
Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
const content = '''
String b;
String a;
''';
const expectedContent = '''
String a;
String b;
''';
setDocumentChangesSupport(false);
await verifyActionEdits(
content,
expectedContent,
command: Commands.sortMembers,
);
}
Future<void> test_availableAsCodeActionLiteral() async {
const content = '';
await expectAction(
content,
command: Commands.sortMembers,
);
}
Future<void> test_availableAsCommand() async {
newFile(mainFilePath, '');
setSupportedCodeActionKinds(null); // no codeActionLiteralSupport
await initialize();
var actions = await getCodeActions(mainFileUri);
var action = findCommand(actions, Commands.sortMembers)!;
action.map(
(command) {},
(codeActionLiteral) => throw 'Expected command, got codeActionLiteral',
);
}
Future<void> test_failsIfClientDoesntApplyEdits() async {
const content = '''
String b;
String a;
''';
var codeAction = await expectAction(
content,
command: Commands.sortMembers,
);
var command = codeAction.command!;
var commandResponse = handleExpectedRequest<Object?,
ApplyWorkspaceEditParams, ApplyWorkspaceEditResult>(
Method.workspace_applyEdit,
ApplyWorkspaceEditParams.fromJson,
() => executeCommand(command),
// Claim that we failed tpo apply the edits. This is what the client
// would do if the edits provided were for an old version of the
// document.
handler: (edit) => ApplyWorkspaceEditResult(
applied: false, failureReason: 'Document changed'),
);
// Ensure the request returned an error (error responses are thrown by
// the test helper to make consuming success results simpler).
await expectLater(commandResponse,
throwsA(isResponseError(ServerErrorCodes.ClientFailedToApplyEdit)));
}
Future<void> test_fileHasErrors_failsSilentlyForAutomatic() async {
var content = 'invalid dart code';
var codeAction = await expectAction(
content,
command: Commands.sortMembers,
triggerKind: CodeActionTriggerKind.Automatic,
);
var command = codeAction.command!;
// Expect a valid null result.
var response = await executeCommand(command);
expect(response, isNull);
}
Future<void> test_fileHasErrors_failsWithErrorForManual() async {
var content = 'invalid dart code';
var codeAction = await expectAction(
content,
command: Commands.sortMembers,
);
var command = codeAction.command!;
// Ensure the request returned an error (error responses are thrown by
// the test helper to make consuming success results simpler).
await expectLater(executeCommand(command),
throwsA(isResponseError(ServerErrorCodes.FileHasErrors)));
}
Future<void> test_nonDartFile() async {
await expectNoAction(
filePath: pubspecFilePath,
simplePubspecContent,
command: Commands.sortMembers,
);
}
Future<void> test_unavailableWhenNotRequested() async {
var content = '';
setSupportedCodeActionKinds([CodeActionKind.Refactor]); // not Source
await expectNoAction(
content,
command: Commands.sortMembers,
);
}
Future<void> test_unavailableWithoutApplyEditSupport() async {
var content = '';
setApplyEditSupport(false);
await expectNoAction(
content,
command: Commands.sortMembers,
);
}
}