| // Copyright (c) 2025, 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:test/test.dart'; | |
| import '../lsp/code_actions_mixin.dart'; | |
| import '../lsp/request_helpers_mixin.dart'; | |
| import '../lsp/server_abstract.dart'; | |
| import '../tool/lsp_spec/matchers.dart'; | |
| import 'shared_test_interface.dart'; | |
| /// Shared tests used by both LSP + Legacy server tests and/or integration. | |
| mixin SharedOrganizeImportsSourceCodeActionsTests | |
| on | |
| SharedTestInterface, | |
| SharedSourceCodeActionsTestMixin, | |
| CodeActionsTestMixin, | |
| LspRequestHelpersMixin, | |
| LspEditHelpersMixin, | |
| LspVerifyEditHelpersMixin, | |
| ClientCapabilitiesHelperMixin { | |
| 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 verifyCodeActionLiteralEdits( | |
| 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 verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| command: Commands.organizeImports, | |
| ); | |
| } | |
| Future<void> test_availableAsCodeActionLiteral() async { | |
| const content = ''; | |
| await expectCodeActionLiteral(content, command: Commands.organizeImports); | |
| } | |
| Future<void> test_availableAsCommand() async { | |
| createFile(testFilePath, ''); | |
| setSupportedCodeActionKinds(null); // no codeActionLiteralSupport | |
| await initializeServer(); | |
| var actions = await getCodeActions(testFileUri); | |
| var action = findCommand(actions, Commands.organizeImports)!; | |
| action.map( | |
| (codeActionLiteral) => throw 'Expected command, got codeActionLiteral', | |
| (command) {}, | |
| ); | |
| } | |
| Future<void> test_fileHasErrors_failsSilentlyForAutomatic() async { | |
| failTestOnErrorDiagnostic = false; | |
| var content = 'invalid dart code'; | |
| var codeAction = await expectCodeActionLiteral( | |
| 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 { | |
| failTestOnErrorDiagnostic = false; | |
| var content = 'invalid dart code'; | |
| var codeAction = await expectCodeActionLiteral( | |
| 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_noEdits() async { | |
| const content = ''' | |
| import 'dart:async'; | |
| import 'dart:math'; | |
| Completer? foo; | |
| int minified(int x, int y) => min(x, y); | |
| '''; | |
| var codeAction = await expectCodeActionLiteral( | |
| 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); | |
| } | |
| } | |
| /// Shared tests used by both LSP + Legacy server tests and/or integration. | |
| mixin SharedSortMembersSourceCodeActionsTests | |
| on | |
| SharedTestInterface, | |
| SharedSourceCodeActionsTestMixin, | |
| CodeActionsTestMixin, | |
| LspRequestHelpersMixin, | |
| LspEditHelpersMixin, | |
| LspVerifyEditHelpersMixin, | |
| ClientCapabilitiesHelperMixin { | |
| Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async { | |
| const content = ''' | |
| String? b; | |
| String? a; | |
| '''; | |
| const expectedContent = ''' | |
| String? a; | |
| String? b; | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| 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 verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| command: Commands.sortMembers, | |
| ); | |
| } | |
| Future<void> test_availableAsCodeActionLiteral() async { | |
| const content = ''; | |
| await expectCodeActionLiteral(content, command: Commands.sortMembers); | |
| } | |
| Future<void> test_availableAsCommand() async { | |
| createFile(testFilePath, ''); | |
| setSupportedCodeActionKinds(null); // no codeActionLiteralSupport | |
| await initializeServer(); | |
| var actions = await getCodeActions(testFileUri); | |
| var action = findCommand(actions, Commands.sortMembers)!; | |
| action.map( | |
| (codeActionLiteral) => throw 'Expected command, got codeActionLiteral', | |
| (command) {}, | |
| ); | |
| } | |
| Future<void> test_failsIfClientDoesntApplyEdits() async { | |
| const content = ''' | |
| String? b; | |
| String? a; | |
| '''; | |
| var codeAction = await expectCodeActionLiteral( | |
| 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) async => 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 { | |
| failTestOnErrorDiagnostic = false; | |
| var content = 'invalid dart code'; | |
| var codeAction = await expectCodeActionLiteral( | |
| 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 { | |
| failTestOnErrorDiagnostic = false; | |
| var content = 'invalid dart code'; | |
| var codeAction = await expectCodeActionLiteral( | |
| 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); | |
| } | |
| } | |
| mixin SharedSourceCodeActionsTestMixin | |
| on | |
| SharedTestInterface, | |
| LspRequestHelpersMixin, | |
| ClientCapabilitiesHelperMixin { | |
| /// 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<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 | |
| Future<void> setUp() async { | |
| await super.setUp(); | |
| setApplyEditSupport(); | |
| setDocumentChangesSupport(); | |
| setSupportedCodeActionKinds([CodeActionKind.Source]); | |
| } | |
| } | |
| /// Shared tests used by both LSP + Legacy server tests and/or integration. | |
| mixin SharedSourceCodeActionsTests | |
| on | |
| SharedTestInterface, | |
| SharedSourceCodeActionsTestMixin, | |
| CodeActionsTestMixin, | |
| LspRequestHelpersMixin, | |
| LspEditHelpersMixin, | |
| LspVerifyEditHelpersMixin, | |
| ClientCapabilitiesHelperMixin { | |
| Future<void> test_filtersCorrectly() async { | |
| createFile(testFilePath, ''); | |
| await initializeServer(); | |
| ofKind(CodeActionKind kind) => getCodeActions(testFileUri, kinds: [kind]); | |
| var serverSupportsFixAll = this.serverSupportsFixAll; | |
| expect( | |
| await ofKind(CodeActionKind.Source), | |
| hasLength(serverSupportsFixAll ? 3 : 2), | |
| ); | |
| expect(await ofKind(CodeActionKind.SourceOrganizeImports), hasLength(1)); | |
| expect(await ofKind(DartCodeActionKind.sortMembers), hasLength(1)); | |
| expect( | |
| await ofKind(DartCodeActionKind.fixAll), | |
| hasLength(serverSupportsFixAll ? 1 : 0), | |
| ); | |
| expect(await ofKind(CodeActionKind('source.foo')), isEmpty); | |
| expect(await ofKind(CodeActionKind.Refactor), isEmpty); | |
| } | |
| } |