Skip to content

Commit 0b2d73b

Browse files
kallentuCommit Queue
authored andcommitted
[analysis_server] Dot shorthands: Add ConvertToDotShorthand assist.
An assist to convert `E.id` to its dot shorthand equivalent `.id`. The assist handles constructors, static methods, and static fields/getters. I kept this as an assist because there's no diagnostic that corresponds to this. If we have a lint that's more opinionated, happy to associate this fix with it, but until then, it's a general assist. Added a bunch of tests for what I could think of testing. Let me know if there are any suggestions. Bug: #60994 Change-Id: I30b9670103e516bccf8aa46b704c353910796629 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/444202 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Kallen Tu <[email protected]>
1 parent 1499a30 commit 0b2d73b

File tree

6 files changed

+915
-0
lines changed

6 files changed

+915
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,11 @@ class _ContextTypeVisitor extends SimpleAstVisitor<DartType> {
853853
return null;
854854
}
855855

856+
@override
857+
DartType? visitInstanceCreationExpression(InstanceCreationExpression node) {
858+
return _visitParent(node);
859+
}
860+
856861
@override
857862
DartType? visitIsExpression(IsExpression node) {
858863
if (node.isOperator.end < offset) {

pkg/analysis_server/lib/src/services/correction/assist.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ abstract final class DartAssistKind {
111111
DartAssistKindPriority.default_,
112112
'Convert to use a URI',
113113
);
114+
static const convertToDotShorthand = AssistKind(
115+
'dart.assist.convert.ToDotShorthand',
116+
DartAssistKindPriority.default_,
117+
'Convert to dot shorthand',
118+
);
114119
static const convertToDoubleQuotedString = AssistKind(
115120
'dart.assist.convert.toDoubleQuotedString',
116121
DartAssistKindPriority.default_,

pkg/analysis_server/lib/src/services/correction/assist_internal.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import 'package:analysis_server/src/services/correction/dart/convert_into_is_not
2525
import 'package:analysis_server/src/services/correction/dart/convert_map_from_iterable_to_for_literal.dart';
2626
import 'package:analysis_server/src/services/correction/dart/convert_part_of_to_uri.dart';
2727
import 'package:analysis_server/src/services/correction/dart/convert_quotes.dart';
28+
import 'package:analysis_server/src/services/correction/dart/convert_to_dot_shorthand.dart';
2829
import 'package:analysis_server/src/services/correction/dart/convert_to_expression_function_body.dart';
2930
import 'package:analysis_server/src/services/correction/dart/convert_to_field_parameter.dart';
3031
import 'package:analysis_server/src/services/correction/dart/convert_to_generic_function_syntax.dart';
@@ -105,6 +106,7 @@ const Set<ProducerGenerator> _builtInGenerators = {
105106
ConvertMapFromIterableToForLiteral.new,
106107
ConvertPartOfToUri.new,
107108
ConvertSwitchExpressionToSwitchStatement.new,
109+
ConvertToDotShorthand.new,
108110
ConvertToDoubleQuotes.new,
109111
ConvertToExpressionFunctionBody.new,
110112
ConvertToFieldParameter.new,
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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:analysis_server/src/services/completion/dart/feature_computer.dart';
6+
import 'package:analysis_server/src/services/correction/assist.dart';
7+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
8+
import 'package:analyzer/dart/analysis/features.dart';
9+
import 'package:analyzer/dart/ast/ast.dart';
10+
import 'package:analyzer/dart/element/element.dart';
11+
import 'package:analyzer/dart/element/type.dart';
12+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
13+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
14+
import 'package:analyzer_plugin/utilities/range_factory.dart';
15+
16+
class ConvertToDotShorthand extends ResolvedCorrectionProducer {
17+
ConvertToDotShorthand({required super.context});
18+
19+
@override
20+
CorrectionApplicability get applicability =>
21+
CorrectionApplicability.automatically;
22+
23+
@override
24+
AssistKind get assistKind => DartAssistKind.convertToDotShorthand;
25+
26+
@override
27+
Future<void> compute(ChangeBuilder builder) async {
28+
if (!isEnabled(Feature.dot_shorthands)) return;
29+
await computeAssistForNode(builder, node.parent);
30+
}
31+
32+
Future<void> computeAssistForNode(
33+
ChangeBuilder builder,
34+
AstNode? node,
35+
) async {
36+
switch (node) {
37+
// e.g. `A.nam^ed()`
38+
case ConstructorName constructorName:
39+
// e.g. `pref^ix.A.named()`
40+
case NamedType(parent: ConstructorName constructorName):
41+
await convertFromConstructorName(builder, constructorName);
42+
// e.g. `A.meth^od()`
43+
case MethodInvocation():
44+
await convertFromMethodInvocation(builder, node);
45+
// e.g. `A.gett^er`
46+
case PrefixedIdentifier():
47+
await convertFromPrefixedIdentifier(builder, node);
48+
}
49+
}
50+
51+
/// Whether the element of [node] matches the [typeElement], allowing us to
52+
/// convert the typed identifier to a dot shorthand.
53+
///
54+
/// In the following example, the node `B.getter` is unable to be converted
55+
/// to a valid dot shorthand, and this method would return `false`.
56+
///
57+
/// ```dart
58+
/// class A {}
59+
/// class B {
60+
/// static A get getter => A();
61+
/// }
62+
///
63+
/// A f() {
64+
/// return B.getter;
65+
/// }
66+
/// ```
67+
bool contextTypeMatchesTypeElement(AstNode node, Element? typeElement) {
68+
var featureComputer = FeatureComputer(
69+
unitResult.libraryElement.typeSystem,
70+
unitResult.libraryElement.typeProvider,
71+
);
72+
var contextType = featureComputer.computeContextType(node, node.offset);
73+
if (contextType is! InterfaceType) return false;
74+
return contextType.element == typeElement;
75+
}
76+
77+
/// Converts a constructor to a dot shorthand.
78+
/// (e.g. `E.named()` to `.named()` or `E()` to `.new()`)
79+
Future<void> convertFromConstructorName(
80+
ChangeBuilder builder,
81+
ConstructorName node,
82+
) async {
83+
if (!contextTypeMatchesTypeElement(node, node.type.element)) return;
84+
85+
// Dot shorthand constructors don't have explicit type arguments.
86+
// Disallow the assist if the user provided explicit arguments.
87+
if (node.type.typeArguments != null) return;
88+
89+
if (node.name != null) {
90+
// Converts a named constructor e.g. `E.named()` to `.named()`.
91+
await builder.addDartFileEdit(file, (builder) {
92+
builder.addDeletion(range.node(node.type));
93+
});
94+
} else {
95+
// Converts an unnamed constructor e.g. `E()` to `.new()`.
96+
await builder.addDartFileEdit(file, (builder) {
97+
builder.addSimpleReplacement(range.node(node.type), '.new');
98+
});
99+
}
100+
}
101+
102+
/// Converts a method invocation to a dot shorthand.
103+
/// (e.g. `E.id()` to `.id()`)
104+
Future<void> convertFromMethodInvocation(
105+
ChangeBuilder builder,
106+
MethodInvocation node,
107+
) async {
108+
var target = node.target;
109+
if (target is SimpleIdentifier) {
110+
if (!contextTypeMatchesTypeElement(node, target.element)) return;
111+
await builder.addDartFileEdit(file, (builder) {
112+
builder.addDeletion(range.node(target));
113+
});
114+
}
115+
}
116+
117+
/// Converts a prefix identifier to a dot shorthand. (e.g. `E.id` to `.id`)
118+
Future<void> convertFromPrefixedIdentifier(
119+
ChangeBuilder builder,
120+
PrefixedIdentifier node,
121+
) async {
122+
Identifier prefix;
123+
if (node.prefix.element is PrefixElement) {
124+
prefix = node;
125+
} else {
126+
prefix = node.prefix;
127+
}
128+
if (!contextTypeMatchesTypeElement(node, prefix.element)) return;
129+
await builder.addDartFileEdit(file, (builder) {
130+
builder.addDeletion(range.node(prefix));
131+
});
132+
}
133+
}

0 commit comments

Comments
 (0)