blob: a38cf14cfe58ee6edf16bd23eb37a839e314f399 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.jetbrains.plugins.groovy.refactoring.inline;
import com.intellij.lang.refactoring.InlineHandler;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.codeInspection.utils.ControlFlowUtils;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariableDeclaration;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrCodeBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.branch.GrReturnStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrCallExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrReflectedMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrVariableDeclarationOwner;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyNameSuggestionUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringBundle;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil;
import org.jetbrains.plugins.groovy.refactoring.NameValidator;
import org.jetbrains.plugins.groovy.refactoring.util.AnySupers;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author ilyas
*/
public class GroovyMethodInliner implements InlineHandler.Inliner {
private final GrMethod myMethod;
private static final Logger LOG = Logger.getInstance("#org.jetbrains.plugins.groovy.refactoring.inline.GroovyMethodInliner");
public GroovyMethodInliner(GrMethod method) {
myMethod = method;
}
@Override
@Nullable
public MultiMap<PsiElement, String> getConflicts(@NotNull PsiReference reference, @NotNull PsiElement referenced) {
PsiElement element = reference.getElement();
if (!(element instanceof GrExpression && element.getParent() instanceof GrCallExpression)) {
final MultiMap<PsiElement, String> map = new MultiMap<PsiElement, String>();
map.putValue(element, GroovyRefactoringBundle.message("cannot.inline.reference.0", element.getText()));
return map;
}
GrCallExpression call = (GrCallExpression) element.getParent();
Collection<GroovyInlineMethodUtil.ReferenceExpressionInfo> infos = GroovyInlineMethodUtil.collectReferenceInfo(myMethod);
return collectConflicts(call, infos);
}
@NotNull
private static MultiMap<PsiElement, String> collectConflicts(@NotNull GrCallExpression call, @NotNull Collection<GroovyInlineMethodUtil.ReferenceExpressionInfo> infos) {
MultiMap<PsiElement, String> conflicts = new MultiMap<PsiElement, String>();
for (GroovyInlineMethodUtil.ReferenceExpressionInfo info : infos) {
if (!PsiUtil.isAccessible(call, info.declaration)) {
if (info.declaration instanceof PsiMethod) {
String className = info.containingClass.getName();
String signature = GroovyRefactoringUtil.getMethodSignature((PsiMethod)info.declaration);
String name = CommonRefactoringUtil.htmlEmphasize(className + "." + signature);
conflicts.putValue(info.declaration, GroovyRefactoringBundle.message("method.is.not.accessible.form.context.0", name));
}
else if (info.declaration instanceof PsiField) {
if (!(info.declaration instanceof GrField && ((GrField)info.declaration).getGetters().length > 0)) { // conflict if field doesn't have implicit getters
String className = info.containingClass.getName();
String name = CommonRefactoringUtil.htmlEmphasize(className + "." + info.getPresentation());
conflicts.putValue(info.declaration, GroovyRefactoringBundle.message("field.is.not.accessible.form.context.0", name));
}
}
}
AnySupers visitor = new AnySupers();
info.expression.accept(visitor);
if (visitor.containsSupers()) {
conflicts.putValue(info.expression, GroovyRefactoringBundle.message("super.reference.is.used"));
}
}
return conflicts;
}
@Override
public void inlineUsage(@NotNull UsageInfo usage, @NotNull PsiElement referenced) {
PsiElement element=usage.getElement();
if (!(element instanceof GrExpression && element.getParent() instanceof GrCallExpression)) return;
final Editor editor = getCurrentEditorIfApplicable(element);
GrCallExpression call = (GrCallExpression) element.getParent();
RangeMarker marker = inlineReferenceImpl(call, myMethod, isOnExpressionOrReturnPlace(call), GroovyInlineMethodUtil.isTailMethodCall(call), editor);
// highlight replaced result
if (marker != null) {
Project project = referenced.getProject();
TextRange range = TextRange.create(marker);
GroovyRefactoringUtil.highlightOccurrencesByRanges(project, editor, new TextRange[]{range});
WindowManager.getInstance().getStatusBar(project).setInfo(GroovyRefactoringBundle.message("press.escape.to.remove.the.highlighting"));
if (editor != null) {
editor.getCaretModel().moveToOffset(marker.getEndOffset());
}
}
}
@Nullable
private static Editor getCurrentEditorIfApplicable(@NotNull PsiElement element) {
final Project project = element.getProject();
final Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
if (editor != null &&
editor.getDocument() == PsiDocumentManager.getInstance(project).getDocument(element.getContainingFile())) {
return editor;
}
return null;
}
@Nullable
static RangeMarker inlineReferenceImpl(@NotNull GrCallExpression call,
@NotNull GrMethod method,
boolean resultOfCallExplicitlyUsed,
boolean isTailMethodCall,
@Nullable Editor editor) {
try {
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(call.getProject());
final Project project = call.getProject();
// Variable declaration for qualifier expression
GrVariableDeclaration qualifierDeclaration = null;
GrReferenceExpression innerQualifier = null;
GrExpression qualifier = null;
if (call instanceof GrMethodCallExpression && ((GrMethodCallExpression) call).getInvokedExpression() != null) {
GrExpression invoked = ((GrMethodCallExpression) call).getInvokedExpression();
if (invoked instanceof GrReferenceExpression && ((GrReferenceExpression) invoked).getQualifierExpression() != null) {
qualifier = ((GrReferenceExpression) invoked).getQualifierExpression();
if (PsiUtil.isSuperReference(qualifier)) {
qualifier = null;
}
else if (!GroovyInlineMethodUtil.isSimpleReference(qualifier)) {
String qualName = generateQualifierName(call, method, project, qualifier);
qualifier = (GrExpression)PsiUtil.skipParentheses(qualifier, false);
qualifierDeclaration = factory.createVariableDeclaration(ArrayUtil.EMPTY_STRING_ARRAY, qualifier, null, qualName);
innerQualifier = (GrReferenceExpression) factory.createExpressionFromText(qualName);
} else {
innerQualifier = (GrReferenceExpression) qualifier;
}
}
}
GrMethod _method = prepareNewMethod(call, method, qualifier);
GrExpression result = getAloneResultExpression(_method);
if (result != null) {
GrExpression expression = call.replaceWithExpression(result, false);
TextRange range = expression.getTextRange();
return editor != null ? editor.getDocument().createRangeMarker(range.getStartOffset(), range.getEndOffset(), true) : null;
}
GrMethod newMethod = prepareNewMethod(call, method, innerQualifier);
String resultName = InlineMethodConflictSolver.suggestNewName("result", newMethod, call);
// Add variable for method result
Collection<GrStatement> returnStatements = ControlFlowUtils.collectReturns(newMethod.getBlock());
final int returnCount = returnStatements.size();
PsiType methodType = method.getInferredReturnType();
GrOpenBlock body = newMethod.getBlock();
assert body != null;
GrExpression replaced;
if (resultOfCallExplicitlyUsed && !isTailMethodCall) {
GrExpression resultExpr = null;
if (PsiType.VOID.equals(methodType)) {
resultExpr = factory.createExpressionFromText("null");
}
else if (returnCount == 1) {
final GrExpression returnExpression = ControlFlowUtils.extractReturnExpression(returnStatements.iterator().next());
if (returnExpression != null) {
resultExpr = factory.createExpressionFromText(returnExpression.getText());
}
}
else if (returnCount > 1) {
resultExpr = factory.createExpressionFromText(resultName);
}
if (resultExpr == null) {
resultExpr = factory.createExpressionFromText("null");
}
replaced = call.replaceWithExpression(resultExpr, false);
}
else {
replaced = call;
}
// Calculate anchor to insert before
GrExpression enclosingExpr = GroovyRefactoringUtil.addBlockIntoParent(replaced);
GrVariableDeclarationOwner owner = PsiTreeUtil.getParentOfType(enclosingExpr, GrVariableDeclarationOwner.class);
assert owner != null;
PsiElement element = enclosingExpr;
while (element != null && element.getParent() != owner) {
element = element.getParent();
}
assert element != null && element instanceof GrStatement;
GrStatement anchor = (GrStatement) element;
if (!resultOfCallExplicitlyUsed) {
assert anchor == enclosingExpr;
}
// add qualifier reference declaration
if (qualifierDeclaration != null) {
owner.addVariableDeclarationBefore(qualifierDeclaration, anchor);
}
// Process method return statements
if (returnCount > 1 && PsiType.VOID != methodType && !isTailMethodCall) {
PsiType type = methodType != null && methodType.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) ? null : methodType;
GrVariableDeclaration resultDecl = factory.createVariableDeclaration(ArrayUtil.EMPTY_STRING_ARRAY, "", type, resultName);
GrStatement statement = ((GrStatementOwner) owner).addStatementBefore(resultDecl, anchor);
JavaCodeStyleManager.getInstance(statement.getProject()).shortenClassReferences(statement);
// Replace all return statements with assignments to 'result' variable
for (GrStatement returnStatement : returnStatements) {
GrExpression value = ControlFlowUtils.extractReturnExpression(returnStatement);
if (value != null) {
GrExpression assignment = factory.createExpressionFromText(resultName + " = " + value.getText());
returnStatement.replaceWithStatement(assignment);
} else {
returnStatement.replaceWithStatement(factory.createExpressionFromText(resultName + " = null"));
}
}
}
if (!isTailMethodCall && resultOfCallExplicitlyUsed && returnCount == 1) {
returnStatements.iterator().next().removeStatement();
}
else if (!isTailMethodCall && (PsiType.VOID.equals(methodType) || returnCount == 1)) {
for (GrStatement returnStatement : returnStatements) {
if (returnStatement instanceof GrReturnStatement) {
final GrExpression returnValue = ((GrReturnStatement)returnStatement).getReturnValue();
if (returnValue != null && GroovyRefactoringUtil.hasSideEffect(returnValue)) {
returnStatement.replaceWithStatement(returnValue);
continue;
}
}
else if (GroovyRefactoringUtil.hasSideEffect(returnStatement)) {
continue;
}
returnStatement.removeStatement();
}
}
// Add all method statements
GrStatement[] statements = body.getStatements();
for (GrStatement statement : statements) {
((GrStatementOwner) owner).addStatementBefore(statement, anchor);
}
if (resultOfCallExplicitlyUsed && !isTailMethodCall) {
TextRange range = replaced.getTextRange();
RangeMarker marker = editor != null ? editor.getDocument().createRangeMarker(range.getStartOffset(), range.getEndOffset(), true) : null;
reformatOwner(owner);
return marker;
} else {
GrStatement stmt;
if (isTailMethodCall && enclosingExpr.getParent() instanceof GrReturnStatement) {
stmt = (GrReturnStatement) enclosingExpr.getParent();
} else {
stmt = enclosingExpr;
}
stmt.removeStatement();
reformatOwner(owner);
return null;
}
} catch (IncorrectOperationException e) {
LOG.error(e);
}
return null;
}
@NotNull
private static String generateQualifierName(@NotNull GrCallExpression call, @Nullable GrMethod method, @NotNull final Project project, @NotNull GrExpression qualifier) {
String[] possibleNames = GroovyNameSuggestionUtil.suggestVariableNames(qualifier, new NameValidator() {
@Override
public String validateName(String name, boolean increaseNumber) {
return name;
}
@Override
public Project getProject() {
return project;
}
});
String qualName = possibleNames[0];
qualName = InlineMethodConflictSolver.suggestNewName(qualName, method, call);
return qualName;
}
private static void reformatOwner(@Nullable GrVariableDeclarationOwner owner) throws IncorrectOperationException {
if (owner == null) return;
PsiFile file = owner.getContainingFile();
Project project = file.getProject();
PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
Document document = manager.getDocument(file);
if (document != null) {
manager.doPostponedOperationsAndUnblockDocument(document);
CodeStyleManager.getInstance(project).adjustLineIndent(file, owner.getTextRange());
}
}
/**
* Prepare temporary method with non-conflicting local names
*/
@NotNull
private static GrMethod prepareNewMethod(@NotNull GrCallExpression call, @NotNull GrMethod method, @Nullable GrExpression qualifier) throws IncorrectOperationException {
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(method.getProject());
if (method instanceof GrReflectedMethod) {
method = ((GrReflectedMethod)method).getBaseMethod();
}
GrMethod newMethod = factory.createMethodFromText(method.getText(), call);
if (qualifier != null) {
Collection<GroovyInlineMethodUtil.ReferenceExpressionInfo> infos = GroovyInlineMethodUtil.collectReferenceInfo(method);
GroovyInlineMethodUtil.addQualifiersToInnerReferences(newMethod, infos, qualifier);
}
ArrayList<PsiNamedElement> innerDefinitions = new ArrayList<PsiNamedElement>();
collectInnerDefinitions(newMethod.getBlock(), innerDefinitions);
// there are only local variables and parameters (possible of inner closures)
for (PsiNamedElement namedElement : innerDefinitions) {
String name = namedElement.getName();
if (name != null) {
String newName = qualifier instanceof GrReferenceExpression ?
InlineMethodConflictSolver.suggestNewName(name, method, call, ((GrReferenceExpression)qualifier).getReferenceName()) :
InlineMethodConflictSolver.suggestNewName(name, method, call);
if (!newName.equals(namedElement.getName())) {
final Collection<PsiReference> refs = ReferencesSearch.search(namedElement).findAll();
for (PsiReference ref : refs) {
PsiElement element = ref.getElement();
if (element instanceof GrReferenceExpression) {
GrExpression newExpr = factory.createExpressionFromText(newName);
((GrReferenceExpression) element).replaceWithExpression(newExpr, false);
}
}
namedElement.setName(newName);
}
}
}
GroovyInlineMethodUtil.replaceParametersWithArguments(call, newMethod);
return newMethod;
}
private static void collectInnerDefinitions(@Nullable PsiElement element, ArrayList<PsiNamedElement> defintions) {
if (element == null) return;
for (PsiElement child : element.getChildren()) {
if (child instanceof GrVariable && !(child instanceof GrParameter)) {
defintions.add((GrVariable) child);
}
if (!(child instanceof GrClosableBlock)) {
collectInnerDefinitions(child, defintions);
}
}
}
/**
* Get method result expression (if it is alone in method)
*
* @return null if method has more or less than one return statement or has void type
*/
@Nullable
static GrExpression getAloneResultExpression(@NotNull GrMethod method) {
GrOpenBlock body = method.getBlock();
assert body != null;
GrStatement[] statements = body.getStatements();
if (statements.length == 1) {
if (statements[0] instanceof GrExpression) return (GrExpression) statements[0];
if (statements[0] instanceof GrReturnStatement) {
GrExpression value = ((GrReturnStatement) statements[0]).getReturnValue();
if (value == null && PsiUtil.getSmartReturnType(method) != PsiType.VOID) {
return GroovyPsiElementFactory.getInstance(method.getProject()).createExpressionFromText("null");
}
return value;
}
}
return null;
}
/*
Method call is used as expression in some enclosing expression or
is method return result
*/
private static boolean isOnExpressionOrReturnPlace(@NotNull GrCallExpression call) {
PsiElement parent = call.getParent();
if (!(parent instanceof GrVariableDeclarationOwner)) {
return true;
}
// tail calls in methods and closures
GrVariableDeclarationOwner owner = (GrVariableDeclarationOwner) parent;
if (owner instanceof GrClosableBlock ||
owner instanceof GrOpenBlock && owner.getParent() instanceof GrMethod) {
GrStatement[] statements = ((GrCodeBlock) owner).getStatements();
assert statements.length > 0;
GrStatement last = statements[statements.length - 1];
if (last == call) return true;
if (last instanceof GrReturnStatement && call == ((GrReturnStatement) last).getReturnValue()) {
return true;
}
}
return false;
}
}