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();