| /* |
| * 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.intentions.style.parameterToEntry; |
| |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.progress.Task; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.*; |
| import com.intellij.psi.search.GlobalSearchScope; |
| import com.intellij.psi.search.searches.MethodReferencesSearch; |
| import com.intellij.psi.search.searches.ReferencesSearch; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.refactoring.ui.ConflictsDialog; |
| import com.intellij.refactoring.util.CommonRefactoringUtil; |
| import com.intellij.util.Function; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.HashSet; |
| import com.intellij.util.containers.MultiMap; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.plugins.groovy.intentions.GroovyIntentionsBundle; |
| import org.jetbrains.plugins.groovy.intentions.base.Intention; |
| import org.jetbrains.plugins.groovy.intentions.base.PsiElementPredicate; |
| import org.jetbrains.plugins.groovy.lang.psi.GrNamedElement; |
| import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase; |
| 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.signatures.GrClosureSignature; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrParametersOwner; |
| 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.arguments.GrNamedArgument; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall; |
| 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.params.GrParameter; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameterList; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.GrMapType; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.signatures.GrClosureSignatureUtil; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.statements.typedef.members.GrMethodImpl; |
| import org.jetbrains.plugins.groovy.lang.psi.util.GroovyPropertyUtils; |
| import org.jetbrains.plugins.groovy.refactoring.GroovyValidationUtil; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| |
| /** |
| * @author ilyas |
| */ |
| public class ConvertParameterToMapEntryIntention extends Intention { |
| |
| private static final Logger LOG = |
| Logger.getInstance("#org.jetbrains.plugins.groovy.intentions.style.ConvertParameterToMapEntryIntention"); |
| @NonNls private static final String CLOSURE_CAPTION = "closure"; |
| @NonNls private static final String CLOSURE_CAPTION_CAP = "Closure"; |
| @NonNls private static final String METHOD_CAPTION = "method"; |
| @NonNls private static final String METHOD_CAPTION_CAP = "Method"; |
| @NonNls private static final String REFACTORING_NAME = "Convert Parameter to Map Entry"; |
| @NonNls private static final String MAP_TYPE_TEXT = "Map"; |
| @NonNls private static final String[] MY_POSSIBLE_NAMES = new String[]{"attrs", "args", "params", "map"}; |
| |
| @Override |
| protected void processIntention(@NotNull final PsiElement element, final Project project, Editor editor) throws IncorrectOperationException { |
| // Method or closure to be refactored |
| final GrParametersOwner owner = PsiTreeUtil.getParentOfType(element, GrParametersOwner.class); |
| final Collection<PsiElement> occurrences = new ArrayList<PsiElement>(); |
| // Find all referenced expressions |
| final boolean success = collectOwnerOccurrences(project, owner, occurrences); |
| if (!success) return; |
| // Checking for Groovy files only |
| final boolean isClosure = owner instanceof GrClosableBlock; |
| if (!checkOwnerOccurrences(project, occurrences, isClosure)) return; |
| |
| // To add or not to add new parameter for map entries |
| final GrParameter firstParam = getFirstParameter(owner); |
| |
| switch (analyzeForNamedArguments(owner, occurrences)) { |
| case ERROR: { |
| final GrNamedElement namedElement = getReferencedElement(owner); |
| LOG.assertTrue(namedElement != null); |
| final String msg = GroovyIntentionsBundle |
| .message("wrong.first.parameter.type", isClosure ? CLOSURE_CAPTION_CAP : METHOD_CAPTION_CAP, namedElement.getName(), |
| firstParam.getName()); |
| showErrorMessage(msg, project); |
| return; |
| } |
| case MUST_BE_MAP: { |
| if (firstParam == getAppropriateParameter(element)) { |
| final String msg = GroovyIntentionsBundle.message("convert.cannot.itself"); |
| showErrorMessage(msg, project); |
| return; |
| } |
| performRefactoring(element, owner, occurrences, false, null, false); |
| break; |
| } |
| case IS_NOT_MAP: { |
| if (!ApplicationManager.getApplication().isUnitTestMode()) { |
| final String[] possibleNames = generateValidNames(MY_POSSIBLE_NAMES, firstParam); |
| |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| final GroovyMapParameterDialog dialog = new GroovyMapParameterDialog(project, possibleNames, true) { |
| @Override |
| protected void doOKAction() { |
| String name = getEnteredName(); |
| MultiMap<PsiElement, String> conflicts = new MultiMap<PsiElement, String>(); |
| assert name != null; |
| GroovyValidationUtil.validateNewParameterName(firstParam, conflicts, name); |
| if (isClosure) { |
| findClosureConflictUsages(conflicts, occurrences); |
| } |
| if (reportConflicts(conflicts, project)) { |
| performRefactoring(element, owner, occurrences, createNewFirst(), name, specifyTypeExplicitly()); |
| } |
| super.doOKAction(); |
| } |
| }; |
| dialog.show(); |
| } |
| }); |
| } |
| else { |
| //todo add statictics manager |
| performRefactoring(element, owner, occurrences, true, |
| (new GroovyValidationUtil.ParameterNameSuggester("attrs", firstParam)).generateName(), true); |
| } |
| break; |
| } |
| } |
| } |
| |
| private static void findClosureConflictUsages(MultiMap<PsiElement, String> conflicts, |
| Collection<PsiElement> occurrences) { |
| for (PsiElement occurrence : occurrences) { |
| PsiElement origin = occurrence; |
| while (occurrence instanceof GrReferenceExpression) { |
| occurrence = occurrence.getParent(); |
| } |
| if (occurrence instanceof GrArgumentList) { |
| conflicts.putValue(origin, GroovyIntentionsBundle.message("closure.used.as.variable")); |
| } |
| } |
| } |
| |
| private static String[] generateValidNames(final String[] names, final GrParameter param) { |
| return ContainerUtil.map2Array(names, String.class, new Function<String, String>() { |
| @Override |
| public String fun(final String s) { |
| return (new GroovyValidationUtil.ParameterNameSuggester(s, param)).generateName(); |
| } |
| }); |
| } |
| |
| private static void performRefactoring(final PsiElement element, |
| final GrParametersOwner owner, |
| final Collection<PsiElement> occurrences, |
| final boolean createNewFirstParam, |
| @Nullable final String mapParamName, |
| final boolean specifyMapType) { |
| final GrParameter param = getAppropriateParameter(element); |
| assert param != null; |
| final String paramName = param.getName(); |
| final String mapName = createNewFirstParam ? mapParamName : getFirstParameter(owner).getName(); |
| |
| |
| final Project project = element.getProject(); |
| final Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| final GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(project); |
| |
| final GrParameterList list = owner.getParameterList(); |
| assert list != null; |
| final int index = list.getParameterNumber(param); |
| if (!createNewFirstParam && index <= 0) { // bad undo |
| return; |
| } |
| |
| //Remove old arguments from occurrences |
| //final List<GrCall> calls = getCallOccurrences(occurrences); |
| try { |
| for (PsiElement occurrence : occurrences) { |
| GrReferenceExpression refExpr = null; |
| GroovyResolveResult resolveResult = null; |
| boolean isExplicitGetterCall = false; |
| if (occurrence instanceof GrReferenceExpression) { |
| final PsiElement parent = occurrence.getParent(); |
| if (parent instanceof GrCall) { |
| refExpr = (GrReferenceExpression)occurrence; |
| resolveResult = refExpr.advancedResolve(); |
| final PsiElement resolved = resolveResult.getElement(); |
| if (resolved instanceof PsiMethod && |
| GroovyPropertyUtils.isSimplePropertyGetter(((PsiMethod)resolved)) && |
| //check for explicit getter call |
| ((PsiMethod)resolved).getName().equals(refExpr.getReferenceName())) { |
| isExplicitGetterCall = true; |
| } |
| } |
| else if (parent instanceof GrReferenceExpression) { |
| resolveResult = ((GrReferenceExpression)parent).advancedResolve(); |
| final PsiElement resolved = resolveResult.getElement(); |
| if (resolved instanceof PsiMethod && "call".equals(((PsiMethod)resolved).getName())) { |
| refExpr = (GrReferenceExpression)parent; |
| } |
| } |
| } |
| if (refExpr == null) continue; |
| final GrClosureSignature signature = generateSignature(owner, refExpr); |
| if (signature == null) continue; |
| GrCall call; |
| if (isExplicitGetterCall) { |
| PsiElement parent = refExpr.getParent(); |
| LOG.assertTrue(parent instanceof GrCall); |
| parent = parent.getParent(); |
| if (parent instanceof GrReferenceExpression && "call".equals(((GrReferenceExpression)parent).getReferenceName())) { |
| parent = parent.getParent(); |
| } |
| if (parent instanceof GrCall) { |
| call = (GrCall)parent; |
| } |
| else { |
| continue; |
| } |
| } |
| else { |
| call = (GrCall)refExpr.getParent(); |
| } |
| |
| if (resolveResult.isInvokedOnProperty()) { |
| final PsiElement parent = call.getParent(); |
| if (parent instanceof GrCall) { |
| call = (GrCall)parent; |
| } |
| else if (parent instanceof GrReferenceExpression && parent.getParent() instanceof GrCall) { |
| final PsiElement resolved = ((GrReferenceExpression)parent).resolve(); |
| if (resolved instanceof PsiMethod && "call".equals(((PsiMethod)resolved).getName())) { |
| call = (GrCall)parent.getParent(); |
| } |
| else { |
| continue; |
| } |
| } |
| } |
| |
| final GrClosureSignatureUtil.ArgInfo<PsiElement>[] argInfos = GrClosureSignatureUtil.mapParametersToArguments(signature, call); |
| if (argInfos == null) continue; |
| final GrClosureSignatureUtil.ArgInfo<PsiElement> argInfo = argInfos[index]; |
| |
| final GrNamedArgument namedArg; |
| if (argInfo.isMultiArg) { |
| if (argInfo.args.isEmpty()) continue; |
| String arg = "[" + StringUtil.join(ContainerUtil.map(argInfo.args, new Function<PsiElement, String>() { |
| @Override |
| public String fun(PsiElement element) { |
| return element.getText(); |
| } |
| }), ", ") + "]"; |
| for (PsiElement psiElement : argInfo.args) { |
| psiElement.delete(); |
| } |
| namedArg = factory.createNamedArgument(paramName, factory.createExpressionFromText(arg)); |
| } |
| else { |
| if (argInfo.args.isEmpty()) continue; |
| final PsiElement argument = argInfo.args.iterator().next(); |
| assert argument instanceof GrExpression; |
| namedArg = factory.createNamedArgument(paramName, (GrExpression)argument); |
| argument.delete(); |
| } |
| call.addNamedArgument(namedArg); |
| } |
| } |
| catch (IncorrectOperationException e) { |
| LOG.error(e); |
| } |
| |
| //Replace of occurrences of old parameter in closure/method |
| final Collection<PsiReference> references = ReferencesSearch.search(param).findAll(); |
| for (PsiReference ref : references) { |
| final PsiElement elt = ref.getElement(); |
| if (elt instanceof GrReferenceExpression) { |
| GrReferenceExpression expr = (GrReferenceExpression)elt; |
| final GrExpression newExpr = factory.createExpressionFromText(mapName + "." + paramName); |
| expr.replaceWithExpression(newExpr, true); |
| } |
| } |
| |
| //Add new map parameter to closure/method if it's necessary |
| if (createNewFirstParam) { |
| try { |
| final GrParameter newParam = factory.createParameter(mapName, specifyMapType ? MAP_TYPE_TEXT : "", null); |
| list.addAfter(newParam, null); |
| } |
| catch (IncorrectOperationException e) { |
| LOG.error(e); |
| } |
| } |
| |
| //Eliminate obsolete parameter from parameter list |
| param.delete(); |
| } |
| }; |
| |
| CommandProcessor.getInstance().executeCommand(project, new Runnable() { |
| @Override |
| public void run() { |
| ApplicationManager.getApplication().runWriteAction(runnable); |
| } |
| }, REFACTORING_NAME, null); |
| } |
| |
| |
| @Nullable |
| private static GrParameter getAppropriateParameter(final PsiElement element) { |
| if (element instanceof GrParameter) { |
| return (GrParameter)element; |
| } |
| if (element instanceof GrReferenceExpression) { |
| final GrReferenceExpression expr = (GrReferenceExpression)element; |
| final PsiElement resolved = expr.resolve(); |
| LOG.assertTrue(resolved instanceof GrParameter); |
| return ((GrParameter)resolved); |
| } |
| LOG.error("Selected expression is not resolved to method/closure parameter"); |
| return null; |
| } |
| |
| @Nullable |
| private static GrClosureSignature generateSignature(GrParametersOwner owner, GrReferenceExpression refExpr) { |
| if (owner instanceof PsiMethod) { |
| final GroovyResolveResult resolveResult = refExpr.advancedResolve(); |
| final PsiSubstitutor substitutor = resolveResult.getSubstitutor(); |
| return GrClosureSignatureUtil.createSignature((PsiMethod)owner, substitutor); |
| } |
| else if (owner instanceof GrClosableBlock) { |
| return GrClosureSignatureUtil.createSignature((GrClosableBlock)owner); |
| } |
| return null; |
| } |
| |
| /** |
| * @param owner Method or closure |
| * @param occurrences references to owner |
| * @return true if there we use owner's first parameter as map, false if we need to add ne one as fist map |
| */ |
| private static FIRST_PARAMETER_KIND analyzeForNamedArguments(final GrParametersOwner owner, final Collection<PsiElement> occurrences) { |
| boolean thereAreNamedArguments = false; |
| for (PsiElement occurrence : occurrences) { |
| if (occurrence instanceof GrReferenceExpression && occurrence.getParent() instanceof GrCall) { |
| final GrCall call = (GrCall)occurrence.getParent(); |
| final GrArgumentList args = call.getArgumentList(); |
| if (args != null && args.getNamedArguments().length > 0) { |
| thereAreNamedArguments = true; |
| } |
| } |
| if (thereAreNamedArguments) break; |
| } |
| if (thereAreNamedArguments) { |
| if (firstOwnerParameterMustBeMap(owner)) { |
| return FIRST_PARAMETER_KIND.MUST_BE_MAP; |
| } |
| return FIRST_PARAMETER_KIND.ERROR; |
| } |
| return FIRST_PARAMETER_KIND.IS_NOT_MAP; |
| } |
| |
| private static boolean firstOwnerParameterMustBeMap(final GrParametersOwner owner) { |
| final GrParameter first = getFirstParameter(owner); |
| final PsiType type = first.getTypeGroovy(); |
| final PsiClassType mapType = GrMapType.create(GlobalSearchScope.allScope(owner.getProject())); |
| // First parameter may be used as map |
| return type == null || type.isConvertibleFrom(mapType); |
| } |
| |
| @NotNull |
| private static GrParameter getFirstParameter(final GrParametersOwner owner) { |
| final GrParameter[] params = owner.getParameters(); |
| LOG.assertTrue(params.length > 0); |
| return params[0]; |
| } |
| |
| protected enum FIRST_PARAMETER_KIND { |
| IS_NOT_MAP, MUST_BE_MAP, ERROR |
| } |
| |
| @Nullable |
| private static GrNamedElement getReferencedElement(final GrParametersOwner owner) { |
| if (owner instanceof GrMethodImpl) return ((GrMethodImpl)owner); |
| if (owner instanceof GrClosableBlock) { |
| final PsiElement parent = owner.getParent(); |
| if (parent instanceof GrVariable && ((GrVariable)parent).getInitializerGroovy() == owner) return ((GrVariable)parent); |
| } |
| return null; |
| } |
| |
| private static boolean checkOwnerOccurrences(final Project project, final Collection<PsiElement> occurrences, final boolean isClosure) { |
| boolean result = true; |
| final StringBuilder msg = new StringBuilder(); |
| msg.append(GroovyIntentionsBundle.message("conversion.not.allowed.in.non.groovy.files", isClosure ? CLOSURE_CAPTION : METHOD_CAPTION)); |
| for (PsiElement element : occurrences) { |
| final PsiFile file = element.getContainingFile(); |
| if (!(file instanceof GroovyFileBase)) { |
| result = false; |
| msg.append("\n").append(file.getName()); |
| } |
| } |
| if (!result) { |
| showErrorMessage(msg.toString(), project); |
| return false; |
| } |
| return true; |
| } |
| |
| private static boolean collectOwnerOccurrences(final Project project, |
| final GrParametersOwner owner, |
| final Collection<PsiElement> occurrences) { |
| final PsiElement namedElem = getReferencedElement(owner); |
| if (namedElem == null) return true; |
| final Ref<Boolean> result = new Ref<Boolean>(true); |
| final Task task = new Task.Modal(project, GroovyIntentionsBundle |
| .message("find.method.ro.closure.usages.0", owner instanceof GrClosableBlock ? CLOSURE_CAPTION : METHOD_CAPTION), true) { |
| @Override |
| public void run(@NotNull final ProgressIndicator indicator) { |
| final Collection<PsiReference> references = Collections.synchronizedSet(new HashSet<PsiReference>()); |
| final Processor<PsiReference> consumer = new Processor<PsiReference>() { |
| @Override |
| public boolean process(PsiReference psiReference) { |
| references.add(psiReference); |
| return true; |
| } |
| }; |
| ReferencesSearch.search(namedElem).forEach(consumer); |
| boolean isProperty = ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() { |
| @Override |
| public Boolean compute() { |
| return namedElem instanceof GrField && ((GrField)namedElem).isProperty(); |
| } |
| }); |
| if (isProperty) { |
| final GrAccessorMethod[] getters = ApplicationManager.getApplication().runReadAction(new Computable<GrAccessorMethod[]>() { |
| @Override |
| public GrAccessorMethod[] compute() { |
| return ((GrField)namedElem).getGetters(); |
| } |
| }); |
| for (GrAccessorMethod getter : getters) { |
| MethodReferencesSearch.search(getter).forEach(consumer); |
| } |
| } |
| for (final PsiReference reference : references) { |
| ApplicationManager.getApplication().runReadAction(new Runnable() { |
| @Override |
| public void run() { |
| final PsiElement element = reference.getElement(); |
| if (element != null) { |
| occurrences.add(element); |
| } |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onCancel() { |
| result.set(false); |
| } |
| |
| @Override |
| public void onSuccess() { |
| result.set(true); |
| } |
| }; |
| ProgressManager.getInstance().run(task); |
| return result.get().booleanValue(); |
| } |
| |
| @Override |
| @NotNull |
| protected PsiElementPredicate getElementPredicate() { |
| return new MyPsiElementPredicate(); |
| } |
| |
| private static class MyPsiElementPredicate implements PsiElementPredicate { |
| @Override |
| public boolean satisfiedBy(final PsiElement element) { |
| GrParameter parameter = null; |
| if (element instanceof GrParameter) { |
| parameter = (GrParameter)element; |
| } |
| else if (element instanceof GrReferenceExpression) { |
| GrReferenceExpression expr = (GrReferenceExpression)element; |
| if (expr.getQualifierExpression() != null) return false; |
| final PsiElement resolved = expr.resolve(); |
| if (resolved instanceof GrParameter) { |
| parameter = (GrParameter)resolved; |
| } |
| } |
| if (parameter == null) return false; |
| if (parameter.isOptional()) return false; |
| |
| GrParametersOwner owner = PsiTreeUtil.getParentOfType(element, GrParametersOwner.class); |
| if (!(owner instanceof GrClosableBlock || owner instanceof GrMethod)) return false; |
| return checkForMapParameters(owner); |
| } |
| } |
| |
| private static boolean checkForMapParameters(GrParametersOwner owner) { |
| final GrParameter[] parameters = owner.getParameters(); |
| if (parameters.length != 1) return true; |
| |
| final GrParameter parameter = parameters[0]; |
| final PsiType type = parameter.getTypeGroovy(); |
| if (!(type instanceof PsiClassType)) return true; |
| |
| final PsiClass psiClass = ((PsiClassType)type).resolve(); |
| return psiClass == null || !CommonClassNames.JAVA_UTIL_MAP.equals(psiClass.getQualifiedName()); |
| } |
| |
| private static void showErrorMessage(String message, final Project project) { |
| CommonRefactoringUtil.showErrorMessage(REFACTORING_NAME, message, null, project); |
| } |
| |
| private static boolean reportConflicts(final MultiMap<PsiElement, String> conflicts, final Project project) { |
| if (conflicts.isEmpty()) return true; |
| ConflictsDialog conflictsDialog = new ConflictsDialog(project, conflicts); |
| conflictsDialog.show(); |
| return conflictsDialog.isOK(); |
| } |
| } |