| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'package:analysis_server/lsp_protocol/protocol.dart'; | |
| import 'package:analysis_server/src/lsp/constants.dart'; | |
| import 'package:analysis_server/src/lsp/extensions/code_action.dart'; | |
| import 'package:analysis_server/src/services/correction/fix.dart'; | |
| import 'package:analysis_server/src/services/correction/fix_internal.dart'; | |
| import 'package:analyzer/analysis_rule/analysis_rule.dart'; | |
| import 'package:analyzer/analysis_rule/rule_state.dart'; | |
| import 'package:analyzer/error/error.dart'; | |
| import 'package:analyzer/src/lint/registry.dart'; | |
| import 'package:analyzer/src/test_utilities/test_code_format.dart'; | |
| import 'package:linter/src/rules.dart'; | |
| import 'package:test/test.dart'; | |
| import '../lsp/code_actions_mixin.dart'; | |
| import '../lsp/request_helpers_mixin.dart'; | |
| import '../lsp/server_abstract.dart'; | |
| import '../utils/test_code_extensions.dart'; | |
| import 'shared_test_interface.dart'; | |
| /// Shared tests used by both LSP + Legacy server tests and/or integration. | |
| mixin SharedFixesCodeActionsTests | |
| on | |
| SharedTestInterface, | |
| CodeActionsTestMixin, | |
| LspRequestHelpersMixin, | |
| LspEditHelpersMixin, | |
| LspVerifyEditHelpersMixin, | |
| ClientCapabilitiesHelperMixin { | |
| String get analysisOptionsPath => | |
| pathContext.join(projectFolderPath, 'analysis_options.yaml'); | |
| @override | |
| Future<void> setUp() async { | |
| await super.setUp(); | |
| // Fix tests are likely to have diagnostics that need fixing. | |
| failTestOnErrorDiagnostic = false; | |
| setApplyEditSupport(); | |
| setDocumentChangesSupport(); | |
| setSupportedCodeActionKinds([CodeActionKind.QuickFix]); | |
| registerBuiltInFixGenerators(); | |
| } | |
| Future<void> test_addImport_noPreference() async { | |
| createFile( | |
| pathContext.join(projectFolderPath, 'lib', 'class.dart'), | |
| 'class MyClass {}', | |
| ); | |
| var code = TestCode.parse(''' | |
| MyCla^ss? a; | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| position: code.position.position, | |
| ); | |
| var codeActionTitles = codeActions.map((action) => action.title); | |
| expect( | |
| codeActionTitles, | |
| // With no preference, server defaults to absolute. | |
| containsAllInOrder([ | |
| "Add 'import' of 'package:test/class.dart'", | |
| "Add 'import' of 'class.dart'", | |
| ]), | |
| ); | |
| } | |
| Future<void> test_addImport_preferAbsolute() async { | |
| _enableLints(['always_use_package_imports']); | |
| createFile( | |
| pathContext.join(projectFolderPath, 'lib', 'class.dart'), | |
| 'class MyClass {}', | |
| ); | |
| var code = TestCode.parse(''' | |
| MyCla^ss? a; | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| position: code.position.position, | |
| ); | |
| var codeActionTitles = codeActions.map((action) => action.title); | |
| expect( | |
| codeActionTitles, | |
| containsAllInOrder(["Add 'import' of 'package:test/class.dart'"]), | |
| ); | |
| } | |
| Future<void> test_addImport_preferRelative() async { | |
| _enableLints(['prefer_relative_imports']); | |
| createFile( | |
| pathContext.join(projectFolderPath, 'lib', 'class.dart'), | |
| 'class MyClass {}', | |
| ); | |
| var code = TestCode.parse(''' | |
| MyCla^ss? a; | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| position: code.position.position, | |
| ); | |
| var codeActionTitles = codeActions.map((action) => action.title); | |
| expect( | |
| codeActionTitles, | |
| containsAllInOrder(["Add 'import' of 'class.dart'"]), | |
| ); | |
| } | |
| Future<void> test_analysisOptions() async { | |
| registerLintRules(); | |
| // To ensure there's an associated code action, we manually deprecate an | |
| // existing lint (`camel_case_types`) for the duration of this test. | |
| // Fetch the "actual" lint so we can restore it after the test. | |
| var camelCaseTypes = Registry.ruleRegistry.getRule('camel_case_types')!; | |
| // Overwrite it. | |
| Registry.ruleRegistry.registerLintRule(_DeprecatedCamelCaseTypes()); | |
| // Now we can assume it will have an action associated... | |
| try { | |
| const content = r''' | |
| linter: | |
| rules: | |
| - prefer_is_empty | |
| - [!camel_case_types!] | |
| - lines_longer_than_80_chars | |
| '''; | |
| const expectedContent = r''' | |
| linter: | |
| rules: | |
| - prefer_is_empty | |
| - lines_longer_than_80_chars | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| filePath: analysisOptionsPath, | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.removeLint'), | |
| title: "Remove 'camel_case_types'", | |
| ); | |
| } finally { | |
| // Restore the "real" `camel_case_types`. | |
| Registry.ruleRegistry.registerLintRule(camelCaseTypes); | |
| } | |
| } | |
| Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async { | |
| // This code should get a fix to remove the unused import. | |
| const content = ''' | |
| import 'dart:async'; | |
| [!import!] 'dart:convert'; | |
| Future foo; | |
| '''; | |
| const expectedContent = ''' | |
| import 'dart:async'; | |
| Future foo; | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.remove.unusedImport'), | |
| title: DartFixKind.removeUnusedImport.message, | |
| ); | |
| } | |
| Future<void> test_appliesCorrectEdits_withoutDocumentChangesSupport() async { | |
| // This code should get a fix to remove the unused import. | |
| const content = ''' | |
| import 'dart:async'; | |
| [!import!] 'dart:convert'; | |
| Future foo; | |
| '''; | |
| const expectedContent = ''' | |
| import 'dart:async'; | |
| Future foo; | |
| '''; | |
| setDocumentChangesSupport(false); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.remove.unusedImport'), | |
| title: DartFixKind.removeUnusedImport.message, | |
| ); | |
| } | |
| Future<void> test_codeActionLiterals_supported() async { | |
| const content = ''' | |
| void f(String a) => [!print(a!)!]; | |
| '''; | |
| const expectedContent = ''' | |
| void f(String a) => print(a); | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.remove.nonNullAssertion'), | |
| title: DartFixKind.removeNonNullAssertion.message, | |
| ); | |
| } | |
| Future<void> test_codeActionLiterals_unsupported() async { | |
| setSupportedCodeActionKinds(null); // no codeActionLiteralSupport | |
| const content = ''' | |
| void f(String a) => [!print(a!)!]; | |
| '''; | |
| const expectedContent = ''' | |
| void f(String a) => print(a); | |
| '''; | |
| await verifyCommandCodeActionEdits( | |
| content, | |
| expectedContent, | |
| command: Commands.applyCodeAction, | |
| title: DartFixKind.removeNonNullAssertion.message, | |
| ); | |
| } | |
| Future<void> test_createFile() async { | |
| const content = ''' | |
| import '[!createFile.dart!]'; | |
| '''; | |
| const expectedContent = ''' | |
| >>>>>>>>>> lib/createFile.dart created | |
| // TODO Implement this library.<<<<<<<<<< | |
| '''; | |
| setFileCreateSupport(); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.file'), | |
| title: "Create file 'createFile.dart'", | |
| ); | |
| } | |
| Future<void> test_filtersCorrectly() async { | |
| setSupportedCodeActionKinds([ | |
| CodeActionKind.QuickFix, | |
| CodeActionKind.Refactor, | |
| ]); | |
| var code = TestCode.parse(''' | |
| import 'dart:async'; | |
| [!import!] 'dart:convert'; | |
| Future foo; | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| ofKind(CodeActionKind kind) => | |
| getCodeActions(testFileUri, range: code.range.range, kinds: [kind]); | |
| // The code above will return a 'quickfix.remove.unusedImport'. | |
| expect(await ofKind(CodeActionKind.QuickFix), isNotEmpty); | |
| expect(await ofKind(CodeActionKind('quickfix.remove')), isNotEmpty); | |
| expect(await ofKind(CodeActionKind('quickfix.remove.foo')), isEmpty); | |
| expect(await ofKind(CodeActionKind('quickfix.other')), isEmpty); | |
| expect(await ofKind(CodeActionKind.Refactor), isNotEmpty); | |
| expect(await ofKind(CodeActionKind('refactor.add')), isNotEmpty); | |
| expect(await ofKind(CodeActionKind('refactor.remove')), isEmpty); | |
| } | |
| Future<void> test_fixAll_logsExecution() async { | |
| const content = ''' | |
| void f(String a) { | |
| [!print(a!!)!]; | |
| print(a!!); | |
| } | |
| '''; | |
| var action = await expectCodeActionLiteral( | |
| content, | |
| kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'), | |
| title: DartFixKind.removeNonNullAssertionMulti.message, | |
| ); | |
| await executeCommand(action.command!); | |
| expectCommandLogged('dart.fix.remove.nonNullAssertion.multi'); | |
| } | |
| Future<void> test_fixAll_notWhenNoBatchFix() async { | |
| // Some fixes (for example 'create function foo') are not available in the | |
| // batch processor, so should not generate fix-all-in-file fixes even if there | |
| // are multiple instances. | |
| var code = TestCode.parse(''' | |
| var a = [!foo!](); | |
| var b = bar(); | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var allFixes = await getCodeActions(testFileUri, range: code.range.range); | |
| // Expect only the single-fix, there should be no apply-all. | |
| expect(allFixes, hasLength(2)); | |
| var fixTitle = allFixes.first.map((f) => f.title, (f) => f.title); | |
| expect(fixTitle, equals("Create function 'foo'")); | |
| var fixTitle2 = allFixes.last.map((f) => f.title, (f) => f.title); | |
| expect(fixTitle2, equals("Create class 'foo'")); | |
| } | |
| Future<void> test_fixAll_notWhenSingle() async { | |
| const content = ''' | |
| void f(String a) { | |
| [!print(a!)!]; | |
| } | |
| '''; | |
| await expectNoAction( | |
| content, | |
| kind: CodeActionKind('quickfix'), | |
| title: DartFixKind.removeNonNullAssertionMulti.message, | |
| ); | |
| } | |
| /// Ensure the "fix all in file" action doesn't appear against an unfixable | |
| /// item just because the diagnostic is also reported in a location that | |
| /// is fixable. | |
| /// | |
| /// https://github.com/dart-lang/sdk/issues/53021 | |
| Future<void> test_fixAll_unfixable() async { | |
| registerLintRules(); | |
| createFile(analysisOptionsPath, ''' | |
| linter: | |
| rules: | |
| - non_constant_identifier_names | |
| '''); | |
| const content = ''' | |
| /// This is unfixable because it's a top-level. It should not have a "fix all | |
| /// in file" action. | |
| var aaa_a^aa = ''; | |
| void f() { | |
| /// These are here to ensure there's > 1 instance of this diagnostic to | |
| /// allow "fix all in file" to appear. | |
| final bbb_bbb = 0; | |
| final ccc_ccc = 0; | |
| } | |
| '''; | |
| await expectNoAction( | |
| content, | |
| kind: CodeActionKind('quickfix.rename.toCamelCase.multi'), | |
| title: 'Rename to camel case everywhere in file', | |
| ); | |
| } | |
| Future<void> test_fixAll_whenMultiple() async { | |
| const content = ''' | |
| void f(String a) { | |
| [!print(a!!)!]; | |
| print(a!!); | |
| } | |
| '''; | |
| const expectedContent = ''' | |
| void f(String a) { | |
| print(a); | |
| print(a); | |
| } | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.remove.nonNullAssertion.multi'), | |
| title: DartFixKind.removeNonNullAssertionMulti.message, | |
| ); | |
| } | |
| Future<void> test_ignoreDiagnostic_afterOtherFixes() async { | |
| var code = TestCode.parse(''' | |
| void main() { | |
| Uint8List inputBytes = Uin^t8List.fromList(List.filled(100000000, 0)); | |
| } | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var position = code.position.position; | |
| var range = Range(start: position, end: position); | |
| var codeActions = await getCodeActions(testFileUri, range: range); | |
| var codeActionKinds = codeActions.map( | |
| (item) => | |
| item.map((literal) => literal.kind?.toString(), (command) => null), | |
| ); | |
| expect( | |
| codeActionKinds, | |
| containsAllInOrder([ | |
| // Non-ignore fixes (order doesn't matter here, but this is what | |
| // server produces). | |
| 'quickfix.create.class.uppercase', | |
| 'quickfix.create.mixin.uppercase', | |
| 'quickfix.create.localVariable', | |
| 'quickfix.remove.unusedLocalVariable', | |
| // Ignore fixes last, with line sorted above file. | |
| 'quickfix.ignore.line', | |
| 'quickfix.ignore.file', | |
| ]), | |
| ); | |
| } | |
| Future<void> test_ignoreDiagnosticForFile() async { | |
| const content = ''' | |
| // Header comment | |
| // Header comment | |
| // Header comment | |
| // This comment is attached to the below import | |
| import 'dart:async'; | |
| [!import!] 'dart:convert'; | |
| Future foo; | |
| '''; | |
| const expectedContent = ''' | |
| // Header comment | |
| // Header comment | |
| // Header comment | |
| // ignore_for_file: unused_import | |
| // This comment is attached to the below import | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| Future foo; | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.ignore.file'), | |
| title: "Ignore 'unused_import' for the whole file", | |
| ); | |
| } | |
| Future<void> test_ignoreDiagnosticForLine() async { | |
| const content = ''' | |
| import 'dart:async'; | |
| [!import!] 'dart:convert'; | |
| Future foo; | |
| '''; | |
| const expectedContent = ''' | |
| import 'dart:async'; | |
| // ignore: unused_import | |
| import 'dart:convert'; | |
| Future foo; | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.ignore.line'), | |
| title: "Ignore 'unused_import' for this line", | |
| ); | |
| } | |
| Future<void> test_legacySnippetTextEdit() async { | |
| setLegacySnippetTextEditSupport(); | |
| const content = ''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class ^B extends A {} | |
| '''; | |
| const expectedContent = r''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class B extends A { | |
| @override | |
| void m() { | |
| // TODO: implement m$0 | |
| } | |
| } | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.missingOverrides'), | |
| title: 'Create 1 missing override', | |
| ); | |
| } | |
| Future<void> | |
| test_legacySnippetTextEdit_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(cell)} ${4:param0}) {} | |
| } | |
| '''; | |
| setLegacySnippetTextEditSupport(); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.method'), | |
| title: "Create method 'c'", | |
| ); | |
| } | |
| /// Ensure braces aren't over-escaped in snippet choices. | |
| /// https://github.com/dart-lang/sdk/issues/54403 | |
| Future<void> | |
| test_legacySnippetTextEdit_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 | |
| } | |
| } | |
| '''; | |
| setLegacySnippetTextEditSupport(); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.missingOverrides'), | |
| title: 'Create 1 missing override', | |
| ); | |
| } | |
| Future<void> | |
| test_legacySnippetTextEdit_extractVariable_functionTypeNestedParameters() async { | |
| const content = ''' | |
| void f() { | |
| useFunction(te^st); | |
| } | |
| useFunction(int g(a, b)) {} | |
| '''; | |
| const expectedContent = r''' | |
| void f() { | |
| ${1:int Function(a, b)} ${2:test}; | |
| useFunction(test); | |
| } | |
| useFunction(int g(a, b)) {} | |
| '''; | |
| setLegacySnippetTextEditSupport(); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.localVariable'), | |
| title: "Create local variable 'test'", | |
| ); | |
| } | |
| /// The non-standard snippets we supported are only supported for | |
| /// [CodeActionLiteral]s and not for [Command]s (which go via | |
| /// workspace/applyEdit) so even if enabled, they should not be returned. | |
| Future<void> test_legacySnippetTextEdit_unsupportedForCommands() async { | |
| setSupportedCodeActionKinds(null); // no codeActionLiteralSupport | |
| setLegacySnippetTextEditSupport(); // will not do anything | |
| const content = ''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class ^B extends A {} | |
| '''; | |
| // No $0 placeholder in this content (unlike in `test_legacySnippetTextEdit`). | |
| const expectedContent = r''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class B extends A { | |
| @override | |
| void m() { | |
| // TODO: implement m | |
| } | |
| } | |
| '''; | |
| await verifyCommandCodeActionEdits( | |
| content, | |
| expectedContent, | |
| command: Commands.applyCodeAction, | |
| title: 'Create 1 missing override', | |
| ); | |
| } | |
| Future<void> test_logsExecution() async { | |
| var code = TestCode.parse(''' | |
| [!import!] 'dart:convert'; | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| range: code.range.range, | |
| ); | |
| var fixAction = findCodeActionLiteral( | |
| codeActions, | |
| title: 'Remove unused import', | |
| kind: CodeActionKind('quickfix.remove.unusedImport'), | |
| )!; | |
| await executeCommand(fixAction.command!); | |
| expectCommandLogged('dart.fix.remove.unusedImport'); | |
| } | |
| /// Repro for https://github.com/Dart-Code/Dart-Code/issues/4462. | |
| /// | |
| /// Original code only included a fix on its first error (which in this sample | |
| /// is the opening brace) and not the whole range of the error. | |
| Future<void> test_multilineError() async { | |
| registerLintRules(); | |
| createFile(analysisOptionsPath, ''' | |
| linter: | |
| rules: | |
| - prefer_expression_function_bodies | |
| '''); | |
| var code = TestCode.parse(''' | |
| int foo() { | |
| [!return!] 1; | |
| } | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| range: code.range.range, | |
| ); | |
| var fixAction = findCodeActionLiteral( | |
| codeActions, | |
| title: 'Convert to expression body', | |
| kind: CodeActionKind('quickfix.convert.toExpressionBody'), | |
| ); | |
| expect(fixAction, isNotNull); | |
| } | |
| Future<void> test_noDuplicates_differentFix() async { | |
| // For convenience, quick-fixes are usually returned for the entire line, | |
| // though this can lead to duplicate entries (by title) when multiple | |
| // diagnostics have their own fixes of the same type. | |
| // | |
| // Expect only the only one nearest to the start of the range to be returned. | |
| var code = TestCode.parse(''' | |
| void f() { | |
| var a = []; | |
| print(a!!);^ | |
| } | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| position: code.position.position, | |
| ); | |
| var removeNnaAction = findCodeActionLiteral( | |
| codeActions, | |
| title: DartFixKind.removeNonNullAssertion.message, | |
| kind: CodeActionKind('quickfix.remove.nonNullAssertion'), | |
| )!; | |
| // Ensure the action is for the diagnostic on the second bang which was | |
| // closest to the range requested. | |
| var diagnostics = removeNnaAction.diagnostics; | |
| var secondBangPos = positionFromOffset(code.code.indexOf('!);'), code.code); | |
| expect(diagnostics, hasLength(1)); | |
| var diagStart = diagnostics!.first.range.start; | |
| expect(diagStart, equals(secondBangPos)); | |
| } | |
| Future<void> test_noDuplicates_sameFix() async { | |
| var code = TestCode.parse(''' | |
| var a = [Test, Test, Te[!!]st]; | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| range: code.range.range, | |
| ); | |
| var createClassAction = findCodeActionLiteral( | |
| codeActions, | |
| title: "Create class 'Test'", | |
| kind: CodeActionKind('quickfix.create.class.uppercase'), | |
| )!; | |
| expect(createClassAction.diagnostics, hasLength(3)); | |
| } | |
| Future<void> test_noDuplicates_withDocumentChangesSupport() async { | |
| var code = TestCode.parse(''' | |
| var a = [Test, Test, Te[!!]st]; | |
| '''); | |
| createFile(testFilePath, code.code); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| testFileUri, | |
| range: code.range.range, | |
| ); | |
| var createClassActions = findCodeActionLiteral( | |
| codeActions, | |
| title: "Create class 'Test'", | |
| kind: CodeActionKind('quickfix.create.class.uppercase'), | |
| )!; | |
| expect(createClassActions.diagnostics, hasLength(3)); | |
| } | |
| Future<void> test_organizeImportsFix_namedOrganizeImports() async { | |
| registerLintRules(); | |
| createFile(analysisOptionsPath, ''' | |
| linter: | |
| rules: | |
| - directives_ordering | |
| '''); | |
| // This code should get a fix to sort the imports. | |
| const content = ''' | |
| import 'dart:io'; | |
| [!import 'dart:async'!]; | |
| Completer a; | |
| ProcessInfo b; | |
| '''; | |
| const expectedContent = ''' | |
| import 'dart:async'; | |
| import 'dart:io'; | |
| Completer a; | |
| ProcessInfo b; | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.organize.imports'), | |
| title: DartFixKind.organizeImports.message, | |
| ); | |
| } | |
| Future<void> test_outsideRoot() async { | |
| var otherFilePath = pathContext.normalize( | |
| pathContext.join(projectFolderPath, '..', 'otherProject', 'foo.dart'), | |
| ); | |
| var otherFileUri = pathContext.toUri(otherFilePath); | |
| createFile(otherFilePath, 'bad code to create error'); | |
| await initializeServer(); | |
| var codeActions = await getCodeActions( | |
| otherFileUri, | |
| position: startOfDocPos, | |
| ); | |
| expect(codeActions, isEmpty); | |
| } | |
| Future<void> test_pubspec() async { | |
| const content = '^'; | |
| var expectedContent = | |
| ''' | |
| name: $testPackageName | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| filePath: pubspecFilePath, | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.add.name'), | |
| title: "Add 'name' key", | |
| ); | |
| } | |
| Future<void> test_pubspec_withError() async { | |
| registerLintRules(); | |
| createFile(analysisOptionsPath, ''' | |
| linter: | |
| rules: | |
| - package_names | |
| '''); | |
| const content = r''' | |
| name: class^ | |
| '''; | |
| await expectNoAction(filePath: pubspecFilePath, content); | |
| } | |
| Future<void> test_snippetTextEdit() async { | |
| setSnippetTextEditSupport(); | |
| const content = ''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class ^B extends A {} | |
| '''; | |
| const expectedContent = r''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class B extends A { | |
| @override | |
| void m() { | |
| // TODO: implement m$0 | |
| } | |
| } | |
| '''; | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.missingOverrides'), | |
| title: 'Create 1 missing override', | |
| ); | |
| } | |
| Future<void> | |
| test_snippetTextEdit_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(cell)} ${4:param0}) {} | |
| } | |
| '''; | |
| setSnippetTextEditSupport(); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.method'), | |
| title: "Create method 'c'", | |
| ); | |
| } | |
| /// Ensure braces aren't over-escaped in snippet choices. | |
| /// https://github.com/dart-lang/sdk/issues/54403 | |
| Future<void> | |
| test_snippetTextEdit_createMissingOverrides_recordBraces() async { | |
| const content = ''' | |
| abstract class A { | |
| void m(Iterable<({int a, int b})> r); | |
| } | |
| class ^B extends A {} | |
| '''; | |
| const expectedContent = r''' | |
| abstract class A { | |
| void m(Iterable<({int a, int b})> r); | |
| } | |
| class B extends A { | |
| @override | |
| void m(${1|Iterable<({int a\, int b})>,Object|} ${2:r}) { | |
| // TODO: implement m$0 | |
| } | |
| } | |
| '''; | |
| setSnippetTextEditSupport(); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.missingOverrides'), | |
| title: 'Create 1 missing override', | |
| ); | |
| } | |
| Future<void> | |
| test_snippetTextEdit_extractVariable_functionTypeNestedParameters() async { | |
| const content = ''' | |
| void f() { | |
| useFunction(te^st); | |
| } | |
| useFunction(int g(a, b)) {} | |
| '''; | |
| const expectedContent = r''' | |
| void f() { | |
| ${1:int Function(a, b)} ${2:test}; | |
| useFunction(test); | |
| } | |
| useFunction(int g(a, b)) {} | |
| '''; | |
| setSnippetTextEditSupport(); | |
| await verifyCodeActionLiteralEdits( | |
| content, | |
| expectedContent, | |
| kind: CodeActionKind('quickfix.create.localVariable'), | |
| title: "Create local variable 'test'", | |
| ); | |
| } | |
| /// Currently, SnippetTextEdits are only supported for | |
| /// [CodeActionLiteral]s and not for [Command]s (which go via | |
| /// workspace/applyEdit) so even if enabled, they should not be returned. | |
| Future<void> test_snippetTextEdit_unsupportedForCommands() async { | |
| setSupportedCodeActionKinds(null); // no codeActionLiteralSupport | |
| setSnippetTextEditSupport(); // will not do anything | |
| const content = ''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class ^B extends A {} | |
| '''; | |
| // No $0 placeholder in this content (unlike in `test_snippetTextEdit`). | |
| const expectedContent = r''' | |
| abstract class A { | |
| void m(); | |
| } | |
| class B extends A { | |
| @override | |
| void m() { | |
| // TODO: implement m | |
| } | |
| } | |
| '''; | |
| await verifyCommandCodeActionEdits( | |
| content, | |
| expectedContent, | |
| command: Commands.applyCodeAction, | |
| title: 'Create 1 missing override', | |
| ); | |
| } | |
| void _enableLints(List<String> lintNames) { | |
| registerLintRules(); | |
| var lintsYaml = lintNames.map((name) => ' - $name\n').join(); | |
| createFile(analysisOptionsPath, ''' | |
| linter: | |
| rules: | |
| $lintsYaml | |
| '''); | |
| } | |
| } | |
| /// A version of `camel_case_types` that is deprecated. | |
| class _DeprecatedCamelCaseTypes extends AnalysisRule { | |
| 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.', | |
| uniqueName: 'LintCode.camel_case_types', | |
| ); | |
| new() | |
| : super( | |
| name: 'camel_case_types', | |
| state: RuleState.deprecated(), | |
| description: '', | |
| ); | |
| @override | |
| DiagnosticCode get diagnosticCode => code; | |
| } |