| // Copyright (c) 2021, 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:_fe_analyzer_shared/src/scanner/token.dart'; | |
| import 'package:analysis_server/src/utilities/index_range.dart'; | |
| import 'package:analyzer/dart/ast/ast.dart'; | |
| import 'package:analyzer/source/line_info.dart'; | |
| import 'package:analyzer/source/source_range.dart'; | |
| import 'package:analyzer/src/utilities/extensions/ast.dart'; | |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; | |
| class TokenWithOptionalComma { | |
| final Token token; | |
| /// `true` if a comma is previously included. | |
| final bool includesComma; | |
| new(this.token, this.includesComma); | |
| } | |
| extension RangeFactoryExtensions on RangeFactory { | |
| /// Return a source range that covers the given [node] in the containing | |
| /// [list]. This includes a leading or trailing comma, as appropriate, and any | |
| /// leading or trailing comments. The [lineInfo] is used to differentiate | |
| /// trailing comments (on the same line as the end of the node) from leading | |
| /// comments (on lines between the start of the node and the preceding comma). | |
| /// | |
| /// Throws an `ArgumentError` if the [node] is not an element of the [list]. | |
| /// | |
| /// This method is useful for deleting a node in a list. | |
| /// | |
| /// See [nodeWithComments]. | |
| SourceRange nodeInListWithComments<T extends AstNode>( | |
| LineInfo lineInfo, | |
| NodeList<T> list, | |
| T node, | |
| ) { | |
| // TODO(brianwilkerson): Improve the name and signature of this method and | |
| // make it part of the API of either `RangeFactory` or | |
| // `DartFileEditBuilder`. The implementation currently assumes that the | |
| // list is an argument list, and we might want to generalize that. | |
| // TODO(brianwilkerson): Consider adding parameters to allow us to access the | |
| // left and right parentheses in cases where the only element of the list | |
| // is being removed. | |
| // TODO(brianwilkerson): Consider adding a `separator` parameter so that we | |
| // can handle things like statements in a block. | |
| if (list.length == 1) { | |
| if (list[0] != node) { | |
| throw ArgumentError('The node must be in the list.'); | |
| } | |
| // If there's only one item in the list, then delete everything including | |
| // any leading or trailing comments, including any trailing comma. | |
| var leading = _leadingComment(lineInfo, node.beginToken); | |
| var trailing = trailingComment( | |
| lineInfo, | |
| node.endToken, | |
| returnComma: true, | |
| ); | |
| return startEnd(leading, trailing.token); | |
| } | |
| var index = list.indexOf(node); | |
| if (index < 0) { | |
| throw ArgumentError('The node must be in the list.'); | |
| } | |
| if (index == 0) { | |
| // If this is the first item in the list, then delete everything from the | |
| // leading comment for this item to the leading comment for the next item. | |
| // This will include the comment after this item. | |
| var thisLeadingComment = _leadingComment(lineInfo, node.beginToken); | |
| var nextLeadingComment = _leadingComment(lineInfo, list[1].beginToken); | |
| return startStart(thisLeadingComment, nextLeadingComment); | |
| } else { | |
| // If this isn't the first item in the list, then delete everything from | |
| // the end of the previous item, after the comma and any trailing comment, | |
| // to the end of this item, also after the comma and any trailing comment. | |
| var previousTrailingComment = trailingComment( | |
| lineInfo, | |
| list[index - 1].endToken, | |
| returnComma: false, | |
| ); | |
| var thisTrailingComment = trailingComment( | |
| lineInfo, | |
| node.endToken, | |
| returnComma: previousTrailingComment.includesComma, | |
| ); | |
| var previousToken = previousTrailingComment.token; | |
| if (!previousTrailingComment.includesComma && | |
| thisTrailingComment.includesComma) { | |
| // But if this item has comma and the previous didn't, then | |
| // we'd be deleting both commas, which would leave invalid code. We | |
| // can't leave the comment, so instead we leave the preceding comma. | |
| previousToken = previousToken.next!; | |
| } | |
| return endEnd(previousToken, thisTrailingComment.token); | |
| } | |
| } | |
| /// Return a list of the ranges that cover all of the elements in the [list] | |
| /// whose index is in the list of [indexes]. | |
| List<SourceRange> nodesInList<T extends AstNode>( | |
| List<T> list, | |
| List<int> indexes, | |
| ) { | |
| var ranges = <SourceRange>[]; | |
| var indexRanges = IndexRange.contiguousSubRanges(indexes); | |
| if (indexRanges.length == 1) { | |
| var indexRange = indexRanges[0]; | |
| if (indexRange.lower == 0 && indexRange.upper == list.length - 1) { | |
| ranges.add(startEnd(list[indexRange.lower], list[indexRange.upper])); | |
| return ranges; | |
| } | |
| } | |
| for (var indexRange in indexRanges) { | |
| if (indexRange.lower == 0) { | |
| ranges.add( | |
| startStart(list[indexRange.lower], list[indexRange.upper + 1]), | |
| ); | |
| } else { | |
| ranges.add(endEnd(list[indexRange.lower - 1], list[indexRange.upper])); | |
| } | |
| } | |
| return ranges; | |
| } | |
| /// Return a source range that covers the given [node] with any leading and | |
| /// trailing comments. | |
| /// | |
| /// The range begins at the start of any leading comment token (excluding any | |
| /// token considered a trailing comment for the previous node) or the start | |
| /// of the node itself if there are none. | |
| /// | |
| /// The range ends at the end of the trailing comment token or the end of the | |
| /// node itself if there is not one. | |
| /// | |
| /// See [nodeInListWithComments]. | |
| SourceRange nodeWithComments(LineInfo lineInfo, AstNode node) { | |
| var beginToken = node.beginToken; | |
| // If the node is the first thing in the unit, leading comments are treated | |
| // as headers and should never be included in the range. | |
| var isFirstItem = beginToken == node.root.beginToken; | |
| var thisLeadingComment = isFirstItem | |
| ? beginToken | |
| : _leadingComment(lineInfo, beginToken); | |
| var thisTrailingComment = trailingComment( | |
| lineInfo, | |
| node.endToken, | |
| returnComma: false, | |
| ); | |
| return startEnd(thisLeadingComment, thisTrailingComment.token); | |
| } | |
| /// Return the trailing comment token following the [token] if it is on the | |
| /// same line as the [token], or return the [token] if there is no trailing | |
| /// comment or if the comment is on a different line than the [token]. | |
| /// | |
| /// If there is a trailing comment, the returned `includesComma` indicates | |
| /// that there is a `comma` between the token and the trailing comment. | |
| /// | |
| /// If [returnComma] is `true` and there is a comma after the | |
| /// [token], then the comma will be returned when the [token] would have been. | |
| /// | |
| TokenWithOptionalComma trailingComment( | |
| LineInfo lineInfo, | |
| Token token, { | |
| required bool returnComma, | |
| }) { | |
| var lastToken = token; | |
| var nextToken = lastToken.next!; | |
| var includesComma = | |
| nextToken.type == TokenType.COMMA && | |
| _shouldIncludeCommentsAfterComma(lineInfo, nextToken); | |
| // There are comments after the comma that follows token, which are probably | |
| // meant to apply to token, so we must actually proceed with nextToken | |
| // instead of token. | |
| if (includesComma) { | |
| lastToken = nextToken; | |
| nextToken = lastToken.next!; | |
| } | |
| Token? comment = nextToken.precedingComments; | |
| // If there is no comment after the next comma, and the comma is on a | |
| // different line than the token. | |
| if (comment == null && | |
| includesComma && | |
| _areDifferentLines(lineInfo, token, lastToken)) { | |
| comment = lastToken.precedingComments; | |
| lastToken = token; | |
| } | |
| if (comment != null) { | |
| if (lineInfo.onSameLine(comment.offset, lastToken.offset)) { | |
| var next = comment.next; | |
| while (next != null && | |
| lineInfo.onSameLine(next.offset, lastToken.offset)) { | |
| comment = next; | |
| next = next.next; | |
| } | |
| return TokenWithOptionalComma(comment!, includesComma); | |
| } | |
| } | |
| return TokenWithOptionalComma(returnComma ? lastToken : token, false); | |
| } | |
| /// Return `true` if the line number of the given [token] is different than | |
| /// the line number of the [other]. | |
| bool _areDifferentLines(LineInfo lineInfo, Token token, Token other) => | |
| !lineInfo.onSameLine(token.offset, other.offset); | |
| /// Return the left-most comment immediately before the [token] that is not on | |
| /// the same line as the first non-comment token before the [token]. Return | |
| /// the [token] if there is no such comment. | |
| Token _leadingComment(LineInfo lineInfo, Token token) { | |
| var previous = token.previous; | |
| if (previous == null || previous.isEof) { | |
| return token.precedingCommentOrThis; | |
| } | |
| Token? comment = token.precedingComments; | |
| if (_areDifferentLines(lineInfo, token, previous)) { | |
| while (comment != null) { | |
| if (_areDifferentLines(lineInfo, previous, comment)) { | |
| break; | |
| } | |
| comment = comment.next; | |
| } | |
| } | |
| return comment ?? token; | |
| } | |
| /// Return `true` if the comments preceding the token after the given [comma] | |
| /// should be included. | |
| /// | |
| /// This happens when the comments precede a closing token (e.g. parenthesis), | |
| /// or if the token next to [comma] is on a different line. | |
| bool _shouldIncludeCommentsAfterComma(LineInfo lineInfo, Token comma) { | |
| var tokenAfterComma = comma.next!; | |
| var tokenTypeAfterComma = tokenAfterComma.type; | |
| // Include the comment if the token next to comma is a closing token | |
| // (e.g. closing parenthesis). | |
| if (tokenTypeAfterComma == TokenType.CLOSE_CURLY_BRACKET || | |
| tokenTypeAfterComma == TokenType.CLOSE_PAREN || | |
| tokenTypeAfterComma == TokenType.CLOSE_SQUARE_BRACKET) { | |
| return true; | |
| } | |
| // Include the comment if the token next to comma is not on the same | |
| // line as the comma. | |
| return _areDifferentLines(lineInfo, comma, tokenAfterComma); | |
| } | |
| } |