blob: cf9cd107bd6c882fb94adc437ca1451ce887d87d [file] [log] [blame]
// 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.
import 'dart:async';
import 'dart:collection';
import 'package:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/services/correction/name_suggestion.dart';
import 'package:analysis_server/src/services/correction/selection_analyzer.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:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
const String _TOKEN_SEPARATOR = "\uFFFF";
/**
* [ExtractLocalRefactoring] implementation.
*/
class ExtractLocalRefactoringImpl extends RefactoringImpl
implements ExtractLocalRefactoring {
final ResolveResult resolveResult;
final int selectionOffset;
final int selectionLength;
SourceRange selectionRange;
CorrectionUtils utils;
String name;
bool extractAll = true;
final List<int> coveringExpressionOffsets = <int>[];
final List<int> coveringExpressionLengths = <int>[];
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 Map<Element, int> elementIds = <Element, int>{};
Set<String> excludedVariableNames = new Set<String>();
ExtractLocalRefactoringImpl(
this.resolveResult, this.selectionOffset, this.selectionLength) {
selectionRange = new SourceRange(selectionOffset, selectionLength);
utils = new CorrectionUtils(unit, buffer: resolveResult.content);
}
String get file => resolveResult.path;
@override
String get refactoringName => 'Extract Local Variable';
CompilationUnit get unit => resolveResult.unit;
CompilationUnitElement get unitElement => unit.declaredElement;
String get _declarationKeyword {
if (_isPartOfConstantExpression(rootExpression)) {
return "const";
} else {
return "var";
}
}
@override
Future<RefactoringStatus> checkFinalConditions() {
RefactoringStatus result = new RefactoringStatus();
result.addStatus(checkName());
return new Future.value(result);
}
@override
Future<RefactoringStatus> checkInitialConditions() {
RefactoringStatus result = new RefactoringStatus();
// selection
result.addStatus(_checkSelection());
if (result.hasFatalError) {
return new Future.value(result);
}
// occurrences
_prepareOccurrences();
_prepareOffsetsLengths();
// names
excludedVariableNames =
utils.findPossibleLocalVariableConflicts(selectionOffset);
_prepareNames();
// done
return new Future.value(result);
}
@override
RefactoringStatus checkName() {
RefactoringStatus result = new RefactoringStatus();
result.addStatus(validateVariableName(name));
if (excludedVariableNames.contains(name)) {
result.addError(
format("The name '{0}' is already used in the scope.", name));
}
return result;
}
@override
Future<SourceChange> createChange() {
SourceChange change = new SourceChange(refactoringName);
// prepare occurrences
List<SourceRange> occurrences;
if (extractAll) {
occurrences = this.occurrences;
} else {
occurrences = [selectionRange];
}
occurrences.sort((a, b) => a.offset - b.offset);
// 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);
doSourceChange_addElementEdit(change, unitElement, edit);
return new Future.value(change);
}
// prepare positions
List<Position> positions = <Position>[];
int occurrencesShift = 0;
void addPosition(int offset) {
positions.add(new Position(file, offset));
}
// add variable declaration
{
String declarationCode;
int nameOffsetInDeclarationCode;
if (stringLiteralPart != null) {
declarationCode = 'var ';
nameOffsetInDeclarationCode = declarationCode.length;
declarationCode += "$name = '$stringLiteralPart';";
} else {
String keyword = _declarationKeyword;
String initializerCode = utils.getRangeText(selectionRange);
declarationCode = '$keyword ';
nameOffsetInDeclarationCode = declarationCode.length;
declarationCode += '$name = $initializerCode;';
}
// prepare location for declaration
AstNode target = _findDeclarationTarget(occurrences);
String eol = utils.endOfLine;
// insert variable declaration
if (target is Statement) {
String prefix = utils.getNodePrefix(target);
SourceEdit edit =
new SourceEdit(target.offset, 0, declarationCode + eol + prefix);
doSourceChange_addElementEdit(change, unitElement, edit);
addPosition(edit.offset + nameOffsetInDeclarationCode);
occurrencesShift = edit.replacement.length;
} else if (target is ExpressionFunctionBody) {
String prefix = utils.getNodePrefix(target.parent);
String indent = utils.getIndent(1);
Expression expr = target.expression;
{
String code = '{' + eol + prefix + indent;
addPosition(
target.offset + code.length + nameOffsetInDeclarationCode);
code += declarationCode + eol;
code += prefix + indent + 'return ';
SourceEdit edit =
new SourceEdit(target.offset, expr.offset - target.offset, code);
occurrencesShift = target.offset + code.length - expr.offset;
doSourceChange_addElementEdit(change, unitElement, edit);
}
doSourceChange_addElementEdit(
change,
unitElement,
new SourceEdit(
expr.end, target.end - expr.end, ';' + eol + prefix + '}'));
}
}
// prepare replacement
String occurrenceReplacement = name;
if (stringLiteralPart != null) {
occurrenceReplacement = "\${$name}";
occurrencesShift += 2;
}
// replace occurrences with variable reference
for (SourceRange range in occurrences) {
SourceEdit edit = newSourceEdit_range(range, occurrenceReplacement);
addPosition(range.offset + occurrencesShift);
occurrencesShift += name.length - range.length;
doSourceChange_addElementEdit(change, unitElement, edit);
}
// add the linked group
change.addLinkedEditGroup(new LinkedEditGroup(
positions,
name.length,
names
.map((name) => new LinkedEditSuggestion(
name, LinkedEditSuggestionKind.VARIABLE))
.toList()));
// done
return new Future.value(change);
}
@override
bool isAvailable() {
return !_checkSelection().hasFatalError;
}
/**
* Checks if [selectionRange] selects [Expression] which can be extracted, and
* location of this [Expression] in AST allows extracting.
*/
RefactoringStatus _checkSelection() {
if (selectionOffset <= 0) {
return new RefactoringStatus.fatal(
'The selection offset must be greater than zero.');
}
if (selectionOffset + selectionLength >= resolveResult.content.length) {
return new RefactoringStatus.fatal(
'The selection end offset must be less then the length of the file.');
}
String selectionStr;
// exclude whitespaces
{
selectionStr = utils.getRangeText(selectionRange);
int numLeading = countLeadingWhitespaces(selectionStr);
int numTrailing = countTrailingWhitespaces(selectionStr);
int offset = selectionRange.offset + numLeading;
int end = selectionRange.end - numTrailing;
selectionRange = new SourceRange(offset, end - offset);
}
// get covering node
AstNode coveringNode =
new NodeLocator(selectionRange.offset, selectionRange.end)
.searchWithin(unit);
// compute covering expressions
for (AstNode node = coveringNode; node != null; node = node.parent) {
AstNode parent = node.parent;
// skip some nodes
if (node is ArgumentList ||
node is AssignmentExpression ||
node is NamedExpression ||
node is TypeArgumentList) {
continue;
}
if (node is ConstructorName || node is Label || node is TypeName) {
rootExpression = null;
coveringExpressionOffsets.clear();
coveringExpressionLengths.clear();
continue;
}
// cannot extract the name part of a property access
if (parent is PrefixedIdentifier && parent.identifier == node ||
parent is PropertyAccess && parent.propertyName == node) {
continue;
}
// stop if not an Expression
if (node is! Expression) {
break;
}
// stop at void method invocations
if (node is MethodInvocation) {
MethodInvocation invocation = node;
Element element = invocation.methodName.staticElement;
if (element is ExecutableElement &&
element.returnType != null &&
element.returnType.isVoid) {
if (rootExpression == null) {
return new RefactoringStatus.fatal(
'Cannot extract the void expression.',
newLocation_fromNode(node));
}
break;
}
}
// fatal selection problems
if (coveringExpressionOffsets.isEmpty) {
if (node is SimpleIdentifier) {
if (node.inDeclarationContext()) {
return new RefactoringStatus.fatal(
'Cannot extract the name part of a declaration.',
newLocation_fromNode(node));
}
Element element = node.staticElement;
if (element is FunctionElement || element is MethodElement) {
continue;
}
}
if (parent is AssignmentExpression && parent.leftHandSide == node) {
return new RefactoringStatus.fatal(
'Cannot extract the left-hand side of an assignment.',
newLocation_fromNode(node));
}
}
// set selected expression
if (coveringExpressionOffsets.isEmpty) {
rootExpression = node;
}
// add the expression range
coveringExpressionOffsets.add(node.offset);
coveringExpressionLengths.add(node.length);
}
// We need an enclosing function.
// If it has a block body, we can add a new variable declaration statement
// into this block. If it has an expression body, we can convert it into
// the block body first.
if (coveringNode == null ||
coveringNode.getAncestor((node) => node is FunctionBody) == null) {
return new RefactoringStatus.fatal(
'An expression inside a function must be selected '
'to activate this refactoring.');
}
// part of string literal
if (coveringNode is StringLiteral) {
if (selectionRange.length != 0 &&
selectionRange.offset > coveringNode.offset &&
selectionRange.end < coveringNode.end) {
stringLiteralPart = selectionStr;
return new RefactoringStatus();
}
}
// single node selected
if (rootExpression != null) {
singleExpression = rootExpression;
selectionRange = range.node(singleExpression);
wholeStatementExpression = singleExpression.parent is ExpressionStatement;
return new RefactoringStatus();
}
// invalid selection
return new RefactoringStatus.fatal(
'Expression must be selected to activate this refactoring.');
}
/**
* Return an unique identifier for the given [Element], or `null` if [element]
* is `null`.
*/
int _encodeElement(Element element) {
if (element == null) {
return null;
}
int id = elementIds[element];
if (id == null) {
id = elementIds.length;
elementIds[element] = id;
}
return id;
}
/**
* Returns an [Element]-sensitive encoding of [tokens].
* Each [Token] with a [LocalVariableElement] has a suffix of the element id.
*
* So, we can distinguish different local variables with the same name, if
* there are multiple variables with the same name are declared in the
* function we are searching occurrences in.
*/
String _encodeExpressionTokens(Expression expr, List<Token> tokens) {
// no expression, i.e. a part of a string
if (expr == null) {
return tokens.join(_TOKEN_SEPARATOR);
}
// prepare Token -> LocalElement map
Map<Token, Element> map = new HashMap<Token, Element>(
equals: (Token a, Token b) => a.lexeme == b.lexeme,
hashCode: (Token t) => t.lexeme.hashCode);
expr.accept(new _TokenLocalElementVisitor(map));
// map and join tokens
return tokens.map((Token token) {
String tokenString = token.lexeme;
// append token's Element id
Element element = map[token];
if (element != null) {
int elementId = _encodeElement(element);
if (elementId != null) {
tokenString += '-$elementId';
}
}
// done
return tokenString;
}).join(_TOKEN_SEPARATOR);
}
/**
* Return the [AstNode] to defined the variable before.
* It should be accessible by all the given [occurrences].
*/
AstNode _findDeclarationTarget(List<SourceRange> occurrences) {
List<AstNode> nodes = _findNodes(occurrences);
AstNode commonParent = getNearestCommonAncestor(nodes);
// Block
if (commonParent is Block) {
List<AstNode> firstParents = getParents(nodes[0]);
int commonIndex = firstParents.indexOf(commonParent);
return firstParents[commonIndex + 1];
}
// ExpressionFunctionBody
AstNode expressionBody = _getEnclosingExpressionBody(commonParent);
if (expressionBody != null) {
return expressionBody;
}
// single Statement
AstNode target = commonParent.getAncestor((node) => node is Statement);
while (target.parent is! Block) {
target = target.parent;
}
return target;
}
/**
* 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(range.offset).searchWithin(unit);
nodes.add(node);
}
return nodes;
}
/**
* Returns the [ExpressionFunctionBody] that encloses [node], or `null`
* if [node] is not enclosed with an [ExpressionFunctionBody].
*/
ExpressionFunctionBody _getEnclosingExpressionBody(AstNode node) {
while (node != null) {
if (node is Statement) {
return null;
}
if (node is ExpressionFunctionBody) {
return node;
}
node = node.parent;
}
return null;
}
/**
* Checks 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.isConst;
}
if (node is InstanceCreationExpression) {
return node.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 _prepareNames() {
names.clear();
if (stringLiteralPart != null) {
names.addAll(getVariableNameSuggestionsForText(
stringLiteralPart, excludedVariableNames));
} else if (singleExpression != null) {
names.addAll(getVariableNameSuggestionsForExpression(
singleExpression.staticType,
singleExpression,
excludedVariableNames));
}
}
/**
* Prepares all occurrences of the source which matches given selection,
* sorted by offsets.
*/
void _prepareOccurrences() {
occurrences.clear();
elementIds.clear();
// prepare selection
String selectionSource;
{
String rawSelectionSource = utils.getRangeText(selectionRange);
List<Token> selectionTokens = TokenUtils.getTokens(rawSelectionSource);
selectionSource =
_encodeExpressionTokens(rootExpression, selectionTokens);
}
// prepare enclosing function
AstNode enclosingFunction;
{
AstNode selectionNode =
new NodeLocator(selectionOffset).searchWithin(unit);
enclosingFunction = getEnclosingExecutableNode(selectionNode);
}
// visit function
enclosingFunction
.accept(new _OccurrencesVisitor(this, occurrences, selectionSource));
}
void _prepareOffsetsLengths() {
offsets.clear();
lengths.clear();
for (SourceRange occurrence in occurrences) {
offsets.add(occurrence.offset);
lengths.add(occurrence.length);
}
}
}
/**
* [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.',
newLocation_fromNode(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.staticElement;
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 [location].
*/
void _invalidSelection(String message, Location location) {
status.addFatalError(message, location);
reset();
}
bool _isFirstSelectedNode(AstNode node) => node == firstSelectedNode;
}
class _HasStatementVisitor extends GeneralizingAstVisitor {
bool result = false;
_HasStatementVisitor();
@override
visitStatement(Statement node) {
result = true;
}
}
class _OccurrencesVisitor extends GeneralizingAstVisitor<Object> {
final ExtractLocalRefactoringImpl ref;
final List<SourceRange> occurrences;
final String selectionSource;
_OccurrencesVisitor(this.ref, this.occurrences, this.selectionSource);
@override
Object visitBinaryExpression(BinaryExpression node) {
if (!_hasStatements(node)) {
_tryToFindOccurrenceFragments(node);
return null;
}
return super.visitBinaryExpression(node);
}
@override
Object visitExpression(Expression node) {
if (ref._isExtractable(range.node(node))) {
_tryToFindOccurrence(node);
}
return super.visitExpression(node);
}
@override
Object visitStringLiteral(StringLiteral node) {
if (ref.stringLiteralPart != null) {
int length = ref.stringLiteralPart.length;
String value = ref.utils.getNodeText(node);
int lastIndex = 0;
while (true) {
int index = value.indexOf(ref.stringLiteralPart, lastIndex);
if (index == -1) {
break;
}
lastIndex = index + length;
int start = node.offset + index;
SourceRange range = new SourceRange(start, length);
occurrences.add(range);
}
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) {
_HasStatementVisitor visitor = new _HasStatementVisitor();
root.accept(visitor);
return visitor.result;
}
void _tryToFindOccurrence(Expression node) {
String nodeSource = ref.utils.getNodeText(node);
List<Token> nodeTokens = TokenUtils.getTokens(nodeSource);
nodeSource = ref._encodeExpressionTokens(node, nodeTokens);
if (nodeSource == selectionSource) {
_addOccurrence(range.node(node));
}
}
void _tryToFindOccurrenceFragments(Expression node) {
int nodeOffset = node.offset;
String nodeSource = ref.utils.getNodeText(node);
List<Token> nodeTokens = TokenUtils.getTokens(nodeSource);
nodeSource = ref._encodeExpressionTokens(node, nodeTokens);
// 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 start = nodeOffset + startToken.offset;
int end = nodeOffset + endToken.end;
_addOccurrence(range.startOffsetEndOffset(start, end));
}
}
}
class _TokenLocalElementVisitor extends RecursiveAstVisitor {
final Map<Token, Element> map;
_TokenLocalElementVisitor(this.map);
visitSimpleIdentifier(SimpleIdentifier node) {
Element element = node.staticElement;
if (element is LocalVariableElement) {
map[node.token] = element;
}
}
}