[analysis_server] Move CodeActions tests to shared mixins
The next CL will make CodeActions a shared handler (so it can run for both LSP and legacy servers). This moves the tests to a shared mixin (without any changes) to keep that change smaller and easier to review if the tests do end up requiring changes.
Not all tests are moved yet - plugins are not (because the plugin code is not the same across server types), not "Fix All" (because it will require some additional changes to not be LSP-specific compared to others).
Change-Id: Ib4727ef1b1cc5b96d98cdbd6e17bf4b7b2791e3e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428003
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/test/lsp/code_actions_assists_test.dart b/pkg/analysis_server/test/lsp/code_actions_assists_test.dart
index 12dab69..5568c5a 100644
--- a/pkg/analysis_server/test/lsp/code_actions_assists_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_assists_test.dart
@@ -3,11 +3,9 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
-import 'dart:convert';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
-import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/extensions/code_action.dart';
import 'package:analysis_server/src/services/correction/assist_internal.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
@@ -18,7 +16,7 @@
import '../lsp/code_actions_mixin.dart';
import '../lsp/server_abstract.dart';
-import '../utils/lsp_protocol_extensions.dart';
+import '../shared/shared_code_actions_assists_tests.dart';
import '../utils/test_code_extensions.dart';
void main() {
@@ -29,7 +27,10 @@
@reflectiveTest
class AssistsCodeActionsTest extends AbstractLspAnalysisServerTest
- with LspSharedTestMixin, CodeActionsTestMixin {
+ with
+ LspSharedTestMixin,
+ CodeActionsTestMixin,
+ SharedAssistsCodeActionsTests {
@override
void setUp() {
super.setUp();
@@ -43,229 +44,6 @@
writeTestPackageConfig(flutter: true);
}
- 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 verifyCodeActionLiteralEdits(
- 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 verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('refactor.add.showCombinator'),
- title: "Add explicit 'show' combinator",
- );
- }
-
- Future<void> test_codeActionLiterals_supported() async {
- setSnippetTextEditSupport();
- setSupportedCodeActionKinds([CodeActionKind.Refactor]);
-
- var code = TestCode.parse('''
-import 'package:flutter/widgets.dart';
-Widget build() {
- return Te^xt('');
-}
-''');
-
- var action = await expectCodeAction(
- code,
- kind: CodeActionKind('refactor.flutter.wrap.center'),
- title: 'Wrap with Center',
- );
-
- // Ensure we are a CodeAction literal.
- expect(action.isCodeActionLiteral, true);
-
- await verifyCodeActionEdits(action, r'''
->>>>>>>>>> lib/test.dart
-import 'package:flutter/widgets.dart';
-Widget build() {
- return Center($0child: Text(''));
-}
-''');
- }
-
- Future<void> test_codeActionLiterals_unsupported() async {
- setSnippetTextEditSupport();
- setSupportedCodeActionKinds(null); // no codeActionLiteralSupport
-
- var code = TestCode.parse('''
-import 'package:flutter/widgets.dart';
-Widget build() {
- return Te[!!]xt('');
-}
-''');
-
- var action = await expectCodeAction(
- openTargetFile: true, // Open document to verify we get a version back.
- code,
- title: 'Wrap with Center',
- command: Commands.applyCodeAction,
- commandArgs: [
- {
- 'textDocument': {'uri': testFileUri.toString(), 'version': 1},
- 'range': code.range.range.toJson(),
- 'kind': 'refactor.flutter.wrap.center',
- 'loggedAction': 'dart.assist.flutter.wrap.center',
- },
- ],
- );
-
- // We don't support literals, so we expect the raw command instead.
- expect(action.isCommand, true);
- var command = action.asCommand;
-
- // Verify that executing the command produces the correct edits (which will
- // come back via `workspace/applyEdit`).
- await verifyCommandEdits(command, r'''
->>>>>>>>>> lib/test.dart
-import 'package:flutter/widgets.dart';
-Widget build() {
- return Center($0child: Text(''));
-}
-''');
-
- expectCommandLogged(Commands.applyCodeAction);
- expectCommandLogged('dart.assist.flutter.wrap.center');
- }
-
- 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
-
- createFile(testFilePath, '');
- await initializeServer();
-
- var request = makeRequest(
- Method.textDocument_codeAction,
- _RawParams('''
- {
- "textDocument": {
- "uri": "$testFileUri"
- },
- "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 verifyCodeActionLiteralEdits(
- 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 expectCodeActionLiteral(
- content,
- kind: CodeActionKind('refactor.add.showCombinator'),
- title: "Add explicit 'show' combinator",
- );
-
- await executeCommand(action.command!);
- expectCommandLogged('dart.assist.add.showCombinator');
- }
-
- Future<void> test_nonDartFile() async {
- setSupportedCodeActionKinds([CodeActionKind.Refactor]);
-
- createFile(pubspecFilePath, simplePubspecContent);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- pubspecFileUri,
- range: startOfDocRange,
- );
- expect(codeActions, isEmpty);
- }
-
Future<void> test_plugin() async {
failTestOnErrorDiagnostic = false;
@@ -345,242 +123,4 @@
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 verifyCodeActionLiteralEdits(
- 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 verifyCodeActionLiteralEdits(
- 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 expectCodeActionLiteral(
- 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());
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- position: code.position.position,
- );
- var codeActionTitles = codeActions.map((action) => action.title);
-
- expect(
- codeActionTitles,
- 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 verifyCodeActionLiteralEdits(
- 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;
}
diff --git a/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart b/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
index 2c63d00..b4cfe67 100644
--- a/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
@@ -2,25 +2,19 @@
// 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/analysis_server.dart';
import 'package:analysis_server/src/lsp/extensions/code_action.dart';
-import 'package:analysis_server/src/services/correction/fix_internal.dart';
-import 'package:analyzer/src/dart/error/lint_codes.dart';
-import 'package:analyzer/src/lint/linter.dart';
-import 'package:analyzer/src/lint/registry.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:linter/src/rules.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
-import '../lsp/code_actions_mixin.dart';
-import '../lsp/server_abstract.dart';
+import '../shared/shared_code_actions_fixes_tests.dart';
import '../utils/test_code_extensions.dart';
+import 'code_actions_mixin.dart';
+import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
@@ -28,34 +22,21 @@
});
}
-/// A version of `camel_case_types` that is deprecated.
-class DeprecatedCamelCaseTypes extends LintRule {
- static const LintCode code = LintCode(
- 'camel_case_types',
- "The type name '{0}' isn't an UpperCamelCase identifier.",
- correctionMessage:
- 'Try changing the name to follow the UpperCamelCase style.',
- hasPublishedDocs: true,
- );
-
- DeprecatedCamelCaseTypes()
- : super(
- name: 'camel_case_types',
- state: State.deprecated(),
- description: '',
- );
-
- @override
- LintCode get lintCode => code;
-}
-
@reflectiveTest
class FixesCodeActionsTest extends AbstractLspAnalysisServerTest
- with LspSharedTestMixin, CodeActionsTestMixin {
+ with
+ LspSharedTestMixin,
+ CodeActionsTestMixin,
+ // Most tests are defined in a shared mixin.
+ SharedFixesCodeActionsTests {
/// Helper to check plugin fixes for [filePath].
///
/// Used to ensure that both Dart and non-Dart files fixes are returned.
Future<void> checkPluginResults(String filePath) async {
+ // TODO(dantup): Abstract plugin support to the shared test interface so
+ // that the plugin tests can also move to the shared mixins and run for
+ // both servers.
+
// This code should get a fix to replace 'foo' with 'bar'.'
const content = '''
[!foo!]
@@ -110,626 +91,6 @@
);
}
- @override
- void setUp() {
- super.setUp();
-
- // Fix tests are likely to have diagnostics that need fixing.
- failTestOnErrorDiagnostic = false;
-
- setApplyEditSupport();
- setDocumentChangesSupport();
- setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
-
- registerBuiltInFixGenerators();
- }
-
- Future<void> test_addImport_noPreference() async {
- createFile(
- pathContext.join(projectFolderPath, 'lib', 'class.dart'),
- 'class MyClass {}',
- );
-
- var code = TestCode.parse('''
-MyCla^ss? a;
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- position: code.position.position,
- );
- var codeActionTitles = codeActions.map((action) => action.title);
-
- expect(
- codeActionTitles,
- // With no preference, server defaults to absolute.
- containsAllInOrder([
- "Import library 'package:test/class.dart'",
- "Import library 'class.dart'",
- ]),
- );
- }
-
- Future<void> test_addImport_preferAbsolute() async {
- _enableLints(['always_use_package_imports']);
-
- createFile(
- pathContext.join(projectFolderPath, 'lib', 'class.dart'),
- 'class MyClass {}',
- );
-
- var code = TestCode.parse('''
-MyCla^ss? a;
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- position: code.position.position,
- );
- var codeActionTitles = codeActions.map((action) => action.title);
-
- expect(
- codeActionTitles,
- containsAllInOrder(["Import library 'package:test/class.dart'"]),
- );
- }
-
- Future<void> test_addImport_preferRelative() async {
- _enableLints(['prefer_relative_imports']);
-
- createFile(
- pathContext.join(projectFolderPath, 'lib', 'class.dart'),
- 'class MyClass {}',
- );
-
- var code = TestCode.parse('''
-MyCla^ss? a;
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- position: code.position.position,
- );
- var codeActionTitles = codeActions.map((action) => action.title);
-
- expect(
- codeActionTitles,
- containsAllInOrder(["Import library 'class.dart'"]),
- );
- }
-
- Future<void> test_analysisOptions() async {
- registerLintRules();
-
- // To ensure there's an associated code action, we manually deprecate an
- // existing lint (`camel_case_types`) for the duration of this test.
-
- // Fetch the "actual" lint so we can restore it after the test.
- var camelCaseTypes = Registry.ruleRegistry.getRule('camel_case_types')!;
-
- // Overwrite it.
- Registry.ruleRegistry.registerLintRule(DeprecatedCamelCaseTypes());
-
- // Now we can assume it will have an action associated...
-
- try {
- const content = r'''
-linter:
- rules:
- - prefer_is_empty
- - [!camel_case_types!]
- - lines_longer_than_80_chars
-''';
-
- const expectedContent = r'''
-linter:
- rules:
- - prefer_is_empty
- - lines_longer_than_80_chars
-''';
-
- await verifyCodeActionLiteralEdits(
- filePath: analysisOptionsPath,
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.removeLint'),
- title: "Remove 'camel_case_types'",
- );
- } finally {
- // Restore the "real" `camel_case_types`.
- Registry.ruleRegistry.registerLintRule(camelCaseTypes);
- }
- }
-
- 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';
-
-Future foo;
-''';
-
- const expectedContent = '''
-import 'dart:async';
-
-Future foo;
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.remove.unusedImport'),
- title: 'Remove unused import',
- );
- }
-
- 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';
-
-Future foo;
-''';
-
- const expectedContent = '''
-import 'dart:async';
-
-Future foo;
-''';
-
- setDocumentChangesSupport(false);
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.remove.unusedImport'),
- title: 'Remove unused import',
- );
- }
-
- Future<void> test_createFile() async {
- const content = '''
-import '[!createFile.dart!]';
-''';
-
- const expectedContent = '''
->>>>>>>>>> lib/createFile.dart created
-// TODO Implement this library.<<<<<<<<<<
-''';
-
- setFileCreateSupport();
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.create.file'),
- title: "Create file 'createFile.dart'",
- );
- }
-
- Future<void> test_filtersCorrectly() async {
- setSupportedCodeActionKinds([
- CodeActionKind.QuickFix,
- CodeActionKind.Refactor,
- ]);
-
- var code = TestCode.parse('''
-import 'dart:async';
-[!import!] 'dart:convert';
-
-Future foo;
-''');
- createFile(testFilePath, code.code);
- await initializeServer();
-
- ofKind(CodeActionKind kind) =>
- getCodeActions(testFileUri, range: code.range.range, kinds: [kind]);
-
- // The code above will return a 'quickfix.remove.unusedImport'.
- expect(await ofKind(CodeActionKind.QuickFix), isNotEmpty);
- expect(await ofKind(CodeActionKind('quickfix.remove')), isNotEmpty);
- expect(await ofKind(CodeActionKind('quickfix.remove.foo')), isEmpty);
- expect(await ofKind(CodeActionKind('quickfix.other')), isEmpty);
- expect(await ofKind(CodeActionKind.Refactor), isEmpty);
- }
-
- Future<void> test_fixAll_logsExecution() async {
- const content = '''
-void f(String a) {
- [!print(a!!)!];
- print(a!!);
-}
-''';
-
- var action = await expectCodeActionLiteral(
- content,
- kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
- title: "Remove '!'s in file",
- );
-
- await executeCommand(action.command!);
- expectCommandLogged('dart.fix.remove.nonNullAssertion.multi');
- }
-
- Future<void> test_fixAll_notWhenNoBatchFix() async {
- // 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.
- var code = TestCode.parse('''
-var a = [!foo!]();
-var b = bar();
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var allFixes = await getCodeActions(testFileUri, range: code.range.range);
-
- // Expect only the single-fix, there should be no apply-all.
- expect(allFixes, hasLength(1));
- var fixTitle = allFixes.first.map((f) => f.title, (f) => f.title);
- expect(fixTitle, equals("Create function 'foo'"));
- }
-
- Future<void> test_fixAll_notWhenSingle() async {
- const content = '''
-void f(String a) {
- [!print(a!)!];
-}
-''';
-
- await expectNoAction(
- content,
- kind: CodeActionKind('quickfix'),
- title: "Remove '!'s in file",
- );
- }
-
- /// Ensure the "fix all in file" action doesn't appear against an unfixable
- /// item just because the diagnostic is also reported in a location that
- /// is fixable.
- ///
- /// https://github.com/dart-lang/sdk/issues/53021
- Future<void> test_fixAll_unfixable() async {
- registerLintRules();
- createFile(analysisOptionsPath, '''
-linter:
- rules:
- - non_constant_identifier_names
- ''');
-
- const content = '''
-/// This is unfixable because it's a top-level. It should not have a "fix all
-/// in file" action.
-var aaa_a^aa = '';
-
-void f() {
- /// These are here to ensure there's > 1 instance of this diagnostic to
- /// allow "fix all in file" to appear.
- final bbb_bbb = 0;
- final ccc_ccc = 0;
-}
-''';
-
- await expectNoAction(
- content,
- kind: CodeActionKind('quickfix.rename.toCamelCase.multi'),
- title: 'Rename to camel case everywhere in file',
- );
- }
-
- Future<void> test_fixAll_whenMultiple() async {
- const content = '''
-void f(String a) {
- [!print(a!!)!];
- print(a!!);
-}
-''';
-
- const expectedContent = '''
-void f(String a) {
- print(a);
- print(a);
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
- title: "Remove '!'s in file",
- );
- }
-
- Future<void> test_ignoreDiagnostic_afterOtherFixes() async {
- var code = TestCode.parse('''
-void main() {
- Uint8List inputBytes = Uin^t8List.fromList(List.filled(100000000, 0));
-}
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var position = code.position.position;
- var range = Range(start: position, end: position);
- var codeActions = await getCodeActions(testFileUri, range: range);
- var codeActionKinds = codeActions.map(
- (item) =>
- item.map((literal) => literal.kind?.toString(), (command) => null),
- );
-
- expect(
- codeActionKinds,
- containsAllInOrder([
- // Non-ignore fixes (order doesn't matter here, but this is what
- // server produces).
- 'quickfix.create.class',
- 'quickfix.create.mixin',
- 'quickfix.create.localVariable',
- 'quickfix.remove.unusedLocalVariable',
- // Ignore fixes last, with line sorted above file.
- 'quickfix.ignore.line',
- 'quickfix.ignore.file',
- ]),
- );
- }
-
- Future<void> test_ignoreDiagnosticForFile() async {
- const content = '''
-// Header comment
-// Header comment
-// Header comment
-
-// This comment is attached to the below import
-import 'dart:async';
-[!import!] 'dart:convert';
-
-Future foo;
-''';
-
- const expectedContent = '''
-// Header comment
-// Header comment
-// Header comment
-
-// ignore_for_file: unused_import
-
-// This comment is attached to the below import
-import 'dart:async';
-import 'dart:convert';
-
-Future foo;
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.ignore.file'),
- title: "Ignore 'unused_import' for the whole file",
- );
- }
-
- Future<void> test_ignoreDiagnosticForLine() async {
- const content = '''
-import 'dart:async';
-[!import!] 'dart:convert';
-
-Future foo;
-''';
-
- const expectedContent = '''
-import 'dart:async';
-// ignore: unused_import
-import 'dart:convert';
-
-Future foo;
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.ignore.line'),
- title: "Ignore 'unused_import' for this line",
- );
- }
-
- Future<void> test_logsExecution() async {
- var code = TestCode.parse('''
-[!import!] 'dart:convert';
-''');
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- range: code.range.range,
- );
- var fixAction =
- findCodeActionLiteral(
- codeActions,
- title: 'Remove unused import',
- kind: CodeActionKind('quickfix.remove.unusedImport'),
- )!;
-
- await executeCommand(fixAction.command!);
- expectCommandLogged('dart.fix.remove.unusedImport');
- }
-
- /// Repro for https://github.com/Dart-Code/Dart-Code/issues/4462.
- ///
- /// Original code only included a fix on its first error (which in this sample
- /// is the opening brace) and not the whole range of the error.
- Future<void> test_multilineError() async {
- registerLintRules();
- createFile(analysisOptionsPath, '''
-linter:
- rules:
- - prefer_expression_function_bodies
- ''');
-
- var code = TestCode.parse('''
-int foo() {
- [!return!] 1;
-}
- ''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- range: code.range.range,
- );
- var fixAction = findCodeActionLiteral(
- codeActions,
- title: 'Convert to expression body',
- kind: CodeActionKind('quickfix.convert.toExpressionBody'),
- );
- expect(fixAction, isNotNull);
- }
-
- Future<void> test_noDuplicates_differentFix() async {
- // For convenience, quick-fixes are usually returned for the entire line,
- // though this can lead to duplicate entries (by title) when multiple
- // diagnostics have their own fixes of the same type.
- //
- // Expect only the only one nearest to the start of the range to be returned.
- var code = TestCode.parse('''
-void f() {
- var a = [];
- print(a!!);^
-}
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- position: code.position.position,
- );
- var removeNnaAction =
- findCodeActionLiteral(
- codeActions,
- title: "Remove the '!'",
- kind: CodeActionKind('quickfix.remove.nonNullAssertion'),
- )!;
-
- // Ensure the action is for the diagnostic on the second bang which was
- // closest to the range requested.
- var diagnostics = removeNnaAction.diagnostics;
- var secondBangPos = positionFromOffset(code.code.indexOf('!);'), code.code);
- expect(diagnostics, hasLength(1));
- var diagStart = diagnostics!.first.range.start;
- expect(diagStart, equals(secondBangPos));
- }
-
- Future<void> test_noDuplicates_sameFix() async {
- var code = TestCode.parse('''
-var a = [Test, Test, Te[!!]st];
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- range: code.range.range,
- );
- var createClassAction =
- findCodeActionLiteral(
- codeActions,
- title: "Create class 'Test'",
- kind: CodeActionKind('quickfix.create.class'),
- )!;
-
- expect(createClassAction.diagnostics, hasLength(3));
- }
-
- Future<void> test_noDuplicates_withDocumentChangesSupport() async {
- setApplyEditSupport();
- setDocumentChangesSupport();
- setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
-
- var code = TestCode.parse('''
-var a = [Test, Test, Te[!!]st];
-''');
-
- createFile(testFilePath, code.code);
- await initializeServer();
-
- var codeActions = await getCodeActions(
- testFileUri,
- range: code.range.range,
- );
- var createClassActions =
- findCodeActionLiteral(
- codeActions,
- title: "Create class 'Test'",
- kind: CodeActionKind('quickfix.create.class'),
- )!;
-
- expect(createClassActions.diagnostics, hasLength(3));
- }
-
- Future<void> test_organizeImportsFix_namedOrganizeImports() async {
- registerLintRules();
- createFile(analysisOptionsPath, '''
-linter:
- rules:
- - directives_ordering
- ''');
-
- // This code should get a fix to sort the imports.
- const content = '''
-import 'dart:io';
-[!import 'dart:async'!];
-
-Completer a;
-ProcessInfo b;
-''';
-
- const expectedContent = '''
-import 'dart:async';
-import 'dart:io';
-
-Completer a;
-ProcessInfo b;
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.organize.imports'),
- title: 'Organize Imports',
- );
- }
-
- Future<void> test_outsideRoot() async {
- var otherFilePath = pathContext.normalize(
- pathContext.join(projectFolderPath, '..', 'otherProject', 'foo.dart'),
- );
- var otherFileUri = pathContext.toUri(otherFilePath);
- createFile(otherFilePath, 'bad code to create error');
- await initializeServer();
-
- var codeActions = await getCodeActions(
- otherFileUri,
- position: startOfDocPos,
- );
- expect(codeActions, isEmpty);
- }
-
Future<void> test_plugin_dart() async {
if (!AnalysisServer.supportsPlugins) return;
return await checkPluginResults(testFilePath);
@@ -784,117 +145,4 @@
containsAllInOrder(['High', 'Remove unused import', 'Low']),
);
}
-
- Future<void> test_pubspec() async {
- const content = '^';
-
- const expectedContent = r'''
-name: my_project
-''';
-
- await verifyCodeActionLiteralEdits(
- filePath: pubspecFilePath,
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.add.name'),
- title: "Add 'name' key",
- );
- }
-
- Future<void> test_snippets_createMethod_functionTypeNestedParameters() async {
- const content = '''
-class A {
- void a() => c^((cell) => cell.south);
- void b() => c((cell) => cell.west);
-}
-''';
-
- const expectedContent = r'''
-class A {
- void a() => c((cell) => cell.south);
- void b() => c((cell) => cell.west);
-
- ${1:void} ${2:c}(${3:Function(dynamic cell)} ${4:param0}) {}
-}
-''';
-
- setSnippetTextEditSupport();
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.create.method'),
- title: "Create method 'c'",
- );
- }
-
- /// Ensure braces aren't over-escaped in snippet choices.
- /// https://github.com/dart-lang/sdk/issues/54403
- Future<void> test_snippets_createMissingOverrides_recordBraces() async {
- const content = '''
-abstract class A {
- void m(Iterable<({int a, int b})> r);
-}
-
-class ^B extends A {}
-''';
-
- const expectedContent = r'''
-abstract class A {
- void m(Iterable<({int a, int b})> r);
-}
-
-class B extends A {
- @override
- void m(${1|Iterable<({int a\, int b})>,Object|} ${2:r}) {
- // TODO: implement m$0
- }
-}
-''';
-
- setSnippetTextEditSupport();
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.create.missingOverrides'),
- title: 'Create 1 missing override',
- );
- }
-
- Future<void>
- test_snippets_extractVariable_functionTypeNestedParameters() async {
- const content = '''
-void f() {
- useFunction(te^st);
-}
-
-useFunction(int g(a, b)) {}
-''';
-
- const expectedContent = r'''
-void f() {
- ${1:int Function(dynamic a, dynamic b)} ${2:test};
- useFunction(test);
-}
-
-useFunction(int g(a, b)) {}
-''';
-
- setSnippetTextEditSupport();
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- kind: CodeActionKind('quickfix.create.localVariable'),
- title: "Create local variable 'test'",
- );
- }
-
- void _enableLints(List<String> lintNames) {
- registerLintRules();
- var lintsYaml = lintNames.map((name) => ' - $name\n').join();
- createFile(analysisOptionsPath, '''
-linter:
- rules:
-$lintsYaml
-''');
- }
}
diff --git a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
index 5291422..024fa78 100644
--- a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
@@ -2,17 +2,12 @@
// 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:analysis_server/src/lsp/handlers/commands/perform_refactor.dart';
-import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
-import '../tool/lsp_spec/matchers.dart';
-import '../utils/test_code_extensions.dart';
+import '../shared/shared_code_actions_refactor_tests.dart';
import 'code_actions_mixin.dart';
import 'request_helpers_mixin.dart';
import 'server_abstract.dart';
@@ -30,444 +25,23 @@
}
@reflectiveTest
-class ConvertGetterToMethodCodeActionsTest extends RefactorCodeActionsTest {
- final refactorTitle = 'Convert Getter to Method';
-
- Future<void> test_refactor() async {
- const content = '''
-int get ^test => 42;
-void f() {
- var a = test;
- var b = test;
-}
-''';
- const expectedContent = '''
-int test() => 42;
-void f() {
- var a = test();
- var b = test();
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: refactorTitle,
- );
- }
-
- Future<void> test_setter_notAvailable() async {
- const content = '''
-set ^a(String value) {}
-''';
-
- await expectNoAction(
- content,
- command: Commands.performRefactor,
- title: refactorTitle,
- );
- }
-}
+class ConvertGetterToMethodCodeActionsTest extends RefactorCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedConvertGetterToMethodRefactorCodeActionsTests {}
@reflectiveTest
-class ConvertMethodToGetterCodeActionsTest extends RefactorCodeActionsTest {
- final refactorTitle = 'Convert Method to Getter';
-
- Future<void> test_constructor_notAvailable() async {
- const content = '''
-class A {
- ^A();
-}
-''';
-
- await expectNoAction(
- content,
- command: Commands.performRefactor,
- title: refactorTitle,
- );
- }
-
- Future<void> test_refactor() async {
- const content = '''
-int ^test() => 42;
-void f() {
- var a = test();
- var b = test();
-}
-''';
- const expectedContent = '''
-int get test => 42;
-void f() {
- var a = test;
- var b = test;
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: refactorTitle,
- );
- }
-}
+class ConvertMethodToGetterCodeActionsTest extends RefactorCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedConvertMethodToGetterRefactorCodeActionsTests {}
@reflectiveTest
class ExtractMethodRefactorCodeActionsTest extends RefactorCodeActionsTest
- with LspProgressNotificationsMixin {
- final extractMethodTitle = 'Extract Method';
-
- Future<void> test_appliesCorrectEdits() async {
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
- const expectedContent = '''
-void f() {
- print('Test!');
- newMethod();
-}
-
-void newMethod() {
- print('Test!');
-}
-''';
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
- }
-
- Future<void> test_cancelsInProgress() async {
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
- const expectedContent = '''
->>>>>>>>>> lib/test.dart
-void f() {
- print('Test!');
- newMethod();
-}
-
-void newMethod() {
- print('Test!');
-}
-''';
-
- var codeAction = await expectCodeActionLiteral(
- content,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
-
- // Respond to any applyEdit requests from the server with successful responses
- // and capturing the last edit.
- late WorkspaceEdit edit;
- requestsFromServer.listen((request) {
- if (request.method == Method.workspace_applyEdit) {
- var params = ApplyWorkspaceEditParams.fromJson(
- request.params as Map<String, Object?>,
- );
- edit = params.edit;
- respondTo(request, ApplyWorkspaceEditResult(applied: true));
- }
- });
-
- // Send two requests together.
- var req1 = executeCommand(codeAction.command!);
- var req2 = executeCommand(codeAction.command!);
-
- // Expect the first will have cancelled the second.
- await expectLater(
- req1,
- throwsA(
- isResponseError(
- ErrorCodes.RequestCancelled,
- message:
- 'Another workspace/executeCommand request for a refactor was started',
- ),
- ),
- );
- await req2;
-
- // Ensure applying the changes will give us the expected content.
- verifyEdit(edit, expectedContent);
- }
-
- Future<void> test_contentModified() async {
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
-
- var codeAction = await expectCodeActionLiteral(
- content,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- openTargetFile: true,
- );
-
- // Use a Completer to control when the refactor handler starts computing.
- var completer = Completer<void>();
- PerformRefactorCommandHandler.delayAfterResolveForTests = completer.future;
- try {
- // Send an edit request immediately after the refactor request.
- var req1 = executeCommand(codeAction.command!);
- var req2 = replaceFile(100, testFileUri, '// new test content');
- completer.complete();
-
- // Expect the first to fail because of the modified content.
- await expectLater(
- req1,
- throwsA(isResponseError(ErrorCodes.ContentModified)),
- );
- await req2;
- } finally {
- // Ensure we never leave an incomplete future if anything above throws.
- PerformRefactorCommandHandler.delayAfterResolveForTests = null;
- }
- }
-
- Future<void> test_filtersCorrectly() async {
- // Support everything (empty prefix matches all)
- setSupportedCodeActionKinds([CodeActionKind.Empty]);
-
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
- var code = TestCode.parse(content);
- createFile(testFilePath, code.code);
- await initializeServer();
-
- ofKind(CodeActionKind kind) =>
- getCodeActions(testFileUri, range: code.range.range, kinds: [kind]);
-
- // The code above will return a 'refactor.extract' (as well as some other
- // refactors, but not rewrite).
- expect(await ofKind(CodeActionKind.Refactor), isNotEmpty);
- expect(await ofKind(CodeActionKind.RefactorExtract), isNotEmpty);
- expect(await ofKind(CodeActionKind('refactor.extract.foo')), isEmpty);
- expect(await ofKind(CodeActionKind.RefactorRewrite), isEmpty);
- }
-
- Future<void> test_generatesNames() async {
- const content = '''
-Object? F() {
- return Container([!Text('Test!')!]);
-}
-
-Object? Container(Object? text) => null;
-Object? Text(Object? text) => null;
-''';
- const expectedContent = '''
-Object? F() {
- return Container(text());
-}
-
-Object? text() => Text('Test!');
-
-Object? Container(Object? text) => null;
-Object? Text(Object? text) => null;
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
- }
-
- Future<void> test_invalidLocation() async {
- const content = '''
-import 'dart:convert';
-^
-void f() {}
-''';
-
- await expectNoAction(
- content,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
- }
-
- Future<void> test_invalidLocation_importPrefix() async {
- const content = '''
-import 'dart:io' as io;
-
-i^o.File? a;
-''';
-
- await expectNoAction(
- content,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
- }
-
- Future<void> test_logsAction() async {
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
-
- setDocumentChangesSupport(false);
- var action = await expectCodeActionLiteral(
- content,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
-
- await executeCommandForEdits(action.command!);
- expectCommandLogged('dart.refactor.extract_method');
- }
-
- Future<void> test_progress_clientProvided() async {
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
- const expectedContent = '''
-void f() {
- print('Test!');
- newMethod();
-}
-
-void newMethod() {
- print('Test!');
-}
-''';
-
- // Expect begin/end progress updates without a create, since the
- // token was supplied by us (the client).
- expect(progressUpdates, emitsInOrder(['BEGIN', 'END']));
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- commandWorkDoneToken: clientProvidedTestWorkDoneToken,
- );
- }
-
- Future<void> test_progress_notSupported() async {
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
- const expectedContent = '''
-void f() {
- print('Test!');
- newMethod();
-}
-
-void newMethod() {
- print('Test!');
-}
-''';
-
- var didGetProgressNotifications = false;
- progressUpdates.listen((_) => didGetProgressNotifications = true);
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
-
- expect(didGetProgressNotifications, isFalse);
- }
-
- Future<void> test_progress_serverGenerated() async {
- const content = '''
-void f() {
- print('Test!');
- [!print('Test!');!]
-}
-''';
- const expectedContent = '''
-void f() {
- print('Test!');
- newMethod();
-}
-
-void newMethod() {
- print('Test!');
-}
-''';
-
- // Expect create/begin/end progress updates, because in this case the server
- // generates the token.
- expect(progressUpdates, emitsInOrder(['CREATE', 'BEGIN', 'END']));
-
- setWorkDoneProgressSupport();
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
- }
-
- Future<void> test_validLocation_failsInitialValidation() async {
- const content = '''
-f() {
- var a = 0;
- doFoo([!() => print(a)!]);
- print(a);
-}
-
-void doFoo(void Function() a) => a();
-
-''';
- var codeAction = await expectCodeActionLiteral(
- content,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
- var command = codeAction.command!;
-
- // Call the `refactor.validate` command with the same arguments.
- // Clients that want validation behaviour will need to implement this
- // themselves (via middleware).
- var response = await executeCommand(
- Command(
- title: command.title,
- command: Commands.validateRefactor,
- arguments: command.arguments,
- ),
- decoder: ValidateRefactorResult.fromJson,
- );
-
- expect(response.valid, isFalse);
- expect(
- response.message,
- contains('Cannot extract the closure as a method'),
- );
- }
-
+ with
+ LspProgressNotificationsMixin,
+ // Most tests are defined in a shared mixin.
+ SharedExtractMethodRefactorCodeActionsTests {
/// Test if the client does not call refactor.validate it still gets a
/// sensible `showMessage` call and not a failed request.
Future<void> test_validLocation_failsInitialValidation_noValidation() async {
@@ -505,476 +79,37 @@
contains('Cannot extract the closure as a method'),
);
}
-
- Future<void> test_validLocation_passesInitialValidation() async {
- const content = '''
-f() {
- doFoo([!() => print(1)!]);
-}
-
-void doFoo(void Function() a) => a();
-
-''';
-
- var codeAction = await expectCodeActionLiteral(
- content,
- command: Commands.performRefactor,
- title: extractMethodTitle,
- );
- var command = codeAction.command!;
-
- // Call the `Commands.validateRefactor` command with the same arguments.
- // Clients that want validation behaviour will need to implement this
- // themselves (via middleware).
- var response = await executeCommand(
- Command(
- title: command.title,
- command: Commands.validateRefactor,
- arguments: command.arguments,
- ),
- decoder: ValidateRefactorResult.fromJson,
- );
-
- expect(response.valid, isTrue);
- expect(response.message, isNull);
- }
}
@reflectiveTest
-class ExtractVariableRefactorCodeActionsTest extends RefactorCodeActionsTest {
- final convertMethodToGetterTitle = 'Convert Method to Getter';
- final extractVariableTitle = 'Extract Local Variable';
- final inlineMethodTitle = 'Inline Method';
-
- Future<void> test_appliesCorrectEdits() async {
- const content = '''
-void f() {
- foo([!1 + 2!]);
-}
-
-void foo(int arg) {}
-''';
- const expectedContent = '''
-void f() {
- var arg = 1 + 2;
- foo(arg);
-}
-
-void foo(int arg) {}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractVariableTitle,
- );
- }
-
- Future<void> test_doesNotCreateNameConflicts() async {
- const content = '''
-void f() {
- var arg = "test";
- foo([!1 + 2!]);
-}
-
-void foo(int arg) {}
-''';
- const expectedContent = '''
-void f() {
- var arg = "test";
- var arg2 = 1 + 2;
- foo(arg2);
-}
-
-void foo(int arg) {}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractVariableTitle,
- );
- }
-
- Future<void> test_inlineMethod_function_startOfParameterList() async {
- const content = '''
-test^(a, b) {
- print(a);
- print(b);
-}
-void f() {
- test(1, 2);
-}
-''';
- const expectedContent = '''
-void f() {
- print(1);
- print(2);
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: inlineMethodTitle,
- );
- }
-
- Future<void> test_inlineMethod_function_startOfTypeParameterList() async {
- const content = '''
-test^<T>(T a, T b) {
- print(a);
- print(b);
-}
-void f() {
- test(1, 2);
-}
-''';
- const expectedContent = '''
-void f() {
- print(1);
- print(2);
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: inlineMethodTitle,
- );
- }
-
- Future<void> test_inlineMethod_method_startOfParameterList() async {
- const content = '''
-class A {
- test^(a, b) {
- print(a);
- print(b);
- }
- void f() {
- test(1, 2);
- }
-}
-''';
- const expectedContent = '''
-class A {
- void f() {
- print(1);
- print(2);
- }
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: inlineMethodTitle,
- );
- }
-
- Future<void> test_inlineMethod_method_startOfTypeParameterList() async {
- const content = '''
-class A {
- test^<T>(T a, T b) {
- print(a);
- print(b);
- }
- void f() {
- test(1, 2);
- }
-}
-''';
- const expectedContent = '''
-class A {
- void f() {
- print(1);
- print(2);
- }
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: inlineMethodTitle,
- );
- }
-
- Future<void> test_methodToGetter_function_startOfParameterList() async {
- const content = '''
-int test^() => 42;
-''';
- const expectedContent = '''
-int get test => 42;
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: convertMethodToGetterTitle,
- );
- }
-
- Future<void> test_methodToGetter_function_startOfTypeParameterList() async {
- const content = '''
-int test^<T>() => 42;
-''';
- const expectedContent = '''
-int get test<T> => 42;
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: convertMethodToGetterTitle,
- );
- }
-
- Future<void> test_methodToGetter_method_startOfParameterList() async {
- const content = '''
-class A {
- int test^() => 42;
-}
-''';
- const expectedContent = '''
-class A {
- int get test => 42;
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: convertMethodToGetterTitle,
- );
- }
-
- Future<void> test_methodToGetter_method_startOfTypeParameterList() async {
- const content = '''
-class A {
- int test^<T>() => 42;
-}
-''';
- const expectedContent = '''
-class A {
- int get test<T> => 42;
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: convertMethodToGetterTitle,
- );
- }
-}
+class ExtractVariableRefactorCodeActionsTest extends RefactorCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedExtractVariableRefactorCodeActionsTests {}
@reflectiveTest
-class ExtractWidgetRefactorCodeActionsTest extends RefactorCodeActionsTest {
- final extractWidgetTitle = 'Extract Widget';
-
- String get expectedNewWidgetConstructorDeclaration => '''
-const NewWidget({
- super.key,
- });
-''';
-
+class ExtractWidgetRefactorCodeActionsTest extends RefactorCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedExtractWidgetRefactorCodeActionsTests {
@override
void setUp() {
super.setUp();
writeTestPackageConfig(flutter: true);
}
-
- Future<void> test_appliesCorrectEdits() async {
- const content = '''
-import 'package:flutter/material.dart';
-
-class MyWidget extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return new Row(
- children: <Widget>[
- new [!Column!](
- children: <Widget>[
- new Text('AAA'),
- new Text('BBB'),
- ],
- ),
- new Text('CCC'),
- new Text('DDD'),
- ],
- );
- }
-}
-''';
- var expectedContent = '''
-import 'package:flutter/material.dart';
-
-class MyWidget extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return new Row(
- children: <Widget>[
- NewWidget(),
- new Text('CCC'),
- new Text('DDD'),
- ],
- );
- }
-}
-
-class NewWidget extends StatelessWidget {
- $expectedNewWidgetConstructorDeclaration
- @override
- Widget build(BuildContext context) {
- return new Column(
- children: <Widget>[
- new Text('AAA'),
- new Text('BBB'),
- ],
- );
- }
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: extractWidgetTitle,
- openTargetFile: true,
- );
- }
-
- Future<void> test_invalidLocation() async {
- const content = '''
-import 'dart:convert';
-^
-void f() {}
-''';
-
- await expectNoAction(
- content,
- command: Commands.performRefactor,
- title: extractWidgetTitle,
- );
- }
}
@reflectiveTest
-class InlineLocalVariableRefactorCodeActionsTest
- extends RefactorCodeActionsTest {
- final inlineVariableTitle = 'Inline Local Variable';
-
- Future<void> test_appliesCorrectEdits() async {
- const content = '''
-void f() {
- var a^ = 1;
- print(a);
- print(a);
- print(a);
-}
-''';
- const expectedContent = '''
-void f() {
- print(1);
- print(1);
- print(1);
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: inlineVariableTitle,
- openTargetFile: true,
- );
- }
-}
+class InlineLocalVariableRefactorCodeActionsTest extends RefactorCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedInlineLocalVariableRefactorCodeActionsTests {}
@reflectiveTest
-class InlineMethodRefactorCodeActionsTest extends RefactorCodeActionsTest {
- final inlineMethodTitle = 'Inline Method';
-
- Future<void> test_inlineAtCallSite() async {
- const content = '''
-void foo1() {
- ba^r();
-}
-
-void foo2() {
- bar();
-}
-
-void bar() {
- print('test');
-}
-''';
- const expectedContent = '''
-void foo1() {
- print('test');
-}
-
-void foo2() {
- bar();
-}
-
-void bar() {
- print('test');
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: inlineMethodTitle,
- );
- }
-
- Future<void> test_inlineAtMethod() async {
- const content = '''
-void foo1() {
- bar();
-}
-
-void foo2() {
- bar();
-}
-
-void ba^r() {
- print('test');
-}
-''';
- const expectedContent = '''
-void foo1() {
- print('test');
-}
-
-void foo2() {
- print('test');
-}
-''';
-
- await verifyCodeActionLiteralEdits(
- content,
- expectedContent,
- command: Commands.performRefactor,
- title: inlineMethodTitle,
- );
- }
-}
+class InlineMethodRefactorCodeActionsTest extends RefactorCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedInlineMethodRefactorCodeActionsTests {}
abstract class RefactorCodeActionsTest extends AbstractLspAnalysisServerTest
with LspSharedTestMixin, CodeActionsTestMixin {
diff --git a/pkg/analysis_server/test/lsp/code_actions_source_test.dart b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
index 3e0e806..06ec5c0 100644
--- a/pkg/analysis_server/test/lsp/code_actions_source_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
@@ -11,7 +11,7 @@
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
-import '../tool/lsp_spec/matchers.dart';
+import '../shared/shared_code_actions_source_tests.dart';
import 'code_actions_mixin.dart';
import 'server_abstract.dart';
@@ -360,308 +360,13 @@
}
@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 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_filtersCorrectly() async {
- createFile(testFilePath, '');
- await initializeServer();
-
- ofKind(CodeActionKind kind) => getCodeActions(testFileUri, 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 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);
- }
-}
+class OrganizeImportsSourceCodeActionsTest extends AbstractSourceCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedOrganizeImportsSourceCodeActionsTests {}
@reflectiveTest
-class SortMembersSourceCodeActionsTest extends AbstractSourceCodeActionsTest {
- 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) => 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);
- }
-}
+class SortMembersSourceCodeActionsTest extends AbstractSourceCodeActionsTest
+ with
+ // Most tests are defined in a shared mixin.
+ SharedSortMembersSourceCodeActionsTests {}
diff --git a/pkg/analysis_server/test/shared/shared_code_actions_assists_tests.dart b/pkg/analysis_server/test/shared/shared_code_actions_assists_tests.dart
new file mode 100644
index 0000000..3a4eb79
--- /dev/null
+++ b/pkg/analysis_server/test/shared/shared_code_actions_assists_tests.dart
@@ -0,0 +1,490 @@
+// 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 'dart:convert';
+
+import 'package:analysis_server/lsp_protocol/protocol.dart';
+import 'package:analysis_server/src/lsp/constants.dart';
+import 'package:analysis_server/src/lsp/extensions/code_action.dart';
+import 'package:analyzer/src/test_utilities/test_code_format.dart';
+import 'package:test/test.dart';
+
+import '../lsp/code_actions_mixin.dart';
+import '../lsp/request_helpers_mixin.dart';
+import '../lsp/server_abstract.dart';
+import '../utils/lsp_protocol_extensions.dart';
+import '../utils/test_code_extensions.dart';
+import 'shared_test_interface.dart';
+
+/// Shared tests used by both LSP + Legacy server tests and/or integration.
+mixin SharedAssistsCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ 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 verifyCodeActionLiteralEdits(
+ 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 verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('refactor.add.showCombinator'),
+ title: "Add explicit 'show' combinator",
+ );
+ }
+
+ Future<void> test_codeActionLiterals_supported() async {
+ setSnippetTextEditSupport();
+ setSupportedCodeActionKinds([CodeActionKind.Refactor]);
+
+ var code = TestCode.parse('''
+import 'package:flutter/widgets.dart';
+Widget build() {
+ return Te^xt('');
+}
+''');
+
+ var action = await expectCodeAction(
+ code,
+ kind: CodeActionKind('refactor.flutter.wrap.center'),
+ title: 'Wrap with Center',
+ );
+
+ // Ensure we are a CodeAction literal.
+ expect(action.isCodeActionLiteral, true);
+
+ await verifyCodeActionEdits(action, r'''
+>>>>>>>>>> lib/test.dart
+import 'package:flutter/widgets.dart';
+Widget build() {
+ return Center($0child: Text(''));
+}
+''');
+ }
+
+ Future<void> test_codeActionLiterals_unsupported() async {
+ setSnippetTextEditSupport();
+ setSupportedCodeActionKinds(null); // no codeActionLiteralSupport
+
+ var code = TestCode.parse('''
+import 'package:flutter/widgets.dart';
+Widget build() {
+ return Te[!!]xt('');
+}
+''');
+
+ var action = await expectCodeAction(
+ openTargetFile: true, // Open document to verify we get a version back.
+ code,
+ title: 'Wrap with Center',
+ command: Commands.applyCodeAction,
+ commandArgs: [
+ {
+ 'textDocument': {'uri': testFileUri.toString(), 'version': 1},
+ 'range': code.range.range.toJson(),
+ 'kind': 'refactor.flutter.wrap.center',
+ 'loggedAction': 'dart.assist.flutter.wrap.center',
+ },
+ ],
+ );
+
+ // We don't support literals, so we expect the raw command instead.
+ expect(action.isCommand, true);
+ var command = action.asCommand;
+
+ // Verify that executing the command produces the correct edits (which will
+ // come back via `workspace/applyEdit`).
+ await verifyCommandEdits(command, r'''
+>>>>>>>>>> lib/test.dart
+import 'package:flutter/widgets.dart';
+Widget build() {
+ return Center($0child: Text(''));
+}
+''');
+
+ expectCommandLogged(Commands.applyCodeAction);
+ expectCommandLogged('dart.assist.flutter.wrap.center');
+ }
+
+ 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
+
+ createFile(testFilePath, '');
+ await initializeServer();
+
+ var request = makeRequest(
+ Method.textDocument_codeAction,
+ _RawParams('''
+ {
+ "textDocument": {
+ "uri": "$testFileUri"
+ },
+ "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 verifyCodeActionLiteralEdits(
+ 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 expectCodeActionLiteral(
+ content,
+ kind: CodeActionKind('refactor.add.showCombinator'),
+ title: "Add explicit 'show' combinator",
+ );
+
+ await executeCommand(action.command!);
+ expectCommandLogged('dart.assist.add.showCombinator');
+ }
+
+ Future<void> test_nonDartFile() async {
+ setSupportedCodeActionKinds([CodeActionKind.Refactor]);
+
+ createFile(pubspecFilePath, simplePubspecContent);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ pubspecFileUri,
+ range: startOfDocRange,
+ );
+ expect(codeActions, isEmpty);
+ }
+
+ 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 verifyCodeActionLiteralEdits(
+ 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 verifyCodeActionLiteralEdits(
+ 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 expectCodeActionLiteral(
+ 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());
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ position: code.position.position,
+ );
+ var codeActionTitles = codeActions.map((action) => action.title);
+
+ expect(
+ codeActionTitles,
+ 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 verifyCodeActionLiteralEdits(
+ 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;
+}
diff --git a/pkg/analysis_server/test/shared/shared_code_actions_fixes_tests.dart b/pkg/analysis_server/test/shared/shared_code_actions_fixes_tests.dart
new file mode 100644
index 0000000..14c3a6c
--- /dev/null
+++ b/pkg/analysis_server/test/shared/shared_code_actions_fixes_tests.dart
@@ -0,0 +1,788 @@
+// 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/extensions/code_action.dart';
+import 'package:analysis_server/src/services/correction/fix_internal.dart';
+import 'package:analyzer/src/dart/error/lint_codes.dart';
+import 'package:analyzer/src/lint/linter.dart';
+import 'package:analyzer/src/lint/registry.dart';
+import 'package:analyzer/src/test_utilities/test_code_format.dart';
+import 'package:linter/src/rules.dart';
+import 'package:test/test.dart';
+
+import '../lsp/code_actions_mixin.dart';
+import '../lsp/request_helpers_mixin.dart';
+import '../lsp/server_abstract.dart';
+import '../utils/test_code_extensions.dart';
+import 'shared_test_interface.dart';
+
+/// Shared tests used by both LSP + Legacy server tests and/or integration.
+mixin SharedFixesCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ String get analysisOptionsPath =>
+ pathContext.join(projectFolderPath, 'analysis_options.yaml');
+
+ @override
+ void setUp() {
+ super.setUp();
+
+ // Fix tests are likely to have diagnostics that need fixing.
+ failTestOnErrorDiagnostic = false;
+
+ setApplyEditSupport();
+ setDocumentChangesSupport();
+ setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
+
+ registerBuiltInFixGenerators();
+ }
+
+ Future<void> test_addImport_noPreference() async {
+ createFile(
+ pathContext.join(projectFolderPath, 'lib', 'class.dart'),
+ 'class MyClass {}',
+ );
+
+ var code = TestCode.parse('''
+MyCla^ss? a;
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ position: code.position.position,
+ );
+ var codeActionTitles = codeActions.map((action) => action.title);
+
+ expect(
+ codeActionTitles,
+ // With no preference, server defaults to absolute.
+ containsAllInOrder([
+ "Import library 'package:test/class.dart'",
+ "Import library 'class.dart'",
+ ]),
+ );
+ }
+
+ Future<void> test_addImport_preferAbsolute() async {
+ _enableLints(['always_use_package_imports']);
+
+ createFile(
+ pathContext.join(projectFolderPath, 'lib', 'class.dart'),
+ 'class MyClass {}',
+ );
+
+ var code = TestCode.parse('''
+MyCla^ss? a;
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ position: code.position.position,
+ );
+ var codeActionTitles = codeActions.map((action) => action.title);
+
+ expect(
+ codeActionTitles,
+ containsAllInOrder(["Import library 'package:test/class.dart'"]),
+ );
+ }
+
+ Future<void> test_addImport_preferRelative() async {
+ _enableLints(['prefer_relative_imports']);
+
+ createFile(
+ pathContext.join(projectFolderPath, 'lib', 'class.dart'),
+ 'class MyClass {}',
+ );
+
+ var code = TestCode.parse('''
+MyCla^ss? a;
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ position: code.position.position,
+ );
+ var codeActionTitles = codeActions.map((action) => action.title);
+
+ expect(
+ codeActionTitles,
+ containsAllInOrder(["Import library 'class.dart'"]),
+ );
+ }
+
+ Future<void> test_analysisOptions() async {
+ registerLintRules();
+
+ // To ensure there's an associated code action, we manually deprecate an
+ // existing lint (`camel_case_types`) for the duration of this test.
+
+ // Fetch the "actual" lint so we can restore it after the test.
+ var camelCaseTypes = Registry.ruleRegistry.getRule('camel_case_types')!;
+
+ // Overwrite it.
+ Registry.ruleRegistry.registerLintRule(_DeprecatedCamelCaseTypes());
+
+ // Now we can assume it will have an action associated...
+
+ try {
+ const content = r'''
+linter:
+ rules:
+ - prefer_is_empty
+ - [!camel_case_types!]
+ - lines_longer_than_80_chars
+''';
+
+ const expectedContent = r'''
+linter:
+ rules:
+ - prefer_is_empty
+ - lines_longer_than_80_chars
+''';
+
+ await verifyCodeActionLiteralEdits(
+ filePath: analysisOptionsPath,
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.removeLint'),
+ title: "Remove 'camel_case_types'",
+ );
+ } finally {
+ // Restore the "real" `camel_case_types`.
+ Registry.ruleRegistry.registerLintRule(camelCaseTypes);
+ }
+ }
+
+ 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';
+
+Future foo;
+''';
+
+ const expectedContent = '''
+import 'dart:async';
+
+Future foo;
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.remove.unusedImport'),
+ title: 'Remove unused import',
+ );
+ }
+
+ 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';
+
+Future foo;
+''';
+
+ const expectedContent = '''
+import 'dart:async';
+
+Future foo;
+''';
+
+ setDocumentChangesSupport(false);
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.remove.unusedImport'),
+ title: 'Remove unused import',
+ );
+ }
+
+ Future<void> test_createFile() async {
+ const content = '''
+import '[!createFile.dart!]';
+''';
+
+ const expectedContent = '''
+>>>>>>>>>> lib/createFile.dart created
+// TODO Implement this library.<<<<<<<<<<
+''';
+
+ setFileCreateSupport();
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.create.file'),
+ title: "Create file 'createFile.dart'",
+ );
+ }
+
+ Future<void> test_filtersCorrectly() async {
+ setSupportedCodeActionKinds([
+ CodeActionKind.QuickFix,
+ CodeActionKind.Refactor,
+ ]);
+
+ var code = TestCode.parse('''
+import 'dart:async';
+[!import!] 'dart:convert';
+
+Future foo;
+''');
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ ofKind(CodeActionKind kind) =>
+ getCodeActions(testFileUri, range: code.range.range, kinds: [kind]);
+
+ // The code above will return a 'quickfix.remove.unusedImport'.
+ expect(await ofKind(CodeActionKind.QuickFix), isNotEmpty);
+ expect(await ofKind(CodeActionKind('quickfix.remove')), isNotEmpty);
+ expect(await ofKind(CodeActionKind('quickfix.remove.foo')), isEmpty);
+ expect(await ofKind(CodeActionKind('quickfix.other')), isEmpty);
+ expect(await ofKind(CodeActionKind.Refactor), isEmpty);
+ }
+
+ Future<void> test_fixAll_logsExecution() async {
+ const content = '''
+void f(String a) {
+ [!print(a!!)!];
+ print(a!!);
+}
+''';
+
+ var action = await expectCodeActionLiteral(
+ content,
+ kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
+ title: "Remove '!'s in file",
+ );
+
+ await executeCommand(action.command!);
+ expectCommandLogged('dart.fix.remove.nonNullAssertion.multi');
+ }
+
+ Future<void> test_fixAll_notWhenNoBatchFix() async {
+ // 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.
+ var code = TestCode.parse('''
+var a = [!foo!]();
+var b = bar();
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var allFixes = await getCodeActions(testFileUri, range: code.range.range);
+
+ // Expect only the single-fix, there should be no apply-all.
+ expect(allFixes, hasLength(1));
+ var fixTitle = allFixes.first.map((f) => f.title, (f) => f.title);
+ expect(fixTitle, equals("Create function 'foo'"));
+ }
+
+ Future<void> test_fixAll_notWhenSingle() async {
+ const content = '''
+void f(String a) {
+ [!print(a!)!];
+}
+''';
+
+ await expectNoAction(
+ content,
+ kind: CodeActionKind('quickfix'),
+ title: "Remove '!'s in file",
+ );
+ }
+
+ /// Ensure the "fix all in file" action doesn't appear against an unfixable
+ /// item just because the diagnostic is also reported in a location that
+ /// is fixable.
+ ///
+ /// https://github.com/dart-lang/sdk/issues/53021
+ Future<void> test_fixAll_unfixable() async {
+ registerLintRules();
+ createFile(analysisOptionsPath, '''
+linter:
+ rules:
+ - non_constant_identifier_names
+ ''');
+
+ const content = '''
+/// This is unfixable because it's a top-level. It should not have a "fix all
+/// in file" action.
+var aaa_a^aa = '';
+
+void f() {
+ /// These are here to ensure there's > 1 instance of this diagnostic to
+ /// allow "fix all in file" to appear.
+ final bbb_bbb = 0;
+ final ccc_ccc = 0;
+}
+''';
+
+ await expectNoAction(
+ content,
+ kind: CodeActionKind('quickfix.rename.toCamelCase.multi'),
+ title: 'Rename to camel case everywhere in file',
+ );
+ }
+
+ Future<void> test_fixAll_whenMultiple() async {
+ const content = '''
+void f(String a) {
+ [!print(a!!)!];
+ print(a!!);
+}
+''';
+
+ const expectedContent = '''
+void f(String a) {
+ print(a);
+ print(a);
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
+ title: "Remove '!'s in file",
+ );
+ }
+
+ Future<void> test_ignoreDiagnostic_afterOtherFixes() async {
+ var code = TestCode.parse('''
+void main() {
+ Uint8List inputBytes = Uin^t8List.fromList(List.filled(100000000, 0));
+}
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var position = code.position.position;
+ var range = Range(start: position, end: position);
+ var codeActions = await getCodeActions(testFileUri, range: range);
+ var codeActionKinds = codeActions.map(
+ (item) =>
+ item.map((literal) => literal.kind?.toString(), (command) => null),
+ );
+
+ expect(
+ codeActionKinds,
+ containsAllInOrder([
+ // Non-ignore fixes (order doesn't matter here, but this is what
+ // server produces).
+ 'quickfix.create.class',
+ 'quickfix.create.mixin',
+ 'quickfix.create.localVariable',
+ 'quickfix.remove.unusedLocalVariable',
+ // Ignore fixes last, with line sorted above file.
+ 'quickfix.ignore.line',
+ 'quickfix.ignore.file',
+ ]),
+ );
+ }
+
+ Future<void> test_ignoreDiagnosticForFile() async {
+ const content = '''
+// Header comment
+// Header comment
+// Header comment
+
+// This comment is attached to the below import
+import 'dart:async';
+[!import!] 'dart:convert';
+
+Future foo;
+''';
+
+ const expectedContent = '''
+// Header comment
+// Header comment
+// Header comment
+
+// ignore_for_file: unused_import
+
+// This comment is attached to the below import
+import 'dart:async';
+import 'dart:convert';
+
+Future foo;
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.ignore.file'),
+ title: "Ignore 'unused_import' for the whole file",
+ );
+ }
+
+ Future<void> test_ignoreDiagnosticForLine() async {
+ const content = '''
+import 'dart:async';
+[!import!] 'dart:convert';
+
+Future foo;
+''';
+
+ const expectedContent = '''
+import 'dart:async';
+// ignore: unused_import
+import 'dart:convert';
+
+Future foo;
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.ignore.line'),
+ title: "Ignore 'unused_import' for this line",
+ );
+ }
+
+ Future<void> test_logsExecution() async {
+ var code = TestCode.parse('''
+[!import!] 'dart:convert';
+''');
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ range: code.range.range,
+ );
+ var fixAction =
+ findCodeActionLiteral(
+ codeActions,
+ title: 'Remove unused import',
+ kind: CodeActionKind('quickfix.remove.unusedImport'),
+ )!;
+
+ await executeCommand(fixAction.command!);
+ expectCommandLogged('dart.fix.remove.unusedImport');
+ }
+
+ /// Repro for https://github.com/Dart-Code/Dart-Code/issues/4462.
+ ///
+ /// Original code only included a fix on its first error (which in this sample
+ /// is the opening brace) and not the whole range of the error.
+ Future<void> test_multilineError() async {
+ registerLintRules();
+ createFile(analysisOptionsPath, '''
+linter:
+ rules:
+ - prefer_expression_function_bodies
+ ''');
+
+ var code = TestCode.parse('''
+int foo() {
+ [!return!] 1;
+}
+ ''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ range: code.range.range,
+ );
+ var fixAction = findCodeActionLiteral(
+ codeActions,
+ title: 'Convert to expression body',
+ kind: CodeActionKind('quickfix.convert.toExpressionBody'),
+ );
+ expect(fixAction, isNotNull);
+ }
+
+ Future<void> test_noDuplicates_differentFix() async {
+ // For convenience, quick-fixes are usually returned for the entire line,
+ // though this can lead to duplicate entries (by title) when multiple
+ // diagnostics have their own fixes of the same type.
+ //
+ // Expect only the only one nearest to the start of the range to be returned.
+ var code = TestCode.parse('''
+void f() {
+ var a = [];
+ print(a!!);^
+}
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ position: code.position.position,
+ );
+ var removeNnaAction =
+ findCodeActionLiteral(
+ codeActions,
+ title: "Remove the '!'",
+ kind: CodeActionKind('quickfix.remove.nonNullAssertion'),
+ )!;
+
+ // Ensure the action is for the diagnostic on the second bang which was
+ // closest to the range requested.
+ var diagnostics = removeNnaAction.diagnostics;
+ var secondBangPos = positionFromOffset(code.code.indexOf('!);'), code.code);
+ expect(diagnostics, hasLength(1));
+ var diagStart = diagnostics!.first.range.start;
+ expect(diagStart, equals(secondBangPos));
+ }
+
+ Future<void> test_noDuplicates_sameFix() async {
+ var code = TestCode.parse('''
+var a = [Test, Test, Te[!!]st];
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ range: code.range.range,
+ );
+ var createClassAction =
+ findCodeActionLiteral(
+ codeActions,
+ title: "Create class 'Test'",
+ kind: CodeActionKind('quickfix.create.class'),
+ )!;
+
+ expect(createClassAction.diagnostics, hasLength(3));
+ }
+
+ Future<void> test_noDuplicates_withDocumentChangesSupport() async {
+ setApplyEditSupport();
+ setDocumentChangesSupport();
+ setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
+
+ var code = TestCode.parse('''
+var a = [Test, Test, Te[!!]st];
+''');
+
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ testFileUri,
+ range: code.range.range,
+ );
+ var createClassActions =
+ findCodeActionLiteral(
+ codeActions,
+ title: "Create class 'Test'",
+ kind: CodeActionKind('quickfix.create.class'),
+ )!;
+
+ expect(createClassActions.diagnostics, hasLength(3));
+ }
+
+ Future<void> test_organizeImportsFix_namedOrganizeImports() async {
+ registerLintRules();
+ createFile(analysisOptionsPath, '''
+linter:
+ rules:
+ - directives_ordering
+ ''');
+
+ // This code should get a fix to sort the imports.
+ const content = '''
+import 'dart:io';
+[!import 'dart:async'!];
+
+Completer a;
+ProcessInfo b;
+''';
+
+ const expectedContent = '''
+import 'dart:async';
+import 'dart:io';
+
+Completer a;
+ProcessInfo b;
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.organize.imports'),
+ title: 'Organize Imports',
+ );
+ }
+
+ Future<void> test_outsideRoot() async {
+ var otherFilePath = pathContext.normalize(
+ pathContext.join(projectFolderPath, '..', 'otherProject', 'foo.dart'),
+ );
+ var otherFileUri = pathContext.toUri(otherFilePath);
+ createFile(otherFilePath, 'bad code to create error');
+ await initializeServer();
+
+ var codeActions = await getCodeActions(
+ otherFileUri,
+ position: startOfDocPos,
+ );
+ expect(codeActions, isEmpty);
+ }
+
+ Future<void> test_pubspec() async {
+ const content = '^';
+
+ const expectedContent = r'''
+name: my_project
+''';
+
+ await verifyCodeActionLiteralEdits(
+ filePath: pubspecFilePath,
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.add.name'),
+ title: "Add 'name' key",
+ );
+ }
+
+ Future<void> test_snippets_createMethod_functionTypeNestedParameters() async {
+ const content = '''
+class A {
+ void a() => c^((cell) => cell.south);
+ void b() => c((cell) => cell.west);
+}
+''';
+
+ const expectedContent = r'''
+class A {
+ void a() => c((cell) => cell.south);
+ void b() => c((cell) => cell.west);
+
+ ${1:void} ${2:c}(${3:Function(dynamic cell)} ${4:param0}) {}
+}
+''';
+
+ setSnippetTextEditSupport();
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.create.method'),
+ title: "Create method 'c'",
+ );
+ }
+
+ /// Ensure braces aren't over-escaped in snippet choices.
+ /// https://github.com/dart-lang/sdk/issues/54403
+ Future<void> test_snippets_createMissingOverrides_recordBraces() async {
+ const content = '''
+abstract class A {
+ void m(Iterable<({int a, int b})> r);
+}
+
+class ^B extends A {}
+''';
+
+ const expectedContent = r'''
+abstract class A {
+ void m(Iterable<({int a, int b})> r);
+}
+
+class B extends A {
+ @override
+ void m(${1|Iterable<({int a\, int b})>,Object|} ${2:r}) {
+ // TODO: implement m$0
+ }
+}
+''';
+
+ setSnippetTextEditSupport();
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.create.missingOverrides'),
+ title: 'Create 1 missing override',
+ );
+ }
+
+ Future<void>
+ test_snippets_extractVariable_functionTypeNestedParameters() async {
+ const content = '''
+void f() {
+ useFunction(te^st);
+}
+
+useFunction(int g(a, b)) {}
+''';
+
+ const expectedContent = r'''
+void f() {
+ ${1:int Function(dynamic a, dynamic b)} ${2:test};
+ useFunction(test);
+}
+
+useFunction(int g(a, b)) {}
+''';
+
+ setSnippetTextEditSupport();
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ kind: CodeActionKind('quickfix.create.localVariable'),
+ title: "Create local variable 'test'",
+ );
+ }
+
+ void _enableLints(List<String> lintNames) {
+ registerLintRules();
+ var lintsYaml = lintNames.map((name) => ' - $name\n').join();
+ createFile(analysisOptionsPath, '''
+linter:
+ rules:
+$lintsYaml
+''');
+ }
+}
+
+/// A version of `camel_case_types` that is deprecated.
+class _DeprecatedCamelCaseTypes extends LintRule {
+ static const LintCode code = LintCode(
+ 'camel_case_types',
+ "The type name '{0}' isn't an UpperCamelCase identifier.",
+ correctionMessage:
+ 'Try changing the name to follow the UpperCamelCase style.',
+ hasPublishedDocs: true,
+ );
+
+ _DeprecatedCamelCaseTypes()
+ : super(
+ name: 'camel_case_types',
+ state: State.deprecated(),
+ description: '',
+ );
+
+ @override
+ LintCode get lintCode => code;
+}
diff --git a/pkg/analysis_server/test/shared/shared_code_actions_refactor_tests.dart b/pkg/analysis_server/test/shared/shared_code_actions_refactor_tests.dart
new file mode 100644
index 0000000..6317671
--- /dev/null
+++ b/pkg/analysis_server/test/shared/shared_code_actions_refactor_tests.dart
@@ -0,0 +1,962 @@
+// 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:analysis_server/src/lsp/handlers/commands/perform_refactor.dart';
+import 'package:analyzer/src/test_utilities/test_code_format.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 '../utils/test_code_extensions.dart';
+import 'shared_test_interface.dart';
+
+mixin SharedConvertGetterToMethodRefactorCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ final refactorTitle = 'Convert Getter to Method';
+
+ Future<void> test_refactor() async {
+ const content = '''
+int get ^test => 42;
+void f() {
+ var a = test;
+ var b = test;
+}
+''';
+ const expectedContent = '''
+int test() => 42;
+void f() {
+ var a = test();
+ var b = test();
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: refactorTitle,
+ );
+ }
+
+ Future<void> test_setter_notAvailable() async {
+ const content = '''
+set ^a(String value) {}
+''';
+
+ await expectNoAction(
+ content,
+ command: Commands.performRefactor,
+ title: refactorTitle,
+ );
+ }
+}
+
+mixin SharedConvertMethodToGetterRefactorCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ final refactorTitle = 'Convert Method to Getter';
+
+ Future<void> test_constructor_notAvailable() async {
+ const content = '''
+class A {
+ ^A();
+}
+''';
+
+ await expectNoAction(
+ content,
+ command: Commands.performRefactor,
+ title: refactorTitle,
+ );
+ }
+
+ Future<void> test_refactor() async {
+ const content = '''
+int ^test() => 42;
+void f() {
+ var a = test();
+ var b = test();
+}
+''';
+ const expectedContent = '''
+int get test => 42;
+void f() {
+ var a = test;
+ var b = test;
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: refactorTitle,
+ );
+ }
+}
+
+mixin SharedExtractMethodRefactorCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin,
+ LspProgressNotificationsMixin {
+ final extractMethodTitle = 'Extract Method';
+
+ Future<void> test_appliesCorrectEdits() async {
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+ const expectedContent = '''
+void f() {
+ print('Test!');
+ newMethod();
+}
+
+void newMethod() {
+ print('Test!');
+}
+''';
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+ }
+
+ Future<void> test_cancelsInProgress() async {
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+ const expectedContent = '''
+>>>>>>>>>> lib/test.dart
+void f() {
+ print('Test!');
+ newMethod();
+}
+
+void newMethod() {
+ print('Test!');
+}
+''';
+
+ var codeAction = await expectCodeActionLiteral(
+ content,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+
+ // Respond to any applyEdit requests from the server with successful responses
+ // and capturing the last edit.
+ late WorkspaceEdit edit;
+ requestsFromServer.listen((request) {
+ if (request.method == Method.workspace_applyEdit) {
+ var params = ApplyWorkspaceEditParams.fromJson(
+ request.params as Map<String, Object?>,
+ );
+ edit = params.edit;
+ respondTo(request, ApplyWorkspaceEditResult(applied: true));
+ }
+ });
+
+ // Send two requests together.
+ var req1 = executeCommand(codeAction.command!);
+ var req2 = executeCommand(codeAction.command!);
+
+ // Expect the first will have cancelled the second.
+ await expectLater(
+ req1,
+ throwsA(
+ isResponseError(
+ ErrorCodes.RequestCancelled,
+ message:
+ 'Another workspace/executeCommand request for a refactor was started',
+ ),
+ ),
+ );
+ await req2;
+
+ // Ensure applying the changes will give us the expected content.
+ verifyEdit(edit, expectedContent);
+ }
+
+ Future<void> test_contentModified() async {
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+
+ var codeAction = await expectCodeActionLiteral(
+ content,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ openTargetFile: true,
+ );
+
+ // Use a Completer to control when the refactor handler starts computing.
+ var completer = Completer<void>();
+ PerformRefactorCommandHandler.delayAfterResolveForTests = completer.future;
+ try {
+ // Send an edit request immediately after the refactor request.
+ var req1 = executeCommand(codeAction.command!);
+ var req2 = replaceFile(100, testFileUri, '// new test content');
+ completer.complete();
+
+ // Expect the first to fail because of the modified content.
+ await expectLater(
+ req1,
+ throwsA(isResponseError(ErrorCodes.ContentModified)),
+ );
+ await req2;
+ } finally {
+ // Ensure we never leave an incomplete future if anything above throws.
+ PerformRefactorCommandHandler.delayAfterResolveForTests = null;
+ }
+ }
+
+ Future<void> test_filtersCorrectly() async {
+ // Support everything (empty prefix matches all)
+ setSupportedCodeActionKinds([CodeActionKind.Empty]);
+
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+ var code = TestCode.parse(content);
+ createFile(testFilePath, code.code);
+ await initializeServer();
+
+ ofKind(CodeActionKind kind) =>
+ getCodeActions(testFileUri, range: code.range.range, kinds: [kind]);
+
+ // The code above will return a 'refactor.extract' (as well as some other
+ // refactors, but not rewrite).
+ expect(await ofKind(CodeActionKind.Refactor), isNotEmpty);
+ expect(await ofKind(CodeActionKind.RefactorExtract), isNotEmpty);
+ expect(await ofKind(CodeActionKind('refactor.extract.foo')), isEmpty);
+ expect(await ofKind(CodeActionKind.RefactorRewrite), isEmpty);
+ }
+
+ Future<void> test_generatesNames() async {
+ const content = '''
+Object? F() {
+ return Container([!Text('Test!')!]);
+}
+
+Object? Container(Object? text) => null;
+Object? Text(Object? text) => null;
+''';
+ const expectedContent = '''
+Object? F() {
+ return Container(text());
+}
+
+Object? text() => Text('Test!');
+
+Object? Container(Object? text) => null;
+Object? Text(Object? text) => null;
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+ }
+
+ Future<void> test_invalidLocation() async {
+ const content = '''
+import 'dart:convert';
+^
+void f() {}
+''';
+
+ await expectNoAction(
+ content,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+ }
+
+ Future<void> test_invalidLocation_importPrefix() async {
+ const content = '''
+import 'dart:io' as io;
+
+i^o.File? a;
+''';
+
+ await expectNoAction(
+ content,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+ }
+
+ Future<void> test_logsAction() async {
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+
+ setDocumentChangesSupport(false);
+ var action = await expectCodeActionLiteral(
+ content,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+
+ await executeCommandForEdits(action.command!);
+ expectCommandLogged('dart.refactor.extract_method');
+ }
+
+ Future<void> test_progress_clientProvided() async {
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+ const expectedContent = '''
+void f() {
+ print('Test!');
+ newMethod();
+}
+
+void newMethod() {
+ print('Test!');
+}
+''';
+
+ // Expect begin/end progress updates without a create, since the
+ // token was supplied by us (the client).
+ expect(progressUpdates, emitsInOrder(['BEGIN', 'END']));
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ commandWorkDoneToken: clientProvidedTestWorkDoneToken,
+ );
+ }
+
+ Future<void> test_progress_notSupported() async {
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+ const expectedContent = '''
+void f() {
+ print('Test!');
+ newMethod();
+}
+
+void newMethod() {
+ print('Test!');
+}
+''';
+
+ var didGetProgressNotifications = false;
+ progressUpdates.listen((_) => didGetProgressNotifications = true);
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+
+ expect(didGetProgressNotifications, isFalse);
+ }
+
+ Future<void> test_progress_serverGenerated() async {
+ const content = '''
+void f() {
+ print('Test!');
+ [!print('Test!');!]
+}
+''';
+ const expectedContent = '''
+void f() {
+ print('Test!');
+ newMethod();
+}
+
+void newMethod() {
+ print('Test!');
+}
+''';
+
+ // Expect create/begin/end progress updates, because in this case the server
+ // generates the token.
+ expect(progressUpdates, emitsInOrder(['CREATE', 'BEGIN', 'END']));
+
+ setWorkDoneProgressSupport();
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+ }
+
+ Future<void> test_validLocation_failsInitialValidation() async {
+ const content = '''
+f() {
+ var a = 0;
+ doFoo([!() => print(a)!]);
+ print(a);
+}
+
+void doFoo(void Function() a) => a();
+
+''';
+ var codeAction = await expectCodeActionLiteral(
+ content,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+ var command = codeAction.command!;
+
+ // Call the `refactor.validate` command with the same arguments.
+ // Clients that want validation behaviour will need to implement this
+ // themselves (via middleware).
+ var response = await executeCommand(
+ Command(
+ title: command.title,
+ command: Commands.validateRefactor,
+ arguments: command.arguments,
+ ),
+ decoder: ValidateRefactorResult.fromJson,
+ );
+
+ expect(response.valid, isFalse);
+ expect(
+ response.message,
+ contains('Cannot extract the closure as a method'),
+ );
+ }
+
+ Future<void> test_validLocation_passesInitialValidation() async {
+ const content = '''
+f() {
+ doFoo([!() => print(1)!]);
+}
+
+void doFoo(void Function() a) => a();
+
+''';
+
+ var codeAction = await expectCodeActionLiteral(
+ content,
+ command: Commands.performRefactor,
+ title: extractMethodTitle,
+ );
+ var command = codeAction.command!;
+
+ // Call the `Commands.validateRefactor` command with the same arguments.
+ // Clients that want validation behaviour will need to implement this
+ // themselves (via middleware).
+ var response = await executeCommand(
+ Command(
+ title: command.title,
+ command: Commands.validateRefactor,
+ arguments: command.arguments,
+ ),
+ decoder: ValidateRefactorResult.fromJson,
+ );
+
+ expect(response.valid, isTrue);
+ expect(response.message, isNull);
+ }
+}
+
+mixin SharedExtractVariableRefactorCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ final convertMethodToGetterTitle = 'Convert Method to Getter';
+ final extractVariableTitle = 'Extract Local Variable';
+ final inlineMethodTitle = 'Inline Method';
+
+ Future<void> test_appliesCorrectEdits() async {
+ const content = '''
+void f() {
+ foo([!1 + 2!]);
+}
+
+void foo(int arg) {}
+''';
+ const expectedContent = '''
+void f() {
+ var arg = 1 + 2;
+ foo(arg);
+}
+
+void foo(int arg) {}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractVariableTitle,
+ );
+ }
+
+ Future<void> test_doesNotCreateNameConflicts() async {
+ const content = '''
+void f() {
+ var arg = "test";
+ foo([!1 + 2!]);
+}
+
+void foo(int arg) {}
+''';
+ const expectedContent = '''
+void f() {
+ var arg = "test";
+ var arg2 = 1 + 2;
+ foo(arg2);
+}
+
+void foo(int arg) {}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractVariableTitle,
+ );
+ }
+
+ Future<void> test_inlineMethod_function_startOfParameterList() async {
+ const content = '''
+test^(a, b) {
+ print(a);
+ print(b);
+}
+void f() {
+ test(1, 2);
+}
+''';
+ const expectedContent = '''
+void f() {
+ print(1);
+ print(2);
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: inlineMethodTitle,
+ );
+ }
+
+ Future<void> test_inlineMethod_function_startOfTypeParameterList() async {
+ const content = '''
+test^<T>(T a, T b) {
+ print(a);
+ print(b);
+}
+void f() {
+ test(1, 2);
+}
+''';
+ const expectedContent = '''
+void f() {
+ print(1);
+ print(2);
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: inlineMethodTitle,
+ );
+ }
+
+ Future<void> test_inlineMethod_method_startOfParameterList() async {
+ const content = '''
+class A {
+ test^(a, b) {
+ print(a);
+ print(b);
+ }
+ void f() {
+ test(1, 2);
+ }
+}
+''';
+ const expectedContent = '''
+class A {
+ void f() {
+ print(1);
+ print(2);
+ }
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: inlineMethodTitle,
+ );
+ }
+
+ Future<void> test_inlineMethod_method_startOfTypeParameterList() async {
+ const content = '''
+class A {
+ test^<T>(T a, T b) {
+ print(a);
+ print(b);
+ }
+ void f() {
+ test(1, 2);
+ }
+}
+''';
+ const expectedContent = '''
+class A {
+ void f() {
+ print(1);
+ print(2);
+ }
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: inlineMethodTitle,
+ );
+ }
+
+ Future<void> test_methodToGetter_function_startOfParameterList() async {
+ const content = '''
+int test^() => 42;
+''';
+ const expectedContent = '''
+int get test => 42;
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: convertMethodToGetterTitle,
+ );
+ }
+
+ Future<void> test_methodToGetter_function_startOfTypeParameterList() async {
+ const content = '''
+int test^<T>() => 42;
+''';
+ const expectedContent = '''
+int get test<T> => 42;
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: convertMethodToGetterTitle,
+ );
+ }
+
+ Future<void> test_methodToGetter_method_startOfParameterList() async {
+ const content = '''
+class A {
+ int test^() => 42;
+}
+''';
+ const expectedContent = '''
+class A {
+ int get test => 42;
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: convertMethodToGetterTitle,
+ );
+ }
+
+ Future<void> test_methodToGetter_method_startOfTypeParameterList() async {
+ const content = '''
+class A {
+ int test^<T>() => 42;
+}
+''';
+ const expectedContent = '''
+class A {
+ int get test<T> => 42;
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: convertMethodToGetterTitle,
+ );
+ }
+}
+
+mixin SharedExtractWidgetRefactorCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ final extractWidgetTitle = 'Extract Widget';
+
+ String get expectedNewWidgetConstructorDeclaration => '''
+const NewWidget({
+ super.key,
+ });
+''';
+
+ Future<void> test_appliesCorrectEdits() async {
+ const content = '''
+import 'package:flutter/material.dart';
+
+class MyWidget extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new Row(
+ children: <Widget>[
+ new [!Column!](
+ children: <Widget>[
+ new Text('AAA'),
+ new Text('BBB'),
+ ],
+ ),
+ new Text('CCC'),
+ new Text('DDD'),
+ ],
+ );
+ }
+}
+''';
+ var expectedContent = '''
+import 'package:flutter/material.dart';
+
+class MyWidget extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new Row(
+ children: <Widget>[
+ NewWidget(),
+ new Text('CCC'),
+ new Text('DDD'),
+ ],
+ );
+ }
+}
+
+class NewWidget extends StatelessWidget {
+ $expectedNewWidgetConstructorDeclaration
+ @override
+ Widget build(BuildContext context) {
+ return new Column(
+ children: <Widget>[
+ new Text('AAA'),
+ new Text('BBB'),
+ ],
+ );
+ }
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: extractWidgetTitle,
+ openTargetFile: true,
+ );
+ }
+
+ Future<void> test_invalidLocation() async {
+ const content = '''
+import 'dart:convert';
+^
+void f() {}
+''';
+
+ await expectNoAction(
+ content,
+ command: Commands.performRefactor,
+ title: extractWidgetTitle,
+ );
+ }
+}
+
+mixin SharedInlineLocalVariableRefactorCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ final inlineVariableTitle = 'Inline Local Variable';
+
+ Future<void> test_appliesCorrectEdits() async {
+ const content = '''
+void f() {
+ var a^ = 1;
+ print(a);
+ print(a);
+ print(a);
+}
+''';
+ const expectedContent = '''
+void f() {
+ print(1);
+ print(1);
+ print(1);
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: inlineVariableTitle,
+ openTargetFile: true,
+ );
+ }
+}
+
+mixin SharedInlineMethodRefactorCodeActionsTests
+ on
+ SharedTestInterface,
+ CodeActionsTestMixin,
+ LspRequestHelpersMixin,
+ LspEditHelpersMixin,
+ LspVerifyEditHelpersMixin,
+ ClientCapabilitiesHelperMixin {
+ final inlineMethodTitle = 'Inline Method';
+
+ Future<void> test_inlineAtCallSite() async {
+ const content = '''
+void foo1() {
+ ba^r();
+}
+
+void foo2() {
+ bar();
+}
+
+void bar() {
+ print('test');
+}
+''';
+ const expectedContent = '''
+void foo1() {
+ print('test');
+}
+
+void foo2() {
+ bar();
+}
+
+void bar() {
+ print('test');
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: inlineMethodTitle,
+ );
+ }
+
+ Future<void> test_inlineAtMethod() async {
+ const content = '''
+void foo1() {
+ bar();
+}
+
+void foo2() {
+ bar();
+}
+
+void ba^r() {
+ print('test');
+}
+''';
+ const expectedContent = '''
+void foo1() {
+ print('test');
+}
+
+void foo2() {
+ print('test');
+}
+''';
+
+ await verifyCodeActionLiteralEdits(
+ content,
+ expectedContent,
+ command: Commands.performRefactor,
+ title: inlineMethodTitle,
+ );
+ }
+}
diff --git a/pkg/analysis_server/test/shared/shared_code_actions_source_tests.dart b/pkg/analysis_server/test/shared/shared_code_actions_source_tests.dart
new file mode 100644
index 0000000..4427655
--- /dev/null
+++ b/pkg/analysis_server/test/shared/shared_code_actions_source_tests.dart
@@ -0,0 +1,335 @@
+// 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,
+ 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_filtersCorrectly() async {
+ createFile(testFilePath, '');
+ await initializeServer();
+
+ ofKind(CodeActionKind kind) => getCodeActions(testFileUri, 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 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,
+ 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) => 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);
+ }
+}