Skip to content

Commit 006caad

Browse files
FMorschelCommit Queue
authored andcommitted
[DAS] Inline enclosing else block assist
Fixes #56924 Change-Id: Ib697e7b29671331f4e5b87a2472e3be60f89ffa1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/391021 Reviewed-by: Samuel Rawlins <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]> Auto-Submit: Felipe Morschel <[email protected]> Commit-Queue: Konstantin Shcheglov <[email protected]>
1 parent c919ade commit 006caad

File tree

5 files changed

+1137
-0
lines changed

5 files changed

+1137
-0
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,16 @@ abstract final class DartAssistKind {
362362
DartAssistKindPriority.DEFAULT,
363363
"Invert 'if' statement",
364364
);
365+
static const JOIN_ELSE_WITH_IF = AssistKind(
366+
'dart.assist.inlineElseBlock',
367+
DartAssistKindPriority.DEFAULT,
368+
"Join the 'else' block with inner 'if' statement",
369+
);
370+
static const JOIN_IF_WITH_ELSE = AssistKind(
371+
'dart.assist.inlineEnclosingElseBlock',
372+
DartAssistKindPriority.DEFAULT,
373+
"Join 'if' statement with outer 'else' block",
374+
);
365375
static const JOIN_IF_WITH_INNER = AssistKind(
366376
'dart.assist.joinWithInnerIf',
367377
DartAssistKindPriority.DEFAULT,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import 'package:analysis_server/src/services/correction/dart/import_add_show.dar
6060
import 'package:analysis_server/src/services/correction/dart/inline_invocation.dart';
6161
import 'package:analysis_server/src/services/correction/dart/invert_conditional_expression.dart';
6262
import 'package:analysis_server/src/services/correction/dart/invert_if_statement.dart';
63+
import 'package:analysis_server/src/services/correction/dart/join_else_with_if.dart';
6364
import 'package:analysis_server/src/services/correction/dart/join_if_with_inner.dart';
6465
import 'package:analysis_server/src/services/correction/dart/join_if_with_outer.dart';
6566
import 'package:analysis_server/src/services/correction/dart/join_variable_declaration.dart';
@@ -147,6 +148,8 @@ class AssistProcessor {
147148
InlineInvocation.new,
148149
InvertConditionalExpression.new,
149150
InvertIfStatement.new,
151+
JoinElseWithIf.new,
152+
JoinIfWithElse.new,
150153
JoinIfWithInner.new,
151154
JoinIfWithOuter.new,
152155
JoinVariableDeclaration.new,
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
// Copyright (c) 2024, 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/correction/assist.dart';
6+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
7+
import 'package:analyzer/dart/ast/ast.dart';
8+
import 'package:analyzer/dart/ast/token.dart';
9+
import 'package:analyzer_plugin/utilities/assist/assist.dart';
10+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
11+
import 'package:analyzer_plugin/utilities/range_factory.dart';
12+
13+
/// A correction processor that joins the `else` block of an `if` statement
14+
/// with the inner `if` statement.
15+
///
16+
/// This implementation triggers only on the enclosing `else` keyword of an if
17+
/// statement that contains an inner `if` statement.
18+
///
19+
/// The enclosing else block must have only one statement which is the inner
20+
/// `if` statement.
21+
class JoinElseWithIf extends _JoinIfWithElseBlock {
22+
JoinElseWithIf({required super.context})
23+
: super(DartAssistKind.JOIN_ELSE_WITH_IF);
24+
25+
@override
26+
Future<void> compute(ChangeBuilder builder) async {
27+
var enclosingIfStatement = node;
28+
if (enclosingIfStatement is! IfStatement) {
29+
return;
30+
}
31+
// Checks if there is an `else` keyword in the enclosing `if` statement.
32+
var elseKeyword = enclosingIfStatement.elseKeyword;
33+
if (elseKeyword == null) {
34+
return;
35+
}
36+
// Check if the cursor is over the `else` keyword of the enclosing `if`.
37+
if (elseKeyword.offset > selectionOffset ||
38+
elseKeyword.end < selectionEnd) {
39+
return;
40+
}
41+
var elseStatement = enclosingIfStatement.elseStatement;
42+
if (elseStatement == null) {
43+
return;
44+
}
45+
// Check if the enclosing else block has only one statement which is the
46+
// inner `if` statement.
47+
if (elseStatement case Block(:var statements) when statements.length == 1) {
48+
if (statements.first case IfStatement innerIfStatement) {
49+
await _compute(
50+
builder,
51+
_getStatements(innerIfStatement),
52+
enclosingIfStatement,
53+
);
54+
}
55+
}
56+
}
57+
}
58+
59+
/// A correction processor that joins the `else` block of an `if` statement
60+
/// with the inner `if` statement.
61+
///
62+
/// This implementation triggers only on the inner `if` keyword of an if
63+
/// statement that is inside the `else` block of an enclosing `if` statement.
64+
///
65+
/// The enclosing else block must have only one statement which is the inner
66+
/// `if` statement.
67+
class JoinIfWithElse extends _JoinIfWithElseBlock {
68+
JoinIfWithElse({required super.context})
69+
: super(DartAssistKind.JOIN_IF_WITH_ELSE);
70+
71+
@override
72+
Future<void> compute(ChangeBuilder builder) async {
73+
var innerIfStatement = node;
74+
if (innerIfStatement is! IfStatement) {
75+
return;
76+
}
77+
// Check if the cursor is over the `if` keyword of the inner `if` statement.
78+
if (innerIfStatement.ifKeyword case var keyword
79+
when keyword.offset > selectionOffset || keyword.end < selectionEnd) {
80+
return;
81+
}
82+
var block = innerIfStatement.parent;
83+
IfStatement enclosingIfStatement;
84+
// If the parent is a block, the look for the enclosing `if` statement.
85+
if (block case Block(:var statements, parent: var blockParent)
86+
// Checks if the enclosing else block has only one statement which is
87+
// the inner `if` statement.
88+
when statements.length == 1 &&
89+
// This is just a precaution since it should alyways be true.
90+
statements.first == innerIfStatement &&
91+
// Checks if the parent is an `else` block of an enclosing `if`.
92+
blockParent is IfStatement &&
93+
blockParent.elseStatement == block) {
94+
enclosingIfStatement = blockParent;
95+
} else {
96+
return;
97+
}
98+
await _compute(
99+
builder,
100+
_getStatements(innerIfStatement),
101+
enclosingIfStatement,
102+
);
103+
}
104+
}
105+
106+
/// A correction processor that joins the `else` block of an `if` statement
107+
/// with the inner `if` statement.
108+
///
109+
/// This implements [_compute] and [_getStatements] to help the subclasses
110+
/// with this functionality.
111+
///
112+
/// Here is an example:
113+
///
114+
/// ```dart
115+
/// void f() {
116+
/// if (1 == 1) {
117+
/// } else {
118+
/// if (2 == 2) {
119+
/// print(0);
120+
/// }
121+
/// }
122+
/// }
123+
/// ```
124+
///
125+
/// Becomes:
126+
///
127+
/// ```dart
128+
/// void f() {
129+
/// if (1 == 1) {
130+
/// } else if (2 == 2) {
131+
/// print(0);
132+
/// }
133+
/// }
134+
/// ```
135+
abstract class _JoinIfWithElseBlock extends ResolvedCorrectionProducer {
136+
@override
137+
final AssistKind assistKind;
138+
139+
_JoinIfWithElseBlock(this.assistKind, {required super.context});
140+
141+
@override
142+
CorrectionApplicability get applicability =>
143+
// TODO(applicability): comment on why.
144+
CorrectionApplicability.singleLocation;
145+
146+
String _blockSource(Block block, String? startCommentsSource, String prefix,
147+
String? endCommentSource) {
148+
var lineRanges = range.node(block);
149+
var blockSource = utils.getRangeText(lineRanges);
150+
blockSource = utils.indentSourceLeftRight(blockSource).trimRight();
151+
var rightBraceIndex =
152+
blockSource.lastIndexOf(TokenType.CLOSE_CURLY_BRACKET.lexeme);
153+
var blockAfterRightBrace = blockSource.substring(rightBraceIndex);
154+
// If starting comments, insert them after the first new line.
155+
if (startCommentsSource != null) {
156+
var firstNewLine = blockSource.indexOf(eol);
157+
// If the block is missing new lines, add it (else).
158+
if (firstNewLine != -1) {
159+
var blockBeforeComment = blockSource.substring(0, firstNewLine);
160+
var blockAfterComment =
161+
blockSource.substring(firstNewLine, rightBraceIndex);
162+
blockSource = '$blockBeforeComment$eol$startCommentsSource'
163+
'$blockAfterComment';
164+
} else {
165+
var leftBraceIndex =
166+
blockSource.indexOf(TokenType.OPEN_CURLY_BRACKET.lexeme);
167+
var blockAfterComment =
168+
blockSource.substring(leftBraceIndex + 1, rightBraceIndex);
169+
if (!blockAfterComment.startsWith('$prefix${utils.oneIndent}')) {
170+
blockAfterComment = '$prefix${utils.oneIndent}$blockAfterComment';
171+
}
172+
blockSource = '{$eol$startCommentsSource$eol$blockAfterComment';
173+
}
174+
} else {
175+
blockSource = blockSource.substring(0, rightBraceIndex);
176+
}
177+
if (endCommentSource != null) {
178+
blockSource = blockSource.trimRight();
179+
blockSource += '$eol$endCommentSource$eol$prefix';
180+
}
181+
blockSource += blockAfterRightBrace;
182+
return blockSource;
183+
}
184+
185+
/// Receives the [ChangeBuilder] and the enclosing and inner `if` statements.
186+
/// It then joins the `else` block of the outer `if` statement with the inner
187+
/// `if` statement.
188+
Future<void> _compute(ChangeBuilder builder, List<Statement> statements,
189+
IfStatement outerIfStatement) async {
190+
var elseKeyword = outerIfStatement.elseKeyword;
191+
if (elseKeyword == null) {
192+
return;
193+
}
194+
var elseStatement = outerIfStatement.elseStatement;
195+
if (elseStatement == null) {
196+
return;
197+
}
198+
199+
// Comments after the main `else` keyword and before the block are not
200+
// handled.
201+
if (elseStatement.beginToken.precedingComments != null) {
202+
return;
203+
}
204+
205+
var prefix = utils.getNodePrefix(outerIfStatement);
206+
207+
await builder.addDartFileEdit(file, (builder) {
208+
var source = '';
209+
for (var statement in statements) {
210+
String newBlockSource;
211+
212+
source += ' else ';
213+
214+
CommentToken? beforeIfKeywordComments;
215+
CommentToken? beforeConditionComments;
216+
if (statement is IfStatement) {
217+
beforeIfKeywordComments = statement.beginToken.precedingComments;
218+
beforeConditionComments = statement.ifKeyword.next?.precedingComments;
219+
var elseCondition = statement.expression;
220+
var elseConditionSource = utils.getNodeText(elseCondition);
221+
if (statement.caseClause case var elseCaseClause?) {
222+
elseConditionSource += ' ${utils.getNodeText(elseCaseClause)}';
223+
}
224+
source += 'if ($elseConditionSource) ';
225+
statement = statement.thenStatement;
226+
}
227+
228+
var endingComment = statement.endToken.next?.precedingComments;
229+
var endCommentSource = _joinCommentsSources([
230+
if (endingComment case var comment?) comment,
231+
], prefix);
232+
233+
var beginCommentsSource = _joinCommentsSources([
234+
if (beforeIfKeywordComments case var comment?) comment,
235+
if (beforeConditionComments case var comment?) comment,
236+
if (statement.beginToken.precedingComments case var comment?) comment,
237+
], prefix);
238+
239+
if (statement case Block block) {
240+
newBlockSource = _blockSource(
241+
block, beginCommentsSource, prefix, endCommentSource);
242+
} else {
243+
var statementSource = utils.getNodeText(statement);
244+
// Add indentation for the else statement if it is missing.
245+
if (!statementSource.startsWith(prefix)) {
246+
statementSource = '$prefix$statementSource';
247+
}
248+
source += '{$eol';
249+
if (beginCommentsSource != null) {
250+
source += '$beginCommentsSource$eol';
251+
}
252+
newBlockSource = '${utils.oneIndent}$statementSource';
253+
if (endCommentSource != null) {
254+
newBlockSource += '$eol$endCommentSource';
255+
}
256+
newBlockSource += '$eol$prefix}';
257+
}
258+
source += newBlockSource;
259+
}
260+
261+
builder.addSimpleReplacement(
262+
range.startOffsetEndOffset(
263+
elseKeyword.offset - 1,
264+
elseStatement.end,
265+
),
266+
source,
267+
);
268+
});
269+
}
270+
271+
/// Returns the list of statements in the `else` block of the `if` statement.
272+
List<Statement> _getStatements(IfStatement innerIfStatement) {
273+
var elses = <Statement>[innerIfStatement];
274+
var currentElse = innerIfStatement.elseStatement;
275+
while (currentElse != null) {
276+
if (currentElse is IfStatement) {
277+
elses.add(currentElse);
278+
currentElse = currentElse.elseStatement;
279+
} else {
280+
elses.add(currentElse);
281+
break;
282+
}
283+
}
284+
return elses;
285+
}
286+
287+
String? _joinCommentsSources(List<CommentToken> comments, String prefix) {
288+
if (comments.isEmpty) {
289+
return null;
290+
}
291+
String source = '';
292+
for (var comment in comments) {
293+
var commentsSource = comment.lexeme;
294+
var nextComment = comment.next;
295+
var nextCommentStart = eol + prefix + utils.oneIndent;
296+
while (nextComment is CommentToken) {
297+
commentsSource += nextCommentStart + nextComment.lexeme;
298+
nextComment = nextComment.next;
299+
}
300+
source = '$source$eol$commentsSource';
301+
}
302+
return '$prefix${utils.oneIndent}${source.trim()}';
303+
}
304+
}

0 commit comments

Comments
 (0)