Initial 'Extract Local' implementation.

Missing:

1. suggested names;
2. offsets and lengths of occurrences.
3. some clean ups.

R=brianwilkerson@google.com
BUG=

Review URL: https://codereview.chromium.org//489973002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@39434 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
scheglov@google.com
2014-08-20 22:20:19 +00:00
parent afb008d480
commit d67a4a4458
9 changed files with 1776 additions and 15 deletions
@@ -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++) {
@@ -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 <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].
*
@@ -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
@@ -673,6 +764,19 @@ class CorrectionUtils {
String invertCondition(Expression expression) =>
_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.
@@ -734,6 +838,40 @@ class CorrectionUtils {
return replaceSourceIndent(oldSource, oldIndent, newIndent);
}
/**
* @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;
}
}
@@ -851,6 +1073,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) {
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.
*/
@@ -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;
}
}
@@ -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<String> names = <String>[];
final List<int> offsets = <int>[];
final List<int> lengths = <int>[];
Expression rootExpression;
Expression singleExpression;
bool wholeStatementExpression = false;
String stringLiteralPart;
final List<SourceRange> occurrences = <SourceRange>[];
final Set<String> excludedVariableNames = new Set<String>();
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<RefactoringStatus> 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<RefactoringStatus> 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<Change> createChange() {
Change change = new Change(refactoringName);
// prepare occurrences
List<SourceRange> 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<AstNode> _findNodes(List<SourceRange> ranges) {
List<AstNode> nodes = <AstNode>[];
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<SourceRange> occurrences) {
List<AstNode> nodes = _findNodes(occurrences);
List<AstNode> 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<String> 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
* <code>null</code>.
*/
List<SourceRange> _prepareOccurrences() {
// prepare selection
String selectionSource;
{
String rawSelectionSource = utils.getRangeText(selectionRange);
List<Token> 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<bool> result;
_HasStatementVisitor(this.result);
@override
visitStatement(Statement node) {
result[0] = true;
}
}
class _OccurrencesVisitor extends GeneralizingAstVisitor<Object> {
final ExtractLocalRefactoringImpl ref;
List<SourceRange> 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<bool> result = [false];
root.accept(new _HasStatementVisitor(result));
return result[0];
}
void _tryToFindOccurrence(Expression node) {
String nodeSource = ref.utils.getNodeText(node);
List<Token> 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<Token> 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);
}
}
}
@@ -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<int> 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<String> get names;
/**
* The offsets of the expressions that would be replaced by a reference to
* the variable.
*/
List<int> 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.
*/
@@ -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);
@@ -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.
@@ -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);
}
}
@@ -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();
+1
View File
@@ -32,6 +32,7 @@ abstract class Comparable<T> {
class String implements Comparable<String> {
bool get isEmpty => false;
bool get isNotEmpty => false;
int get length => 0;
}
class bool extends Object {}