blob: 67e46259ea2a7824d7d756d93d150293730127ca [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.codeInsight.TargetElementUtilBase;
import com.intellij.lang.refactoring.InlineHandler;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.help.HelpManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.psi.*;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.HelpID;
import com.intellij.refactoring.inline.InlineOptionsDialog;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.IncorrectOperationException;
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.GroovyFile;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.GrListOrMap;
import org.jetbrains.plugins.groovy.lang.psi.api.signatures.GrClosureSignature;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrBlockStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrIfStatement;
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.arguments.GrArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
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.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrCallExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMember;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrTypeElement;
import org.jetbrains.plugins.groovy.lang.psi.impl.signatures.GrClosureSignatureUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringBundle;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil;
import java.util.*;
/**
* @author ilyas
*/
@SuppressWarnings({"SuspiciousMethodCalls"})
public class GroovyInlineMethodUtil {
public static final String REFACTORING_NAME = GroovyRefactoringBundle.message("inline.method.title");
private GroovyInlineMethodUtil() {
}
@Nullable
public static InlineHandler.Settings inlineMethodSettings(GrMethod method, Editor editor, boolean invokedOnReference) {
final Project project = method.getProject();
if (method.isConstructor()) {
String message = GroovyRefactoringBundle.message("refactoring.cannot.be.applied.to.constructors", REFACTORING_NAME);
showErrorMessage(message, project, editor);
return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
}
if (invokedOnReference) {
PsiReference reference = editor != null ? TargetElementUtilBase.findReference(editor, editor.getCaretModel().getOffset()) : null;
if (reference == null) return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
PsiElement element = reference.getElement();
if (!(element instanceof GrExpression && element.getParent() instanceof GrCallExpression)) {
String message = GroovyRefactoringBundle.message("refactoring.is.available.only.for.method.calls", REFACTORING_NAME);
showErrorMessage(message, project, editor);
return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
}
GrCallExpression call = (GrCallExpression)element.getParent();
if (PsiTreeUtil.getParentOfType(element, GrParameter.class) != null) {
String message = GroovyRefactoringBundle.message("refactoring.is.not.supported.in.parameter.initializers", REFACTORING_NAME);
showErrorMessage(message, project, editor);
return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
}
GroovyRefactoringUtil.highlightOccurrences(project, editor, new GrExpression[]{call});
if (hasBadReturns(method) && !isTailMethodCall(call)) {
String message = GroovyRefactoringBundle.message("refactoring.is.not.supported.when.return.statement.interrupts.the.execution.flow", REFACTORING_NAME);
showErrorMessage(message, project, editor);
return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
}
}
else {
if (hasBadReturns(method)) {
String message = GroovyRefactoringBundle.message("refactoring.is.not.supported.when.return.statement.interrupts.the.execution.flow", REFACTORING_NAME);
showErrorMessage(message, project, editor);
return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
}
}
if (method.getBlock() == null) {
String message = method.hasModifierProperty(PsiModifier.ABSTRACT)
? GroovyRefactoringBundle.message("refactoring.cannot.be.applied.to.abstract.methods", REFACTORING_NAME)
: GroovyRefactoringBundle.message("refactoring.cannot.be.applied.no.sources.attached", REFACTORING_NAME);
showErrorMessage(message, project, editor);
return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
}
return inlineMethodDialogResult(method, project, invokedOnReference);
}
/**
* Checks whether given method call is tail call of other method or closure
*
* @param call [tail?] Method call
* @return
*/
static boolean isTailMethodCall(GrCallExpression call) {
GrStatement stmt = call;
PsiElement parent = call.getParent();
// return statement
if (parent instanceof GrReturnStatement) {
stmt = ((GrReturnStatement) parent);
parent = parent.getParent();
}
// method body result
if (parent instanceof GrOpenBlock) {
if (parent.getParent() instanceof GrMethod) {
GrStatement[] statements = ((GrOpenBlock) parent).getStatements();
return statements.length > 0 && stmt == statements[statements.length - 1];
}
}
// closure result
if (parent instanceof GrClosableBlock) {
GrStatement[] statements = ((GrClosableBlock) parent).getStatements();
return statements.length > 0 && stmt == statements[statements.length - 1];
}
// todo add for inner method block statements
// todo test me!
if (stmt instanceof GrReturnStatement) {
GrMethod method = PsiTreeUtil.getParentOfType(stmt, GrMethod.class);
if (method != null) {
Collection<GrStatement> returnStatements = ControlFlowUtils.collectReturns(method.getBlock());
return returnStatements.contains(stmt) && !hasBadReturns(method);
}
}
return false;
}
/**
* Shows dialog with question to inline
*/
@Nullable
private static InlineHandler.Settings inlineMethodDialogResult(GrMethod method, Project project, boolean invokedOnReference) {
Application application = ApplicationManager.getApplication();
if (!application.isUnitTestMode()) {
final InlineMethodDialog dialog = new InlineMethodDialog(project, method, invokedOnReference, checkMethodForRecursion(method));
dialog.show();
if (!dialog.isOK()) {
WindowManager.getInstance().getStatusBar(project).setInfo(GroovyRefactoringBundle.message("press.escape.to.remove.the.highlighting"));
return InlineHandler.Settings.CANNOT_INLINE_SETTINGS;
}
else {
return new InlineHandler.Settings() {
@Override
public boolean isOnlyOneReferenceToInline() {
return dialog.isInlineThisOnly();
}
};
}
}
return new InlineHandler.Settings() {
@Override
public boolean isOnlyOneReferenceToInline() {
return true;
}
};
}
private static boolean hasBadReturns(GrMethod method) {
Collection<GrStatement> returnStatements = ControlFlowUtils.collectReturns(method.getBlock());
GrOpenBlock block = method.getBlock();
if (block == null || returnStatements.isEmpty()) return false;
boolean checked = checkTailOpenBlock(block, returnStatements);
return !(checked && returnStatements.isEmpty());
}
public static boolean checkTailIfStatement(GrIfStatement ifStatement, Collection<GrStatement> returnStatements) {
GrStatement thenBranch = ifStatement.getThenBranch();
GrStatement elseBranch = ifStatement.getElseBranch();
if (elseBranch == null) return false;
boolean tb = false;
boolean eb = false;
if (thenBranch instanceof GrReturnStatement) {
tb = returnStatements.remove(thenBranch);
} else if (thenBranch instanceof GrBlockStatement) {
tb = checkTailOpenBlock(((GrBlockStatement) thenBranch).getBlock(), returnStatements);
}
if (elseBranch instanceof GrReturnStatement) {
eb = returnStatements.remove(elseBranch);
} else if (elseBranch instanceof GrBlockStatement) {
eb = checkTailOpenBlock(((GrBlockStatement) elseBranch).getBlock(), returnStatements);
}
return tb && eb;
}
private static boolean checkTailOpenBlock(GrOpenBlock block, Collection<GrStatement> returnStatements) {
if (block == null) return false;
GrStatement[] statements = block.getStatements();
if (statements.length == 0) return false;
GrStatement last = statements[statements.length - 1];
if (returnStatements.contains(last)) {
returnStatements.remove(last);
return true;
}
if (last instanceof GrIfStatement) {
return checkTailIfStatement(((GrIfStatement) last), returnStatements);
}
return false;
}
private static void showErrorMessage(String message, final Project project, Editor editor) {
CommonRefactoringUtil.showErrorHint(project, editor, message, REFACTORING_NAME, HelpID.INLINE_METHOD);
}
static boolean isStaticMethod(@NotNull GrMethod method) {
return method.hasModifierProperty(PsiModifier.STATIC);
}
static boolean areInSameClass(PsiElement element, GrMethod method) {
PsiElement parent = element;
while (!(parent == null || parent instanceof PsiClass || parent instanceof PsiFile)) {
parent = parent.getParent();
}
if (parent instanceof PsiClass) {
PsiClass methodClass = method.getContainingClass();
return parent == methodClass;
}
if (parent instanceof GroovyFile) {
PsiElement mParent = method.getParent();
return mParent instanceof GroovyFile && mParent == parent;
}
return false;
}
public static Collection<ReferenceExpressionInfo> collectReferenceInfo(GrMethod method) {
ArrayList<ReferenceExpressionInfo> list = new ArrayList<ReferenceExpressionInfo>();
collectReferenceInfoImpl(list, method, method);
return list;
}
private static void collectReferenceInfoImpl(Collection<ReferenceExpressionInfo> infos, PsiElement elem, GrMethod method) {
if (elem instanceof GrReferenceExpression) {
GrReferenceExpression expr = (GrReferenceExpression) elem;
PsiReference ref = expr.getReference();
if (ref != null) {
PsiElement declaration = ref.resolve();
if (declaration instanceof GrMember) {
int offsetInMethod = expr.getTextRange().getStartOffset() - method.getTextRange().getStartOffset();
GrMember member = (GrMember) declaration;
infos.add(new ReferenceExpressionInfo(expr, offsetInMethod, member, member.getContainingClass()));
}
}
}
for (PsiElement element : elem.getChildren()) {
collectReferenceInfoImpl(infos, element, method);
}
}
public static boolean isSimpleReference(GrExpression qualifier) {
if (!(qualifier instanceof GrReferenceExpression)) return false;
GrExpression qual = ((GrReferenceExpression) qualifier).getQualifierExpression();
return qual == null || isSimpleReference(qual);
}
static class ReferenceExpressionInfo {
public final PsiMember declaration;
public final GrReferenceExpression expression;
public final int offsetInMethod;
public final PsiClass containingClass;
@Nullable
public String getPresentation() {
return declaration.getName();
}
public boolean isStatic() {
return declaration.hasModifierProperty(PsiModifier.STATIC);
}
public ReferenceExpressionInfo(GrReferenceExpression expression, int offsetInMethod, PsiMember declaration, PsiClass containingClass) {
this.expression = expression;
this.offsetInMethod = offsetInMethod;
this.declaration = declaration;
this.containingClass = containingClass;
}
}
static void addQualifiersToInnerReferences(GrMethod method, Collection<ReferenceExpressionInfo> infos, @NotNull GrExpression qualifier)
throws IncorrectOperationException {
Set<GrReferenceExpression> exprs = new HashSet<GrReferenceExpression>();
for (ReferenceExpressionInfo info : infos) {
PsiReference ref = method.findReferenceAt(info.offsetInMethod);
if (ref != null && ref.getElement() instanceof GrReferenceExpression) {
GrReferenceExpression refExpr = (GrReferenceExpression) ref.getElement();
if (refExpr.getQualifierExpression() == null) {
exprs.add(refExpr);
}
}
}
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(qualifier.getProject());
for (GrReferenceExpression expr : exprs) {
GrExpression qual = factory.createExpressionFromText(qualifier.getText());
expr.setQualifier(qual);
}
}
private static boolean checkMethodForRecursion(GrMethod method) {
return checkCalls(method.getBlock(), method);
}
private static boolean checkCalls(PsiElement scope, PsiMethod method) {
if (scope instanceof GrMethodCall) {
PsiMethod refMethod = ((GrMethodCall)scope).resolveMethod();
if (method.equals(refMethod)) return true;
}
for (PsiElement child = scope.getFirstChild(); child != null; child = child.getNextSibling()) {
if (checkCalls(child, method)) return true;
}
return false;
}
static class InlineMethodDialog extends InlineOptionsDialog {
public static final String REFACTORING_NAME = GroovyRefactoringBundle.message("inline.method.title");
private final boolean myAllowInlineThisOnly;
private final PsiMethod myMethod;
public InlineMethodDialog(Project project,
PsiMethod method,
boolean invokedOnReference,
final boolean allowInlineThisOnly) {
super(project, true, method);
myMethod = method;
myAllowInlineThisOnly = allowInlineThisOnly;
myInvokedOnReference = invokedOnReference;
setTitle(REFACTORING_NAME);
init();
}
@Override
protected String getBorderTitle() {
return GroovyRefactoringBundle.message("inline.method.border.title");
}
@Override
protected String getNameLabelText() {
return GroovyRefactoringBundle.message("inline.method.label", GroovyRefactoringUtil.getMethodSignature(myMethod));
}
@Override
protected String getInlineAllText() {
return myMethod.isWritable()
? GroovyRefactoringBundle.message("all.invocations.and.remove.the.method")
: GroovyRefactoringBundle.message("all.invocations.in.project");
}
@Override
protected String getInlineThisText() {
return GroovyRefactoringBundle.message("this.invocation.only.and.keep.the.method");
}
@Override
protected boolean isInlineThis() {
return false;
}
@Override
protected void doAction() {
if (getOKAction().isEnabled()) {
close(OK_EXIT_CODE);
}
}
@Override
protected void doHelpAction() {
HelpManager.getInstance().invokeHelp(HelpID.INLINE_METHOD);
}
@Override
protected boolean canInlineThisOnly() {
return myAllowInlineThisOnly;
}
}
/**
* Inline method call's arguments as its parameters
*
* @param call method call
* @param method given method
*/
public static void replaceParametersWithArguments(GrCallExpression call, GrMethod method) throws IncorrectOperationException {
GrParameter[] parameters = method.getParameters();
if (parameters.length == 0) return;
GrArgumentList argumentList = call.getArgumentList();
if (argumentList == null) {
setDefaultValuesToParameters(method, null, call);
return;
}
Project project = call.getProject();
final GroovyResolveResult resolveResult = call.advancedResolve();
GrClosureSignature signature = GrClosureSignatureUtil.createSignature(method, resolveResult.getSubstitutor());
if (signature == null) {
return;
}
GrClosureSignatureUtil.ArgInfo<PsiElement>[] infos = GrClosureSignatureUtil.mapParametersToArguments(
signature,
call.getNamedArguments(),
call.getExpressionArguments(),
call.getClosureArguments(),
call, true, false
);
if (infos == null) return;
for (int i = 0; i < infos.length; i++) {
GrClosureSignatureUtil.ArgInfo<PsiElement> argInfo = infos[i];
GrParameter parameter = parameters[i];
final GrExpression arg = inferArg(signature, parameters, parameter, argInfo, project);
if (arg != null) {
replaceAllOccurrencesWithExpression(method, call, arg, parameter);
}
}
}
@Nullable
private static GrExpression inferArg(GrClosureSignature signature,
GrParameter[] parameters,
GrParameter parameter,
GrClosureSignatureUtil.ArgInfo<PsiElement> argInfo,
Project project) {
if (argInfo == null) return null;
List<PsiElement> arguments = argInfo.args;
if (argInfo.isMultiArg) { //arguments for Map and varArg
final PsiType type = parameter.getDeclaredType();
return GroovyRefactoringUtil.generateArgFromMultiArg(signature.getSubstitutor(), arguments, type, project);
}
else { //arguments for simple parameters
if (arguments.size() == 1) { //arg exists
PsiElement arg = arguments.iterator().next();
if (isVararg(parameter, parameters)) {
if (arg instanceof GrSafeCastExpression) {
PsiElement expr = ((GrSafeCastExpression)arg).getOperand();
if (expr instanceof GrListOrMap && !((GrListOrMap)expr).isMap()) {
return ((GrListOrMap)expr);
}
}
}
return (GrExpression)arg;
}
else { //arg is skipped. Parameter is optional
return parameter.getInitializerGroovy();
}
}
}
private static boolean isVararg(GrParameter p, GrParameter[] parameters) {
return parameters[parameters.length - 1] == p && p.getType() instanceof PsiArrayType;
}
/**
* replaces parameter occurrences in method with its default values (if it's possible)
*
* @param method given method
* @param nameFilter specified parameter names (which ma have default initializers)
* @param call
*/
private static void setDefaultValuesToParameters(GrMethod method, @Nullable Collection<String> nameFilter, GrCallExpression call) throws IncorrectOperationException {
if (nameFilter == null) {
nameFilter = new ArrayList<String>();
for (GrParameter parameter : method.getParameters()) {
nameFilter.add(parameter.getName());
}
}
GrParameter[] parameters = method.getParameters();
for (GrParameter parameter : parameters) {
GrExpression initializer = parameter.getInitializerGroovy();
if (nameFilter.contains(parameter.getName()) && initializer != null) {
replaceAllOccurrencesWithExpression(method, call, initializer, parameter);
}
}
}
private static void replaceAllOccurrencesWithExpression(GrMethod method,
GrCallExpression call,
GrExpression oldExpression,
GrParameter parameter) {
Collection<PsiReference> refs = ReferencesSearch.search(parameter, new LocalSearchScope(method), false).findAll();
final GroovyPsiElementFactory elementFactory = GroovyPsiElementFactory.getInstance(call.getProject());
GrExpression expression = elementFactory.createExpressionFromText(oldExpression.getText());
if (GroovyRefactoringUtil.hasSideEffect(expression) && refs.size() > 1 || !hasUnresolvableWriteAccess(refs, oldExpression)) {
final String oldName = parameter.getName();
final String newName = InlineMethodConflictSolver.suggestNewName(oldName, method, call);
expression = elementFactory.createExpressionFromText(newName);
final GrOpenBlock body = method.getBlock();
final GrStatement[] statements = body.getStatements();
GrStatement anchor = null;
if (statements.length > 0) {
anchor = statements[0];
}
body.addStatementBefore(elementFactory.createStatementFromText(createVariableDefinitionText(parameter, oldExpression, newName)),
anchor);
}
for (PsiReference ref : refs) {
PsiElement element = ref.getElement();
if (element instanceof GrReferenceExpression) {
((GrReferenceExpression)element).replaceWithExpression(expression, true);
}
}
}
private static String createVariableDefinitionText(GrParameter parameter, GrExpression expression, String varName) {
StringBuilder buffer = new StringBuilder();
final PsiModifierList modifierList = parameter.getModifierList();
buffer.append(modifierList.getText().trim());
if (buffer.length() > 0) buffer.append(' ');
final GrTypeElement typeElement = parameter.getTypeElementGroovy();
if (typeElement != null) {
buffer.append(typeElement.getText()).append(' ');
}
if (buffer.length() == 0) {
buffer.append("def ");
}
buffer.append(varName).append(" = ").append(expression.getText());
return buffer.toString();
}
/**
* @param refs collection of references to method parameters. It is considered that these references have no qualifiers
*/
private static boolean containsWriteAccess(Collection<PsiReference> refs) {
for (PsiReference ref : refs) {
final PsiElement element = ref.getElement();
final PsiElement parent = element.getParent();
if (parent instanceof GrAssignmentExpression && ((GrAssignmentExpression)parent).getLValue() == element) return true;
if (parent instanceof GrUnaryExpression) return true;
}
return false;
}
private static boolean hasUnresolvableWriteAccess(Collection<PsiReference> refs, GrExpression expression) {
if (containsWriteAccess(refs)) {
if (expression instanceof GrReferenceExpression) {
final PsiElement resolved = ((GrReferenceExpression)expression).resolve();
if (resolved instanceof GrVariable && !(resolved instanceof PsiField)) {
final boolean isFinal = ((GrVariable)resolved).hasModifierProperty(PsiModifier.FINAL);
if (!isFinal) {
final PsiReference lastRef =
Collections.max(ReferencesSearch.search(resolved).findAll(), new Comparator<PsiReference>() {
@Override
public int compare(PsiReference o1, PsiReference o2) {
return o1.getElement().getTextRange().getStartOffset() - o2.getElement().getTextRange().getStartOffset();
}
});
return lastRef.getElement() == expression;
}
}
}
return false;
}
return true;
}
}