[analyzer] Support "Inline Method" refactor in LSP

Fixes https://github.com/Dart-Code/Dart-Code/issues/547.

Change-Id: I02f905673820ef22b5b10055f32710d660beaa2c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/197380
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
index 5a8d94b..317b6a8 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
@@ -164,6 +164,11 @@
             InlineLocalRefactoring(server.searchEngine, result, offset);
         return success(refactor);
 
+      case RefactoringKind.INLINE_METHOD:
+        final refactor =
+            InlineMethodRefactoring(server.searchEngine, result, offset);
+        return success(refactor);
+
       default:
         return error(ServerErrorCodes.InvalidCommandArguments,
             'Unknown RefactoringKind $kind was supplied to $commandName');
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
index 2d786af..322e9d8d 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
@@ -409,6 +409,7 @@
         }
       }
 
+      // Inlines
       if (shouldIncludeKind(CodeActionKind.RefactorInline)) {
         // Inline Local Variable
         if (InlineLocalRefactoring(server.searchEngine, unit, offset)
@@ -416,6 +417,13 @@
           refactorActions.add(createRefactor(CodeActionKind.RefactorInline,
               'Inline Local Variable', RefactoringKind.INLINE_LOCAL_VARIABLE));
         }
+
+        // Inline Method
+        if (InlineMethodRefactoring(server.searchEngine, unit, offset)
+            .isAvailable()) {
+          refactorActions.add(createRefactor(CodeActionKind.RefactorInline,
+              'Inline Method', RefactoringKind.INLINE_METHOD));
+        }
       }
 
       return refactorActions;
diff --git a/pkg/analysis_server/lib/src/services/refactoring/inline_method.dart b/pkg/analysis_server/lib/src/services/refactoring/inline_method.dart
index 47362ca..1cff243 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/inline_method.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/inline_method.dart
@@ -290,6 +290,39 @@
     return Future.value(change);
   }
 
+  @override
+  bool isAvailable() {
+    return !_checkOffset().hasFatalError;
+  }
+
+  /// Checks if [offset] is a method that can be inlined.
+  RefactoringStatus _checkOffset() {
+    var fatalStatus = RefactoringStatus.fatal(
+        'Method declaration or reference must be selected to activate this refactoring.');
+
+    var identifier = NodeLocator(offset).searchWithin(resolveResult.unit);
+    if (identifier is! SimpleIdentifier) {
+      return fatalStatus;
+    }
+    var element = identifier.writeOrReadElement;
+    if (element is! ExecutableElement) {
+      return fatalStatus;
+    }
+    if (element.isSynthetic) {
+      return fatalStatus;
+    }
+    // maybe operator
+    if (element.isOperator) {
+      return RefactoringStatus.fatal('Cannot inline operator.');
+    }
+    // maybe [a]sync*
+    if (element.isGenerator) {
+      return RefactoringStatus.fatal('Cannot inline a generator.');
+    }
+
+    return RefactoringStatus();
+  }
+
   _SourcePart _createSourcePart(SourceRange range) {
     var source = _methodUtils.getRangeText(range);
     var prefix = getLinePrefix(source);
diff --git a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
index 23b8552..cdb3485 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
@@ -242,6 +242,10 @@
 
   /// The name of the method (or function) being inlined.
   String? get methodName;
+
+  /// Return `true` if refactoring is available, possibly without checking all
+  /// initial conditions.
+  bool isAvailable();
 }
 
 /// [Refactoring] to move/rename a file.
diff --git a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
index 8d87a88..791c627d 100644
--- a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
@@ -19,6 +19,7 @@
     defineReflectiveTests(ExtractWidgetRefactorCodeActionsTest);
     defineReflectiveTests(ExtractVariableRefactorCodeActionsTest);
     defineReflectiveTests(InlineLocalVariableRefactorCodeActionsTest);
+    defineReflectiveTests(InlineMethodRefactorCodeActionsTest);
   });
 }
 
@@ -515,7 +516,7 @@
 @reflectiveTest
 class InlineLocalVariableRefactorCodeActionsTest
     extends AbstractCodeActionsTest {
-  final extractVariableTitle = 'Inline Local Variable';
+  final inlineVariableTitle = 'Inline Local Variable';
 
   Future<void> test_appliesCorrectEdits() async {
     const content = '''
@@ -539,7 +540,86 @@
     final codeActions = await getCodeActions(mainFileUri.toString(),
         position: positionFromMarker(content));
     final codeAction = findCommand(
-        codeActions, Commands.performRefactor, extractVariableTitle)!;
+        codeActions, Commands.performRefactor, inlineVariableTitle)!;
+
+    await verifyCodeActionEdits(
+        codeAction, withoutMarkers(content), expectedContent);
+  }
+}
+
+@reflectiveTest
+class InlineMethodRefactorCodeActionsTest extends AbstractCodeActionsTest {
+  final inlineMethodTitle = 'Inline Method';
+
+  Future<void> test_inlineAtCallSite() async {
+    const content = '''
+void foo1() {
+  ba^r();
+}
+
+void foo2() {
+  bar();
+}
+
+void bar() {
+  print('test');
+}
+    ''';
+    const expectedContent = '''
+void foo1() {
+  print('test');
+}
+
+void foo2() {
+  bar();
+}
+
+void bar() {
+  print('test');
+}
+    ''';
+    newFile(mainFilePath, content: withoutMarkers(content));
+    await initialize();
+
+    final codeActions = await getCodeActions(mainFileUri.toString(),
+        position: positionFromMarker(content));
+    final codeAction =
+        findCommand(codeActions, Commands.performRefactor, inlineMethodTitle)!;
+
+    await verifyCodeActionEdits(
+        codeAction, withoutMarkers(content), expectedContent);
+  }
+
+  Future<void> test_inlineAtMethod() async {
+    const content = '''
+void foo1() {
+  bar();
+}
+
+void foo2() {
+  bar();
+}
+
+void ba^r() {
+  print('test');
+}
+    ''';
+    const expectedContent = '''
+void foo1() {
+  print('test');
+}
+
+void foo2() {
+  print('test');
+}
+    ''';
+    newFile(mainFilePath, content: withoutMarkers(content));
+    await initialize();
+
+    final codeActions = await getCodeActions(mainFileUri.toString(),
+        position: positionFromMarker(content));
+    final codeAction =
+        findCommand(codeActions, Commands.performRefactor, inlineMethodTitle)!;
 
     await verifyCodeActionEdits(
         codeAction, withoutMarkers(content), expectedContent);