blob: d69f07d97c5a722d8a3bc01388b5ec08e3437fa1 [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/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 '../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 {
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',
categories: {LintRuleCategory.style},
state: State.deprecated(),
description: '',
details: '',
);
@override
LintCode get lintCode => code;
}
@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 {
// This code should get a fix to replace 'foo' with 'bar'.'
const content = '''
[!foo!]
''';
const expectedContent = '''
bar
''';
var 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,
);
await verifyActionEdits(
filePath: filePath,
content,
expectedContent,
kind: CodeActionKind('quickfix.fooToBar'),
title: "Change 'foo' to 'bar'",
);
}
@override
void setUp() {
super.setUp();
setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
}
Future<void> test_addImport_noPreference() async {
newFile(
join(projectFolderPath, 'lib', 'class.dart'),
'class MyClass {}',
);
var code = TestCode.parse('''
MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
var 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 {}',
);
var code = TestCode.parse('''
MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
var 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 {}',
);
var code = TestCode.parse('''
MyCla^ss? a;
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
var 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
''';
await verifyActionEdits(
filePath: analysisOptionsPath,
content,
expectedContent,
kind: CodeActionKind('quickfix.removeLint'),
title: "Remove 'camel_case_types'",
);
} 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;
''';
await verifyActionEdits(
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 verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.remove.unusedImport'),
title: 'Remove unused import',
);
}
Future<void> test_createFile() async {
const content = '''
import '[!newfile.dart!]';
''';
const expectedContent = '''
>>>>>>>>>> lib/newfile.dart created
// TODO Implement this library.<<<<<<<<<<
''';
setFileCreateSupport();
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.create.file'),
title: "Create file 'newfile.dart'",
);
}
Future<void> test_filtersCorrectly() async {
setSupportedCodeActionKinds(
[CodeActionKind.QuickFix, CodeActionKind.Refactor]);
var code = TestCode.parse('''
import 'dart:async';
[!import!] 'dart:convert';
Future foo;
''');
newFile(mainFilePath, code.code);
await initialize();
ofKind(CodeActionKind kind) => getCodeActions(
mainFileUri,
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.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 expectAction(
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();
''');
newFile(mainFilePath, code.code);
await initialize();
var allFixes = await getCodeActions(mainFileUri, 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();
newFile(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 verifyActionEdits(
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));
}
''');
newFile(mainFilePath, code.code);
await initialize();
var position = code.position.position;
var range = Range(start: position, end: position);
var codeActions = await getCodeActions(mainFileUri, range: range);
var 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;
''';
await verifyActionEdits(
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 verifyActionEdits(
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';
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, range: code.range.range);
var fixAction = findAction(codeActions,
title: 'Remove unused import',
kind: CodeActionKind('quickfix.remove.unusedImport'))!;
await executeCommand(fixAction.command!);
expectCommandLogged('dart.fix.remove.unusedImport');
}
Future<void> test_macroGenerated() async {
setDartTextDocumentContentProviderSupport();
var macroFilePath = join(projectFolderPath, 'lib', 'test.macro.dart');
var code = TestCode.parse('''
void f() {
js^on.encode('');
}
''');
newFile(macroFilePath, code.code);
await initialize();
var codeActions = await getCodeActions(
uriConverter.toClientUri(macroFilePath),
position: code.position.position);
expect(codeActions, isEmpty);
}
/// 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
''');
var code = TestCode.parse('''
int foo() {
[!return!] 1;
}
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, range: code.range.range);
var fixAction = findAction(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!!);^
}
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, position: code.position.position);
var removeNnaAction = findAction(codeActions,
title: "Remove the '!'",
kind: CodeActionKind('quickfix.remove.nonNullAssertion'));
// Expect only one of the fixes.
expect(removeNnaAction, isNotNull);
// Ensure the action is for the diagnostic on the second bang which was
// closest to the range requested.
var secondBangPos = positionFromOffset(code.code.indexOf('!);'), code.code);
expect(removeNnaAction!.diagnostics, hasLength(1));
var diagStart = removeNnaAction.diagnostics!.first.range.start;
expect(diagStart, equals(secondBangPos));
}
Future<void> test_noDuplicates_sameFix() async {
var code = TestCode.parse('''
var a = [Test, Test, Te[!!]st];
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, range: code.range.range);
var createClassActions = findAction(codeActions,
title: "Create class 'Test'",
kind: CodeActionKind('quickfix.create.class'));
expect(createClassActions, isNotNull);
expect(createClassActions!.diagnostics, hasLength(3));
}
Future<void> test_noDuplicates_withDocumentChangesSupport() async {
setApplyEditSupport();
setDocumentChangesSupport();
setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
var code = TestCode.parse('''
var a = [Test, Test, Te[!!]st];
''');
newFile(mainFilePath, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, range: code.range.range);
var createClassActions = findAction(codeActions,
title: "Create class 'Test'",
kind: CodeActionKind('quickfix.create.class'));
expect(createClassActions, isNotNull);
expect(createClassActions!.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;
''';
await verifyActionEdits(
content,
expectedContent,
kind: CodeActionKind('quickfix.organize.imports'),
title: 'Organize Imports',
);
}
Future<void> test_outsideRoot() async {
var otherFilePath = convertPath('/home/otherProject/foo.dart');
var otherFileUri = pathContext.toUri(otherFilePath);
newFile(otherFilePath, 'bad code to create error');
await initialize();
var 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.
var code = TestCode.parse('''
[!import!] 'dart:convert';
''');
// Provide two plugin results that should sort either side of the server fix.
var 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, code.code);
await initialize();
var codeActions =
await getCodeActions(mainFileUri, range: code.range.range);
var codeActionTitles = codeActions.map((action) =>
action.map((command) => command.title, (action) => action.title));
expect(
codeActionTitles,
containsAllInOrder([
'High',
'Remove unused import',
'Low',
]),
);
}
Future<void> test_pubspec() async {
const content = '^';
const expectedContent = r'''
name: my_project
''';
await verifyActionEdits(
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:c}(${2:Function(dynamic cell)} ${3:param0}) {}
}
''';
setSnippetTextEditSupport();
await verifyActionEdits(
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 verifyActionEdits(
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 verifyActionEdits(
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();
newFile(analysisOptionsPath, '''
linter:
rules:
$lintsYaml
''');
}
}