diff --git a/pkg/analysis_server/lib/src/services/correction/strings.dart b/pkg/analysis_server/lib/src/services/correction/strings.dart index a72b6f6837b..ff5c49584d4 100644 --- a/pkg/analysis_server/lib/src/services/correction/strings.dart +++ b/pkg/analysis_server/lib/src/services/correction/strings.dart @@ -41,6 +41,22 @@ int compareStrings(String a, String b) { return a.compareTo(b); } +/** + * Counts how many times [sub] appears in [str]. + */ +int countMatches(String str, String sub) { + if (isEmpty(str) || isEmpty(sub)) { + return 0; + } + int count = 0; + int idx = 0; + while ((idx = str.indexOf(sub, idx)) != -1) { + count++; + idx += sub.length; + } + return count; +} + /** * Checks if [str] is `null`, empty or is whitespace. */ @@ -101,6 +117,7 @@ String removeStart(String str, String remove) { return str; } + String repeat(String s, int n) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < n; i++) { diff --git a/pkg/analysis_server/lib/src/services/correction/util.dart b/pkg/analysis_server/lib/src/services/correction/util.dart index 2023d2545d4..1c4b26c840f 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'; @@ -15,6 +17,20 @@ import 'package:analyzer/src/generated/scanner.dart'; import 'package:analyzer/src/generated/source.dart'; +/** + * @return true if given [List]s are identical at given position. + */ +bool allListsIdentical(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]. * @@ -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 getImportNamespace(ImportElement imp) { return namespace.definedNames; } +/** + * @return the nearest common ancestor [AstNode] of the given [AstNode]s. + */ +AstNode getNearestCommonAncestor(List nodes) { + // may be no nodes + if (nodes.isEmpty) { + return null; + } + // prepare parents + List> parents = []; + for (AstNode node in nodes) { + parents.add(getParents(node)); + } + // find min length + int minLength = 1 << 20; + for (List 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 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 parents = new List(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 @@ -673,6 +764,19 @@ class CorrectionUtils { String invertCondition(Expression expression) => _invertCondition0(expression)._source; + /** + * @return true 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. @@ -734,6 +838,40 @@ class CorrectionUtils { return replaceSourceIndent(oldSource, oldIndent, newIndent); } + /** + * @return true 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 true 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 operands = _getOperandsInOrderFor(binaryExpression); + List 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 operands) { + return _selectionIncludesNonWhitespaceOutsideRange(selection, rangeNodes(operands)); + } + + /** + * @return true 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 operands which are completely covered by given + * [SourceRange]. Range should start and end between given [Expression]s. + */ + static List _getOperandsForSourceRange(List operands, SourceRange range) { + assert(!operands.isEmpty); + List 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 _getOperandsInOrderFor(BinaryExpression groupRoot) { + List operands = []; + TokenType groupOperatorType = groupRoot.operator.type; + groupRoot.accept(new _OrderedOperandsVisitor(groupOperatorType, operands)); + return operands; + } } @@ -851,6 +1073,66 @@ class CorrectionUtils_InsertDesc { } +/** + * Utilities to work with [Token]s. + */ +class TokenUtils { + /** + * @return the first [KeywordToken] with given [Keyword], may be null if + * not found. + */ + static KeywordToken findKeywordToken(List 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 null if not + * found. + */ + static Token findToken(List 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 null, may be empty if no + * tokens or some exception happens. + */ + static List getTokens(String s) { + try { + List 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 true if given [Token]s contain only single [Token] with given + * [TokenType]. + */ + static bool hasOnly(List tokens, TokenType type) => + tokens.length == 1 && tokens[0].type == type; +} + /** * A container with a source and its precedence. */ @@ -893,3 +1175,20 @@ class _InvertedCondition { static _InvertedCondition _simple(String source) => new _InvertedCondition(2147483647, source); } + + +class _OrderedOperandsVisitor extends GeneralizingAstVisitor { + final TokenType groupOperatorType; + final List 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; + } +} diff --git a/pkg/analysis_server/lib/src/services/refactoring/extract_local.dart b/pkg/analysis_server/lib/src/services/refactoring/extract_local.dart new file mode 100644 index 00000000000..88b29de37d9 --- /dev/null +++ b/pkg/analysis_server/lib/src/services/refactoring/extract_local.dart @@ -0,0 +1,530 @@ +// Copyright (c) 2014, 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. + +library services.src.refactoring.extract_local; + +import 'dart:async'; + +import 'package:analysis_server/src/protocol2.dart' show SourceEdit; +import 'package:analysis_server/src/services/correction/change.dart'; +import 'package:analysis_server/src/services/correction/selection_analyzer.dart'; +import 'package:analysis_server/src/services/correction/source_range.dart'; +import 'package:analysis_server/src/services/correction/status.dart'; +import 'package:analysis_server/src/services/correction/strings.dart'; +import 'package:analysis_server/src/services/correction/util.dart'; +import 'package:analysis_server/src/services/refactoring/naming_conventions.dart'; +import 'package:analysis_server/src/services/refactoring/refactoring.dart'; +import 'package:analysis_server/src/services/refactoring/refactoring_internal.dart'; +import 'package:analysis_server/src/services/search/element_visitors.dart'; +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:analyzer/src/generated/java_core.dart'; +import 'package:analyzer/src/generated/scanner.dart'; +import 'package:analyzer/src/generated/source.dart'; + + +const String _TOKEN_SEPARATOR = "\uFFFF"; + + +/** + * [ExtractLocalRefactoring] implementation. + */ +class ExtractLocalRefactoringImpl extends RefactoringImpl implements + ExtractLocalRefactoring { + final CompilationUnit unit; + final int selectionOffset; + final int selectionLength; + String file; + SourceRange selectionRange; + CorrectionUtils utils; + + String name; + bool extractAll = true; + final List names = []; + final List offsets = []; + final List lengths = []; + + Expression rootExpression; + Expression singleExpression; + bool wholeStatementExpression = false; + String stringLiteralPart; + final List occurrences = []; + final Set excludedVariableNames = new Set(); + + ExtractLocalRefactoringImpl(this.unit, this.selectionOffset, + this.selectionLength) { + file = unit.element.source.fullName; + selectionRange = new SourceRange(selectionOffset, selectionLength); + utils = new CorrectionUtils(unit); + } + + String get declarationKeyword { + if (_isPartOfConstantExpression(rootExpression)) { + return "const"; + } else { + return "var"; + } + } + + @override + String get refactoringName => 'Extract Local Variable'; + + @override + Future checkFinalConditions() { + RefactoringStatus result = new RefactoringStatus(); + if (excludedVariableNames.contains(name)) { + result.addWarning( + format( + "A variable with name '{0}' is already defined in the visible scope.", + name)); + } + return new Future.value(result); + } + + @override + Future checkInitialConditions() { + RefactoringStatus result = new RefactoringStatus(); + // selection + result.addStatus(_checkSelection()); + // occurrences + if (!result.hasFatalError) { + _prepareOccurrences(); + _prepareExcludedNames(); + } + // suggested names + _prepareNames(); + // done + return new Future.value(result); + } + + @override + RefactoringStatus checkName() { + return validateVariableName(name); + } + + @override + Future createChange() { + Change change = new Change(refactoringName); + // prepare occurrences + List occurrences; + if (extractAll) { + occurrences = this.occurrences; + } else { + occurrences = [selectionRange]; + } + // If the whole expression of a statement is selected, like '1 + 2', + // then convert it into a variable declaration statement. + if (wholeStatementExpression && occurrences.length == 1) { + String keyword = declarationKeyword; + String declarationSource = '$keyword $name = '; + SourceEdit edit = + new SourceEdit(singleExpression.offset, 0, declarationSource); + change.addEdit(file, edit); + return new Future.value(change); + } + // add variable declaration + { + String declarationSource; + if (stringLiteralPart != null) { + declarationSource = "var ${name} = '${stringLiteralPart}';"; + } else { + String keyword = declarationKeyword; + String initializerSource = utils.getRangeText(selectionRange); + declarationSource = "${keyword} ${name} = ${initializerSource};"; + } + // prepare location for declaration + Statement targetStatement = _findTargetStatement(occurrences); + String prefix = utils.getNodePrefix(targetStatement); + // insert variable declaration + String eol = utils.endOfLine; + SourceEdit edit = new SourceEdit( + targetStatement.offset, + 0, + '${declarationSource}${eol}${prefix}'); + change.addEdit(file, edit); + } + // prepare replacement + String occurrenceReplacement = name; + if (stringLiteralPart != null) { + occurrenceReplacement = "\${$name}"; + } + // replace occurrences with variable reference + for (SourceRange range in occurrences) { + SourceEdit edit = editFromRange(range, occurrenceReplacement); + change.addEdit(file, edit); + } + // done + return new Future.value(change); + } + + @override + bool requiresPreview() => false; + + /** + * Checks if [selectionRange] selects [Expression] which can be extracted, and + * location of this [DartExpression] in AST allows extracting. + */ + RefactoringStatus _checkSelection() { + _ExtractExpressionAnalyzer _selectionAnalyzer = + new _ExtractExpressionAnalyzer(selectionRange); + unit.accept(_selectionAnalyzer); + AstNode coveringNode = _selectionAnalyzer.coveringNode; + // may be fatal error + { + RefactoringStatus status = _selectionAnalyzer.status; + if (status.hasFatalError) { + return status; + } + } + // we need enclosing block to add variable declaration statement + if (coveringNode == null || + coveringNode.getAncestor((node) => node is Block) == null) { + return new RefactoringStatus.fatal( + 'Expression inside of function must be selected ' + 'to activate this refactoring.'); + } + // part of string literal + if (coveringNode is StringLiteral) { + stringLiteralPart = utils.getRangeText(selectionRange); + if (stringLiteralPart.startsWith("'") || + stringLiteralPart.startsWith('"') || + stringLiteralPart.endsWith("'") || + stringLiteralPart.endsWith('"')) { + return new RefactoringStatus.fatal( + 'Cannot extract only leading or trailing quote of string literal.'); + } + return new RefactoringStatus(); + } + // single node selected + if (_selectionAnalyzer.selectedNodes.length == 1 && + !utils.selectionIncludesNonWhitespaceOutsideNode( + selectionRange, + _selectionAnalyzer.firstSelectedNode)) { + AstNode selectedNode = _selectionAnalyzer.firstSelectedNode; + if (selectedNode is Expression) { + rootExpression = selectedNode; + singleExpression = rootExpression; + wholeStatementExpression = + singleExpression.parent is ExpressionStatement; + return new RefactoringStatus(); + } + } + // fragment of binary expression selected + if (coveringNode is BinaryExpression) { + BinaryExpression binaryExpression = coveringNode; + if (utils.validateBinaryExpressionRange( + binaryExpression, + selectionRange)) { + rootExpression = binaryExpression; + singleExpression = null; + return new RefactoringStatus(); + } + } + // invalid selection + return new RefactoringStatus.fatal( + 'Expression must be selected to activate this refactoring.'); + } + + /** + * Returns [AstNode]s at the offsets of the given [SourceRange]s. + */ + List _findNodes(List ranges) { + List nodes = []; + for (SourceRange range in ranges) { + AstNode node = new NodeLocator.con1(range.offset).searchWithin(unit); + nodes.add(node); + } + return nodes; + } + + /** + * @return the [Statement] such that variable declaration added before it will be visible in + * all given occurrences. + */ + Statement _findTargetStatement(List occurrences) { + List nodes = _findNodes(occurrences); + List firstParents = getParents(nodes[0]); + AstNode commonParent = getNearestCommonAncestor(nodes); + if (commonParent is Block) { + int commonIndex = firstParents.indexOf(commonParent); + return firstParents[commonIndex + 1] as Statement; + } else { + return commonParent.getAncestor((node) => node is Statement); + } + } + + /** + * @return `true` if it is OK to extract the node with the given [SourceRange]. + */ + bool _isExtractable(SourceRange range) { + _ExtractExpressionAnalyzer analyzer = new _ExtractExpressionAnalyzer(range); + utils.unit.accept(analyzer); + return analyzer.status.isOK; + } + + bool _isPartOfConstantExpression(AstNode node) { + if (node is TypedLiteral) { + return node.constKeyword != null; + } + if (node is InstanceCreationExpression) { + InstanceCreationExpression creation = node; + return creation.isConst; + } + if (node is ArgumentList || + node is ConditionalExpression || + node is BinaryExpression || + node is ParenthesizedExpression || + node is PrefixExpression || + node is Literal || + node is MapLiteralEntry) { + return _isPartOfConstantExpression(node.parent); + } + return false; + } + + void _prepareExcludedNames() { + excludedVariableNames.clear(); + // TODO(scheglov) clean up? + AstNode enclosingNode = + new NodeLocator.con1(selectionOffset).searchWithin(unit); + Block enclosingBlock = enclosingNode.getAncestor((node) => node is Block); + if (enclosingBlock != null) { + SourceRange newVariableVisibleRange = + rangeStartEnd(selectionRange, enclosingBlock.end); + ExecutableElement enclosingExecutable = + getEnclosingExecutableElement(enclosingNode); + if (enclosingExecutable != null) { + visitChildren(enclosingExecutable, (Element element) { + if (element is LocalElement) { + SourceRange elementRange = element.visibleRange; + if (elementRange != null && + elementRange.intersects(newVariableVisibleRange)) { + excludedVariableNames.add(element.displayName); + } + } + return true; + }); + } + } + } + + void _prepareNames() { + names.clear(); + // TODO(scheglov) implement +// Set excluded = excludedVariableNames; +// if (_stringLiteralPart != null) { +// return getVariableNameSuggestions(_stringLiteralPart, excluded); +// } else if (_singleExpression != null) { +// _guessedNames = CorrectionUtils.getVariableNameSuggestions2(_singleExpression.staticType, _singleExpression, excluded); +// } else { +// _guessedNames = ArrayUtils.EMPTY_STRING_ARRAY; +// } + } + + /** + * @return all occurrences of the source which matches given selection, sorted by offset. First + * [SourceRange] is same as the given selection. May be empty, but not + * null. + */ + List _prepareOccurrences() { + // prepare selection + String selectionSource; + { + String rawSelectionSource = utils.getRangeText(selectionRange); + List selectionTokens = TokenUtils.getTokens(rawSelectionSource); + selectionSource = selectionTokens.join(_TOKEN_SEPARATOR); + } + // prepare enclosing function + AstNode enclosingFunction; + { + AstNode selectionNode = + new NodeLocator.con1(selectionOffset).searchWithin(unit); + enclosingFunction = getEnclosingExecutableNode(selectionNode); + } + // visit function + enclosingFunction.accept( + new _OccurrencesVisitor(this, occurrences, selectionSource)); + // done + return occurrences; + } +} + + +/** + * [SelectionAnalyzer] for [ExtractLocalRefactoringImpl]. + */ +class _ExtractExpressionAnalyzer extends SelectionAnalyzer { + final RefactoringStatus status = new RefactoringStatus(); + + _ExtractExpressionAnalyzer(SourceRange selection) : super(selection); + + /** + * Records fatal error with given message. + */ + void invalidSelection(String message) { + _invalidSelection(message, null); + } + + @override + Object visitAssignmentExpression(AssignmentExpression node) { + super.visitAssignmentExpression(node); + Expression lhs = node.leftHandSide; + if (_isFirstSelectedNode(lhs)) { + _invalidSelection( + 'Cannot extract the left-hand side of an assignment.', + new RefactoringStatusContext.forNode(lhs)); + } + return null; + } + + @override + Object visitSimpleIdentifier(SimpleIdentifier node) { + super.visitSimpleIdentifier(node); + if (_isFirstSelectedNode(node)) { + // name of declaration + if (node.inDeclarationContext()) { + invalidSelection('Cannot extract the name part of a declaration.'); + } + // method name + Element element = node.bestElement; + if (element is FunctionElement || element is MethodElement) { + invalidSelection('Cannot extract a single method name.'); + } + // name in property access + AstNode parent = node.parent; + if (parent is PrefixedIdentifier && identical(parent.identifier, node)) { + invalidSelection('Cannot extract name part of a property access.'); + } + if (parent is PropertyAccess && identical(parent.propertyName, node)) { + invalidSelection('Cannot extract name part of a property access.'); + } + } + return null; + } + + /** + * Records fatal error with given message and [RefactoringStatusContext]. + */ + void _invalidSelection(String message, RefactoringStatusContext context) { + status.addFatalError(message, context); + reset(); + } + + bool _isFirstSelectedNode(AstNode node) => identical(firstSelectedNode, node); +} + + +class _HasStatementVisitor extends GeneralizingAstVisitor { + final List result; + + _HasStatementVisitor(this.result); + + @override + visitStatement(Statement node) { + result[0] = true; + } +} + + +class _OccurrencesVisitor extends GeneralizingAstVisitor { + final ExtractLocalRefactoringImpl ref; + + List occurrences; + + String selectionSource; + + _OccurrencesVisitor(this.ref, this.occurrences, this.selectionSource); + + @override + Object visitBinaryExpression(BinaryExpression node) { + if (!_hasStatements(node)) { + _tryToFindOccurrenceFragment(node); + return null; + } + return super.visitBinaryExpression(node); + } + + @override + Object visitExpression(Expression node) { + if (ref._isExtractable(rangeNode(node))) { + _tryToFindOccurrence(node); + } + return super.visitExpression(node); + } + + @override + Object visitSimpleStringLiteral(SimpleStringLiteral node) { + if (ref.stringLiteralPart != null) { + int occuLength = ref.stringLiteralPart.length; + String value = node.value; + int valueOffset = node.offset + (node.isMultiline ? 3 : 1); + int lastIndex = 0; + while (true) { + int index = value.indexOf(ref.stringLiteralPart, lastIndex); + if (index == -1) { + break; + } + lastIndex = index + occuLength; + int occuStart = valueOffset + index; + SourceRange occuRange = rangeStartLength(occuStart, occuLength); + occurrences.add(occuRange); + } + return null; + } + return visitExpression(node); + } + + void _addOccurrence(SourceRange range) { + if (range.intersects(ref.selectionRange)) { + occurrences.add(ref.selectionRange); + } else { + occurrences.add(range); + } + } + + bool _hasStatements(AstNode root) { + List result = [false]; + root.accept(new _HasStatementVisitor(result)); + return result[0]; + } + + void _tryToFindOccurrence(Expression node) { + String nodeSource = ref.utils.getNodeText(node); + List nodeTokens = TokenUtils.getTokens(nodeSource); + nodeSource = nodeTokens.join(_TOKEN_SEPARATOR); + if (nodeSource == selectionSource) { + SourceRange occuRange = rangeNode(node); + _addOccurrence(occuRange); + } + } + + void _tryToFindOccurrenceFragment(Expression node) { + int nodeOffset = node.offset; + String nodeSource = ref.utils.getNodeText(node); + List nodeTokens = TokenUtils.getTokens(nodeSource); + nodeSource = nodeTokens.join(_TOKEN_SEPARATOR); + // find "selection" in "node" tokens + int lastIndex = 0; + while (true) { + // find next occurrence + int index = nodeSource.indexOf(selectionSource, lastIndex); + if (index == -1) { + break; + } + lastIndex = index + selectionSource.length; + // find start/end tokens + int startTokenIndex = + countMatches(nodeSource.substring(0, index), _TOKEN_SEPARATOR); + int endTokenIndex = + countMatches(nodeSource.substring(0, lastIndex), _TOKEN_SEPARATOR); + Token startToken = nodeTokens[startTokenIndex]; + Token endToken = nodeTokens[endTokenIndex]; + // add occurrence range + int occuStart = nodeOffset + startToken.offset; + int occuEnd = nodeOffset + endToken.end; + SourceRange occuRange = rangeStartEnd(occuStart, occuEnd); + _addOccurrence(occuRange); + } + } +} diff --git a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart index bf63090e109..5c277df432e 100644 --- a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart +++ b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart @@ -8,16 +8,80 @@ import 'dart:async'; import 'package:analysis_server/src/services/correction/change.dart'; import 'package:analysis_server/src/services/correction/status.dart'; -import 'package:analysis_server/src/services/search/search_engine.dart'; +import 'package:analysis_server/src/services/refactoring/extract_local.dart'; import 'package:analysis_server/src/services/refactoring/rename_class_member.dart'; import 'package:analysis_server/src/services/refactoring/rename_constructor.dart'; import 'package:analysis_server/src/services/refactoring/rename_import.dart'; import 'package:analysis_server/src/services/refactoring/rename_library.dart'; import 'package:analysis_server/src/services/refactoring/rename_local.dart'; import 'package:analysis_server/src/services/refactoring/rename_unit_member.dart'; +import 'package:analysis_server/src/services/search/search_engine.dart'; +import 'package:analyzer/src/generated/ast.dart'; import 'package:analyzer/src/generated/element.dart'; +/** + * [Refactoring] to extract an expression into a local variable declaration. + */ +abstract class ExtractLocalRefactoring implements Refactoring { + /** + * Returns a new [ExtractLocalRefactoring] instance. + */ + factory ExtractLocalRefactoring(CompilationUnit unit, int selectionOffset, + int selectionLength) { + return new ExtractLocalRefactoringImpl( + unit, + selectionOffset, + selectionLength); + } + + /** + * True if all occurrences of the expression within the scope in which the + * variable will be defined should be replaced by a reference to the local + * variable. The expression used to initiate the refactoring will always be + * replaced. + */ + void set extractAll(bool extractAll); + + /** + * The lengths of the expressions that would be replaced by a reference to the + * variable. The lengths correspond to the offsets. In other words, for a + * given expression, if the offset of that expression is offsets[i], then the + * length of that expression is lengths[i]. + */ + List get lengths; + + /** + * The name that the local variable should be given. + */ + void set name(String name); + + /** + * The proposed names for the local variable. + * + * The first proposal should be used as the "best guess" (if it exists). + */ + List get names; + + /** + * The offsets of the expressions that would be replaced by a reference to + * the variable. + */ + List get offsets; + + /** + * Validates that the [name] is a valid identifier and is appropriate for + * local variable. + * + * It does not perform all the checks (such as checking for conflicts with any + * existing names in any of the scopes containing the current name), as many + * of these checkes require search engine. Use [checkFinalConditions] for this + * level of checking. + */ + RefactoringStatus checkName(); +} + + /** * Abstract interface for all refactorings. */ diff --git a/pkg/analysis_server/test/services/refactoring/abstract_refactoring.dart b/pkg/analysis_server/test/services/refactoring/abstract_refactoring.dart index 1e43b06c861..78f319fdb86 100644 --- a/pkg/analysis_server/test/services/refactoring/abstract_refactoring.dart +++ b/pkg/analysis_server/test/services/refactoring/abstract_refactoring.dart @@ -2,7 +2,7 @@ // 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. -library test.services.refactoring.rename; +library test.services.refactoring; import 'dart:async'; @@ -89,6 +89,19 @@ abstract class RefactoringTest extends AbstractSingleUnitTest { assertRefactoringStatus(status, RefactoringStatusSeverity.OK); } + /** + * Asserts that [refactoringChange] contains a [FileEdit] for [testFile], and + * it results the [expectedCode]. + */ + void assertTestChangeResult(String expectedCode) { + // prepare FileEdit + FileEdit fileEdit = refactoringChange.getFileEdit(testFile); + expect(fileEdit, isNotNull); + // validate resulting code + String actualCode = applySequence(testCode, fileEdit.edits); + expect(actualCode, expectedCode); + } + void indexTestUnit(String code) { resolveTestUnit(code); index.indexUnit(context, testUnit); diff --git a/pkg/analysis_server/test/services/refactoring/abstract_rename.dart b/pkg/analysis_server/test/services/refactoring/abstract_rename.dart index 6d2dbcc9fa4..dd7580da9d4 100644 --- a/pkg/analysis_server/test/services/refactoring/abstract_rename.dart +++ b/pkg/analysis_server/test/services/refactoring/abstract_rename.dart @@ -83,19 +83,6 @@ class RenameRefactoringTest extends RefactoringTest { }); } - /** - * Asserts that [refactoringChange] contains a [FileEdit] for [testFile], and - * it results the [expectedCode]. - */ - void assertTestChangeResult(String expectedCode) { - // prepare FileEdit - FileEdit fileEdit = refactoringChange.getFileEdit(testFile); - expect(fileEdit, isNotNull); - // validate resulting code - String actualCode = applySequence(testCode, fileEdit.edits); - expect(actualCode, expectedCode); - } - /** * Creates a new [RenameRefactoring] in [refactoring] for the [Element] of * the [SimpleIdentifier] at the given [search] pattern. diff --git a/pkg/analysis_server/test/services/refactoring/extract_local_test.dart b/pkg/analysis_server/test/services/refactoring/extract_local_test.dart new file mode 100644 index 00000000000..1276ac97247 --- /dev/null +++ b/pkg/analysis_server/test/services/refactoring/extract_local_test.dart @@ -0,0 +1,848 @@ +// Copyright (c) 2014, 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. + +library test.services.refactoring.extract_local; + +import 'dart:async'; + +import 'package:analysis_server/src/services/correction/change.dart'; +import 'package:analysis_server/src/services/correction/status.dart'; +import 'package:analysis_server/src/services/refactoring/extract_local.dart'; +import 'package:analysis_testing/reflective_tests.dart'; +import 'package:unittest/unittest.dart'; + +import 'abstract_refactoring.dart'; + + +main() { + groupSep = ' | '; + runReflectiveTests(ExtractLocalTest); +} + + +@ReflectiveTestCase() +class ExtractLocalTest extends RefactoringTest { + ExtractLocalRefactoringImpl refactoring; + + test_checkFinalConditions_sameVariable_after() { + indexTestUnit(''' +main() { + int a = 1 + 2; + var res; +} +'''); + _createRefactoringForString('1 + 2'); + // conflicting name + return refactoring.checkAllConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.WARNING, + expectedMessage: + "A variable with name 'res' is already defined in the visible scope."); + }); + } + + test_checkFinalConditions_sameVariable_before() { + indexTestUnit(''' +main() { + var res; + int a = 1 + 2; +} +'''); + _createRefactoringForString('1 + 2'); + // conflicting name + return refactoring.checkAllConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.WARNING, + expectedMessage: + "A variable with name 'res' is already defined in the visible scope."); + }); + } + + test_checkInitialConditions_assignmentLeftHandSize() { + indexTestUnit(''' +main() { + var v = 0; + v = 1; +} +'''); + _createRefactoringWithSuffix('v', ' = 1;'); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: 'Cannot extract the left-hand side of an assignment.'); + }); + } + + test_checkInitialConditions_methodName_reference() { + indexTestUnit(''' +main() { + main(); +} +'''); + _createRefactoringWithSuffix('main', '();'); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: 'Cannot extract a single method name.'); + }); + } + + test_checkInitialConditions_nameOfProperty_prefixedIdentifier() { + indexTestUnit(''' +main(p) { + p.value; // marker +} +'''); + _createRefactoringWithSuffix('value', '; // marker'); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: 'Cannot extract name part of a property access.'); + }); + } + + test_checkInitialConditions_nameOfProperty_propertyAccess() { + indexTestUnit(''' +main() { + foo().length; // marker +} +String foo() => ''; +'''); + _createRefactoringWithSuffix('length', '; // marker'); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: 'Cannot extract name part of a property access.'); + }); + } + + test_checkInitialConditions_namePartOfDeclaration_variable() { + indexTestUnit(''' +main() { + int vvv = 0; +} +'''); + _createRefactoringWithSuffix('vvv', ' = 0;'); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: 'Cannot extract the name part of a declaration.'); + }); + } + + test_checkInitialConditions_notPartOfFunction() { + indexTestUnit(''' +int a = 1 + 2; +'''); + _createRefactoringForString('1 + 2'); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: + 'Expression inside of function must be selected to activate this refactoring.'); + }); + } + + test_checkInitialConditions_stringSelection_leadingQuote() { + indexTestUnit(''' +main() { + var vvv = 'abc'; +} +'''); + _createRefactoringForString("'a"); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: + 'Cannot extract only leading or trailing quote of string literal.'); + }); + } + + test_checkInitialConditions_stringSelection_trailingQuote() { + indexTestUnit(''' +main() { + var vvv = 'abc'; +} +'''); + _createRefactoringForString("c'"); + // check conditions + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: + 'Cannot extract only leading or trailing quote of string literal.'); + }); + } + + test_checkLocalName() { + indexTestUnit(''' +main() { + int a = 1 + 2; +} +'''); + _createRefactoringForString('1 + 2'); + expect(refactoring.refactoringName, 'Extract Local Variable'); + // null + refactoring.name = null; + assertRefactoringStatus( + refactoring.checkName(), + RefactoringStatusSeverity.ERROR, + expectedMessage: "Variable name must not be null."); + // empty + refactoring.name = ''; + assertRefactoringStatus( + refactoring.checkName(), + RefactoringStatusSeverity.ERROR, + expectedMessage: "Variable name must not be empty."); + // OK + refactoring.name = 'res'; + assertRefactoringStatusOK(refactoring.checkName()); + } + + test_completeStatementExpression() { + indexTestUnit(''' +main(p) { + p.toString(); +} +'''); + _createRefactoringForString('p.toString()'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main(p) { + var res = p.toString(); +} +'''); + } + + test_const_argument_inConstInstanceCreation() { + indexTestUnit(''' +class A { + const A(int a, int b); +} +main() { + const A(1, 2); +} +'''); + _createRefactoringForString('1'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +class A { + const A(int a, int b); +} +main() { + const res = 1; + const A(res, 2); +} +'''); + } + + test_const_inList() { + indexTestUnit(''' +main() { + const [1, 2]; +} +'''); + _createRefactoringForString('1'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + const res = 1; + const [res, 2]; +} +'''); + } + + test_const_inList_inBinaryExpression() { + indexTestUnit(''' +main() { + const [1 + 2, 3]; +} +'''); + _createRefactoringForString('1'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + const res = 1; + const [res + 2, 3]; +} +'''); + } + + test_const_inList_inConditionalExpression() { + indexTestUnit(''' +main(bool b) { + const [b ? 1 : 2, 3]; +} +'''); + _createRefactoringForString('1'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main(bool b) { + const res = 1; + const [b ? res : 2, 3]; +} +'''); + } + + test_const_inList_inParenthesis() { + indexTestUnit(''' +main() { + const [(1), 2]; +} +'''); + _createRefactoringForString('1'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + const res = 1; + const [(res), 2]; +} +'''); + } + + test_const_inList_inPrefixExpression() { + indexTestUnit(''' +main() { + const [!true, 2]; +} +'''); + _createRefactoringForString('true'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + const res = true; + const [!res, 2]; +} +'''); + } + + test_const_inMap_key() { + indexTestUnit(''' +main() { + const {1: 2}; +} +'''); + _createRefactoringForString('1'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + const res = 1; + const {res: 2}; +} +'''); + } + + test_const_inMap_value() { + indexTestUnit(''' +main() { + const {1: 2}; +} +'''); + _createRefactoringForString('2'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + const res = 2; + const {1: res}; +} +'''); + } + + test_fragmentExpression() { + indexTestUnit(''' +main() { + int a = 1 + 2 + 3 + 4; +} +'''); + _createRefactoringForString('2 + 3'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 2 + 3; + int a = 1 + res + 4; +} +'''); + } + + test_fragmentExpression_leadingNotWhitespace() { + indexTestUnit(''' +main() { + int a = 1 + 2 + 3 + 4; +} +'''); + _createRefactoringForString('+ 2'); + // check conditions + return _assertInitialConditions_fatal_selection(); + } + + test_fragmentExpression_leadingPartialSelection() { + indexTestUnit(''' +main() { + int a = 111 + 2 + 3 + 4; +} +'''); + _createRefactoringForString('11 + 2'); + // check conditions + return _assertInitialConditions_fatal_selection(); + } + + test_fragmentExpression_leadingWhitespace() { + indexTestUnit(''' +main() { + int a = 1 + 2 + 3 + 4; +} +'''); + _createRefactoringForString(' 2 + 3'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 2 + 3; + int a = 1 +res + 4; +} +'''); + } + + test_fragmentExpression_notAssociativeOperator() { + indexTestUnit(''' +main() { + int a = 1 - 2 - 3 - 4; +} +'''); + _createRefactoringForString('2 - 3'); + // check conditions + return _assertInitialConditions_fatal_selection(); + } + + test_fragmentExpression_trailingNotWhitespace() { + indexTestUnit(''' +main() { + int a = 1 + 2 + 3 + 4; +} +'''); + _createRefactoringForString('2 + 3 +'); + // check conditions + return _assertInitialConditions_fatal_selection(); + } + + test_fragmentExpression_trailingPartialSelection() { + indexTestUnit(''' +main() { + int a = 1 + 2 + 3 + 444; +} +'''); + _createRefactoringForString('2 + 3 + 44'); + // check conditions + return _assertInitialConditions_fatal_selection(); + } + + test_fragmentExpression_trailingWhitespace() { + indexTestUnit(''' +main() { + int a = 1 + 2 + 3 + 4; +} +'''); + _createRefactoringForString('2 + 3 '); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 2 + 3 ; + int a = 1 + res+ 4; +} +'''); + } + + test_occurences_disableOccurences() { + indexTestUnit(''' +int foo() => 42; +main() { + int a = 1 + foo(); + int b = 2 + foo(); // marker +} +'''); + _createRefactoringWithSuffix('foo()', '; // marker'); + refactoring.extractAll = false; + // apply refactoring + return _assertSuccessfulRefactoring(''' +int foo() => 42; +main() { + int a = 1 + foo(); + var res = foo(); + int b = 2 + res; // marker +} +'''); + } + + test_occurences_ignore_assignmentLeftHandSize() { + indexTestUnit(''' +main() { + int v = 1; + v = 2; + print(() {v = 2;}); + print(1 + (() {v = 2; return 3;})()); + print(v); // marker +} +'''); + _createRefactoringWithSuffix('v', '); // marker'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + int v = 1; + v = 2; + print(() {v = 2;}); + print(1 + (() {v = 2; return 3;})()); + var res = v; + print(res); // marker +} +'''); + } + + test_occurences_ignore_nameOfVariableDeclariton() { + indexTestUnit(''' +main() { + int v = 1; + print(v); // marker +} +'''); + _createRefactoringWithSuffix('v', '); // marker'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + int v = 1; + var res = v; + print(res); // marker +} +'''); + } + + test_occurences_singleExpression() { + indexTestUnit(''' +int foo() => 42; +main() { + int a = 1 + foo(); + int b = 2 + foo(); // marker +} +'''); + _createRefactoringWithSuffix('foo()', '; // marker'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +int foo() => 42; +main() { + var res = foo(); + int a = 1 + res; + int b = 2 + res; // marker +} +'''); + } + + test_occurences_useDominator() { + indexTestUnit(''' +main() { + if (true) { + print(42); + } else { + print(42); + } +} +'''); + _createRefactoringForString('42'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 42; + if (true) { + print(res); + } else { + print(res); + } +} +'''); + } + + test_occurences_whenComment() { + indexTestUnit(''' +int foo() => 42; +main() { + /*int a = 1 + foo();*/ + int b = 2 + foo(); // marker +} +'''); + _createRefactoringWithSuffix('foo()', '; // marker'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +int foo() => 42; +main() { + /*int a = 1 + foo();*/ + var res = foo(); + int b = 2 + res; // marker +} +'''); + } + + test_occurences_withSpace() { + indexTestUnit(''' +int foo(String s) => 42; +main() { + int a = 1 + foo('has space'); + int b = 2 + foo('has space'); // marker +} +'''); + _createRefactoringWithSuffix("foo('has space')", '; // marker'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +int foo(String s) => 42; +main() { + var res = foo('has space'); + int a = 1 + res; + int b = 2 + res; // marker +} +'''); + } + + test_offsets_lengths() { + // TODO(scheglov) implement and test + } + + test_singleExpression() { + indexTestUnit(''' +main() { + int a = 1 + 2; +} +'''); + _createRefactoringForString('1 + 2'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 1 + 2; + int a = res; +} +'''); + } + + test_singleExpression_getter() { + indexTestUnit(''' +class A { + int get foo => 42; +} +main() { + A a = new A(); + int b = 1 + a.foo; // marker +} +'''); + _createRefactoringWithSuffix('a.foo', '; // marker'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +class A { + int get foo => 42; +} +main() { + A a = new A(); + var res = a.foo; + int b = 1 + res; // marker +} +'''); + } + + test_singleExpression_inMethod() { + indexTestUnit(''' +class A { + main() { + print(1 + 2); + } +} +'''); + _createRefactoringForString('1 + 2'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +class A { + main() { + var res = 1 + 2; + print(res); + } +} +'''); + } + + test_singleExpression_leadingNotWhitespace() { + indexTestUnit(''' +main() { + int a = 12 + 345; +} +'''); + _createRefactoringForString('+ 345'); + // check conditions + return _assertInitialConditions_fatal_selection(); + } + + test_singleExpression_leadingWhitespace() { + indexTestUnit(''' +main() { + int a = 12 /*abc*/ + 345; +} +'''); + _createRefactoringForString('12 /*abc*/'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 12 /*abc*/; + int a = res + 345; +} +'''); + } + + /** + * Here we use knowledge how exactly `1 + 2 + 3 + 41 is parsed. We know that + * `1 + 2` will be a separate and complete binary expression, so it can be + * handled as a single expression. + */ + test_singleExpression_partOfBinaryExpression() { + indexTestUnit(''' +main() { + int a = 1 + 2 + 3 + 4; +} +'''); + _createRefactoringForString('1 + 2'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 1 + 2; + int a = res + 3 + 4; +} +'''); + } + + test_singleExpression_trailingComment() { + indexTestUnit(''' +main() { + int a = 1 + 2; +} +'''); + _createRefactoringForString(' 1 + 2'); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 1 + 2; + int a = res; +} +'''); + } + + test_singleExpression_trailingNotWhitespace() { + indexTestUnit(''' +main() { + int a = 12 + 345; +} +'''); + _createRefactoringForString('12 +'); + // check conditions + return _assertInitialConditions_fatal_selection(); + } + + test_singleExpression_trailingWhitespace() { + indexTestUnit(''' +main() { + int a = 1 + 2 ; +} +'''); + _createRefactoringForString('1 + 2 '); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 1 + 2 ; + int a = res; +} +'''); + } + + test_stringLiteral_part() { + indexTestUnit(''' +main() { + print('abcdefgh'); +} +'''); + _createRefactoringForString('cde'); + // apply refactoring + return _assertSuccessfulRefactoring(r''' +main() { + var res = 'cde'; + print('ab${res}fgh'); +} +'''); + } + + test_stringLiteral_whole() { + indexTestUnit(''' +main() { + print('abc'); +} +'''); + _createRefactoringForString("'abc'"); + // apply refactoring + return _assertSuccessfulRefactoring(''' +main() { + var res = 'abc'; + print(res); +} +'''); + } + + Future _assertInitialConditions_fatal_selection() { + return refactoring.checkInitialConditions().then((status) { + assertRefactoringStatus( + status, + RefactoringStatusSeverity.FATAL, + expectedMessage: 'Expression must be selected to activate this refactoring.'); + }); + } + + /** + * Checks that all conditions are OK and the result of applying the [Change] + * to [testUnit] is [expectedCode]. + */ + Future _assertSuccessfulRefactoring(String expectedCode) { + return assertRefactoringConditionsOK().then((_) { + return refactoring.createChange().then((Change refactoringChange) { + this.refactoringChange = refactoringChange; + assertTestChangeResult(expectedCode); + }); + }); + } + + void _createRefactoring(int offset, int length) { + refactoring = new ExtractLocalRefactoringImpl(testUnit, offset, length); + refactoring.name = 'res'; + } + + /** + * Creates a new refactoring in [refactoring] for the selection range of the + * given [search] pattern. + */ + void _createRefactoringForString(String search) { + int offset = findOffset(search); + int length = search.length; + _createRefactoring(offset, length); + } + + void _createRefactoringWithSuffix(String selectionSearch, String suffix) { + int offset = findOffset(selectionSearch + suffix); + int length = selectionSearch.length; + _createRefactoring(offset, length); + } +} diff --git a/pkg/analysis_server/test/services/refactoring/test_all.dart b/pkg/analysis_server/test/services/refactoring/test_all.dart index e035aadeca2..46250a56bd8 100644 --- a/pkg/analysis_server/test/services/refactoring/test_all.dart +++ b/pkg/analysis_server/test/services/refactoring/test_all.dart @@ -6,6 +6,7 @@ library test.services.refactoring; import 'package:unittest/unittest.dart'; +import 'extract_local_test.dart' as extract_local_test; import 'naming_conventions_test.dart' as naming_conventions_test; import 'rename_class_member_test.dart' as rename_class_member_test; import 'rename_constructor_test.dart' as rename_constructor_test; @@ -18,6 +19,7 @@ import 'rename_unit_member_test.dart' as rename_unit_member_test; main() { groupSep = ' | '; group('refactoring', () { + extract_local_test.main(); naming_conventions_test.main(); rename_class_member_test.main(); rename_constructor_test.main(); diff --git a/pkg/analysis_testing/lib/mock_sdk.dart b/pkg/analysis_testing/lib/mock_sdk.dart index 28ca6b1041f..1da830979ca 100644 --- a/pkg/analysis_testing/lib/mock_sdk.dart +++ b/pkg/analysis_testing/lib/mock_sdk.dart @@ -32,6 +32,7 @@ abstract class Comparable { class String implements Comparable { bool get isEmpty => false; bool get isNotEmpty => false; + int get length => 0; } class bool extends Object {}