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:
@@ -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();
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user