// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:analysis_server/lsp_protocol/protocol.dart'; import 'package:analysis_server/src/lsp/constants.dart'; import 'package: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 { /// Wrapper around [checkCodeActionAvailable] for Source actions where /// position/range is irrelevant (so uses [startOfDocPos]). Future 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>> getSourceCodeActions( Uri fileUri, { List? kinds, CodeActionTriggerKind? triggerKind, }) { return getCodeActions( fileUri, position: startOfDocPos, kinds: kinds, triggerKind: triggerKind, ); } } @reflectiveTest class FixAllSourceCodeActionsTest extends AbstractSourceCodeActionsTest { Future test_appliesCorrectEdits() async { const analysisOptionsContent = ''' linter: rules: - unnecessary_new - prefer_collection_literals '''; const content = ''' final a = new Object(); final b = new Set(); '''; const expectedContent = ''' final a = Object(); final b = {}; '''; registerLintRules(); 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); } } @reflectiveTest class OrganizeImportsSourceCodeActionsTest extends AbstractSourceCodeActionsTest { Future 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); '''; 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); } Future 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); '''; newFile(mainFilePath, content); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); final codeActions = await getSourceCodeActions(mainFileUri); final codeAction = findCommand(codeActions, Commands.organizeImports)!; await verifyCodeActionEdits(codeAction, content, expectedContent); } Future test_availableAsCodeActionLiteral() async { newFile(mainFilePath, ''); await initialize( textDocumentCapabilities: withCodeActionKinds( emptyTextDocumentClientCapabilities, [CodeActionKind.Source]), workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await checkSourceCodeActionAvailable( mainFileUri, Commands.organizeImports, 'Organize Imports', asCodeActionLiteral: true, ); } Future test_availableAsCommand() async { newFile(mainFilePath, ''); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await checkSourceCodeActionAvailable( mainFileUri, Commands.organizeImports, 'Organize Imports', asCommand: true, ); } Future test_fileHasErrors_failsSilentlyForAutomatic() async { final content = 'invalid dart code'; newFile(mainFilePath, content); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); final codeActions = await getSourceCodeActions( mainFileUri, triggerKind: CodeActionTriggerKind.Automatic, ); final codeAction = findCommand(codeActions, Commands.organizeImports)!; final command = codeAction.map( (command) => command, (codeAction) => codeAction.command!, ); // Expect a valid null result. final response = await executeCommand(command); expect(response, isNull); } Future 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!, ); // 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 test_filtersCorrectly() async { newFile(mainFilePath, ''); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); ofKind(CodeActionKind kind) => getSourceCodeActions( 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 test_noEdits() async { const content = ''' 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)!; final command = codeAction.map( (command) => command, (codeAction) => codeAction.command!, ); // Execute the command and it should return without needing us to process // a workspace/applyEdit command because there were no edits. final commandResponse = await executeCommand(command); // Successful edits return an empty success() response. expect(commandResponse, isNull); } Future test_unavailableWhenNotRequested() async { newFile(mainFilePath, ''); await initialize( textDocumentCapabilities: withCodeActionKinds( emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]), workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); final codeActions = await getSourceCodeActions(mainFileUri); final codeAction = findCommand(codeActions, Commands.organizeImports); expect(codeAction, isNull); } Future test_unavailableWithoutApplyEditSupport() async { newFile(mainFilePath, ''); await initialize(); final codeActions = await getSourceCodeActions(mainFileUri); final codeAction = findCommand(codeActions, Commands.organizeImports); expect(codeAction, isNull); } } @reflectiveTest class SortMembersSourceCodeActionsTest extends AbstractSourceCodeActionsTest { Future test_appliesCorrectEdits_withDocumentChangesSupport() async { const content = ''' String b; String a; '''; const expectedContent = ''' 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); } Future test_appliesCorrectEdits_withoutDocumentChangesSupport() async { const content = ''' String b; String a; '''; const expectedContent = ''' 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); } Future test_availableAsCodeActionLiteral() async { newFile(mainFilePath, ''); await initialize( textDocumentCapabilities: withCodeActionKinds( emptyTextDocumentClientCapabilities, [CodeActionKind.Source]), workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await checkSourceCodeActionAvailable( mainFileUri, Commands.sortMembers, 'Sort Members', asCodeActionLiteral: true, ); } Future test_availableAsCommand() async { newFile(mainFilePath, ''); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await checkSourceCodeActionAvailable( mainFileUri, Commands.sortMembers, 'Sort Members', asCommand: true, ); } Future test_failsIfClientDoesntApplyEdits() async { const content = ''' 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 commandResponse = handleExpectedRequest( 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 test_fileHasErrors_failsSilentlyForAutomatic() async { final content = 'invalid dart code'; newFile(mainFilePath, content); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); final codeActions = await getSourceCodeActions( mainFileUri, triggerKind: CodeActionTriggerKind.Automatic, ); final codeAction = findCommand(codeActions, Commands.sortMembers)!; final command = codeAction.map( (command) => command, (codeAction) => codeAction.command!, ); // Expect a valid null result. final response = await executeCommand(command); expect(response, isNull); } Future 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!, ); // 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 test_nonDartFile() async { newFile(pubspecFilePath, simplePubspecContent); await initialize( textDocumentCapabilities: withCodeActionKinds( emptyTextDocumentClientCapabilities, [CodeActionKind.Source]), workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); final codeActions = await getSourceCodeActions(pubspecFileUri); expect(codeActions, isEmpty); } Future test_unavailableWhenNotRequested() async { newFile(mainFilePath, ''); await initialize( textDocumentCapabilities: withCodeActionKinds( emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]), workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); final codeActions = await getSourceCodeActions(mainFileUri); final codeAction = findCommand(codeActions, Commands.sortMembers); expect(codeAction, isNull); } Future test_unavailableWithoutApplyEditSupport() async { newFile(mainFilePath, ''); await initialize(); final codeActions = await getSourceCodeActions(mainFileUri); final codeAction = findCommand(codeActions, Commands.sortMembers); expect(codeAction, isNull); } }