blob: 13dc7deacc706db999449f2ebfc653650663daf4 [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.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:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../utils/test_code_extensions.dart';
import 'code_actions_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(FixesCodeActionsTest);
});
}
/// A version of `camel_case_types` that is deprecated.
class DeprecatedCamelCaseTypes extends LintRule {
DeprecatedCamelCaseTypes()
: super(
name: 'camel_case_types',
group: Group.style,
state: State.deprecated(),
description: '',
details: '',
);
}
@reflectiveTest
class FixesCodeActionsTest extends AbstractCodeActionsTest {
/// 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 {
final fileUri = Uri.file(filePath);
// This code should get a fix to replace 'foo' with 'bar'.'
const content = '[[foo]]';
const expectedContent = 'bar';
final pluginResult = plugin.EditGetFixesResult([
plugin.AnalysisErrorFixes(
plugin.AnalysisError(
plugin.AnalysisErrorSeverity.ERROR,
plugin.AnalysisErrorType.HINT,
plugin.Location(filePath, 0, 3, 0, 0),
"Do not use 'foo'",
'do_not_use_foo',
),
fixes: [
plugin.PrioritizedSourceChange(
0,
plugin.SourceChange(
"Change 'foo' to 'bar'",
edits: [
plugin.SourceFileEdit(filePath, 0,
edits: [plugin.SourceEdit(0, 3, 'bar')])
],
id: 'fooToBar',
),
)
],
)
]);
configureTestPlugin(
handler: (request) =>
request is plugin.EditGetFixesParams ? pluginResult : null,
);
newFile(filePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(fileUri, range: rangeFromMarkers(content));
final assist = findEditAction(codeActions,
CodeActionKind('quickfix.fooToBar'), "Change 'foo' to 'bar'")!;
final edit = assist.edit!;
expect(edit.changes, isNotNull);
// Ensure applying the changes will give us the expected content.
final contents = {
filePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[filePath], equals(expectedContent));
}
Future<void> test_addImport_noPreference() async {
newFile(
join(projectFolderPath, 'lib', 'class.dart'),
'class MyClass {}',
);
final code = TestCode.parse('''
MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
final codeActionTitles = codeActions.map((action) =>
action.map((command) => command.title, (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']);
newFile(
join(projectFolderPath, 'lib', 'class.dart'),
'class MyClass {}',
);
final code = TestCode.parse('''
MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
final codeActionTitles = codeActions.map((action) =>
action.map((command) => command.title, (action) => action.title));
expect(
codeActionTitles,
containsAllInOrder([
"Import library 'package:test/class.dart'",
"Import library 'class.dart'",
]),
);
}
Future<void> test_addImport_preferRelative() async {
_enableLints(['prefer_relative_imports']);
newFile(
join(projectFolderPath, 'lib', 'class.dart'),
'class MyClass {}',
);
final code = TestCode.parse('''
MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
final codeActionTitles = codeActions.map((action) =>
action.map((command) => command.title, (action) => action.title));
expect(
codeActionTitles,
containsAllInOrder([
"Import library 'class.dart'",
"Import library 'package:test/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.register(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
''';
newFile(analysisOptionsPath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
// Expect a fix.
final codeActions = await getCodeActions(analysisOptionsUri,
range: rangeFromMarkers(content));
final fix = findEditAction(codeActions,
CodeActionKind('quickfix.removeLint'), "Remove 'camel_case_types'")!;
// Ensure it makes the correct edits.
final edit = fix.edit!;
final contents = {
analysisOptionsPath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[analysisOptionsPath], equals(expectedContent));
} finally {
// Restore the "real" `camel_case_types`.
Registry.ruleRegistry.register(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;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.unusedImport'),
'Remove unused import')!;
// Ensure the edit came back, and using documentChanges.
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
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;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.unusedImport'),
'Remove unused import')!;
// Ensure the edit came back, and using changes.
final edit = fixAction.edit!;
expect(edit.changes, isNotNull);
expect(edit.documentChanges, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_createFile() async {
const content = '''
import '[[newfile.dart]]';
''';
final expectedCreatedFile =
path.join(path.dirname(mainFilePath), 'newfile.dart');
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities: withResourceOperationKinds(
emptyWorkspaceClientCapabilities, [ResourceOperationKind.Create]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(codeActions,
CodeActionKind('quickfix.create.file'), "Create file 'newfile.dart'")!;
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
// Ensure applying the changes creates the file and with the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[expectedCreatedFile], isNotEmpty);
}
Future<void> test_filtersCorrectly() async {
const content = '''
import 'dart:async';
[[import]] 'dart:convert';
Future foo;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities,
[CodeActionKind.QuickFix, CodeActionKind.Refactor],
),
);
ofKind(CodeActionKind kind) => getCodeActions(
mainFileUri,
range: rangeFromMarkers(content),
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.other')), isEmpty);
expect(await ofKind(CodeActionKind.Refactor), isEmpty);
}
Future<void> test_fixAll_logsExecution() async {
const content = '''
void f(String a) {
[[print(a!!)]];
print(a!!);
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
"Remove '!'s in file",
)!;
await executeCommand(fixAction.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.
const content = '''
var a = [[foo]]();
var b = bar();
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final allFixes =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
// Expect only the single-fix, there should be no apply-all.
expect(allFixes, hasLength(1));
final 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!)]];
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions, CodeActionKind('quickfix'), "Remove '!'s in file");
// Should not appear if there was only a single error.
expect(fixAction, isNull);
}
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);
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.nonNullAssertion.multi'),
"Remove '!'s in file",
)!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, fixAction.edit!.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_ignoreDiagnostic_afterOtherFixes() async {
const content = '''
void main() {
Uint8List inputBytes = Uin^t8List.fromList(List.filled(100000000, 0));
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final position = positionFromMarker(content);
final range = Range(start: position, end: position);
final codeActions = await getCodeActions(mainFileUri, range: range);
final codeActionKinds = codeActions.map(
(item) => item.map(
(command) => null,
(action) => action.kind?.toString(),
),
);
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;''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
// Find the ignore action.
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.ignore.file'),
"Ignore 'unused_import' for this file")!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, fixAction.edit!.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
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;''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
// Find the ignore action.
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.ignore.line'),
"Ignore 'unused_import' for this line")!;
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, fixAction.edit!.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_logsExecution() async {
const content = '''
[[import]] 'dart:convert';
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.remove.unusedImport'),
'Remove unused import')!;
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();
newFile(analysisOptionsPath, '''
linter:
rules:
- prefer_expression_function_bodies
''');
final code = TestCode.parse('''
int foo() {
[!return!] 1;
}
''');
newFile(mainFilePath, code.code);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: code.range.range);
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.convert.toExpressionBody'),
'Convert to expression body');
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.
const content = '''
void f() {
var a = [];
print(a!!);^
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final removeNnaAction = findEditActions(codeActions,
CodeActionKind('quickfix.remove.nonNullAssertion'), "Remove the '!'");
// Expect only one of the fixes.
expect(removeNnaAction, hasLength(1));
// Ensure the action is for the diagnostic on the second bang which was
// closest to the range requested.
final secondBangPos =
positionFromOffset(withoutMarkers(content).indexOf('!);'), content);
expect(removeNnaAction.first.diagnostics, hasLength(1));
final diagStart = removeNnaAction.first.diagnostics!.first.range.start;
expect(diagStart, equals(secondBangPos));
}
Future<void> test_noDuplicates_sameFix() async {
const content = '''
var a = [Test, Test, Te[[]]st];
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final createClassActions = findEditActions(codeActions,
CodeActionKind('quickfix.create.class'), "Create class 'Test'");
expect(createClassActions, hasLength(1));
expect(createClassActions.first.diagnostics, hasLength(3));
}
Future<void> test_noDuplicates_withDocumentChangesSupport() async {
const content = '''
var a = [Test, Test, Te[[]]st];
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities: withApplyEditSupport(
withDocumentChangesSupport(emptyWorkspaceClientCapabilities)));
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final createClassActions = findEditActions(codeActions,
CodeActionKind('quickfix.create.class'), "Create class 'Test'");
expect(createClassActions, hasLength(1));
expect(createClassActions.first.diagnostics, hasLength(3));
}
Future<void> test_organizeImportsFix_namedOrganizeImports() async {
registerLintRules();
newFile(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;
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final fixAction = findEditAction(codeActions,
CodeActionKind('quickfix.organize.imports'), 'Organize Imports')!;
// Ensure the edit came back, and using changes.
final edit = fixAction.edit!;
expect(edit.changes, isNotNull);
expect(edit.documentChanges, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[mainFilePath], equals(expectedContent));
}
Future<void> test_outsideRoot() async {
final otherFilePath = convertPath('/home/otherProject/foo.dart');
final otherFileUri = Uri.file(otherFilePath);
newFile(otherFilePath, 'bad code to create error');
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions = await getCodeActions(
otherFileUri,
position: startOfDocPos,
);
expect(codeActions, isEmpty);
}
Future<void> test_plugin_dart() async {
if (!AnalysisServer.supportsPlugins) return;
return await checkPluginResults(mainFilePath);
}
Future<void> test_plugin_nonDart() async {
if (!AnalysisServer.supportsPlugins) return;
return await checkPluginResults(join(projectFolderPath, 'lib', 'foo.foo'));
}
Future<void> test_plugin_sortsWithServer() async {
if (!AnalysisServer.supportsPlugins) return;
// Produces a server fix for removing unused import with a default
// priority of 50.
const content = '''
[[import]] 'dart:convert';
''';
// Provide two plugin results that should sort either side of the server fix.
final pluginResult = plugin.EditGetFixesResult([
plugin.AnalysisErrorFixes(
plugin.AnalysisError(
plugin.AnalysisErrorSeverity.ERROR,
plugin.AnalysisErrorType.HINT,
plugin.Location(mainFilePath, 0, 3, 0, 0),
'Dummy error',
'dummy',
),
fixes: [
plugin.PrioritizedSourceChange(10, plugin.SourceChange('Low')),
plugin.PrioritizedSourceChange(100, plugin.SourceChange('High')),
],
)
]);
configureTestPlugin(
handler: (request) =>
request is plugin.EditGetFixesParams ? pluginResult : null,
);
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
final codeActions =
await getCodeActions(mainFileUri, range: rangeFromMarkers(content));
final codeActionTitles = codeActions.map((action) =>
action.map((command) => command.title, (action) => action.title));
expect(
codeActionTitles,
containsAllInOrder([
'High',
'Remove unused import',
'Low',
]),
);
}
Future<void> test_pubspec() async {
const content = '';
const expectedContent = r'''
name: my_project
''';
newFile(pubspecFilePath, content);
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
);
// Expect a fix.
final codeActions =
await getCodeActions(pubspecFileUri, range: startOfDocRange);
final fix = findEditAction(
codeActions, CodeActionKind('quickfix.add.name'), "Add 'name' key")!;
// Ensure it makes the correct edits.
final edit = fix.edit!;
final contents = {
pubspecFilePath: withoutMarkers(content),
};
applyChanges(contents, edit.changes!);
expect(contents[pubspecFilePath], equals(expectedContent));
}
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:c}(${2:Function(dynamic cell)} ${3:param0}) {}
}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final fixAction = findEditAction(codeActions,
CodeActionKind('quickfix.create.method'), "Create method 'c'")!;
// Ensure the edit came back, and using documentChanges.
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
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)) {}
''';
newFile(mainFilePath, withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
);
final codeActions = await getCodeActions(mainFileUri,
position: positionFromMarker(content));
final fixAction = findEditAction(
codeActions,
CodeActionKind('quickfix.create.localVariable'),
"Create local variable 'test'")!;
// Ensure the edit came back, and using documentChanges.
final edit = fixAction.edit!;
expect(edit.documentChanges, isNotNull);
expect(edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, edit.documentChanges!);
expect(contents[mainFilePath], equals(expectedContent));
}
void _enableLints(List<String> lintNames) {
registerLintRules();
final lintsYaml = lintNames.map((name) => ' - $name\n').join();
newFile(analysisOptionsPath, '''
linter:
rules:
$lintsYaml
''');
}
}