[analysis_server] Dot shorthands: Code completion for methods and constructors.

Update code completion to handle completing static methods and (non-const) constructors.

Additionally, I noticed that we weren't suggesting any completion for extension types? I added the case for it and a few tests.

Bug: https://github.com/dart-lang/sdk/issues/59836
Change-Id: Ib677cd99e51b8a641aa1bfbad5ffc8a8cbdc38c0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/441681
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Kallen Tu <kallentu@google.com>
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart b/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart
index df066ef..1d43627 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart
@@ -88,6 +88,9 @@
   /// possible.
   final bool preferNonInvocation;
 
+  /// Whether suggestions are for completing a dot shorthand.
+  final bool suggestingDotShorthand;
+
   /// Whether unnamed constructors should be suggested as `.new`.
   final bool suggestUnnamedAsNew;
 
@@ -135,6 +138,7 @@
     required this.excludeTypeNames,
     required this.objectPatternAllowed,
     required this.preferNonInvocation,
+    required this.suggestingDotShorthand,
     required this.suggestUnnamedAsNew,
     required this.skipImports,
     required this.excludedNodes,
@@ -1504,7 +1508,8 @@
     }
     if (!mustBeAssignable) {
       var allowNonFactory =
-          containingElement is ClassElement && !containingElement.isAbstract;
+          containingElement is ClassElement && !containingElement.isAbstract ||
+          containingElement is ExtensionTypeElement;
       for (var constructor in constructors) {
         if (constructor.isVisibleIn(request.libraryElement) &&
             (allowNonFactory || constructor.isFactory)) {
@@ -1826,8 +1831,15 @@
       );
     }
 
+    // Use the constructor element's name without the interface type to
+    // calculate the matcher score for dot shorthands.
+    var elementName = element.name;
+    var matcherName =
+        suggestingDotShorthand && elementName != null
+            ? elementName
+            : element.displayName;
     // TODO(keertip): Compute the completion string.
-    var matcherScore = state.matcher.score(element.displayName);
+    var matcherScore = state.matcher.score(matcherName);
     if (matcherScore != -1) {
       var isTearOff =
           preferNonInvocation || (mustBeConstant && !element.isConst);
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart b/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart
index 132a3e0..7dfa427 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart
@@ -172,6 +172,7 @@
     bool excludeTypeNames = false,
     bool objectPatternAllowed = false,
     bool preferNonInvocation = false,
+    bool suggestingDotShorthand = false,
     bool suggestUnnamedAsNew = false,
     Set<AstNode> excludedNodes = const {},
   }) {
@@ -199,7 +200,8 @@
               helper.mustBeStatic == mustBeStatic &&
               helper.mustBeType == mustBeType &&
               helper.preferNonInvocation == preferNonInvocation &&
-              helper.objectPatternAllowed == objectPatternAllowed);
+              helper.objectPatternAllowed == objectPatternAllowed &&
+              helper.suggestingDotShorthand == suggestingDotShorthand);
     }());
     return _declarationHelper ??= DeclarationHelper(
       request: state.request,
@@ -217,6 +219,7 @@
       excludeTypeNames: excludeTypeNames,
       objectPatternAllowed: objectPatternAllowed,
       preferNonInvocation: preferNonInvocation,
+      suggestingDotShorthand: suggestingDotShorthand,
       suggestUnnamedAsNew: suggestUnnamedAsNew,
       skipImports: skipImports,
       excludedNodes: excludedNodes,
@@ -924,6 +927,23 @@
   }
 
   @override
+  void visitDotShorthandInvocation(DotShorthandInvocation node) {
+    var period = node.period;
+    if (offset >= period.end && offset <= node.memberName.end) {
+      var contextType = _computeContextType(node);
+      if (contextType == null) return;
+
+      var element = contextType.element;
+      if (element == null) return;
+
+      declarationHelper(
+        suggestingDotShorthand: true,
+        suggestUnnamedAsNew: true,
+      ).addStaticMembersOfElement(element);
+    }
+  }
+
+  @override
   void visitDotShorthandPropertyAccess(DotShorthandPropertyAccess node) {
     var contextType = _computeContextType(node);
     if (contextType == null) return;
@@ -931,16 +951,16 @@
     var element = contextType.element;
     if (element == null) return;
 
-    var parent = node.parent;
-    var mustBeAssignable =
-        parent is AssignmentExpression && node == parent.leftHandSide;
-    var helper = declarationHelper(
-      mustBeAssignable: mustBeAssignable,
+    // Add all getters, methods, and constructors.
+    // When the user needs completing with a `.` or a `.prefix`, the suggestions
+    // can be any of the three.
+    declarationHelper(
+      suggestingDotShorthand: true,
       preferNonInvocation:
           element is InterfaceElement &&
           state.request.shouldSuggestTearOff(element),
-    );
-    helper.addStaticMembersOfElement(element);
+      suggestUnnamedAsNew: true,
+    ).addStaticMembersOfElement(element);
   }
 
   @override
diff --git a/pkg/analysis_server/test/services/completion/dart/location/constructor_invocation_test.dart b/pkg/analysis_server/test/services/completion/dart/location/constructor_invocation_test.dart
index f0af210..2da7a68 100644
--- a/pkg/analysis_server/test/services/completion/dart/location/constructor_invocation_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/location/constructor_invocation_test.dart
@@ -17,6 +17,42 @@
     with ConstructorInvocationTestCases {}
 
 mixin ConstructorInvocationTestCases on AbstractCompletionDriverTest {
+  Future<void> test_extensionType() async {
+    allowedIdentifiers = {'named'};
+    await computeSuggestions('''
+extension type C(int x) {
+  C.named(this.x);
+}
+void f() {
+  C c = C.^
+}
+''');
+    assertResponse(r'''
+suggestions
+  named
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_extensionType_withPrefix() async {
+    allowedIdentifiers = {'named'};
+    await computeSuggestions('''
+extension type C(int x) {
+  C.named(this.x);
+}
+void f() {
+  C c = C.n^
+}
+''');
+    assertResponse(r'''
+replacement
+  left: 1
+suggestions
+  named
+    kind: constructorInvocation
+''');
+  }
+
   Future<void> test_it() async {
     await computeSuggestions('''
 class C {
diff --git a/pkg/analysis_server/test/services/completion/dart/location/dot_shorthand_invocation_test.dart b/pkg/analysis_server/test/services/completion/dart/location/dot_shorthand_invocation_test.dart
new file mode 100644
index 0000000..ff0b902
--- /dev/null
+++ b/pkg/analysis_server/test/services/completion/dart/location/dot_shorthand_invocation_test.dart
@@ -0,0 +1,297 @@
+// 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 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../../../../client/completion_driver_test.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(DotShorthandInvocationTest);
+  });
+}
+
+@reflectiveTest
+class DotShorthandInvocationTest extends AbstractCompletionDriverTest
+    with DotShorthandInvocationTestCases {}
+
+mixin DotShorthandInvocationTestCases on AbstractCompletionDriverTest {
+  Future<void> test_constructor_class_named() async {
+    allowedIdentifiers = {'named'};
+    await computeSuggestions('''
+class C {
+  C.named();
+}
+void f() {
+  C c = .^
+}
+''');
+    assertResponse(r'''
+suggestions
+  named
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_constructor_class_unnamed() async {
+    allowedIdentifiers = {'new'};
+    await computeSuggestions('''
+class C {}
+void f() {
+  C c = .^
+}
+''');
+    assertResponse(r'''
+suggestions
+  new
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_constructor_class_withParentheses() async {
+    allowedIdentifiers = {'named'};
+    await computeSuggestions('''
+class C {
+  C.named();
+}
+void f() {
+  C c = .^()
+}
+''');
+    assertResponse(r'''
+suggestions
+  named
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_constructor_class_withPrefix() async {
+    allowedIdentifiers = {'named', 'new'};
+    await computeSuggestions('''
+class C {
+  C.named();
+}
+void f() {
+  C c = .n^()
+}
+''');
+    assertResponse(r'''
+replacement
+  left: 1
+suggestions
+  named
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_constructor_extensionType_named() async {
+    allowedIdentifiers = {'named'};
+    await computeSuggestions('''
+extension type C(int x) {
+  C.named(this.x);
+}
+void f() {
+  C c = .^
+}
+''');
+    assertResponse(r'''
+suggestions
+  named
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_constructor_extensionType_unnamed() async {
+    allowedIdentifiers = {'new'};
+    await computeSuggestions('''
+extension type C(int x) {}
+void f() {
+  C c = .^
+}
+''');
+    assertResponse(r'''
+suggestions
+  new
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_constructor_extensionType_withPrefix_named() async {
+    allowedIdentifiers = {'named'};
+    await computeSuggestions('''
+extension type C(int x) {
+  C.named(this.x);
+}
+void f() {
+  C c = .n^
+}
+''');
+    assertResponse(r'''
+replacement
+  left: 1
+suggestions
+  named
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_constructor_extensionType_withPrefix_unnamed() async {
+    allowedIdentifiers = {'new'};
+    await computeSuggestions('''
+extension type C(int x) {}
+void f() {
+  C c = .n^
+}
+''');
+    assertResponse(r'''
+replacement
+  left: 1
+suggestions
+  new
+    kind: constructorInvocation
+''');
+  }
+
+  Future<void> test_method_class() async {
+    allowedIdentifiers = {'method', 'notStatic'};
+    await computeSuggestions('''
+class C {
+  static C method() => C();
+  C notStatic() => C();
+}
+void f() {
+  C c = .^
+}
+''');
+    assertResponse(r'''
+suggestions
+  method
+    kind: methodInvocation
+''');
+  }
+
+  Future<void> test_method_class_chain() async {
+    allowedIdentifiers = {'method', 'anotherMethod', 'notStatic'};
+    await computeSuggestions('''
+class C {
+  static C method() => C();
+  static C anotherMethod() => C();
+  C notStatic() => C();
+}
+void f() {
+  C c = .anotherMethod().^
+}
+''');
+    assertResponse(r'''
+suggestions
+  notStatic
+    kind: methodInvocation
+''');
+  }
+
+  Future<void> test_method_class_chain_withPrefix() async {
+    allowedIdentifiers = {
+      'method',
+      'anotherMethod',
+      'notStatic',
+      'alsoInstance',
+    };
+    await computeSuggestions('''
+class C {
+  static C method() => C();
+  static C anotherMethod() => C();
+  C notStatic() => C();
+  C alsoInstance() => C();
+}
+void f() {
+  C c = .anotherMethod().no^
+}
+''');
+    assertResponse(r'''
+replacement
+  left: 2
+suggestions
+  notStatic
+    kind: methodInvocation
+''');
+  }
+
+  Future<void> test_method_class_withParentheses() async {
+    allowedIdentifiers = {'method', 'notStatic'};
+    await computeSuggestions('''
+class C {
+  static C method() => C();
+  C notStatic() => C();
+}
+void f() {
+  C c = .^()
+}
+''');
+    assertResponse(r'''
+suggestions
+  method
+    kind: methodInvocation
+''');
+  }
+
+  Future<void> test_method_class_withPrefix() async {
+    allowedIdentifiers = {'method', 'anotherMethod', 'notStatic'};
+    await computeSuggestions('''
+class C {
+  static C method() => C();
+  static C anotherMethod() => C();
+  C notStatic() => C();
+}
+void f() {
+  C c = .a^
+}
+''');
+    assertResponse(r'''
+replacement
+  left: 1
+suggestions
+  anotherMethod
+    kind: methodInvocation
+''');
+  }
+
+  Future<void> test_method_extensionType() async {
+    allowedIdentifiers = {'method', 'notStatic'};
+    await computeSuggestions('''
+extension type C(int x) {
+  static C method() => C(1);
+  C notStatic() => C(1);
+}
+void f() {
+  C c = .^
+}
+''');
+    assertResponse(r'''
+suggestions
+  method
+    kind: methodInvocation
+''');
+  }
+
+  Future<void> test_method_extensionType_withPrefix() async {
+    allowedIdentifiers = {'method', 'anotherMethod', 'notStatic'};
+    await computeSuggestions('''
+extension type C(int x) {
+  static C method() => C(1);
+  static C anotherMethod() => C(1);
+  C notStatic() => C(1);
+}
+void f() {
+  C c = .a^
+}
+''');
+    assertResponse(r'''
+replacement
+  left: 1
+suggestions
+  anotherMethod
+    kind: methodInvocation
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/services/completion/dart/location/test_all.dart b/pkg/analysis_server/test/services/completion/dart/location/test_all.dart
index 8017b99..40de867 100644
--- a/pkg/analysis_server/test/services/completion/dart/location/test_all.dart
+++ b/pkg/analysis_server/test/services/completion/dart/location/test_all.dart
@@ -22,6 +22,7 @@
 import 'constructor_invocation_test.dart' as constructor_invocation;
 import 'dart_doc_test.dart' as dart_doc;
 import 'directive_uri_test.dart' as directive_uri;
+import 'dot_shorthand_invocation_test.dart' as dot_shorthand_invocation;
 import 'dot_shorthand_property_access_test.dart'
     as dot_shorthand_property_access;
 import 'enum_constant_test.dart' as enum_constant;
@@ -106,6 +107,7 @@
     constructor_invocation.main();
     dart_doc.main();
     directive_uri.main();
+    dot_shorthand_invocation.main();
     dot_shorthand_property_access.main();
     enum_constant.main();
     enum_declaration.main();
diff --git a/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart b/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart
index e2db6ba..0ea886f 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart
@@ -605,6 +605,12 @@
   }
 
   @override
+  void visitDotShorthandInvocation(DotShorthandInvocation node) {
+    optype.completionLocation = 'DotShorthandPropertyAccess_memberName';
+    optype.includeReturnValueSuggestions = true;
+  }
+
+  @override
   void visitDotShorthandPropertyAccess(DotShorthandPropertyAccess node) {
     optype.completionLocation = 'DotShorthandPropertyAccess_propertyName';
     optype.includeReturnValueSuggestions = true;
diff --git a/pkg/analyzer_plugin/test/src/utilities/completion/optype_test.dart b/pkg/analyzer_plugin/test/src/utilities/completion/optype_test.dart
index 1fbd9a5..1b47c0b 100644
--- a/pkg/analyzer_plugin/test/src/utilities/completion/optype_test.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/completion/optype_test.dart
@@ -1024,6 +1024,22 @@
         returnValue: true);
   }
 
+  Future<void> test_dotShorthandInvocation() async {
+    addTestSource('''
+class C {
+  C.named();
+}
+void f() {
+  C c = .n^()
+}
+''');
+    await assertOpType(
+      completionLocation: 'DotShorthandPropertyAccess_memberName',
+      constructors: true,
+      returnValue: true,
+    );
+  }
+
   Future<void> test_doubleLiteral() async {
     addTestSource('main() { print(1.2^); }');
     await assertOpType();