9831fc4e98
The term "CodeAction" is a bit overloaded. It could mean both an individual result from the `textDocument/codeAction` request (which is a `Command` or a `CodeAction`), or the `CodeAction` type defined in the spec (which the spec refers to as a "Code Action literal"). To reduce confusion where we have similar APIs that operate on "Code Actions" (CodeAction|Command), this renames the `CodeAction` class to `CodeActionLiteral` and we will use the term `CodeAction` to mean either of those types. To make things simpler to review, this change _only_ renames the class, and also swaps the order of the types in some places that used `Either2<Command, CodeAction>` (which is opposite to the spec and some other code). Some further clean up will be done in a separate change. Change-Id: Idcd8265f9229c3450004e68334e98a7b530330a4 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/425300 Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
888 lines
22 KiB
Dart
888 lines
22 KiB
Dart
// 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',
|
|
state: State.deprecated(),
|
|
description: '',
|
|
);
|
|
|
|
@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'",
|
|
command: 'dart.logAction',
|
|
commandArgs: [
|
|
{'action': 'fix from plugin'},
|
|
],
|
|
);
|
|
}
|
|
|
|
@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'"]),
|
|
);
|
|
}
|
|
|
|
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'"]),
|
|
);
|
|
}
|
|
|
|
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 verifyActionEdits(
|
|
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 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((action) => action.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 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');
|
|
}
|
|
|
|
/// 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:void} ${2:c}(${3:Function(dynamic cell)} ${4: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
|
|
''');
|
|
}
|
|
}
|