Chromium Code Reviews| Index: pkg/analysis_server/lib/src/services/correction/util.dart |
| diff --git a/pkg/analysis_server/lib/src/services/correction/util.dart b/pkg/analysis_server/lib/src/services/correction/util.dart |
| index 2023d2545d4e59fbabacfbdc47d7cbf84612a01c..1c4b26c840f4a4466be54ac95f7c41a70f1809bb 100644 |
| --- a/pkg/analysis_server/lib/src/services/correction/util.dart |
| +++ b/pkg/analysis_server/lib/src/services/correction/util.dart |
| @@ -4,6 +4,8 @@ |
| library services.src.correction.util; |
| +import 'dart:math'; |
| + |
| import 'package:analysis_server/src/protocol2.dart' show SourceEdit; |
| import 'package:analysis_server/src/services/correction/source_range.dart'; |
| import 'package:analysis_server/src/services/correction/strings.dart'; |
| @@ -16,6 +18,20 @@ import 'package:analyzer/src/generated/source.dart'; |
| /** |
| + * @return <code>true</code> if given [List]s are identical at given position. |
| + */ |
| +bool allListsIdentical(List<List> lists, int position) { |
| + Object element = lists[0][position]; |
| + for (List list in lists) { |
| + if (list[position] != element) { |
| + return false; |
| + } |
| + } |
| + return true; |
| +} |
| + |
| + |
| +/** |
| * TODO(scheglov) replace with nodes once there will be [CompilationUnit#getComments]. |
| * |
| * Returns [SourceRange]s of all comments in [unit]. |
| @@ -96,6 +112,27 @@ ExecutableElement getEnclosingExecutableElement(AstNode node) { |
| return null; |
| } |
| + |
| +/** |
| + * @return the enclosing executable [AstNode]. |
| + */ |
| + AstNode getEnclosingExecutableNode(AstNode node) { |
| + while (node != null) { |
| + if (node is FunctionDeclaration) { |
| + return node; |
| + } |
| + if (node is ConstructorDeclaration) { |
| + return node; |
| + } |
| + if (node is MethodDeclaration) { |
| + return node; |
| + } |
| + node = node.parent; |
| + } |
| + return null; |
| +} |
| + |
| + |
| /** |
| * Returns [getExpressionPrecedence] for the parent of [node], |
| * or `0` if the parent node is [ParenthesizedExpression]. |
| @@ -110,6 +147,7 @@ int getExpressionParentPrecedence(AstNode node) { |
| return getExpressionPrecedence(parent); |
| } |
| + |
| /** |
| * Returns the precedence of [node] it is an [Expression], negative otherwise. |
| */ |
| @@ -120,6 +158,7 @@ int getExpressionPrecedence(AstNode node) { |
| return -1000; |
| } |
| + |
| /** |
| * Returns the namespace of the given [ImportElement]. |
| */ |
| @@ -129,6 +168,58 @@ Map<String, Element> getImportNamespace(ImportElement imp) { |
| return namespace.definedNames; |
| } |
| +/** |
| + * @return the nearest common ancestor [AstNode] of the given [AstNode]s. |
| + */ |
| +AstNode getNearestCommonAncestor(List<AstNode> nodes) { |
| + // may be no nodes |
| + if (nodes.isEmpty) { |
| + return null; |
| + } |
| + // prepare parents |
| + List<List<AstNode>> parents = []; |
| + for (AstNode node in nodes) { |
| + parents.add(getParents(node)); |
| + } |
| + // find min length |
| + int minLength = 1 << 20; |
| + for (List<AstNode> parentList in parents) { |
| + minLength = min(minLength, parentList.length); |
| + } |
| + // find deepest parent |
| + int i = 0; |
| + for (; i < minLength; i++) { |
| + if (!allListsIdentical(parents, i)) { |
| + break; |
| + } |
| + } |
| + return parents[0][i - 1]; |
| +} |
| + |
| +/** |
| + * @return parent [AstNode]s from [CompilationUnit] (at index "0") to the given one. |
| + */ |
| +List<AstNode> getParents(AstNode node) { |
| + // prepare number of parents |
| + int numParents = 0; |
| + { |
| + AstNode current = node.parent; |
| + while (current != null) { |
| + numParents++; |
| + current = current.parent; |
| + } |
| + } |
| + // fill array of parents |
| + List<AstNode> parents = new List<AstNode>(numParents); |
| + AstNode current = node.parent; |
| + int index = numParents; |
| + while (current != null) { |
| + parents[--index] = current; |
| + current = current.parent; |
| + } |
| + return parents; |
| +} |
| + |
| /** |
| * If given [AstNode] is name of qualified property extraction, returns target from which |
| @@ -674,6 +765,19 @@ class CorrectionUtils { |
| _invertCondition0(expression)._source; |
| /** |
| + * @return <code>true</code> if selection range contains only whitespace or comments |
| + */ |
| + bool isJustWhitespaceOrComment(SourceRange range) { |
| + String trimmedText = getRangeText(range).trim(); |
| + // may be whitespace |
| + if (trimmedText.isEmpty) { |
| + return true; |
| + } |
| + // may be comment |
| + return TokenUtils.getTokens(trimmedText).isEmpty; |
| + } |
| + |
| + /** |
| * Returns the source with indentation changed from [oldIndent] to |
| * [newIndent], keeping indentation of lines relative to each other. |
| */ |
| @@ -735,6 +839,40 @@ class CorrectionUtils { |
| } |
| /** |
| + * @return <code>true</code> if "selection" covers "node" and there are any non-whitespace tokens |
| + * between "selection" and "node" start/end. |
| + */ |
| + bool selectionIncludesNonWhitespaceOutsideNode(SourceRange selection, |
| + AstNode node) { |
| + return _selectionIncludesNonWhitespaceOutsideRange( |
| + selection, |
| + rangeNode(node)); |
| + } |
| + |
| + /** |
| + * @return <code>true</code> if given range of [BinaryExpression] can be extracted. |
| + */ |
| + bool validateBinaryExpressionRange(BinaryExpression binaryExpression, SourceRange range) { |
| + // only parts of associative expression are safe to extract |
| + if (!binaryExpression.operator.type.isAssociativeOperator) { |
| + return false; |
| + } |
| + // prepare selected operands |
| + List<Expression> operands = _getOperandsInOrderFor(binaryExpression); |
| + List<Expression> subOperands = _getOperandsForSourceRange(operands, range); |
| + // if empty, then something wrong with selection |
| + if (subOperands.isEmpty) { |
| + return false; |
| + } |
| + // may be some punctuation included into selection - operators, braces, etc |
| + if (_selectionIncludesNonWhitespaceOutsideOperands(range, subOperands)) { |
| + return false; |
| + } |
| + // OK |
| + return true; |
| + } |
| + |
| + /** |
| * @return the [ImportElement] used to import given [Element] into [library]. |
| * May be `null` if was not imported, i.e. declared in the same library. |
| */ |
| @@ -838,6 +976,90 @@ class CorrectionUtils { |
| } |
| return _InvertedCondition._simple(getNodeText(expression)); |
| } |
| + |
| + bool _selectionIncludesNonWhitespaceOutsideOperands(SourceRange selection, List<Expression> operands) { |
| + return _selectionIncludesNonWhitespaceOutsideRange(selection, rangeNodes(operands)); |
| + } |
| + |
| + /** |
| + * @return <code>true</code> if "selection" covers "range" and there are any non-whitespace tokens |
| + * between "selection" and "range" start/end. |
| + */ |
| + bool _selectionIncludesNonWhitespaceOutsideRange(SourceRange selection, |
| + SourceRange range) { |
| + // selection should cover range |
| + if (!selection.covers(range)) { |
| + return false; |
| + } |
| + // non-whitespace between selection start and range start |
| + if (!isJustWhitespaceOrComment(rangeStartStart(selection, range))) { |
| + return true; |
| + } |
| + // non-whitespace after range |
| + if (!isJustWhitespaceOrComment(rangeEndEnd(range, selection))) { |
| + return true; |
| + } |
| + // only whitespace in selection around range |
| + return false; |
| + } |
| + |
| + /** |
| + * @return [Expression]s from <code>operands</code> which are completely covered by given |
| + * [SourceRange]. Range should start and end between given [Expression]s. |
| + */ |
| + static List<Expression> _getOperandsForSourceRange(List<Expression> operands, SourceRange range) { |
| + assert(!operands.isEmpty); |
| + List<Expression> subOperands = []; |
| + // track range enter/exit |
| + bool entered = false; |
| + bool exited = false; |
| + // may be range starts before or on first operand |
| + if (range.offset <= operands[0].offset) { |
| + entered = true; |
| + } |
| + // iterate over gaps between operands |
| + for (int i = 0; i < operands.length - 1; i++) { |
| + Expression operand = operands[i]; |
| + Expression nextOperand = operands[i + 1]; |
| + SourceRange inclusiveGap = rangeEndStart(operand, nextOperand).getMoveEnd(1); |
| + // add operand, if already entered range |
| + if (entered) { |
| + subOperands.add(operand); |
| + // may be last operand in range |
| + if (range.endsIn(inclusiveGap)) { |
| + exited = true; |
| + } |
| + } else { |
| + // may be first operand in range |
| + if (range.startsIn(inclusiveGap)) { |
| + entered = true; |
| + } |
| + } |
| + } |
| + // check if last operand is in range |
| + Expression lastGroupMember = operands[operands.length - 1]; |
| + if (range.end == lastGroupMember.end) { |
| + subOperands.add(lastGroupMember); |
| + exited = true; |
| + } |
| + // we expect that range covers only given operands |
| + if (!exited) { |
| + return []; |
| + } |
| + // done |
| + return subOperands; |
| + } |
| + |
| + /** |
| + * @return all operands of the given [BinaryExpression] and its children with the same |
| + * operator. |
| + */ |
| + static List<Expression> _getOperandsInOrderFor(BinaryExpression groupRoot) { |
| + List<Expression> operands = []; |
| + TokenType groupOperatorType = groupRoot.operator.type; |
| + groupRoot.accept(new _OrderedOperandsVisitor(groupOperatorType, operands)); |
| + return operands; |
| + } |
| } |
| @@ -852,6 +1074,66 @@ class CorrectionUtils_InsertDesc { |
| /** |
| + * Utilities to work with [Token]s. |
| + */ |
| +class TokenUtils { |
| + /** |
| + * @return the first [KeywordToken] with given [Keyword], may be <code>null</code> if |
| + * not found. |
| + */ |
| + static KeywordToken findKeywordToken(List<Token> tokens, Keyword keyword) { |
| + for (Token token in tokens) { |
| + if (token is KeywordToken) { |
| + KeywordToken keywordToken = token; |
| + if (keywordToken.keyword == keyword) { |
| + return keywordToken; |
| + } |
| + } |
| + } |
| + return null; |
| + } |
| + |
| + /** |
| + * @return the first [Token] with given [TokenType], may be <code>null</code> if not |
| + * found. |
| + */ |
| + static Token findToken(List<Token> tokens, TokenType type) { |
| + for (Token token in tokens) { |
| + if (token.type == type) { |
| + return token; |
| + } |
| + } |
| + return null; |
| + } |
| + |
| + /** |
| + * @return [Token]s of the given Dart source, not <code>null</code>, may be empty if no |
| + * tokens or some exception happens. |
| + */ |
| + static List<Token> getTokens(String s) { |
|
Brian Wilkerson
2014/08/20 22:11:16
This seems slow, especially for large files. Do we
scheglov
2014/08/20 22:19:21
Acknowledged.
|
| + try { |
| + List<Token> tokens = []; |
| + Scanner scanner = new Scanner(null, new CharSequenceReader(s), null); |
| + Token token = scanner.tokenize(); |
| + while (token.type != TokenType.EOF) { |
| + tokens.add(token); |
| + token = token.next; |
| + } |
| + return tokens; |
| + } catch (e) { |
| + return []; |
| + } |
| + } |
| + |
| + /** |
| + * @return <code>true</code> if given [Token]s contain only single [Token] with given |
| + * [TokenType]. |
| + */ |
| + static bool hasOnly(List<Token> tokens, TokenType type) => |
| + tokens.length == 1 && tokens[0].type == type; |
| +} |
| + |
| +/** |
| * A container with a source and its precedence. |
| */ |
| class _InvertedCondition { |
| @@ -893,3 +1175,20 @@ class _InvertedCondition { |
| static _InvertedCondition _simple(String source) => |
| new _InvertedCondition(2147483647, source); |
| } |
| + |
| + |
| +class _OrderedOperandsVisitor extends GeneralizingAstVisitor { |
| + final TokenType groupOperatorType; |
| + final List<Expression> operands; |
| + |
| + _OrderedOperandsVisitor(this.groupOperatorType, this.operands); |
| + |
| + @override |
| + Object visitExpression(Expression node) { |
| + if (node is BinaryExpression && node.operator.type == groupOperatorType) { |
| + return super.visitNode(node); |
| + } |
| + operands.add(node); |
| + return null; |
| + } |
| +} |