/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.engine.services.internal.refactoring;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.dart.engine.ast.AssignmentExpression;
import com.google.dart.engine.ast.AstNode;
import com.google.dart.engine.ast.Block;
import com.google.dart.engine.ast.ClassDeclaration;
import com.google.dart.engine.ast.CompilationUnit;
import com.google.dart.engine.ast.ConstructorInitializer;
import com.google.dart.engine.ast.DoStatement;
import com.google.dart.engine.ast.Expression;
import com.google.dart.engine.ast.ExpressionFunctionBody;
import com.google.dart.engine.ast.ForEachStatement;
import com.google.dart.engine.ast.ForStatement;
import com.google.dart.engine.ast.FunctionExpression;
import com.google.dart.engine.ast.MethodDeclaration;
import com.google.dart.engine.ast.MethodInvocation;
import com.google.dart.engine.ast.ReturnStatement;
import com.google.dart.engine.ast.SimpleIdentifier;
import com.google.dart.engine.ast.Statement;
import com.google.dart.engine.ast.SwitchMember;
import com.google.dart.engine.ast.VariableDeclaration;
import com.google.dart.engine.ast.WhileStatement;
import com.google.dart.engine.ast.visitor.GeneralizingAstVisitor;
import com.google.dart.engine.ast.visitor.RecursiveAstVisitor;
import com.google.dart.engine.element.ClassElement;
import com.google.dart.engine.element.ElementKind;
import com.google.dart.engine.element.VariableElement;
import com.google.dart.engine.scanner.Token;
import com.google.dart.engine.services.assist.AssistContext;
import com.google.dart.engine.services.change.Change;
import com.google.dart.engine.services.change.Edit;
import com.google.dart.engine.services.change.SourceChange;
import com.google.dart.engine.services.internal.correction.CorrectionUtils;
import com.google.dart.engine.services.internal.util.TokenUtils;
import com.google.dart.engine.services.refactoring.ExtractMethodRefactoring;
import com.google.dart.engine.services.refactoring.NamingConventions;
import com.google.dart.engine.services.refactoring.Parameter;
import com.google.dart.engine.services.refactoring.ProgressMonitor;
import com.google.dart.engine.services.refactoring.SubProgressMonitor;
import com.google.dart.engine.services.status.RefactoringStatus;
import com.google.dart.engine.type.Type;
import com.google.dart.engine.utilities.source.SourceRange;
import com.google.dart.engine.utilities.source.SourceRangeFactory;
import org.apache.commons.lang3.StringUtils;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Implementation of {@link ExtractMethodRefactoring}.
*/
public class ExtractMethodRefactoringImpl extends RefactoringImpl implements
ExtractMethodRefactoring {
/**
* Description of the single occurrence of the selected expression or set of statements.
*/
private static class Occurrence {
final SourceRange range;
final boolean isSelection;
final Map<String, String> parameterOldToOccurrenceName = Maps.newHashMap();
public Occurrence(SourceRange range, boolean isSelection) {
this.range = range;
this.isSelection = isSelection;
}
}
/**
* Generalized version of some source, in which references to the specific variables are replaced
* with pattern variables, with back mapping from pattern to original variable names.
*/
private static class SourcePattern {
final Map<String, String> originalToPatternNames = Maps.newHashMap();
String patternSource;
}
private static final String TOKEN_SEPARATOR = "\uFFFF";
/**
* @return the "normalized" version of the given source, which is built form tokens, so ignores
* all comments and spaces.
*/
private static String getNormalizedSource(String s) {
List<Token> selectionTokens = TokenUtils.getTokens(s);
return StringUtils.join(selectionTokens, TOKEN_SEPARATOR);
}
/**
* @return {@code true} if the given {@link AstNode} has {@link MethodInvocation}.
*/
private static boolean hasMethodInvocation(AstNode node) {
final boolean[] result = {false};
node.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitMethodInvocation(MethodInvocation node) {
result[0] = true;
return null;
}
});
return result[0];
}
/**
* @return <code>true</code> if given {@link DartNode} is left hand side of assignment, or
* declaration of the variable.
*/
private static boolean isLeftHandOfAssignment(SimpleIdentifier node) {
if (node.inSetterContext()) {
return true;
}
return node.getParent() instanceof VariableDeclaration
&& ((VariableDeclaration) node.getParent()).getName() == node;
}
private final AssistContext context;
private final SourceRange selectionRange;
private final CompilationUnit unitNode;
private final CorrectionUtils utils;
private String methodName;
private boolean replaceAllOccurrences = true;
private boolean extractGetter;
private ExtractMethodAnalyzer selectionAnalyzer;
private final Set<String> usedNames = Sets.newHashSet();
private final List<Parameter> parameters = Lists.newArrayList();
private final Map<String, Parameter> parametersMap = Maps.newHashMap();
private final Map<String, List<SourceRange>> parameterReferencesMap = Maps.newHashMap();
private Type returnType;
private String returnVariableName;
private AstNode parentMember;
private Expression selectionExpression;
private FunctionExpression selectionFunctionExpression;
private List<Statement> selectionStatements;
private final List<Occurrence> occurrences = Lists.newArrayList();
private boolean staticContext;
public ExtractMethodRefactoringImpl(AssistContext context) throws Exception {
this.context = context;
this.selectionRange = context.getSelectionRange();
this.unitNode = context.getCompilationUnit();
this.utils = new CorrectionUtils(unitNode);
}
@Override
public boolean canExtractGetter() {
if (!parameters.isEmpty()) {
return false;
}
if (selectionExpression != null) {
if (selectionExpression instanceof AssignmentExpression) {
return false;
}
}
if (selectionStatements != null) {
return returnType != null;
}
return true;
}
@Override
public RefactoringStatus checkFinalConditions(ProgressMonitor pm) throws Exception {
pm = checkProgressMonitor(pm);
pm.beginTask("Checking final conditions", 3);
try {
RefactoringStatus result = new RefactoringStatus();
// name
result.merge(NamingConventions.validateMethodName(methodName).escalateErrorToFatal());
pm.worked(1);
// parameters
result.merge(checkParameterNames());
pm.worked(1);
// conflicts
result.merge(checkPossibleConflicts(new SubProgressMonitor(pm, 1)));
// done
return result;
} finally {
pm.done();
}
}
@Override
public RefactoringStatus checkInitialConditions(ProgressMonitor pm) throws Exception {
pm = checkProgressMonitor(pm);
pm.beginTask("Checking initial conditions", 2);
try {
RefactoringStatus result = new RefactoringStatus();
// selection
result.merge(checkSelection());
if (result.hasFatalError()) {
return result;
}
pm.worked(1);
// prepare parts
result.merge(initializeParameters());
initializeOccurrences();
initializeGetter();
pm.worked(1);
// closure cannot have parameters
if (selectionFunctionExpression != null && !parameters.isEmpty()) {
String message = MessageFormat.format(
"Cannot extract closure as method, it references {0} external variable(s).",
parameters.size());
return RefactoringStatus.createFatalErrorStatus(message);
}
// done
return result;
} finally {
pm.done();
}
}
@Override
public RefactoringStatus checkMethodName() {
return NamingConventions.validateMethodName(methodName);
}
@Override
public RefactoringStatus checkParameterNames() {
RefactoringStatus result = new RefactoringStatus();
for (Parameter parameter : parameters) {
result.merge(NamingConventions.validateParameterName(parameter.getNewName()));
for (Parameter other : parameters) {
if (parameter != other && StringUtils.equals(other.getNewName(), parameter.getNewName())) {
result.addError(MessageFormat.format(
"Parameter ''{0}'' already exists",
other.getNewName()));
return result;
}
}
if (parameter.isRenamed() && usedNames.contains(parameter.getNewName())) {
result.addError(MessageFormat.format(
"''{0}'' is already used as a name in the selected code",
parameter.getNewName()));
return result;
}
}
return result;
}
@Override
public Change createChange(ProgressMonitor pm) throws Exception {
pm = checkProgressMonitor(pm);
pm.beginTask("Creating change", 1 + occurrences.size());
try {
SourceChange change = new SourceChange(getRefactoringName(), context.getSource());
// replace occurrences with method invocation
for (Occurrence occurence : occurrences) {
pm.worked(1);
SourceRange range = occurence.range;
// may be replacement of duplicates disabled
if (!replaceAllOccurrences && !occurence.isSelection) {
continue;
}
// prepare invocation source
String invocationSource;
if (selectionFunctionExpression != null) {
invocationSource = methodName;
} else {
StringBuilder sb = new StringBuilder();
// may be returns value
if (returnType != null) {
String returnTypeName = utils.getTypeSource(returnType);
// single variable assignment / return statement
if (returnVariableName != null) {
String occurrenceName = occurence.parameterOldToOccurrenceName.get(returnVariableName);
// may be declare variable
if (!parametersMap.containsKey(returnVariableName)) {
if (returnTypeName.equals("dynamic")) {
sb.append("var ");
} else {
sb.append(returnTypeName);
sb.append(" ");
}
}
// assign the return value
sb.append(occurrenceName);
sb.append(" = ");
} else {
sb.append("return ");
}
}
// invocation itself
sb.append(methodName);
if (!extractGetter) {
sb.append("(");
boolean firstParameter = true;
for (Parameter parameter : parameters) {
// may be comma
if (firstParameter) {
firstParameter = false;
} else {
sb.append(", ");
}
// argument name
{
String parameterOldName = parameter.getOldName();
String argumentName = occurence.parameterOldToOccurrenceName.get(parameterOldName);
sb.append(argumentName);
}
}
sb.append(")");
}
invocationSource = sb.toString();
// statements as extracted with their ";", so add new one after invocation
if (selectionStatements != null) {
invocationSource += ";";
}
}
// add replace edit
Edit edit = new Edit(range, invocationSource);
String msg = MessageFormat.format(occurence.isSelection
? "Substitute statements with call to ''{0}''"
: "Replace duplicate code fragment with call to ''{0}''", methodName);
change.addEdit(edit, msg);
}
// add method declaration
{
// prepare environment
String prefix = utils.getNodePrefix(parentMember);
String eol = utils.getEndOfLine();
// prepare annotations
String annotations = "";
{
// may be "static"
if (staticContext) {
annotations = "static ";
}
}
// prepare declaration source
String declarationSource = null;
{
String returnExpressionSource = getMethodBodySource();
// closure
if (selectionFunctionExpression != null) {
declarationSource = methodName + returnExpressionSource;
if (selectionFunctionExpression.getBody() instanceof ExpressionFunctionBody) {
declarationSource += ";";
}
}
// expression
if (selectionExpression != null) {
// add return type
String returnTypeName = utils.getTypeSource(selectionExpression);
if (returnTypeName != null && !returnTypeName.equals("dynamic")) {
annotations += returnTypeName + " ";
}
// just return expression
declarationSource = annotations + getSignature() + " => " + returnExpressionSource
+ ";";
}
// statements
if (selectionStatements != null) {
if (returnType != null) {
String returnTypeName = utils.getTypeSource(returnType);
if (returnTypeName != null && !returnTypeName.equals("dynamic")) {
annotations += returnTypeName + " ";
}
} else {
annotations += "void ";
}
declarationSource = annotations + getSignature() + " {" + eol;
declarationSource += returnExpressionSource;
if (returnVariableName != null) {
declarationSource += prefix + " return " + returnVariableName + ";" + eol;
}
declarationSource += prefix + "}";
}
}
// insert declaration
if (declarationSource != null) {
int offset = parentMember.getEnd();
Edit edit = new Edit(offset, 0, eol + eol + prefix + declarationSource);
String msg = MessageFormat.format(selectionExpression != null
? "Create new method ''{0}'' from selected expression"
: "Create new method ''{0}'' from selected statements", methodName);
change.addEdit(edit, msg);
}
}
pm.worked(1);
// done
return change;
} finally {
pm.done();
}
}
@Override
public boolean getExtractGetter() {
return extractGetter;
}
@Override
public int getNumberOfOccurrences() {
return occurrences.size();
}
@Override
public Parameter[] getParameters() {
return parameters.toArray(new Parameter[parameters.size()]);
}
@Override
public String getRefactoringName() {
AstNode coveringNode = context.getCoveringNode();
if (coveringNode != null && coveringNode.getAncestor(ClassDeclaration.class) != null) {
return "Extract Method";
}
return "Extract Function";
}
@Override
public boolean getReplaceAllOccurrences() {
return replaceAllOccurrences;
}
@Override
public String getSignature() {
StringBuilder sb = new StringBuilder();
if (extractGetter) {
sb.append("get ");
sb.append(methodName);
} else {
sb.append(methodName);
sb.append("(");
// add all parameters
boolean firstParameter = true;
for (Parameter parameter : parameters) {
// may be comma
if (firstParameter) {
firstParameter = false;
} else {
sb.append(", ");
}
// type
{
String typeSource = parameter.getNewTypeName();
if (!"dynamic".equals(typeSource) && !"".equals(typeSource)) {
sb.append(typeSource);
sb.append(" ");
}
}
// name
sb.append(parameter.getNewName());
}
sb.append(")");
}
// done
return sb.toString();
}
@Override
public void setExtractGetter(boolean extractGetter) {
this.extractGetter = extractGetter;
}
@Override
public void setMethodName(String methodName) {
this.methodName = methodName;
}
@Override
public void setParameters(Parameter[] parameters) {
this.parameters.clear();
Collections.addAll(this.parameters, parameters);
}
@Override
public void setReplaceAllOccurrences(boolean replaceAllOccurrences) {
this.replaceAllOccurrences = replaceAllOccurrences;
}
/**
* Adds a new reference to the parameter with the given name.
*/
private void addParameterReference(String name, SourceRange range) {
List<SourceRange> references = parameterReferencesMap.get(name);
if (references == null) {
references = Lists.newArrayList();
parameterReferencesMap.put(name, references);
}
references.add(range);
}
/**
* Checks if created method will shadow or will be shadowed by other elements.
*/
private RefactoringStatus checkPossibleConflicts(ProgressMonitor pm) throws Exception {
final RefactoringStatus result = new RefactoringStatus();
// top-level function
if (parentMember.getParent() instanceof CompilationUnit) {
CompilationUnit unit = (CompilationUnit) parentMember.getParent();
RenameUnitMemberValidator validator = new RenameUnitMemberValidator(
context.getSearchEngine(),
unit.getElement(),
ElementKind.FUNCTION,
methodName);
result.merge(validator.validate(pm, false));
}
// method of class
if (parentMember.getParent() instanceof ClassDeclaration) {
ClassDeclaration classDeclaration = (ClassDeclaration) parentMember.getParent();
final ClassElement classElement = classDeclaration.getElement();
RenameClassMemberValidator validator = new RenameClassMemberValidator(
context.getSearchEngine(),
ElementKind.METHOD,
classElement,
null,
methodName);
result.merge(validator.validate(pm, false));
}
// OK
return result;
}
/**
* Checks if {@link #selectionRange} selects {@link Expression} which can be extracted, and
* location of this {@link DartExpression} in AST allows extracting.
*/
private RefactoringStatus checkSelection() {
selectionAnalyzer = new ExtractMethodAnalyzer(utils, selectionRange);
unitNode.accept(selectionAnalyzer);
// may be fatal error
{
RefactoringStatus status = selectionAnalyzer.getStatus();
if (status.hasFatalError()) {
return status;
}
}
// check selected nodes
List<AstNode> selectedNodes = selectionAnalyzer.getSelectedNodes();
if (!selectedNodes.isEmpty()) {
AstNode coveringNode = selectionAnalyzer.getCoveringNode();
parentMember = CorrectionUtils.getEnclosingClassOrUnitMember(coveringNode);
// single expression selected
if (selectedNodes.size() == 1
&& !utils.selectionIncludesNonWhitespaceOutsideNode(
selectionRange,
selectionAnalyzer.getFirstSelectedNode())) {
AstNode selectedNode = selectionAnalyzer.getFirstSelectedNode();
if (selectedNode instanceof Expression) {
selectionExpression = (Expression) selectedNode;
// additional check for closure
if (selectionExpression instanceof FunctionExpression) {
selectionFunctionExpression = (FunctionExpression) selectionExpression;
selectionExpression = null;
}
// OK
return new RefactoringStatus();
}
}
// statements selected
{
List<Statement> selectedStatements = Lists.newArrayList();
for (AstNode selectedNode : selectedNodes) {
if (selectedNode instanceof Statement) {
selectedStatements.add((Statement) selectedNode);
}
}
if (selectedStatements.size() == selectedNodes.size()) {
selectionStatements = selectedStatements;
return new RefactoringStatus();
}
}
}
// invalid selection
return RefactoringStatus.createFatalErrorStatus("Can only extract a single expression or a set of statements.");
}
/**
* @return the selected {@link DartExpression} source, with applying new parameter names.
*/
private String getMethodBodySource() {
String source = utils.getText(selectionRange);
// prepare ReplaceEdit operations to replace variables with parameters
List<Edit> replaceEdits = Lists.newArrayList();
for (Parameter parameter : parametersMap.values()) {
List<SourceRange> ranges = parameterReferencesMap.get(parameter.getOldName());
if (ranges != null) {
for (SourceRange range : ranges) {
replaceEdits.add(new Edit(
range.getOffset() - selectionRange.getOffset(),
range.getLength(),
parameter.getNewName()));
}
}
}
// apply replacements
source = CorrectionUtils.applyReplaceEdits(source, replaceEdits);
// change indentation
if (selectionFunctionExpression != null) {
AstNode baseNode = selectionFunctionExpression.getAncestor(Statement.class);
if (baseNode != null) {
String baseIndent = utils.getNodePrefix(baseNode);
String targetIndent = utils.getNodePrefix(parentMember);
source = utils.getIndentSource(source, baseIndent, targetIndent);
source = source.trim();
}
}
if (selectionStatements != null) {
String selectionIndent = utils.getNodePrefix(selectionStatements.get(0));
String targetIndent = utils.getNodePrefix(parentMember) + " ";
source = utils.getIndentSource(source, selectionIndent, targetIndent);
}
// done
return source;
}
private SourcePattern getSourcePattern(final SourceRange partRange) {
String originalSource = utils.getText(partRange.getOffset(), partRange.getLength());
final SourcePattern pattern = new SourcePattern();
final List<Edit> replaceEdits = Lists.newArrayList();
unitNode.accept(new GeneralizingAstVisitor<Void>() {
@Override
public Void visitSimpleIdentifier(SimpleIdentifier node) {
SourceRange nodeRange = SourceRangeFactory.rangeNode(node);
if (partRange.covers(nodeRange)) {
VariableElement variableElement = CorrectionUtils.getLocalOrParameterVariableElement(node);
if (variableElement != null) {
// name of the named expression
if (CorrectionUtils.isNamedExpressionName(node)) {
return null;
}
// continue
String originalName = variableElement.getDisplayName();
String patternName = pattern.originalToPatternNames.get(originalName);
if (patternName == null) {
patternName = "__dartEditorVariable" + pattern.originalToPatternNames.size();
pattern.originalToPatternNames.put(originalName, patternName);
}
replaceEdits.add(new Edit(
nodeRange.getOffset() - partRange.getOffset(),
nodeRange.getLength(),
patternName));
}
}
return null;
}
});
pattern.patternSource = CorrectionUtils.applyReplaceEdits(originalSource, replaceEdits);
return pattern;
}
/**
* Initializes {@link #extractGetter} flag.
*/
private void initializeGetter() {
extractGetter = false;
// may be we cannot at all
if (!canExtractGetter()) {
return;
}
// OK, just expression
if (selectionExpression != null) {
extractGetter = !hasMethodInvocation(selectionExpression);
return;
}
// allow code blocks without cycles
if (selectionStatements != null) {
extractGetter = true;
for (Statement statement : selectionStatements) {
// method is something heavy, so we don't want to extract it as part of getter
if (hasMethodInvocation(statement)) {
extractGetter = false;
return;
}
// don't allow cycles
statement.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitDoStatement(DoStatement node) {
extractGetter = false;
return super.visitDoStatement(node);
}
@Override
public Void visitForEachStatement(ForEachStatement node) {
extractGetter = false;
return super.visitForEachStatement(node);
}
@Override
public Void visitForStatement(ForStatement node) {
extractGetter = false;
return super.visitForStatement(node);
}
@Override
public Void visitWhileStatement(WhileStatement node) {
extractGetter = false;
return super.visitWhileStatement(node);
}
});
}
}
}
/**
* Fills {@link #occurrences} field.
*/
private void initializeOccurrences() {
// prepare selection
SourcePattern selectionPattern = getSourcePattern(selectionRange);
final String selectionSource = getNormalizedSource(selectionPattern.patternSource);
final Map<String, String> patternToSelectionName = HashBiMap.create(
selectionPattern.originalToPatternNames).inverse();
// prepare an enclosing parent - class or unit
AstNode enclosingMemberParent = parentMember.getParent();
// visit nodes which will able to access extracted method
enclosingMemberParent.accept(new GeneralizingAstVisitor<Void>() {
boolean forceStatic = false;
@Override
public Void visitBlock(Block node) {
if (selectionStatements != null) {
visitStatements(node.getStatements());
}
return super.visitBlock(node);
}
@Override
public Void visitConstructorInitializer(ConstructorInitializer node) {
forceStatic = true;
try {
return super.visitConstructorInitializer(node);
} finally {
forceStatic = false;
}
}
@Override
public Void visitExpression(Expression node) {
if (selectionFunctionExpression != null || selectionExpression != null
&& node.getClass() == selectionExpression.getClass()) {
SourceRange nodeRange = SourceRangeFactory.rangeNode(node);
tryToFindOccurrence(nodeRange);
}
return super.visitExpression(node);
}
@Override
public Void visitMethodDeclaration(MethodDeclaration node) {
forceStatic = node.isStatic();
try {
return super.visitMethodDeclaration(node);
} finally {
forceStatic = false;
}
}
@Override
public Void visitSwitchMember(SwitchMember node) {
if (selectionStatements != null) {
visitStatements(node.getStatements());
}
return super.visitSwitchMember(node);
}
/**
* Checks if given {@link SourceRange} matched selection source and adds {@link Occurrence}.
*/
private boolean tryToFindOccurrence(SourceRange nodeRange) {
// check if can be extracted
if (!isExtractable(nodeRange)) {
return false;
}
// prepare normalized node source
SourcePattern nodePattern = getSourcePattern(nodeRange);
String nodeSource = getNormalizedSource(nodePattern.patternSource);
// if matches normalized node source, then add as occurrence
if (nodeSource.equals(selectionSource)) {
Occurrence occurrence = new Occurrence(nodeRange, selectionRange.intersects(nodeRange));
occurrences.add(occurrence);
// prepare mapping of parameter names to the occurrence variables
for (Entry<String, String> entry : nodePattern.originalToPatternNames.entrySet()) {
String patternName = entry.getValue();
String originalName = entry.getKey();
String selectionName = patternToSelectionName.get(patternName);
occurrence.parameterOldToOccurrenceName.put(selectionName, originalName);
}
// update static
if (forceStatic) {
staticContext |= true;
}
// we have match
return true;
}
// no match
return false;
}
private void visitStatements(List<Statement> statements) {
int beginStatementIndex = 0;
int selectionCount = selectionStatements.size();
while (beginStatementIndex + selectionCount <= statements.size()) {
SourceRange nodeRange = SourceRangeFactory.rangeStartEnd(
statements.get(beginStatementIndex),
statements.get(beginStatementIndex + selectionCount - 1));
boolean found = tryToFindOccurrence(nodeRange);
// next statement
if (found) {
beginStatementIndex += selectionCount;
} else {
beginStatementIndex++;
}
}
}
});
}
/**
* Prepares information about used variables, which should be turned into parameters.
*/
private RefactoringStatus initializeParameters() {
parameters.clear();
parametersMap.clear();
parameterReferencesMap.clear();
RefactoringStatus result = new RefactoringStatus();
final List<VariableElement> assignedUsedVariables = Lists.newArrayList();
unitNode.accept(new GeneralizingAstVisitor<Void>() {
@Override
public Void visitSimpleIdentifier(SimpleIdentifier node) {
SourceRange nodeRange = SourceRangeFactory.rangeNode(node);
if (selectionRange.covers(nodeRange)) {
// analyze local variable
VariableElement variableElement = CorrectionUtils.getLocalOrParameterVariableElement(node);
if (variableElement != null) {
// name of the named expression
if (CorrectionUtils.isNamedExpressionName(node)) {
return null;
}
// if declared outside, add parameter
if (!isDeclaredInSelection(variableElement)) {
String variableName = variableElement.getDisplayName();
// add parameter
Parameter parameter = parametersMap.get(variableName);
if (parameter == null) {
Type parameterType = node.getBestType();
String parameterTypeName = utils.getTypeSource(parameterType);
parameter = new ParameterImpl(parameterTypeName, variableName);
parameters.add(parameter);
parametersMap.put(variableName, parameter);
}
// add reference to parameter
addParameterReference(variableName, nodeRange);
}
// remember, if assigned and used after selection
if (isLeftHandOfAssignment(node) && isUsedAfterSelection(variableElement)) {
if (!assignedUsedVariables.contains(variableElement)) {
assignedUsedVariables.add(variableElement);
}
}
}
// remember declaration names
if (node.inDeclarationContext()) {
usedNames.add(node.getName());
}
}
return null;
}
});
// may be ends with "return" statement
if (selectionStatements != null) {
Statement lastStatement = selectionStatements.get(selectionStatements.size() - 1);
if (lastStatement instanceof ReturnStatement) {
Expression expression = ((ReturnStatement) lastStatement).getExpression();
if (expression != null) {
returnType = expression.getBestType();
}
}
}
// may be single variable to return
if (assignedUsedVariables.size() == 1) {
// we cannot both return variable and have explicit return statement
if (returnType != null) {
result.addFatalError("Ambiguous return value: "
+ "Selected block contains assignment(s) to local variables and return statement.");
return result;
}
// prepare to return an assigned variable
VariableElement returnVariable = assignedUsedVariables.get(0);
returnType = returnVariable.getType();
returnVariableName = returnVariable.getDisplayName();
}
// fatal, if multiple variables assigned and used after selection
if (assignedUsedVariables.size() > 1) {
StringBuilder sb = new StringBuilder();
for (VariableElement variable : assignedUsedVariables) {
sb.append(variable.getDisplayName());
sb.append("\n");
}
result.addFatalError(MessageFormat.format("Ambiguous return value: "
+ "Selected block contains more than one assignment to local variables. "
+ "Affected variables are:\\n\\n{0}", sb.toString().trim()));
}
// done
return result;
}
/**
* @return <code>true</code> if the given {@link VariableElement} is declared inside of
* {@link #selectionRange}.
*/
private boolean isDeclaredInSelection(VariableElement element) {
return selectionRange.contains(element.getNameOffset());
}
/**
* @return {@code true} if it is OK to extract the node with the given {@link SourceRange}.
*/
private boolean isExtractable(SourceRange range) {
ExtractMethodAnalyzer analyzer = new ExtractMethodAnalyzer(utils, range);
utils.getUnit().accept(analyzer);
return analyzer.getStatus().isOK();
}
/**
* @return <code>true</code> if the given {@link VariableElement} is referenced after the
* {@link #selectionRange}.
*/
private boolean isUsedAfterSelection(final VariableElement element) {
final AtomicBoolean result = new AtomicBoolean();
parentMember.accept(new GeneralizingAstVisitor<Void>() {
@Override
public Void visitSimpleIdentifier(SimpleIdentifier node) {
VariableElement nodeElement = CorrectionUtils.getLocalVariableElement(node);
if (nodeElement == element) {
int nodeOffset = node.getOffset();
if (nodeOffset > selectionRange.getEnd()) {
result.set(true);
}
}
return null;
}
});
return result.get();
}
}