Skip to content

Commit 9342bac

Browse files
kallentuCommit Queue
authored andcommitted
[parser/analysis_server] Dot shorthands: Code completion for 'const .'
Parser and analysis server changes for adding code completion for `const .^` where `^` is the cursor location. I modified the parser to handle recovering when we were parsing a dot shorthand constructor invocation that was incomplete. Added analyzer tests, code completion tests, and frontend parser tests. Fixes: #59836 Change-Id: Ia200ebd9149658d7563c4942afd749c262c52dc5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/446987 Commit-Queue: Kallen Tu <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Chloe Stefantsova <[email protected]>
1 parent bea092d commit 9342bac

19 files changed

+872
-22
lines changed

pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8481,17 +8481,29 @@ class Parser {
84818481
}
84828482
}
84838483

8484-
bool isDotShorthand = _isDotShorthand(token.next!);
8485-
if (isDotShorthand) {
8486-
Token dot = token.next!;
8484+
// Handling const dot shorthands.
8485+
if (next.isA(TokenType.PERIOD)) {
84878486
listener.beginConstDotShorthand(constKeyword);
8488-
token = parsePrimary(
8489-
dot,
8490-
IdentifierContext.expressionContinuation,
8491-
ConstantPatternContext.explicit,
8492-
);
8493-
listener.handleDotShorthandHead(dot);
8494-
listener.handleDotShorthandContext(dot);
8487+
8488+
if (_isDotShorthand(next)) {
8489+
token = parsePrimary(
8490+
next,
8491+
IdentifierContext.expressionContinuation,
8492+
ConstantPatternContext.explicit,
8493+
);
8494+
} else {
8495+
// Recovery.
8496+
// This is an incomplete dot shorthand like `C c = const .`.
8497+
// This allows for better code completion, assuming the user wanted to
8498+
// write a dot shorthand.
8499+
token = ensureIdentifier(
8500+
next,
8501+
IdentifierContext.expressionContinuation,
8502+
);
8503+
}
8504+
8505+
listener.handleDotShorthandHead(next);
8506+
listener.handleDotShorthandContext(next);
84958507
listener.endConstDotShorthand(constKeyword);
84968508
return token;
84978509
}

pkg/analysis_server/lib/src/services/completion/dart/in_scope_completion_pass.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,22 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
926926
}
927927
}
928928

929+
@override
930+
void visitDotShorthandConstructorInvocation(
931+
DotShorthandConstructorInvocation node,
932+
) {
933+
var period = node.period;
934+
if (offset >= period.end && offset <= node.constructorName.end) {
935+
var contextType = _computeContextType(node);
936+
if (contextType is! InterfaceType) return;
937+
declarationHelper(
938+
mustBeConstant: node.isConst,
939+
suggestingDotShorthand: true,
940+
suggestUnnamedAsNew: true,
941+
).addConstructorNamesForType(type: contextType);
942+
}
943+
}
944+
929945
@override
930946
void visitDotShorthandInvocation(DotShorthandInvocation node) {
931947
var period = node.period;
@@ -937,6 +953,7 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
937953
if (element == null) return;
938954

939955
declarationHelper(
956+
mustBeConstant: node.inConstantContext,
940957
suggestingDotShorthand: true,
941958
suggestUnnamedAsNew: true,
942959
).addStaticMembersOfElement(element);
@@ -956,6 +973,7 @@ class InScopeCompletionPass extends SimpleAstVisitor<void> {
956973
// can be any of the three.
957974
declarationHelper(
958975
suggestingDotShorthand: true,
976+
mustBeConstant: node.inConstantContext,
959977
preferNonInvocation:
960978
element is InterfaceElement &&
961979
state.request.shouldSuggestTearOff(element),
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:test_reflective_loader/test_reflective_loader.dart';
6+
7+
import '../../../../client/completion_driver_test.dart';
8+
9+
void main() {
10+
defineReflectiveSuite(() {
11+
defineReflectiveTests(DotShorthandConstructorInvocationTest);
12+
});
13+
}
14+
15+
@reflectiveTest
16+
class DotShorthandConstructorInvocationTest extends AbstractCompletionDriverTest
17+
with DotShorthandConstructorInvocationTestCases {}
18+
19+
mixin DotShorthandConstructorInvocationTestCases
20+
on AbstractCompletionDriverTest {
21+
Future<void> test_constructor_const() async {
22+
allowedIdentifiers = {'named', 'notConstant'};
23+
await computeSuggestions('''
24+
class C {
25+
const C.named();
26+
C.notConstant();
27+
}
28+
void f() {
29+
C c = const .^
30+
}
31+
''');
32+
assertResponse(r'''
33+
suggestions
34+
named
35+
kind: constructorInvocation
36+
''');
37+
}
38+
39+
Future<void> test_constructor_const_withPrefix() async {
40+
allowedIdentifiers = {'named', 'notConstant'};
41+
await computeSuggestions('''
42+
class C {
43+
const C.named();
44+
C.notConstant();
45+
}
46+
void f() {
47+
C c = const .n^
48+
}
49+
''');
50+
assertResponse(r'''
51+
replacement
52+
left: 1
53+
suggestions
54+
named
55+
kind: constructorInvocation
56+
''');
57+
}
58+
59+
Future<void> test_constructor_const_withPrefix_parentheses() async {
60+
allowedIdentifiers = {'named', 'notConstant'};
61+
await computeSuggestions('''
62+
class C {
63+
const C.named();
64+
C.notConstant();
65+
}
66+
void f() {
67+
C c = const .n^()
68+
}
69+
''');
70+
assertResponse(r'''
71+
replacement
72+
left: 1
73+
suggestions
74+
named
75+
kind: constructorInvocation
76+
''');
77+
}
78+
}

pkg/analysis_server/test/services/completion/dart/location/dot_shorthand_invocation_test.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,70 @@ suggestions
8585
''');
8686
}
8787

88+
Future<void> test_constructor_constantContext() async {
89+
allowedIdentifiers = {'named', 'notConstant'};
90+
await computeSuggestions('''
91+
class C {
92+
const C.named();
93+
C.notConstant();
94+
}
95+
void f() {
96+
const C c = .^
97+
}
98+
''');
99+
assertResponse(r'''
100+
suggestions
101+
named
102+
kind: constructorInvocation
103+
notConstant
104+
kind: constructor
105+
''');
106+
}
107+
108+
Future<void> test_constructor_constantContext_withPrefix() async {
109+
allowedIdentifiers = {'named', 'notConstant'};
110+
await computeSuggestions('''
111+
class C {
112+
const C.named();
113+
C.notConstant();
114+
}
115+
void f() {
116+
const C c = .n^
117+
}
118+
''');
119+
assertResponse(r'''
120+
replacement
121+
left: 1
122+
suggestions
123+
named
124+
kind: constructorInvocation
125+
notConstant
126+
kind: constructor
127+
''');
128+
}
129+
130+
Future<void> test_constructor_constantContext_withPrefix_parentheses() async {
131+
allowedIdentifiers = {'named', 'notConstant'};
132+
await computeSuggestions('''
133+
class C {
134+
const C.named();
135+
C.notConstant();
136+
}
137+
void f() {
138+
const C c = .n^()
139+
}
140+
''');
141+
assertResponse(r'''
142+
replacement
143+
left: 1
144+
suggestions
145+
named
146+
kind: constructorInvocation
147+
notConstant
148+
kind: constructor
149+
''');
150+
}
151+
88152
Future<void> test_constructor_extensionType_named() async {
89153
allowedIdentifiers = {'named'};
90154
await computeSuggestions('''

pkg/analysis_server/test/services/completion/dart/location/test_all.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import 'constructor_declaration_test.dart' as constructor_declaration;
2222
import 'constructor_invocation_test.dart' as constructor_invocation;
2323
import 'dart_doc_test.dart' as dart_doc;
2424
import 'directive_uri_test.dart' as directive_uri;
25+
import 'dot_shorthand_constructor_invocation_test.dart'
26+
as dot_shorthand_constructor_invocation;
2527
import 'dot_shorthand_invocation_test.dart' as dot_shorthand_invocation;
2628
import 'dot_shorthand_property_access_test.dart'
2729
as dot_shorthand_property_access;
@@ -107,6 +109,7 @@ void main() {
107109
constructor_invocation.main();
108110
dart_doc.main();
109111
directive_uri.main();
112+
dot_shorthand_constructor_invocation.main();
110113
dot_shorthand_invocation.main();
111114
dot_shorthand_property_access.main();
112115
enum_constant.main();

pkg/analyzer/lib/src/fasta/ast_builder.dart

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,16 +1569,28 @@ class AstBuilder extends StackListener {
15691569
);
15701570
}
15711571

1572-
var dotShorthand = pop() as DotShorthandInvocationImpl;
1573-
push(
1574-
DotShorthandConstructorInvocationImpl(
1575-
constKeyword: token,
1576-
period: dotShorthand.period,
1577-
constructorName: dotShorthand.memberName,
1578-
typeArguments: dotShorthand.typeArguments,
1579-
argumentList: dotShorthand.argumentList,
1580-
)..isDotShorthand = dotShorthand.isDotShorthand,
1581-
);
1572+
var dotShorthand = pop() as Expression;
1573+
if (dotShorthand is DotShorthandInvocationImpl) {
1574+
push(
1575+
DotShorthandConstructorInvocationImpl(
1576+
constKeyword: token,
1577+
period: dotShorthand.period,
1578+
constructorName: dotShorthand.memberName,
1579+
typeArguments: dotShorthand.typeArguments,
1580+
argumentList: dotShorthand.argumentList,
1581+
)..isDotShorthand = dotShorthand.isDotShorthand,
1582+
);
1583+
} else if (dotShorthand is DotShorthandPropertyAccessImpl) {
1584+
push(
1585+
DotShorthandConstructorInvocationImpl(
1586+
constKeyword: token,
1587+
period: dotShorthand.period,
1588+
constructorName: dotShorthand.propertyName,
1589+
typeArguments: null,
1590+
argumentList: _syntheticArgumentList(dotShorthand.propertyName.token),
1591+
)..isDotShorthand = dotShorthand.isDotShorthand,
1592+
);
1593+
}
15821594
}
15831595

15841596
@override

pkg/analyzer/test/src/dart/analysis/resolve_for_completion_test.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,40 @@ void f(C foo) {var foo.s^.self = C()}
191191
result.assertResolvedNodes(['void f(C foo) {var foo; .s.self = C();}']);
192192
}
193193

194+
test_dotShorthand_const_constructor() async {
195+
var result = await _resolveTestCode(r'''
196+
class C {
197+
const C.named();
198+
}
199+
void f() {
200+
C c = const .^
201+
}
202+
''');
203+
// TODO(kallentu): The parser shouldn't wrap the
204+
// DotShorthandConstructorInvocation in a function expression. This doesn't
205+
// produce anything different with code completion that requires this
206+
// recovered AST, but it would be nice to avoid the extra wrapping around
207+
// the constructor.
208+
result.assertResolvedNodes(['void f() {C c = const .()();}']);
209+
}
210+
211+
test_dotShorthand_const_constructor_prefix() async {
212+
var result = await _resolveTestCode(r'''
213+
class C {
214+
const C.named();
215+
}
216+
void f() {
217+
C c = const .name^
218+
}
219+
''');
220+
// TODO(kallentu): The parser shouldn't wrap the
221+
// DotShorthandConstructorInvocation in a function expression. This doesn't
222+
// produce anything different with code completion that requires this
223+
// recovered AST, but it would be nice to avoid the extra wrapping around
224+
// the constructor.
225+
result.assertResolvedNodes(['void f() {C c = const .name()();}']);
226+
}
227+
194228
test_extension_methodDeclaration_body() async {
195229
var result = await _resolveTestCode(r'''
196230
extension E on int {

pkg/analyzer_plugin/lib/src/utilities/completion/optype.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,9 +604,17 @@ class _OpTypeAstVisitor extends GeneralizingAstVisitor<void> {
604604
}
605605
}
606606

607+
@override
608+
void visitDotShorthandConstructorInvocation(
609+
DotShorthandConstructorInvocation node) {
610+
optype.completionLocation =
611+
'DotShorthandConstructorInvocation_constructorName';
612+
optype.includeConstructorSuggestions = true;
613+
}
614+
607615
@override
608616
void visitDotShorthandInvocation(DotShorthandInvocation node) {
609-
optype.completionLocation = 'DotShorthandPropertyAccess_memberName';
617+
optype.completionLocation = 'DotShorthandInvocation_memberName';
610618
optype.includeReturnValueSuggestions = true;
611619
}
612620

pkg/analyzer_plugin/test/src/utilities/completion/optype_test.dart

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,36 @@ main() {new core.String.from^CharCodes([]);}
10241024
returnValue: true);
10251025
}
10261026

1027+
Future<void> test_dotShorthandConstructorInvocation() async {
1028+
addTestSource('''
1029+
class C {
1030+
const C.named();
1031+
}
1032+
void f() {
1033+
C c = const .n^
1034+
}
1035+
''');
1036+
await assertOpType(
1037+
completionLocation: 'DotShorthandConstructorInvocation_constructorName',
1038+
constructors: true,
1039+
);
1040+
}
1041+
1042+
Future<void> test_dotShorthandConstructorInvocation_noTarget() async {
1043+
addTestSource('''
1044+
class C {
1045+
const C.named();
1046+
}
1047+
void f() {
1048+
C c = const .^
1049+
}
1050+
''');
1051+
await assertOpType(
1052+
completionLocation: 'DotShorthandConstructorInvocation_constructorName',
1053+
constructors: true,
1054+
);
1055+
}
1056+
10271057
Future<void> test_dotShorthandInvocation() async {
10281058
addTestSource('''
10291059
class C {
@@ -1034,7 +1064,7 @@ void f() {
10341064
}
10351065
''');
10361066
await assertOpType(
1037-
completionLocation: 'DotShorthandPropertyAccess_memberName',
1067+
completionLocation: 'DotShorthandInvocation_memberName',
10381068
constructors: true,
10391069
returnValue: true,
10401070
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Color {
2+
final int x;
3+
const Color.red(this.x);
4+
}
5+
void main() {
6+
Color c = const .;
7+
}

0 commit comments

Comments
 (0)