blob: 2432c5bb72c3fa2034a9cb6ff8121e15bcc5069a [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: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;
late SourceRange selectionRange;
late CorrectionUtils utils;
late String name;
bool extractAll = true;
@override
final List<int> coveringExpressionOffsets = <int>[];
@override
final List<int> coveringExpressionLengths = <int>[];
@override
final List<String> names = <String>[];
@override
final List<int> offsets = <int>[];
@override
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 = <String>{};
ExtractLocalRefactoringImpl(
this.resolveResult, this.selectionOffset, this.selectionLength) {
selectionRange = SourceRange(selectionOffset, selectionLength);
utils = 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() {
var result = RefactoringStatus();
result.addStatus(checkName());
return Future.value(result);
}
@override
Future<RefactoringStatus> checkInitialConditions() {
var result = RefactoringStatus();
// selection
result.addStatus(_checkSelection());
if (result.hasFatalError) {
return Future.value(result);
}
// occurrences
_prepareOccurrences();
_prepareOffsetsLengths();
// names
excludedVariableNames =
utils.findPossibleLocalVariableConflicts(selectionOffset);
_prepareNames();
// done
return Future.value(result);
}
@override
RefactoringStatus checkName() {
var result = 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() {
var change = SourceChange(refactoringName);
// prepare occurrences
late final 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.
final singleExpression = this.singleExpression;
if (singleExpression != null &&
singleExpression.parent is ExpressionStatement &&
occurrences.length == 1) {
var keyword = _declarationKeyword;
var declarationSource = '$keyword $name = ';
var edit = SourceEdit(singleExpression.offset, 0, declarationSource);
doSourceChange_addElementEdit(change, unitElement, edit);
return Future.value(change);
}
// prepare positions
var positions = <Position>[];
var occurrencesShift = 0;
void addPosition(int offset) {
positions.add(Position(file, offset));
}
// add variable declaration
{
String declarationCode;
int nameOffsetInDeclarationCode;
if (stringLiteralPart != null) {
declarationCode = 'var ';
nameOffsetInDeclarationCode = declarationCode.length;
declarationCode += "$name = '$stringLiteralPart';";
} else {
var keyword = _declarationKeyword;
var initializerCode = utils.getRangeText(selectionRange);
declarationCode = '$keyword ';
nameOffsetInDeclarationCode = declarationCode.length;
declarationCode += '$name = $initializerCode;';
}
// prepare location for declaration
var target = _findDeclarationTarget(occurrences);
var eol = utils.endOfLine;
// insert variable declaration
if (target is Statement) {
var prefix = utils.getNodePrefix(target);
var edit = 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) {
var prefix = utils.getNodePrefix(target.parent!);
var indent = utils.getIndent(1);
var expr = target.expression;
{
var code = '{' + eol + prefix + indent;
addPosition(
target.offset + code.length + nameOffsetInDeclarationCode);
code += declarationCode + eol;
code += prefix + indent + 'return ';
var edit =
SourceEdit(target.offset, expr.offset - target.offset, code);
occurrencesShift = target.offset + code.length - expr.offset;
doSourceChange_addElementEdit(change, unitElement, edit);
}
doSourceChange_addElementEdit(
change,
unitElement,
SourceEdit(
expr.end, target.end - expr.end, ';' + eol + prefix + '}'));
}
}
// prepare replacement
var occurrenceReplacement = name;
if (stringLiteralPart != null) {
occurrenceReplacement = '\${$name}';
occurrencesShift += 2;
}
// replace occurrences with variable reference
for (var range in occurrences) {
var 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(LinkedEditGroup(
positions,
name.length,
names
.map((name) =>
LinkedEditSuggestion(name, LinkedEditSuggestionKind.VARIABLE))
.toList()));
// done
return 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 RefactoringStatus.fatal(
'The selection offset must be greater than zero.');
}
if (selectionOffset + selectionLength >= resolveResult.content!.length) {
return RefactoringStatus.fatal(
'The selection end offset must be less then the length of the file.');
}
var selectionStr = utils.getRangeText(selectionRange);
// exclude whitespaces
{
var numLeading = countLeadingWhitespaces(selectionStr);
var numTrailing = countTrailingWhitespaces(selectionStr);
var offset = selectionRange.offset + numLeading;
var end = selectionRange.end - numTrailing;
selectionRange = SourceRange(offset, end - offset);
}
// get covering node
var coveringNode = 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 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 RefactoringStatus();
}
}
// compute covering expressions
for (var node = coveringNode; node != null; node = node.parent) {
var 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) {
var invocation = node;
var element = invocation.methodName.staticElement;
if (element is ExecutableElement && element.returnType.isVoid) {
if (singleExpression == null) {
return 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 RefactoringStatus.fatal(
'Cannot extract the name part of a declaration.',
newLocation_fromNode(node));
}
var element = node.staticElement;
if (element is FunctionElement || element is MethodElement) {
continue;
}
}
if (parent is AssignmentExpression && parent.leftHandSide == node) {
return RefactoringStatus.fatal(
'Cannot extract the left-hand side of an assignment.',
newLocation_fromNode(node));
}
}
// set selected expression
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 RefactoringStatus();
}
// invalid selection
return 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;
}
var 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) {
// prepare Token -> LocalElement map
Map<Token, Element> map = HashMap<Token, Element>(
equals: (Token a, Token b) => a.lexeme == b.lexeme,
hashCode: (Token t) => t.lexeme.hashCode);
expr.accept(_TokenLocalElementVisitor(map));
// map and join tokens
var result = tokens.map((Token token) {
var tokenString = token.lexeme;
// append token's Element id
var element = map[token];
if (element != null) {
var 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) {
var nodes = _findNodes(occurrences);
var commonParent = getNearestCommonAncestor(nodes);
// Block
if (commonParent is Block) {
var firstParents = getParents(nodes[0]);
var commonIndex = firstParents.indexOf(commonParent);
return firstParents[commonIndex + 1];
}
// ExpressionFunctionBody
var expressionBody = _getEnclosingExpressionBody(commonParent);
if (expressionBody != null) {
return expressionBody;
}
// single Statement
AstNode? target = commonParent?.thisOrAncestorOfType<Statement>();
while (target != null) {
var parent = target.parent;
if (parent is Block) {
break;
}
target = parent;
}
return target;
}
/// Returns [AstNode]s at the offsets of the given [SourceRange]s.
List<AstNode> _findNodes(List<SourceRange> ranges) {
var nodes = <AstNode>[];
for (var range in ranges) {
var node = 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 == null) {
return false;
}
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();
final stringLiteralPart = this.stringLiteralPart;
final singleExpression = this.singleExpression;
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;
final singleExpression = this.singleExpression;
if (singleExpression != null) {
var tokens = TokenUtils.getNodeTokens(singleExpression);
selectionSource = _encodeExpressionTokens(singleExpression, tokens);
}
// visit function
coveringFunctionBody!.accept(_OccurrencesVisitor(
this, occurrences, selectionSource, unit.featureSet));
}
void _prepareOffsetsLengths() {
offsets.clear();
lengths.clear();
for (var 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) {
var stringLiteralPart = ref.stringLiteralPart;
if (stringLiteralPart != null) {
var length = stringLiteralPart.length;
var value = ref.utils.getNodeText(node);
var lastIndex = 0;
while (true) {
var index = value.indexOf(stringLiteralPart, lastIndex);
if (index == -1) {
break;
}
lastIndex = index + length;
var start = node.offset + index;
var range = 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<void> {
final Map<Token, Element> map;
_TokenLocalElementVisitor(this.map);
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var element = node.staticElement;
if (element is LocalVariableElement) {
map[node.token] = element;
}
}
}