Skip to content

Commit

Permalink
[DAS] Inline enclosing else block assist
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
FMorschel authored and Commit Queue committed Oct 30, 2024
1 parent c919ade commit 006caad
Show file tree
Hide file tree
Showing 5 changed files with 1,137 additions and 0 deletions.
10 changes: 10 additions & 0 deletions pkg/analysis_server/lib/src/services/correction/assist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,16 @@ abstract final class DartAssistKind {
DartAssistKindPriority.DEFAULT,
"Invert 'if' statement",
);
static const JOIN_ELSE_WITH_IF = AssistKind(
'dart.assist.inlineElseBlock',
DartAssistKindPriority.DEFAULT,
"Join the 'else' block with inner 'if' statement",
);
static const JOIN_IF_WITH_ELSE = AssistKind(
'dart.assist.inlineEnclosingElseBlock',
DartAssistKindPriority.DEFAULT,
"Join 'if' statement with outer 'else' block",
);
static const JOIN_IF_WITH_INNER = AssistKind(
'dart.assist.joinWithInnerIf',
DartAssistKindPriority.DEFAULT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import 'package:analysis_server/src/services/correction/dart/import_add_show.dar
import 'package:analysis_server/src/services/correction/dart/inline_invocation.dart';
import 'package:analysis_server/src/services/correction/dart/invert_conditional_expression.dart';
import 'package:analysis_server/src/services/correction/dart/invert_if_statement.dart';
import 'package:analysis_server/src/services/correction/dart/join_else_with_if.dart';
import 'package:analysis_server/src/services/correction/dart/join_if_with_inner.dart';
import 'package:analysis_server/src/services/correction/dart/join_if_with_outer.dart';
import 'package:analysis_server/src/services/correction/dart/join_variable_declaration.dart';
Expand Down Expand Up @@ -147,6 +148,8 @@ class AssistProcessor {
InlineInvocation.new,
InvertConditionalExpression.new,
InvertIfStatement.new,
JoinElseWithIf.new,
JoinIfWithElse.new,
JoinIfWithInner.new,
JoinIfWithOuter.new,
JoinVariableDeclaration.new,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
// Copyright (c) 2024, 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:analysis_server/src/services/correction/assist.dart';
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer_plugin/utilities/assist/assist.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';

/// A correction processor that joins the `else` block of an `if` statement
/// with the inner `if` statement.
///
/// This implementation triggers only on the enclosing `else` keyword of an if
/// statement that contains an inner `if` statement.
///
/// The enclosing else block must have only one statement which is the inner
/// `if` statement.
class JoinElseWithIf extends _JoinIfWithElseBlock {
JoinElseWithIf({required super.context})
: super(DartAssistKind.JOIN_ELSE_WITH_IF);

@override
Future<void> compute(ChangeBuilder builder) async {
var enclosingIfStatement = node;
if (enclosingIfStatement is! IfStatement) {
return;
}
// Checks if there is an `else` keyword in the enclosing `if` statement.
var elseKeyword = enclosingIfStatement.elseKeyword;
if (elseKeyword == null) {
return;
}
// Check if the cursor is over the `else` keyword of the enclosing `if`.
if (elseKeyword.offset > selectionOffset ||
elseKeyword.end < selectionEnd) {
return;
}
var elseStatement = enclosingIfStatement.elseStatement;
if (elseStatement == null) {
return;
}
// Check if the enclosing else block has only one statement which is the
// inner `if` statement.
if (elseStatement case Block(:var statements) when statements.length == 1) {
if (statements.first case IfStatement innerIfStatement) {
await _compute(
builder,
_getStatements(innerIfStatement),
enclosingIfStatement,
);
}
}
}
}

/// A correction processor that joins the `else` block of an `if` statement
/// with the inner `if` statement.
///
/// This implementation triggers only on the inner `if` keyword of an if
/// statement that is inside the `else` block of an enclosing `if` statement.
///
/// The enclosing else block must have only one statement which is the inner
/// `if` statement.
class JoinIfWithElse extends _JoinIfWithElseBlock {
JoinIfWithElse({required super.context})
: super(DartAssistKind.JOIN_IF_WITH_ELSE);

@override
Future<void> compute(ChangeBuilder builder) async {
var innerIfStatement = node;
if (innerIfStatement is! IfStatement) {
return;
}
// Check if the cursor is over the `if` keyword of the inner `if` statement.
if (innerIfStatement.ifKeyword case var keyword
when keyword.offset > selectionOffset || keyword.end < selectionEnd) {
return;
}
var block = innerIfStatement.parent;
IfStatement enclosingIfStatement;
// If the parent is a block, the look for the enclosing `if` statement.
if (block case Block(:var statements, parent: var blockParent)
// Checks if the enclosing else block has only one statement which is
// the inner `if` statement.
when statements.length == 1 &&
// This is just a precaution since it should alyways be true.
statements.first == innerIfStatement &&
// Checks if the parent is an `else` block of an enclosing `if`.
blockParent is IfStatement &&
blockParent.elseStatement == block) {
enclosingIfStatement = blockParent;
} else {
return;
}
await _compute(
builder,
_getStatements(innerIfStatement),
enclosingIfStatement,
);
}
}

/// A correction processor that joins the `else` block of an `if` statement
/// with the inner `if` statement.
///
/// This implements [_compute] and [_getStatements] to help the subclasses
/// with this functionality.
///
/// Here is an example:
///
/// ```dart
/// void f() {
/// if (1 == 1) {
/// } else {
/// if (2 == 2) {
/// print(0);
/// }
/// }
/// }
/// ```
///
/// Becomes:
///
/// ```dart
/// void f() {
/// if (1 == 1) {
/// } else if (2 == 2) {
/// print(0);
/// }
/// }
/// ```
abstract class _JoinIfWithElseBlock extends ResolvedCorrectionProducer {
@override
final AssistKind assistKind;

_JoinIfWithElseBlock(this.assistKind, {required super.context});

@override
CorrectionApplicability get applicability =>
// TODO(applicability): comment on why.
CorrectionApplicability.singleLocation;

String _blockSource(Block block, String? startCommentsSource, String prefix,
String? endCommentSource) {
var lineRanges = range.node(block);
var blockSource = utils.getRangeText(lineRanges);
blockSource = utils.indentSourceLeftRight(blockSource).trimRight();
var rightBraceIndex =
blockSource.lastIndexOf(TokenType.CLOSE_CURLY_BRACKET.lexeme);
var blockAfterRightBrace = blockSource.substring(rightBraceIndex);
// If starting comments, insert them after the first new line.
if (startCommentsSource != null) {
var firstNewLine = blockSource.indexOf(eol);
// If the block is missing new lines, add it (else).
if (firstNewLine != -1) {
var blockBeforeComment = blockSource.substring(0, firstNewLine);
var blockAfterComment =
blockSource.substring(firstNewLine, rightBraceIndex);
blockSource = '$blockBeforeComment$eol$startCommentsSource'
'$blockAfterComment';
} else {
var leftBraceIndex =
blockSource.indexOf(TokenType.OPEN_CURLY_BRACKET.lexeme);
var blockAfterComment =
blockSource.substring(leftBraceIndex + 1, rightBraceIndex);
if (!blockAfterComment.startsWith('$prefix${utils.oneIndent}')) {
blockAfterComment = '$prefix${utils.oneIndent}$blockAfterComment';
}
blockSource = '{$eol$startCommentsSource$eol$blockAfterComment';
}
} else {
blockSource = blockSource.substring(0, rightBraceIndex);
}
if (endCommentSource != null) {
blockSource = blockSource.trimRight();
blockSource += '$eol$endCommentSource$eol$prefix';
}
blockSource += blockAfterRightBrace;
return blockSource;
}

/// Receives the [ChangeBuilder] and the enclosing and inner `if` statements.
/// It then joins the `else` block of the outer `if` statement with the inner
/// `if` statement.
Future<void> _compute(ChangeBuilder builder, List<Statement> statements,
IfStatement outerIfStatement) async {
var elseKeyword = outerIfStatement.elseKeyword;
if (elseKeyword == null) {
return;
}
var elseStatement = outerIfStatement.elseStatement;
if (elseStatement == null) {
return;
}

// Comments after the main `else` keyword and before the block are not
// handled.
if (elseStatement.beginToken.precedingComments != null) {
return;
}

var prefix = utils.getNodePrefix(outerIfStatement);

await builder.addDartFileEdit(file, (builder) {
var source = '';
for (var statement in statements) {
String newBlockSource;

source += ' else ';

CommentToken? beforeIfKeywordComments;
CommentToken? beforeConditionComments;
if (statement is IfStatement) {
beforeIfKeywordComments = statement.beginToken.precedingComments;
beforeConditionComments = statement.ifKeyword.next?.precedingComments;
var elseCondition = statement.expression;
var elseConditionSource = utils.getNodeText(elseCondition);
if (statement.caseClause case var elseCaseClause?) {
elseConditionSource += ' ${utils.getNodeText(elseCaseClause)}';
}
source += 'if ($elseConditionSource) ';
statement = statement.thenStatement;
}

var endingComment = statement.endToken.next?.precedingComments;
var endCommentSource = _joinCommentsSources([
if (endingComment case var comment?) comment,
], prefix);

var beginCommentsSource = _joinCommentsSources([
if (beforeIfKeywordComments case var comment?) comment,
if (beforeConditionComments case var comment?) comment,
if (statement.beginToken.precedingComments case var comment?) comment,
], prefix);

if (statement case Block block) {
newBlockSource = _blockSource(
block, beginCommentsSource, prefix, endCommentSource);
} else {
var statementSource = utils.getNodeText(statement);
// Add indentation for the else statement if it is missing.
if (!statementSource.startsWith(prefix)) {
statementSource = '$prefix$statementSource';
}
source += '{$eol';
if (beginCommentsSource != null) {
source += '$beginCommentsSource$eol';
}
newBlockSource = '${utils.oneIndent}$statementSource';
if (endCommentSource != null) {
newBlockSource += '$eol$endCommentSource';
}
newBlockSource += '$eol$prefix}';
}
source += newBlockSource;
}

builder.addSimpleReplacement(
range.startOffsetEndOffset(
elseKeyword.offset - 1,
elseStatement.end,
),
source,
);
});
}

/// Returns the list of statements in the `else` block of the `if` statement.
List<Statement> _getStatements(IfStatement innerIfStatement) {
var elses = <Statement>[innerIfStatement];
var currentElse = innerIfStatement.elseStatement;
while (currentElse != null) {
if (currentElse is IfStatement) {
elses.add(currentElse);
currentElse = currentElse.elseStatement;
} else {
elses.add(currentElse);
break;
}
}
return elses;
}

String? _joinCommentsSources(List<CommentToken> comments, String prefix) {
if (comments.isEmpty) {
return null;
}
String source = '';
for (var comment in comments) {
var commentsSource = comment.lexeme;
var nextComment = comment.next;
var nextCommentStart = eol + prefix + utils.oneIndent;
while (nextComment is CommentToken) {
commentsSource += nextCommentStart + nextComment.lexeme;
nextComment = nextComment.next;
}
source = '$source$eol$commentsSource';
}
return '$prefix${utils.oneIndent}${source.trim()}';
}
}
Loading

0 comments on commit 006caad

Please sign in to comment.