blob: 64c7b28feb85547b33cd2a7ff7692211f4d0ed14 [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/status.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analysis_server/src/services/linter/lint_names.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/utilities/strings.dart';
import 'package:analyzer/dart/analysis/features.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 ResolvedUnitResult 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>[];
FunctionBody coveringFunctionBody;
Expression singleExpression;
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(resolveResult);
}
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(singleExpression)) {
return "const";
} else if (_isLintEnabled(LintNames.prefer_final_locals)) {
return "final";
} 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 (singleExpression?.parent is ExpressionStatement &&
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.');
}
var selectionStr = utils.getRangeText(selectionRange);
// exclude whitespaces
{
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);
// 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.
coveringFunctionBody = coveringNode?.thisOrAncestorOfType<FunctionBody>();
if (coveringFunctionBody == 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();
}
}
// 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) {
singleExpression = 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 (singleExpression == 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 (singleExpression == null) {
singleExpression = node;
}
// add the expression range
coveringExpressionOffsets.add(node.offset);
coveringExpressionLengths.add(node.length);
}
// single node selected
if (singleExpression != null) {
selectionRange = range.node(singleExpression);
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
var result = 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 result + _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.thisOrAncestorOfType<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;
}
bool _isLintEnabled(String name) {
var analysisOptions = unitElement.context.analysisOptions;
return analysisOptions.isLintEnabled(name);
}
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;
if (singleExpression != null) {
var tokens = TokenUtils.getNodeTokens(singleExpression);
selectionSource = _encodeExpressionTokens(singleExpression, tokens);
}
// visit function
coveringFunctionBody.accept(new _OccurrencesVisitor(
this, occurrences, selectionSource, unit.featureSet));
}
void _prepareOffsetsLengths() {
offsets.clear();
lengths.clear();
for (SourceRange occurrence in occurrences) {
offsets.add(occurrence.offset);
lengths.add(occurrence.length);
}
}
}
class _OccurrencesVisitor extends GeneralizingAstVisitor<void> {
final ExtractLocalRefactoringImpl ref;
final List<SourceRange> occurrences;
final String selectionSource;
final FeatureSet featureSet;
_OccurrencesVisitor(
this.ref, this.occurrences, this.selectionSource, this.featureSet);
@override
void visitExpression(Expression node) {
_tryToFindOccurrence(node);
super.visitExpression(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var parent = node.parent;
if (parent is VariableDeclaration && parent.name == node ||
parent is AssignmentExpression && parent.leftHandSide == node) {
return;
}
super.visitSimpleIdentifier(node);
}
@override
void 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;
}
visitExpression(node);
}
void _addOccurrence(SourceRange range) {
if (range.intersects(ref.selectionRange)) {
occurrences.add(ref.selectionRange);
} else {
occurrences.add(range);
}
}
void _tryToFindOccurrence(Expression node) {
var nodeTokens = TokenUtils.getNodeTokens(node);
var nodeSource = ref._encodeExpressionTokens(node, nodeTokens);
if (nodeSource == selectionSource) {
_addOccurrence(range.node(node));
}
}
}
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;
}
}
}