| // 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: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: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() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(AssistsCodeActionsTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class AssistsCodeActionsTest extends AbstractCodeActionsTest { |
| @override |
| void setUp() { |
| super.setUp(); |
| writeTestPackageConfig( |
| 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!]'; |
| |
| Future? f; |
| '''; |
| |
| const expectedContent = ''' |
| import 'dart:async' show Future; |
| |
| Future? f; |
| '''; |
| await verifyActionEdits( |
| content, |
| expectedContent, |
| kind: CodeActionKind('refactor.add.showCombinator'), |
| title: "Add explicit 'show' combinator", |
| ); |
| } |
| |
| Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async { |
| // This code should get an assist to add a show combinator. |
| const content = ''' |
| import '[!dart:async!]'; |
| |
| Future? f; |
| '''; |
| |
| const expectedContent = ''' |
| import 'dart:async' show Future; |
| |
| Future? f; |
| '''; |
| |
| setDocumentChangesSupport(false); |
| await verifyActionEdits( |
| content, |
| expectedContent, |
| kind: CodeActionKind('refactor.add.showCombinator'), |
| title: "Add explicit 'show' combinator", |
| ); |
| } |
| |
| Future<void> test_errorMessage_invalidIntegers() async { |
| // A VS Code code coverage extension has been seen to use Number.MAX_VALUE |
| // for the character position and resulted in: |
| // |
| // type 'double' is not a subtype of type 'int' |
| // |
| // This test ensures the error message for these invalid params is clearer, |
| // indicating this is not a valid (Dart) int. |
| // https://github.com/dart-lang/sdk/issues/42786 |
| |
| newFile(mainFilePath, ''); |
| await initialize(); |
| |
| var request = makeRequest( |
| Method.textDocument_codeAction, |
| _RawParams(''' |
| { |
| "textDocument": { |
| "uri": "$mainFileUri" |
| }, |
| "context": { |
| "diagnostics": [] |
| }, |
| "range": { |
| "start": { |
| "line": 3, |
| "character": 2 |
| }, |
| "end": { |
| "line": 3, |
| "character": 1.7976931348623157e+308 |
| } |
| } |
| } |
| '''), |
| ); |
| var resp = await sendRequestToServer(request); |
| var error = resp.error!; |
| expect(error.code, equals(ErrorCodes.InvalidParams)); |
| expect( |
| error.message, |
| allOf([ |
| contains('Invalid params for textDocument/codeAction'), |
| contains('params.range.end.character must be of type int'), |
| ])); |
| } |
| |
| Future<void> test_flutterWrap_selection() async { |
| const content = ''' |
| 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 |
| // therefore contain '$0' at the location of the selection/final tabstop. |
| const expectedContent = r''' |
| import 'package:flutter/widgets.dart'; |
| Widget build() { |
| return Center($0child: Text('')); |
| } |
| '''; |
| |
| setSnippetTextEditSupport(); |
| await verifyActionEdits( |
| content, |
| expectedContent, |
| kind: CodeActionKind('refactor.flutter.wrap.center'), |
| title: 'Wrap with Center', |
| ); |
| } |
| |
| Future<void> test_logsExecution() async { |
| const content = ''' |
| import '[!dart:async!]'; |
| |
| Future? f; |
| '''; |
| |
| var action = await expectAction( |
| content, |
| kind: CodeActionKind('refactor.add.showCombinator'), |
| title: "Add explicit 'show' combinator", |
| ); |
| |
| await executeCommand(action.command!); |
| expectCommandLogged('dart.assist.add.showCombinator'); |
| } |
| |
| Future<void> test_macroGenerated() async { |
| setDartTextDocumentContentProviderSupport(); |
| var macroFilePath = join(projectFolderPath, 'lib', 'test.macro.dart'); |
| var code = TestCode.parse(''' |
| int f() { |
| ret^urn 0; |
| } |
| '''); |
| newFile(macroFilePath, code.code); |
| await initialize(); |
| |
| var codeActions = await getCodeActions( |
| uriConverter.toClientUri(macroFilePath), |
| position: code.position.position); |
| expect(codeActions, isEmpty); |
| } |
| |
| Future<void> test_nonDartFile() async { |
| setSupportedCodeActionKinds([CodeActionKind.Refactor]); |
| |
| newFile(pubspecFilePath, simplePubspecContent); |
| await initialize(); |
| |
| var codeActions = |
| await getCodeActions(pubspecFileUri, range: startOfDocRange); |
| expect(codeActions, isEmpty); |
| } |
| |
| 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 |
| '''; |
| |
| var pluginResult = plugin.EditGetAssistsResult([ |
| plugin.PrioritizedSourceChange( |
| 0, |
| plugin.SourceChange( |
| "Change 'foo' to 'bar'", |
| edits: [ |
| plugin.SourceFileEdit(mainFilePath, 0, |
| edits: [plugin.SourceEdit(0, 3, 'bar')]) |
| ], |
| id: 'fooToBar', |
| ), |
| ) |
| ]); |
| configureTestPlugin( |
| handler: (request) => |
| request is plugin.EditGetAssistsParams ? pluginResult : null, |
| ); |
| |
| await verifyActionEdits( |
| content, |
| expectedContent, |
| kind: CodeActionKind('refactor.fooToBar'), |
| title: "Change 'foo' to 'bar'", |
| ); |
| } |
| |
| Future<void> test_plugin_sortsWithServer() async { |
| setSupportedCodeActionKinds([CodeActionKind.Refactor]); |
| |
| if (!AnalysisServer.supportsPlugins) return; |
| // Produces a server assist of "Convert to single quoted string" (with a |
| // priority of 30). |
| var code = TestCode.parse('import "[!dart:async!]";'); |
| |
| // Provide two plugin results that should sort either side of the server assist. |
| var pluginResult = plugin.EditGetAssistsResult([ |
| plugin.PrioritizedSourceChange(10, plugin.SourceChange('Low')), |
| plugin.PrioritizedSourceChange(100, plugin.SourceChange('High')), |
| ]); |
| configureTestPlugin( |
| handler: (request) => |
| request is plugin.EditGetAssistsParams ? pluginResult : null, |
| ); |
| |
| newFile(mainFilePath, code.code); |
| await initialize(); |
| |
| var codeActions = |
| await getCodeActions(mainFileUri, range: code.range.range); |
| var codeActionTitles = codeActions.map((action) => |
| action.map((command) => command.title, (action) => action.title)); |
| |
| expect( |
| codeActionTitles, |
| containsAllInOrder([ |
| 'High', |
| 'Convert to single quoted string', |
| 'Low', |
| ]), |
| ); |
| } |
| |
| Future<void> test_snippetTextEdits_multiEditGroup() async { |
| // As test_snippetTextEdits_singleEditGroup, but uses an assist that |
| // produces multiple linked edit groups. |
| |
| const content = ''' |
| import 'package:flutter/widgets.dart'; |
| build() { |
| return Container( |
| child: Ro^w( |
| children: [ |
| Text('111'), |
| Text('222'), |
| Container(), |
| ], |
| ), |
| ); |
| } |
| '''; |
| |
| const expectedContent = r''' |
| import 'package:flutter/widgets.dart'; |
| build() { |
| return Container( |
| child: ${1:widget}( |
| ${2:child}: Row( |
| children: [ |
| Text('111'), |
| Text('222'), |
| Container(), |
| ], |
| ), |
| ), |
| ); |
| } |
| '''; |
| |
| setSnippetTextEditSupport(); |
| await verifyActionEdits( |
| content, |
| expectedContent, |
| kind: CodeActionKind('refactor.flutter.wrap.generic'), |
| title: 'Wrap with widget...', |
| ); |
| } |
| |
| Future<void> test_snippetTextEdits_singleEditGroup() async { |
| // This tests experimental support for including Snippets in TextEdits. |
| // https://github.com/rust-analyzer/rust-analyzer/blob/b35559a2460e7f0b2b79a7029db0c5d4e0acdb44/docs/dev/lsp-extensions.md#snippet-textedit |
| // |
| // This allows setting the cursor position/selection in TextEdits included |
| // in CodeActions, for example Flutter's "Wrap with widget" assist that |
| // should select the text "widget". |
| |
| const content = ''' |
| import 'package:flutter/widgets.dart'; |
| build() { |
| return Container( |
| child: Row( |
| children: [^ |
| Text('111'), |
| Text('222'), |
| Container(), |
| ], |
| ), |
| ); |
| } |
| '''; |
| |
| // For testing, the snippet will be inserted literally into the text, as |
| // this requires some magic on the client. The expected text should |
| // therefore contain the snippets in the standard format. |
| const expectedContent = r''' |
| import 'package:flutter/widgets.dart'; |
| build() { |
| return Container( |
| child: Row( |
| children: [ |
| ${0:widget}( |
| children: [ |
| Text('111'), |
| Text('222'), |
| Container(), |
| ], |
| ), |
| ], |
| ), |
| ); |
| } |
| '''; |
| |
| setSnippetTextEditSupport(); |
| var verifier = await verifyActionEdits( |
| content, |
| expectedContent, |
| kind: CodeActionKind('refactor.flutter.wrap.generic'), |
| title: 'Wrap with widget...', |
| ); |
| |
| // Also ensure there was a single edit that was correctly marked |
| // as a SnippetTextEdit. |
| var textEdits = extractTextDocumentEdits(verifier.edit.documentChanges!) |
| .expand((tde) => tde.edits) |
| .map((edit) => edit.map( |
| (e) => throw 'Expected SnippetTextEdit, got AnnotatedTextEdit', |
| (e) => e, |
| (e) => throw 'Expected SnippetTextEdit, got TextEdit', |
| )) |
| .toList(); |
| expect(textEdits, hasLength(1)); |
| expect(textEdits.first.insertTextFormat, equals(InsertTextFormat.Snippet)); |
| } |
| |
| Future<void> test_snippetTextEdits_unsupported() async { |
| // This tests experimental support for including Snippets in TextEdits |
| // is not active when the client capabilities do not advertise support for it. |
| // https://github.com/rust-analyzer/rust-analyzer/blob/b35559a2460e7f0b2b79a7029db0c5d4e0acdb44/docs/dev/lsp-extensions.md#snippet-textedit |
| |
| const content = ''' |
| import 'package:flutter/widgets.dart'; |
| build() { |
| return Container( |
| child: Row( |
| children: [^ |
| Text('111'), |
| Text('222'), |
| Container(), |
| ], |
| ), |
| ); |
| } |
| '''; |
| |
| var assist = await expectAction( |
| content, |
| kind: CodeActionKind('refactor.flutter.wrap.generic'), |
| title: 'Wrap with widget...', |
| ); |
| |
| // Extract just TextDocumentEdits, create/rename/delete are not relevant. |
| var edit = assist.edit!; |
| var textDocumentEdits = extractTextDocumentEdits(edit.documentChanges!); |
| var textEdits = textDocumentEdits |
| .expand((tde) => tde.edits) |
| .map((edit) => edit.map((e) => e, (e) => e, (e) => e)) |
| .toList(); |
| |
| // Ensure the edit does _not_ have a format of Snippet, nor does it include |
| // any $ characters that would indicate snippet text. |
| for (var edit in textEdits) { |
| expect(edit, isNot(TypeMatcher<SnippetTextEdit>())); |
| expect(edit.newText, isNot(contains(r'$'))); |
| } |
| } |
| |
| Future<void> test_sort() async { |
| setDocumentChangesSupport(); |
| setSupportedCodeActionKinds([CodeActionKind.Refactor]); |
| |
| var code = TestCode.parse(''' |
| import 'package:flutter/widgets.dart'; |
| |
| build() => Contai^ner(child: Container()); |
| '''); |
| |
| newFile(mainFilePath, code.code); |
| await initialize(); |
| |
| var codeActions = |
| await getCodeActions(mainFileUri, position: code.position.position); |
| var names = codeActions.map( |
| (e) => e.map((command) => command.title, (action) => action.title), |
| ); |
| |
| expect( |
| names, |
| containsAllInOrder([ |
| // Check the ordering for two well-known assists that should always be |
| // sorted this way. |
| // https://github.com/Dart-Code/Dart-Code/issues/3646 |
| 'Wrap with widget...', |
| 'Remove this widget', |
| ]), |
| ); |
| } |
| |
| Future<void> test_surround_editGroupsAndSelection() async { |
| const content = ''' |
| void f() { |
| [!print(0);!] |
| } |
| '''; |
| |
| const expectedContent = r''' |
| void f() { |
| if (${1:condition}) { |
| print(0); |
| }$0 |
| } |
| '''; |
| |
| setSnippetTextEditSupport(); |
| var verifier = await verifyActionEdits( |
| content, |
| expectedContent, |
| kind: CodeActionKind('refactor.surround.if'), |
| title: "Surround with 'if'", |
| ); |
| |
| // Also ensure there was a single edit that was correctly marked |
| // as a SnippetTextEdit. |
| var textEdits = extractTextDocumentEdits(verifier.edit.documentChanges!) |
| .expand((tde) => tde.edits) |
| .map((edit) => edit.map( |
| (e) => throw 'Expected SnippetTextEdit, got AnnotatedTextEdit', |
| (e) => e, |
| (e) => throw 'Expected SnippetTextEdit, got TextEdit', |
| )) |
| .toList(); |
| expect(textEdits, hasLength(1)); |
| expect(textEdits.first.insertTextFormat, equals(InsertTextFormat.Snippet)); |
| } |
| } |
| |
| class _RawParams extends ToJsonable { |
| final String _json; |
| |
| _RawParams(this._json); |
| |
| @override |
| Object toJson() => jsonDecode(_json) as Object; |
| } |