155a64c1b7
This bumps the Registry class to be able to take "rules that are enabled by default" and "rules that need to be explicitly enabled", "lint rules" and "warning rules". Next is to improve the classes. Add parsing for the top-level `plugins` section, and `PluginConfiguration` class to hold this data, and `get pluginConfigurations` on AnalysisOptions. Rename `parseLintRuleConfigs` to `parseLinterSection` to align better with the other functions in engine.dart. Rename `_ruleConfigs` function to `parseRulesSection`. Change-Id: Ib93b7548bfb13cc94381971a5a2780a5dc81e9f1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/392040 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Phil Quitslund <pquitslund@google.com> Commit-Queue: Samuel Rawlins <srawlins@google.com>
873 lines
22 KiB
Dart
873 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'",
|
|
);
|
|
}
|
|
|
|
@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(
|
|
(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: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
|
|
''');
|
|
}
|
|
}
|