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