Add quick fix for `prefer_function_declarations_over_variables` lint
Fixes #48678
Change-Id: I80077e1faa8e9263a690322ef092e1cdbf71c65d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/239429
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/convert_to_function_declaration.dart b/pkg/analysis_server/lib/src/services/correction/dart/convert_to_function_declaration.dart
new file mode 100644
index 0000000..63cea99
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/dart/convert_to_function_declaration.dart
@@ -0,0 +1,113 @@
+// Copyright (c) 2022, 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/src/services/correction/dart/abstract_producer.dart';
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/source/source_range.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:analyzer_plugin/utilities/range_factory.dart';
+
+class ConvertToFunctionDeclaration extends CorrectionProducer {
+ @override
+ bool get canBeAppliedInBulk => true;
+
+ @override
+ bool get canBeAppliedToFile => true;
+
+ @override
+ FixKind get fixKind => DartFixKind.CONVERT_TO_FUNCTION_DECLARATION;
+
+ @override
+ FixKind get multiFixKind => DartFixKind.CONVERT_TO_FUNCTION_DECLARATION_MULTI;
+
+ @override
+ Future<void> compute(ChangeBuilder builder) async {
+ var node = this.node;
+ if (node is! VariableDeclaration) return;
+ var equals = node.equals;
+ if (equals == null) return;
+ var initializer = node.initializer;
+
+ var parent = node.parent;
+ if (parent is! VariableDeclarationList) return;
+ var keyword = parent.keyword;
+ var type = parent.type;
+
+ var variables = parent.variables;
+
+ var grandParent = parent.parent;
+ if (grandParent is! VariableDeclarationStatement) return;
+
+ var previous = _previous(variables, node);
+ var next = _next(variables, node);
+
+ await builder.addDartFileEdit(file, (builder) {
+ void replaceWithNewLine(SourceRange range,
+ {String? before, String? after}) {
+ builder.addReplacement(range, (builder) {
+ if (before != null) {
+ builder.write(before);
+ }
+ builder.write(utils.endOfLine);
+ builder.write(utils.getLinePrefix(range.offset));
+ if (after != null) {
+ builder.write(after);
+ }
+ });
+ }
+
+ if (previous == null) {
+ if (keyword != null) {
+ builder.addDeletion(range.startStart(keyword, keyword.next!));
+ }
+ if (type != null) {
+ builder.addDeletion(range.startStart(type, type.endToken.next!));
+ }
+ } else if (previous.initializer is! FunctionExpression) {
+ var r =
+ range.endStart(previous.endToken, previous.endToken.next!.next!);
+ replaceWithNewLine(r, before: ';');
+ }
+
+ builder.addDeletion(range.endStart(equals.previous!, equals.next!));
+
+ if (next != null) {
+ var r = range.endStart(node.endToken, node.endToken.next!.next!);
+ if (next.initializer is FunctionExpression) {
+ replaceWithNewLine(r);
+ } else {
+ var replacement = '';
+ if (keyword != null) {
+ replacement += '$keyword ';
+ }
+ if (type != null) {
+ replacement += utils.getNodeText(type) + ' ';
+ }
+ replaceWithNewLine(r, after: replacement);
+ }
+ } else if (initializer is FunctionExpression &&
+ initializer.body is BlockFunctionBody) {
+ builder.addDeletion(range.token(grandParent.semicolon));
+ }
+ });
+ }
+
+ VariableDeclaration? _next(
+ NodeList<VariableDeclaration> variables, VariableDeclaration variable) {
+ var i = variables.indexOf(variable);
+ return i < variables.length - 1 ? variables[i + 1] : null;
+ }
+
+ VariableDeclaration? _previous(
+ NodeList<VariableDeclaration> variables, VariableDeclaration variable) {
+ var i = variables.indexOf(variable);
+ return i > 0 ? variables[i - 1] : null;
+ }
+
+ /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
+ static ConvertToFunctionDeclaration newInstance() =>
+ ConvertToFunctionDeclaration();
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
index d2f7ccd..a83ddc1 100644
--- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
+++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
@@ -1741,7 +1741,7 @@
LintCode.prefer_foreach:
status: needsEvaluation
LintCode.prefer_function_declarations_over_variables:
- status: needsEvaluation
+ status: hasFix
LintCode.prefer_generic_function_type_aliases:
status: hasFix
LintCode.prefer_if_elements_to_conditional_expressions:
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index 4f359ad..0a1eee9 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -408,6 +408,16 @@
DartFixKindPriority.IN_FILE,
"Convert to 'Function' syntax everywhere in file",
);
+ static const CONVERT_TO_FUNCTION_DECLARATION = FixKind(
+ 'dart.fix.convert.toFunctionDeclaration',
+ DartFixKindPriority.DEFAULT,
+ 'Convert to function declaration',
+ );
+ static const CONVERT_TO_FUNCTION_DECLARATION_MULTI = FixKind(
+ 'dart.fix.convert.toFunctionDeclaration.multi',
+ DartFixKindPriority.IN_FILE,
+ 'Convert to function declaration everywhere in file',
+ );
static const CONVERT_TO_IF_ELEMENT = FixKind(
'dart.fix.convert.toIfElement',
DartFixKindPriority.DEFAULT,
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index f70b314..33d338e 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -52,6 +52,7 @@
import 'package:analysis_server/src/services/correction/dart/convert_to_cascade.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_contains.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_expression_function_body.dart';
+import 'package:analysis_server/src/services/correction/dart/convert_to_function_declaration.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_generic_function_syntax.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_if_null.dart';
import 'package:analysis_server/src/services/correction/dart/convert_to_initializing_formal.dart';
@@ -531,6 +532,9 @@
LintNames.prefer_for_elements_to_map_fromIterable: [
ConvertMapFromIterableToForLiteral.newInstance,
],
+ LintNames.prefer_function_declarations_over_variables: [
+ ConvertToFunctionDeclaration.newInstance,
+ ],
LintNames.prefer_generic_function_type_aliases: [
ConvertToGenericFunctionSyntax.newInstance,
],
diff --git a/pkg/analysis_server/lib/src/services/linter/lint_names.dart b/pkg/analysis_server/lib/src/services/linter/lint_names.dart
index fe9d319..996a7c2 100644
--- a/pkg/analysis_server/lib/src/services/linter/lint_names.dart
+++ b/pkg/analysis_server/lib/src/services/linter/lint_names.dart
@@ -91,6 +91,8 @@
static const String prefer_final_in_for_each = 'prefer_final_in_for_each';
static const String prefer_final_locals = 'prefer_final_locals';
static const String prefer_final_parameters = 'prefer_final_parameters';
+ static const String prefer_function_declarations_over_variables =
+ 'prefer_function_declarations_over_variables';
static const String prefer_for_elements_to_map_fromIterable =
'prefer_for_elements_to_map_fromIterable';
static const String prefer_generic_function_type_aliases =
diff --git a/pkg/analysis_server/test/src/services/correction/fix/convert_to_function_declaration_test.dart b/pkg/analysis_server/test/src/services/correction/fix/convert_to_function_declaration_test.dart
new file mode 100644
index 0000000..98b3421
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/convert_to_function_declaration_test.dart
@@ -0,0 +1,185 @@
+// Copyright (c) 2022, 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/src/services/correction/fix.dart';
+import 'package:analysis_server/src/services/linter/lint_names.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:test/expect.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(ConvertToFunctionDeclarationBulkTest);
+ defineReflectiveTests(ConvertToFunctionDeclarationInFileTest);
+ defineReflectiveTests(ConvertToFunctionDeclarationTest);
+ });
+}
+
+@reflectiveTest
+class ConvertToFunctionDeclarationBulkTest extends BulkFixProcessorTest {
+ @override
+ String get lintCode => LintNames.prefer_function_declarations_over_variables;
+
+ Future<void> test_bulk() async {
+ await resolveTestCode('''
+void f() {
+ var v1 = () {};
+ var v2 = () {};
+ v1();
+ v2();
+}
+''');
+ await assertHasFix('''
+void f() {
+ v1() {}
+ v2() {}
+ v1();
+ v2();
+}
+''');
+ }
+
+ Future<void> test_declaration_list() async {
+ await resolveTestCode('''
+void f() {
+ var v1 = () {}, v2 = () {};
+ v1();
+ v2();
+}
+''');
+ await assertHasFix('''
+void f() {
+ v1() {}
+ v2() {}
+ v1();
+ v2();
+}
+''');
+ }
+}
+
+@reflectiveTest
+class ConvertToFunctionDeclarationInFileTest extends FixInFileProcessorTest {
+ Future<void> test_file() async {
+ createAnalysisOptionsFile(
+ lints: [LintNames.prefer_function_declarations_over_variables]);
+ await resolveTestCode('''
+void f() {
+ var v = () {
+ var v = () {};
+ v();
+ };
+ v();
+}
+''');
+ var fixes = await getFixesForFirstError();
+ expect(fixes, hasLength(1));
+ assertProduces(fixes.first, '''
+void f() {
+ v() {
+ v() {}
+ v();
+ }
+ v();
+}
+''');
+ }
+}
+
+@reflectiveTest
+class ConvertToFunctionDeclarationTest extends FixProcessorLintTest {
+ @override
+ FixKind get kind => DartFixKind.CONVERT_TO_FUNCTION_DECLARATION;
+
+ @override
+ String get lintCode => LintNames.prefer_function_declarations_over_variables;
+
+ Future<void> test_block_function_body() async {
+ await resolveTestCode('''
+void f() {
+ var v = () {};
+ v();
+}
+''');
+ await assertHasFix('''
+void f() {
+ v() {}
+ v();
+}
+''');
+ }
+
+ Future<void> test_declaration_different() async {
+ await resolveTestCode('''
+void f() {
+ final v1 = 1, v2 = (x, y) {}, v3 = '';
+ v2(v1, v3);
+}
+''');
+ await assertHasFix('''
+void f() {
+ final v1 = 1;
+ v2(x, y) {}
+ final v3 = '';
+ v2(v1, v3);
+}
+''');
+ }
+
+ Future<void> test_expression_function_body() async {
+ await resolveTestCode('''
+void f() {
+ var v = () => 3;
+ v();
+}
+''');
+ await assertHasFix('''
+void f() {
+ v() => 3;
+ v();
+}
+''');
+ }
+
+ Future<void> test_no_initializer() async {
+ await resolveTestCode('''
+typedef F = void Function();
+
+void f() {
+ final F g = () {}, h;
+ g();
+ h = () {};
+ h();
+}
+''');
+ await assertHasFix('''
+typedef F = void Function();
+
+void f() {
+ g() {}
+ final F h;
+ g();
+ h = () {};
+ h();
+}
+''');
+ }
+
+ Future<void> test_type() async {
+ await resolveTestCode('''
+void f() {
+ final String Function() v = () => throw '';
+ v();
+}
+''');
+ await assertHasFix('''
+void f() {
+ v() => throw '';
+ v();
+}
+''');
+ }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index 4f1543a..d2b42a9 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -63,6 +63,8 @@
import 'convert_to_double_quoted_string_test.dart'
as convert_to_double_quoted_string;
import 'convert_to_for_element_test.dart' as convert_to_for_element;
+import 'convert_to_function_declaration_test.dart'
+ as convert_to_function_declaration;
import 'convert_to_generic_function_syntax_test.dart'
as convert_to_generic_function_syntax;
import 'convert_to_if_element_test.dart' as convert_to_if_element;
@@ -279,6 +281,7 @@
convert_to_contains.main();
convert_to_double_quoted_string.main();
convert_to_for_element.main();
+ convert_to_function_declaration.main();
convert_to_generic_function_syntax.main();
convert_to_if_element.main();
convert_to_if_null.main();