Skip to content

Commit b68719c

Browse files
pqCommit Queue
authored and
Commit Queue
committed
argument and assignment switch assists
Fixes: #49960 Change-Id: Ie9865de244bdb5d4e0e2e045408bd4c18229bd0f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/287904 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Phil Quitslund <[email protected]>
1 parent e680c98 commit b68719c

File tree

2 files changed

+217
-23
lines changed

2 files changed

+217
-23
lines changed

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

Lines changed: 201 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ import 'package:analysis_server/src/services/correction/dart/abstract_producer.d
77
import 'package:analyzer/dart/ast/ast.dart';
88
import 'package:analyzer/dart/element/element.dart';
99
import 'package:analyzer/dart/element/type.dart';
10+
import 'package:analyzer/source/source_range.dart';
11+
import 'package:analyzer/src/dart/element/type_system.dart';
1012
import 'package:analyzer_plugin/utilities/assist/assist.dart';
1113
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
1214
import 'package:analyzer_plugin/utilities/range_factory.dart';
1315

1416
class ConvertToSwitchExpression extends CorrectionProducer {
17+
/// Local variable reference used in assignment switch expression generation.
18+
LocalVariableElement? writeElement;
19+
20+
/// Function reference used in argument switch expression generation.
21+
FunctionElement? functionElement;
22+
1523
@override
1624
AssistKind get assistKind => DartAssistKind.CONVERT_TO_SWITCH_EXPRESSION;
1725

@@ -21,13 +29,120 @@ class ConvertToSwitchExpression extends CorrectionProducer {
2129
if (node is! SwitchStatement) return;
2230

2331
var expression = node.expression;
24-
if (!expression.staticType.isExhaustive) return;
32+
if (!isAlwaysExhaustive(expression.staticType)) return;
2533

2634
if (isReturnSwitch(node)) {
2735
await convertReturnSwitchExpression(builder, node);
36+
} else if (isAssignmentSwitch(node)) {
37+
await convertAssignmentSwitchExpression(builder, node);
38+
} else if (isArgumentSwitch(node)) {
39+
await convertArgumentSwitchExpression(builder, node);
2840
}
2941
}
3042

43+
Future<void> convertArgumentSwitchExpression(
44+
ChangeBuilder builder, SwitchStatement node) async {
45+
await builder.addDartFileEdit(file, (builder) {
46+
builder.addSimpleInsertion(node.offset, '${functionElement!.name}(');
47+
48+
var memberCount = node.members.length;
49+
for (var i = 0; i < memberCount; ++i) {
50+
// Sure to be a SwitchPatternCase
51+
var patternCase = node.members[i] as SwitchPatternCase;
52+
builder.addDeletion(
53+
range.startStart(patternCase.keyword, patternCase.guardedPattern));
54+
var colonRange = range.entity(patternCase.colon);
55+
builder.addSimpleReplacement(colonRange, ' => ');
56+
57+
for (var statement in patternCase.statements) {
58+
var hasComment = statement.beginToken.precedingComments != null;
59+
60+
if (statement is ExpressionStatement) {
61+
var invocation = statement.expression;
62+
if (invocation is MethodInvocation) {
63+
var deletion = !hasComment
64+
? range.startOffsetEndOffset(
65+
range.offsetBy(colonRange, 1).offset,
66+
invocation.argumentList.leftParenthesis.end)
67+
: range.startOffsetEndOffset(invocation.offset,
68+
invocation.argumentList.leftParenthesis.end);
69+
builder.addDeletion(deletion);
70+
builder.addDeletion(
71+
range.entity(invocation.argumentList.rightParenthesis));
72+
}
73+
}
74+
75+
if (!hasComment && statement.isThrowExpressionStatement) {
76+
var deletionRange = range.startOffsetEndOffset(
77+
range.offsetBy(colonRange, 1).offset, statement.offset);
78+
builder.addDeletion(deletionRange);
79+
}
80+
81+
if (statement is BreakStatement) {
82+
var deletion = getBreakRange(statement);
83+
builder.addDeletion(deletion);
84+
} else {
85+
var endToken = i < memberCount - 1 ? ',' : '';
86+
builder.addSimpleReplacement(
87+
range.entity(statement.endToken), endToken);
88+
}
89+
}
90+
}
91+
92+
builder.addSimpleInsertion(node.end, ');');
93+
});
94+
}
95+
96+
Future<void> convertAssignmentSwitchExpression(
97+
ChangeBuilder builder, SwitchStatement node) async {
98+
await builder.addDartFileEdit(file, (builder) {
99+
builder.addSimpleInsertion(node.offset, '${writeElement!.name} = ');
100+
101+
var memberCount = node.members.length;
102+
for (var i = 0; i < memberCount; ++i) {
103+
// todo(pq): extract shared replacement logic
104+
105+
// Sure to be a SwitchPatternCase
106+
var patternCase = node.members[i] as SwitchPatternCase;
107+
builder.addDeletion(
108+
range.startStart(patternCase.keyword, patternCase.guardedPattern));
109+
var colonRange = range.entity(patternCase.colon);
110+
builder.addSimpleReplacement(colonRange, ' =>');
111+
112+
for (var statement in patternCase.statements) {
113+
if (statement is ExpressionStatement) {
114+
var expression = statement.expression;
115+
if (expression is AssignmentExpression) {
116+
var hasComment = statement.beginToken.precedingComments != null;
117+
var deletion = !hasComment
118+
? range.startOffsetEndOffset(
119+
range.offsetBy(colonRange, 1).offset,
120+
expression.operator.end)
121+
: range.startOffsetEndOffset(expression.beginToken.offset,
122+
expression.rightHandSide.offset);
123+
builder.addDeletion(deletion);
124+
} else if (expression is ThrowExpression) {
125+
var deletionRange = range.startOffsetEndOffset(
126+
range.offsetBy(colonRange, 1).offset, statement.offset - 1);
127+
builder.addDeletion(deletionRange);
128+
}
129+
}
130+
131+
if (statement is BreakStatement) {
132+
var deletion = getBreakRange(statement);
133+
builder.addDeletion(deletion);
134+
} else {
135+
var endToken = i < memberCount - 1 ? ',' : '';
136+
builder.addSimpleReplacement(
137+
range.entity(statement.endToken), endToken);
138+
}
139+
}
140+
}
141+
142+
builder.addSimpleInsertion(node.end, ';');
143+
});
144+
}
145+
31146
Future<void> convertReturnSwitchExpression(
32147
ChangeBuilder builder, SwitchStatement node) async {
33148
await builder.addDartFileEdit(file, (builder) {
@@ -56,13 +171,10 @@ class ConvertToSwitchExpression extends CorrectionProducer {
56171
builder.addDeletion(deletion);
57172
}
58173

59-
if (!hasComment && statement is ExpressionStatement) {
60-
var expression = statement.expression;
61-
if (expression is ThrowExpression) {
62-
var deletionRange = range.startOffsetEndOffset(
63-
range.offsetBy(colonRange, 1).offset, statement.offset - 1);
64-
builder.addDeletion(deletionRange);
65-
}
174+
if (!hasComment && statement.isThrowExpressionStatement) {
175+
var deletionRange = range.startOffsetEndOffset(
176+
range.offsetBy(colonRange, 1).offset, statement.offset - 1);
177+
builder.addDeletion(deletionRange);
66178
}
67179

68180
var endToken = i < memberCount - 1 ? ',' : '';
@@ -72,6 +184,82 @@ class ConvertToSwitchExpression extends CorrectionProducer {
72184
});
73185
}
74186

187+
SourceRange getBreakRange(BreakStatement statement) {
188+
var previous = (statement.beginToken.precedingComments ??
189+
statement.beginToken.previous)!;
190+
var deletion =
191+
range.startOffsetEndOffset(previous.end, statement.endToken.end);
192+
return deletion;
193+
}
194+
195+
bool isAlwaysExhaustive(DartType? type) {
196+
if (type == null) return false;
197+
return (typeSystem as TypeSystemImpl).isAlwaysExhaustive(type);
198+
}
199+
200+
// todo(pq): refactor the `is` checks to a single `getSwitchKind`
201+
// that only looks at members once
202+
// see: https://dart-review.googlesource.com/c/sdk/+/287904
203+
bool isArgumentSwitch(SwitchStatement node) {
204+
for (var member in node.members) {
205+
if (member is! SwitchPatternCase) return false;
206+
if (member.labels.isNotEmpty) return false;
207+
var statements = member.statements;
208+
209+
if (statements.length == 1 &&
210+
statements.first.isThrowExpressionStatement) {
211+
continue;
212+
}
213+
214+
if (statements.length != 2) return false;
215+
if (statements[1] is! BreakStatement) return false;
216+
var s = statements[0];
217+
if (s is! ExpressionStatement) return false;
218+
var expression = s.expression;
219+
if (expression is! MethodInvocation) return false;
220+
var element = expression.methodName.staticElement;
221+
if (element is! FunctionElement) return false;
222+
if (functionElement == null) {
223+
functionElement = element;
224+
} else {
225+
if (functionElement != element) return false;
226+
}
227+
}
228+
229+
return functionElement != null;
230+
}
231+
232+
bool isAssignmentSwitch(SwitchStatement node) {
233+
for (var member in node.members) {
234+
if (member is! SwitchPatternCase) return false;
235+
if (member.labels.isNotEmpty) return false;
236+
var statements = member.statements;
237+
238+
if (statements.length == 1 &&
239+
statements.first.isThrowExpressionStatement) {
240+
continue;
241+
}
242+
243+
if (statements.length != 2) return false;
244+
if (statements[1] is! BreakStatement) return false;
245+
var s = statements[0];
246+
if (s is! ExpressionStatement) return false;
247+
var expression = s.expression;
248+
if (expression is! AssignmentExpression) return false;
249+
var leftHandSide = expression.leftHandSide;
250+
if (leftHandSide is! SimpleIdentifier) return false;
251+
if (writeElement == null) {
252+
var element = leftHandSide.staticElement;
253+
if (element is! LocalVariableElement) return false;
254+
writeElement = element;
255+
} else {
256+
if (writeElement != leftHandSide.staticElement) return false;
257+
}
258+
}
259+
260+
return writeElement != null;
261+
}
262+
75263
bool isReturnSwitch(SwitchStatement node) {
76264
for (var member in node.members) {
77265
if (member is! SwitchPatternCase) return false;
@@ -88,12 +276,10 @@ class ConvertToSwitchExpression extends CorrectionProducer {
88276
}
89277
}
90278

91-
extension on DartType? {
92-
bool get isExhaustive {
93-
var element = this?.element;
94-
if (element is EnumElement) return true;
95-
if (element is ClassElement) return element.isExhaustive;
96-
if (element is MixinElement) return element.isExhaustive;
97-
return false;
279+
extension on Statement {
280+
bool get isThrowExpressionStatement {
281+
var self = this;
282+
if (self is! ExpressionStatement) return false;
283+
return self.expression is ThrowExpression;
98284
}
99285
}

pkg/analysis_server/test/src/services/correction/assist/convert_to_switch_expression_test.dart

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ class ConvertToSwitchExpressionTest extends AssistProcessorTest {
1919
@override
2020
AssistKind get kind => DartAssistKind.CONVERT_TO_SWITCH_EXPRESSION;
2121

22-
@FailingTest(reason: 'Not yet implemented')
2322
Future<void> test_argument_switchExpression() async {
2423
await resolveTestCode('''
2524
enum Color {
@@ -29,14 +28,16 @@ enum Color {
2928
void f(Color color) {
3029
switch (color) {
3130
case Color.red:
32-
print('red');
31+
print('red'); // Red.
3332
break;
3433
case Color.blue:
3534
print('blue');
3635
break;
36+
// Not green.
3737
case Color.green:
3838
throw 'Green is bad';
3939
case Color.yellow:
40+
// Yellow is OK.
4041
print('yellow');
4142
break;
4243
}
@@ -49,16 +50,18 @@ enum Color {
4950
5051
void f(Color color) {
5152
print(switch (color) {
52-
Color.red => 'red',
53+
Color.red => 'red', // Red.
5354
Color.blue => 'blue',
55+
// Not green.
5456
Color.green => throw 'Green is bad',
55-
Color.yellow => 'yellow'
57+
Color.yellow =>
58+
// Yellow is OK.
59+
'yellow'
5660
});
5761
}
5862
''');
5963
}
6064

61-
@FailingTest(reason: 'Not yet implemented')
6265
Future<void> test_assignment_switchExpression() async {
6366
await resolveTestCode('''
6467
enum Color {
@@ -72,11 +75,13 @@ String f(Color color) {
7275
name = 'red';
7376
break;
7477
case Color.blue:
75-
name = 'blue';
78+
name = 'blue'; // Blue!
7679
break;
80+
// Not green.
7781
case Color.green:
7882
throw 'Green is bad';
7983
case Color.yellow:
84+
// Yellow is OK.
8085
name = 'yellow';
8186
break;
8287
}
@@ -92,9 +97,12 @@ String f(Color color) {
9297
var name = '';
9398
name = switch (color) {
9499
Color.red => 'red',
95-
Color.blue => 'blue',
100+
Color.blue => 'blue', // Blue!
101+
// Not green.
96102
Color.green => throw 'Green is bad',
97-
Color.yellow => 'yellow'
103+
Color.yellow =>
104+
// Yellow is OK.
105+
'yellow'
98106
};
99107
return name;
100108
}

0 commit comments

Comments
 (0)