blob: d753875208c3a1bed94c005b962a4b74c66b4fe1 [file] [log] [blame]
/*
* Copyright 2000-2012 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 com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.TargetElementUtil;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.find.FindManager;
import com.intellij.find.findUsages.FindUsagesHandler;
import com.intellij.find.findUsages.FindUsagesManager;
import com.intellij.find.findUsages.JavaMethodFindUsagesOptions;
import com.intellij.find.impl.FindManagerImpl;
import com.intellij.ide.util.SuperMethodWarningUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.undo.UndoUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.SuggestedNameInfo;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.changeSignature.ChangeSignatureProcessor;
import com.intellij.refactoring.changeSignature.JavaChangeSignatureDialog;
import com.intellij.refactoring.changeSignature.ParameterInfoImpl;
import com.intellij.refactoring.util.RefactoringUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author cdr
* @since Nov 13, 2002
*/
public class ChangeMethodSignatureFromUsageFix implements IntentionAction/*, HighPriorityAction*/ {
final PsiMethod myTargetMethod;
final PsiExpression[] myExpressions;
final PsiSubstitutor mySubstitutor;
final PsiElement myContext;
private final boolean myChangeAllUsages;
private final int myMinUsagesNumberToShowDialog;
ParameterInfoImpl[] myNewParametersInfo;
private static final Logger LOG = Logger.getInstance(ChangeMethodSignatureFromUsageFix.class);
public ChangeMethodSignatureFromUsageFix(@NotNull PsiMethod targetMethod,
@NotNull PsiExpression[] expressions,
@NotNull PsiSubstitutor substitutor,
@NotNull PsiElement context,
boolean changeAllUsages, int minUsagesNumberToShowDialog) {
myTargetMethod = targetMethod;
myExpressions = expressions;
mySubstitutor = substitutor;
myContext = context;
myChangeAllUsages = changeAllUsages;
myMinUsagesNumberToShowDialog = minUsagesNumberToShowDialog;
}
@Override
@NotNull
public String getText() {
final String shortText = getShortText();
if (shortText != null) return shortText;
return QuickFixBundle.message("change.method.signature.from.usage.text",
JavaHighlightUtil.formatMethod(myTargetMethod),
myTargetMethod.getName(),
formatTypesList(myNewParametersInfo, myContext));
}
@Nullable
private String getShortText() {
final StringBuilder buf = new StringBuilder();
final HashSet<ParameterInfoImpl> newParams = new HashSet<ParameterInfoImpl>();
final HashSet<ParameterInfoImpl> removedParams = new HashSet<ParameterInfoImpl>();
final HashSet<ParameterInfoImpl> changedParams = new HashSet<ParameterInfoImpl>();
getNewParametersInfo(myExpressions, myTargetMethod, mySubstitutor, buf, newParams, removedParams, changedParams);
final String targetMethodName = myTargetMethod.getName();
if (myTargetMethod.getContainingClass().findMethodsByName(targetMethodName, true).length == 1) {
if (newParams.size() == 1) {
final ParameterInfoImpl p = newParams.iterator().next();
return QuickFixBundle.message("add.parameter.from.usage.text", p.getTypeText(), (ArrayUtil.find(myNewParametersInfo, p) + 1), targetMethodName);
}
if (removedParams.size() == 1) {
final ParameterInfoImpl p = removedParams.iterator().next();
return QuickFixBundle.message("remove.parameter.from.usage.text", (p.getOldIndex() + 1), targetMethodName);
}
if (changedParams.size() == 1) {
final ParameterInfoImpl p = changedParams.iterator().next();
return QuickFixBundle.message("change.parameter.from.usage.text", (p.getOldIndex() + 1), targetMethodName, myTargetMethod.getParameterList().getParameters()[p.getOldIndex()].getType().getPresentableText(), p.getTypeText());
}
}
return "<html> Change signature of " + targetMethodName + "(" + buf.toString() + ")</html>";
}
@Nullable
private static String formatTypesList(ParameterInfoImpl[] infos, PsiElement context) {
if (infos == null) return null;
StringBuilder result = new StringBuilder();
try {
for (ParameterInfoImpl info : infos) {
PsiType type = info.createType(context, context.getManager());
if (type == null) return null;
if (result.length() != 0) result.append(", ");
result.append(type.getPresentableText());
}
return result.toString();
}
catch (IncorrectOperationException e) {
return null;
}
}
@Override
@NotNull
public String getFamilyName() {
return QuickFixBundle.message("change.method.signature.from.usage.family");
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
if (!myTargetMethod.isValid() || myTargetMethod.getContainingClass() == null) return false;
for (PsiExpression expression : myExpressions) {
if (!expression.isValid()) return false;
}
if (!mySubstitutor.isValid()) return false;
myNewParametersInfo = getNewParametersInfo(myExpressions, myTargetMethod, mySubstitutor);
if (myNewParametersInfo == null || formatTypesList(myNewParametersInfo, myContext) == null) return false;
return !isMethodSignatureExists();
}
public boolean isMethodSignatureExists() {
PsiClass target = myTargetMethod.getContainingClass();
LOG.assertTrue(target != null);
PsiMethod[] methods = target.findMethodsByName(myTargetMethod.getName(), false);
for (PsiMethod method : methods) {
if (PsiUtil.isApplicable(method, PsiSubstitutor.EMPTY, myExpressions)) return true;
}
return false;
}
@Override
public void invoke(@NotNull final Project project, Editor editor, final PsiFile file) {
if (!FileModificationService.getInstance().prepareFileForWrite(file)) return;
final PsiMethod method = SuperMethodWarningUtil.checkSuperMethod(myTargetMethod, RefactoringBundle.message("to.refactor"));
if (method == null) return;
myNewParametersInfo = getNewParametersInfo(myExpressions, myTargetMethod, mySubstitutor);
final List<ParameterInfoImpl> parameterInfos =
performChange(project, editor, file, method, myMinUsagesNumberToShowDialog, myNewParametersInfo, myChangeAllUsages, false);
if (parameterInfos != null) {
myNewParametersInfo = parameterInfos.toArray(new ParameterInfoImpl[parameterInfos.size()]);
}
}
public static List<ParameterInfoImpl> performChange(final Project project,
final Editor editor,
final PsiFile file,
final PsiMethod method,
final int minUsagesNumber,
final ParameterInfoImpl[] newParametersInfo,
final boolean changeAllUsages,
final boolean allowDelegation) {
if (!FileModificationService.getInstance().prepareFileForWrite(method.getContainingFile())) return null;
final FindUsagesManager findUsagesManager = ((FindManagerImpl)FindManager.getInstance(project)).getFindUsagesManager();
final FindUsagesHandler handler = findUsagesManager.getFindUsagesHandler(method, false);
if (handler == null) return null;//on failure or cancel (e.g. cancel of super methods dialog)
final JavaMethodFindUsagesOptions options = new JavaMethodFindUsagesOptions(project);
options.isImplementingMethods = true;
options.isOverridingMethods = true;
options.isUsages = true;
options.isSearchForTextOccurrences = false;
final int[] usagesFound = new int[1];
Runnable runnable = new Runnable() {
@Override
public void run() {
Processor<UsageInfo> processor = new Processor<UsageInfo>() {
@Override
public boolean process(final UsageInfo t) {
return ++usagesFound[0] < minUsagesNumber;
}
};
handler.processElementUsages(method, processor, options);
}
};
String progressTitle = QuickFixBundle.message("searching.for.usages.progress.title");
if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(runnable, progressTitle, true, project)) return null;
if (ApplicationManager.getApplication().isUnitTestMode() || usagesFound[0] < minUsagesNumber) {
ChangeSignatureProcessor processor = new ChangeSignatureProcessor(
project,
method,
false, null,
method.getName(),
method.getReturnType(),
newParametersInfo){
@Override
@NotNull
protected UsageInfo[] findUsages() {
return changeAllUsages ? super.findUsages() : UsageInfo.EMPTY_ARRAY;
}
@Override
protected void performRefactoring(UsageInfo[] usages) {
CommandProcessor.getInstance().setCurrentCommandName(getCommandName());
super.performRefactoring(usages);
}
};
processor.run();
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
UndoUtil.markPsiFileForUndo(file);
}
});
}
else {
final List<ParameterInfoImpl> parameterInfos = newParametersInfo != null
? new ArrayList<ParameterInfoImpl>(Arrays.asList(newParametersInfo))
: new ArrayList<ParameterInfoImpl>();
final PsiReferenceExpression refExpr = TargetElementUtil.findReferenceExpression(editor);
JavaChangeSignatureDialog dialog = JavaChangeSignatureDialog.createAndPreselectNew(project, method, parameterInfos, allowDelegation, refExpr);
dialog.setParameterInfos(parameterInfos);
dialog.show();
return dialog.getParameters();
}
return null;
}
public static String getNewParameterNameByOldIndex(int oldIndex, final ParameterInfoImpl[] parametersInfo) {
if (parametersInfo == null) return null;
for (ParameterInfoImpl info : parametersInfo) {
if (info.oldParameterIndex == oldIndex) {
return info.getName();
}
}
return null;
}
protected ParameterInfoImpl[] getNewParametersInfo(PsiExpression[] expressions,
PsiMethod targetMethod,
PsiSubstitutor substitutor) {
return getNewParametersInfo(expressions, targetMethod, substitutor, new StringBuilder(), new HashSet<ParameterInfoImpl>(), new HashSet<ParameterInfoImpl>(), new HashSet<ParameterInfoImpl>());
}
private ParameterInfoImpl[] getNewParametersInfo(PsiExpression[] expressions,
PsiMethod targetMethod,
PsiSubstitutor substitutor,
final StringBuilder buf,
final HashSet<ParameterInfoImpl> newParams,
final HashSet<ParameterInfoImpl> removedParams,
final HashSet<ParameterInfoImpl> changedParams) {
PsiParameter[] parameters = targetMethod.getParameterList().getParameters();
List<ParameterInfoImpl> result = new ArrayList<ParameterInfoImpl>();
if (expressions.length < parameters.length) {
// find which parameters to remove
int ei = 0;
int pi = 0;
while (ei < expressions.length && pi < parameters.length) {
PsiExpression expression = expressions[ei];
PsiParameter parameter = parameters[pi];
PsiType paramType = substitutor.substitute(parameter.getType());
if (buf.length() > 0) buf.append(", ");
final PsiType parameterType = PsiUtil.convertAnonymousToBaseType(paramType);
final String presentableText = escapePresentableType(parameterType);
final ParameterInfoImpl parameterInfo = new ParameterInfoImpl(pi, parameter.getName(), parameterType);
if (TypeConversionUtil.areTypesAssignmentCompatible(paramType, expression)) {
buf.append(presentableText);
result.add(parameterInfo);
pi++;
ei++;
}
else {
buf.append("<s>").append(presentableText).append("</s>");
removedParams.add(parameterInfo);
pi++;
}
}
if (result.size() != expressions.length) return null;
for(int i = pi; i < parameters.length; i++) {
if (buf.length() > 0) buf.append(", ");
buf.append("<s>").append(escapePresentableType(parameters[i].getType())).append("</s>");
final ParameterInfoImpl parameterInfo = new ParameterInfoImpl(pi, parameters[i].getName(), parameters[i].getType());
removedParams.add(parameterInfo);
}
}
else if (expressions.length > parameters.length) {
if (!findNewParamsPlace(expressions, targetMethod, substitutor, buf, newParams, parameters, result)) return null;
}
else {
//parameter type changed
for (int i = 0; i < parameters.length; i++) {
if (buf.length() > 0) buf.append(", ");
PsiParameter parameter = parameters[i];
PsiExpression expression = expressions[i];
PsiType paramType = substitutor.substitute(parameter.getType());
final String presentableText = escapePresentableType(paramType);
if (TypeConversionUtil.areTypesAssignmentCompatible(paramType, expression)) {
result.add(new ParameterInfoImpl(i, parameter.getName(), paramType));
buf.append(presentableText);
}
else {
PsiType exprType = RefactoringUtil.getTypeByExpression(expression);
if (exprType == null) return null;
if (exprType instanceof PsiDisjunctionType) {
exprType = ((PsiDisjunctionType)exprType).getLeastUpperBound();
}
final ParameterInfoImpl changedParameterInfo = new ParameterInfoImpl(i, parameter.getName(), exprType);
result.add(changedParameterInfo);
changedParams.add(changedParameterInfo);
buf.append("<s>").append(presentableText).append("</s> <b>").append(escapePresentableType(exprType)).append("</b>");
}
}
// do not perform silly refactorings
boolean isSilly = true;
for (int i = 0; i < result.size(); i++) {
PsiParameter parameter = parameters[i];
PsiType paramType = substitutor.substitute(parameter.getType());
ParameterInfoImpl parameterInfo = result.get(i);
String typeText = parameterInfo.getTypeText();
if (!paramType.equalsToText(typeText) && !paramType.getPresentableText().equals(typeText)) {
isSilly = false;
break;
}
}
if (isSilly) return null;
}
return result.toArray(new ParameterInfoImpl[result.size()]);
}
protected static String escapePresentableType(PsiType exprType) {
return StringUtil.escapeXml(exprType.getPresentableText());
}
protected boolean findNewParamsPlace(PsiExpression[] expressions,
PsiMethod targetMethod,
PsiSubstitutor substitutor,
StringBuilder buf,
HashSet<ParameterInfoImpl> newParams,
PsiParameter[] parameters,
List<ParameterInfoImpl> result) {
// find which parameters to introduce and where
Set<String> existingNames = new HashSet<String>();
for (PsiParameter parameter : parameters) {
existingNames.add(parameter.getName());
}
int ei = 0;
int pi = 0;
PsiParameter varargParam = targetMethod.isVarArgs() ? parameters[parameters.length - 1] : null;
while (ei < expressions.length || pi < parameters.length) {
if (buf.length() > 0) buf.append(", ");
PsiExpression expression = ei < expressions.length ? expressions[ei] : null;
PsiParameter parameter = pi < parameters.length ? parameters[pi] : null;
PsiType paramType = parameter == null ? null : substitutor.substitute(parameter.getType());
boolean parameterAssignable = paramType != null && (expression == null || TypeConversionUtil
.areTypesAssignmentCompatible(paramType, expression));
if (parameterAssignable) {
final PsiType type = parameter.getType();
result.add(new ParameterInfoImpl(pi, parameter.getName(), type));
buf.append(escapePresentableType(type));
pi++;
ei++;
}
else if (isArgumentInVarargPosition(expressions, ei, varargParam, substitutor)) {
if (pi == parameters.length - 1) {
assert varargParam != null;
final PsiType type = varargParam.getType();
result.add(new ParameterInfoImpl(pi, varargParam.getName(), type));
buf.append(escapePresentableType(type));
}
pi++;
ei++;
}
else if (expression != null) {
if (varargParam != null && pi >= parameters.length) return false;
PsiType exprType = RefactoringUtil.getTypeByExpression(expression);
if (exprType == null) return false;
if (exprType instanceof PsiDisjunctionType) {
exprType = ((PsiDisjunctionType)exprType).getLeastUpperBound();
}
JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(expression.getProject());
String name = suggestUniqueParameterName(codeStyleManager, expression, exprType, existingNames);
final ParameterInfoImpl newParameterInfo = new ParameterInfoImpl(-1, name, exprType, expression.getText().replace('\n', ' '));
result.add(newParameterInfo);
newParams.add(newParameterInfo);
buf.append("<b>").append(escapePresentableType(exprType)).append("</b>");
ei++;
}
}
if (result.size() != expressions.length && varargParam == null) return false;
return true;
}
static boolean isArgumentInVarargPosition(PsiExpression[] expressions, int ei, PsiParameter varargParam, PsiSubstitutor substitutor) {
if (varargParam == null) return false;
final PsiExpression expression = expressions[ei];
if (expression == null || TypeConversionUtil.areTypesAssignmentCompatible(substitutor.substitute(((PsiEllipsisType)varargParam.getType()).getComponentType()), expression)) {
final int lastExprIdx = expressions.length - 1;
if (ei == lastExprIdx) return true;
return expressions[lastExprIdx].getType() != PsiType.NULL;
}
return false;
}
static String suggestUniqueParameterName(JavaCodeStyleManager codeStyleManager,
PsiExpression expression,
PsiType exprType,
Set<String> existingNames) {
SuggestedNameInfo nameInfo = codeStyleManager.suggestVariableName(VariableKind.PARAMETER, null, expression, exprType);
@NonNls String[] names = nameInfo.names;
if (expression instanceof PsiReferenceExpression) {
final PsiElement resolve = ((PsiReferenceExpression)expression).resolve();
if (resolve instanceof PsiVariable) {
final VariableKind variableKind = codeStyleManager.getVariableKind((PsiVariable)resolve);
final String propertyName = codeStyleManager.variableNameToPropertyName(((PsiVariable)resolve).getName(), variableKind);
final String parameterName = codeStyleManager.propertyNameToVariableName(propertyName, VariableKind.PARAMETER);
names = ArrayUtil.mergeArrays(new String[]{parameterName}, names);
}
}
if (names.length == 0) names = new String[]{"param"};
int suffix = 0;
while (true) {
for (String name : names) {
String suggested = name + (suffix == 0 ? "" : String.valueOf(suffix));
if (existingNames.add(suggested)) {
return suggested;
}
}
suffix++;
}
}
@Override
public boolean startInWriteAction() {
return false;
}
}