|
| 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