constructor reference: don't ignore constructor parameters during method reference...
[idea/community.git] / java / java-impl / src / com / intellij / refactoring / inline / InlineToAnonymousClassHandler.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.refactoring.inline;
17
18 import com.intellij.codeInsight.TargetElementUtil;
19 import com.intellij.lang.StdLanguages;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.editor.Editor;
22 import com.intellij.openapi.progress.ProgressManager;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.util.Ref;
25 import com.intellij.patterns.ElementPattern;
26 import com.intellij.patterns.PlatformPatterns;
27 import com.intellij.psi.*;
28 import com.intellij.psi.search.GlobalSearchScope;
29 import com.intellij.psi.search.searches.ClassInheritorsSearch;
30 import com.intellij.psi.search.searches.FunctionalExpressionSearch;
31 import com.intellij.psi.search.searches.ReferencesSearch;
32 import com.intellij.psi.util.PsiTreeUtil;
33 import com.intellij.psi.util.PsiUtil;
34 import com.intellij.psi.util.TypeConversionUtil;
35 import com.intellij.refactoring.RefactoringBundle;
36 import com.intellij.refactoring.util.CommonRefactoringUtil;
37 import com.intellij.refactoring.util.InlineUtil;
38 import com.intellij.refactoring.util.RefactoringUtil;
39 import com.intellij.util.ArrayUtil;
40 import com.intellij.util.Processor;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import java.util.ArrayList;
45 import java.util.Collection;
46
47 /**
48  * @author yole
49  */
50 public class InlineToAnonymousClassHandler extends JavaInlineActionHandler {
51   static final ElementPattern ourCatchClausePattern = PlatformPatterns.psiElement(PsiTypeElement.class).withParent(PlatformPatterns.psiElement(PsiParameter.class).withParent(
52   PlatformPatterns.psiElement(PsiCatchSection.class)));
53   static final ElementPattern ourThrowsClausePattern = PlatformPatterns.psiElement().withParent(PlatformPatterns.psiElement(PsiReferenceList.class).withFirstChild(
54     PlatformPatterns.psiElement().withText(PsiKeyword.THROWS)));
55
56   @Override
57   public boolean isEnabledOnElement(PsiElement element) {
58     return element instanceof PsiMethod || element instanceof PsiClass;
59   }
60
61   @Override
62   public boolean canInlineElement(final PsiElement element) {
63     if (element.getLanguage() != StdLanguages.JAVA) return false;
64     if (element instanceof PsiMethod) {
65       PsiMethod method = (PsiMethod)element;
66       if (method.isConstructor() && !InlineUtil.isChainingConstructor(method)) {
67         final PsiClass containingClass = method.getContainingClass();
68         if (containingClass == null) return false;
69         return findClassInheritors(containingClass);
70       }
71     }
72     if (!(element instanceof PsiClass)) return false;
73     if (element instanceof PsiAnonymousClass) return false;
74     return findClassInheritors((PsiClass)element);
75   }
76
77   private static boolean findClassInheritors(final PsiClass element) {
78     final Collection<PsiElement> inheritors = new ArrayList<>();
79     if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> ApplicationManager.getApplication().runReadAction(() -> {
80       final PsiClass inheritor = ClassInheritorsSearch.search(element).findFirst();
81       if (inheritor != null) {
82         inheritors.add(inheritor);
83       } else {
84         final PsiFunctionalExpression functionalExpression = FunctionalExpressionSearch.search(element).findFirst();
85         if (functionalExpression != null) {
86           inheritors.add(functionalExpression);
87         }
88       }
89     }), "Searching for class \"" + element.getQualifiedName() + "\" inheritors ...", true, element.getProject())) return false;
90     return inheritors.isEmpty();
91   }
92
93   @Override
94   public boolean canInlineElementInEditor(PsiElement element, Editor editor) {
95     if (canInlineElement(element)) {
96       PsiReference reference = editor != null ? TargetElementUtil.findReference(editor, editor.getCaretModel().getOffset()) : null;
97       if (!InlineMethodHandler.isThisReference(reference)) {
98         if (element instanceof PsiMethod && reference != null) {
99           final PsiElement referenceElement = reference.getElement();
100           return !PsiTreeUtil.isAncestor(((PsiMethod)element).getContainingClass(), referenceElement, false);
101         }
102         return true;
103       }
104     }
105     return false;
106   }
107
108   @Override
109   public void inlineElement(final Project project, final Editor editor, final PsiElement psiElement) {
110     final PsiClass psiClass = psiElement instanceof PsiMethod ? ((PsiMethod) psiElement).getContainingClass() : (PsiClass) psiElement;
111     PsiCall callToInline = findCallToInline(editor);
112
113     final PsiClassType superType = InlineToAnonymousClassProcessor.getSuperType(psiClass);
114     if (superType == null) {
115       CommonRefactoringUtil.showErrorHint(project, editor, "java.lang.Object is not found", RefactoringBundle.message("inline.to.anonymous.refactoring"), null);
116       return;
117     }
118
119     final Ref<String> errorMessage = new Ref<>();
120     if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> ApplicationManager.getApplication().runReadAction(() -> errorMessage.set(getCannotInlineMessage((PsiClass)psiClass.getNavigationElement()))), "Check if inline is possible...", true, project)) return;
121     if (errorMessage.get() != null) {
122       CommonRefactoringUtil.showErrorHint(project, editor, errorMessage.get(), RefactoringBundle.message("inline.to.anonymous.refactoring"), null);
123       return;
124     }
125
126     new InlineToAnonymousClassDialog(project, psiClass, callToInline, canBeInvokedOnReference(callToInline, superType)).show();
127   }
128
129   public static boolean canBeInvokedOnReference(PsiCall callToInline, PsiType superType) {
130     if (callToInline != null) {
131       final PsiElement parent = callToInline.getParent();
132       if (parent instanceof PsiExpressionStatement || parent instanceof PsiSynchronizedStatement) {
133         return true;
134       }
135       else if (parent instanceof PsiReferenceExpression) {
136         return true;
137       }
138       else if (parent instanceof PsiExpressionList) {
139         final PsiMethodCallExpression methodCallExpression = PsiTreeUtil.getParentOfType(parent, PsiMethodCallExpression.class);
140         if (methodCallExpression != null) {
141           int paramIdx = ArrayUtil.find(methodCallExpression.getArgumentList().getExpressions(), callToInline);
142           if (paramIdx != -1) {
143             final JavaResolveResult resolveResult = methodCallExpression.resolveMethodGenerics();
144             final PsiElement resolvedMethod = resolveResult.getElement();
145             if (resolvedMethod instanceof PsiMethod) {
146               PsiType paramType;
147               final PsiParameter[] parameters = ((PsiMethod)resolvedMethod).getParameterList().getParameters();
148               if (paramIdx >= parameters.length) {
149                 final PsiParameter varargParameter = parameters[parameters.length - 1];
150                 paramType = varargParameter.getType();
151               }
152               else {
153                 paramType = parameters[paramIdx].getType();
154               }
155               if (paramType instanceof PsiEllipsisType) {
156                 paramType = ((PsiEllipsisType)paramType).getComponentType();
157               }
158               paramType = resolveResult.getSubstitutor().substitute(paramType);
159
160               final PsiJavaCodeReferenceElement classReference = ((PsiNewExpression)callToInline).getClassOrAnonymousClassReference();
161               if (classReference != null) {
162                 superType = classReference.advancedResolve(false).getSubstitutor().substitute(superType);
163                 if (TypeConversionUtil.isAssignable(paramType, superType)) {
164                   return true;
165                 }
166               }
167             }
168           }
169         }
170       }
171     }
172     return false;
173   }
174
175
176   @Nullable
177   public static PsiCall findCallToInline(final Editor editor) {
178     PsiCall callToInline = null;
179     PsiReference reference = editor != null ? TargetElementUtil.findReference(editor) : null;
180     if (reference != null) {
181       final PsiElement element = reference.getElement();
182       if (element instanceof PsiJavaCodeReferenceElement) {
183         callToInline = RefactoringUtil.getEnclosingConstructorCall((PsiJavaCodeReferenceElement)element);
184       }
185     }
186     return callToInline;
187   }
188
189   @Nullable
190   public static String getCannotInlineMessage(final PsiClass psiClass) {
191     if (psiClass instanceof PsiTypeParameter) {
192       return "Type parameters cannot be inlined";
193     }
194     if (psiClass.isAnnotationType()) {
195       return "Annotation types cannot be inlined";
196     }
197     if (psiClass.isInterface()) {
198       return "Interfaces cannot be inlined";
199     }
200     if (psiClass.isEnum()) {
201       return "Enums cannot be inlined";
202     }
203     if (psiClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
204       return RefactoringBundle.message("inline.to.anonymous.no.abstract");
205     }
206     if (psiClass instanceof PsiCompiledElement) {
207       return "Library classes cannot be inlined";
208     }
209
210     PsiClassType[] classTypes = psiClass.getExtendsListTypes();
211     for(PsiClassType classType: classTypes) {
212       PsiClass superClass = classType.resolve();
213       if (superClass == null) {
214         return "Class cannot be inlined because its superclass cannot be resolved";
215       }
216     }
217
218     final PsiClassType[] interfaces = psiClass.getImplementsListTypes();
219     if (interfaces.length > 1) {
220       return RefactoringBundle.message("inline.to.anonymous.no.multiple.interfaces");
221     }
222     if (interfaces.length == 1) {
223       if (interfaces [0].resolve() == null) {
224         return "Class cannot be inlined because an interface implemented by it cannot be resolved";
225       }
226       final PsiClass superClass = psiClass.getSuperClass();
227       if (superClass != null && !CommonClassNames.JAVA_LANG_OBJECT.equals(superClass.getQualifiedName())) {
228         PsiClassType interfaceType = interfaces[0];
229         if (!isRedundantImplements(superClass, interfaceType)) {
230           return RefactoringBundle.message("inline.to.anonymous.no.superclass.and.interface");
231         }
232       }
233     }
234
235     final GlobalSearchScope searchScope = GlobalSearchScope.projectScope(psiClass.getProject());
236     final PsiMethod[] methods = psiClass.getMethods();
237     for(PsiMethod method: methods) {
238       if (method.isConstructor()) {
239         if (PsiUtil.findReturnStatements(method).length > 0) {
240           return "Class cannot be inlined because its constructor contains 'return' statements";
241         }
242       }
243       else if (method.findSuperMethods().length == 0) {
244         if (!ReferencesSearch.search(method, searchScope).forEach(new AllowedUsagesProcessor(psiClass))) {
245           return "Class cannot be inlined because there are usages of its methods not inherited from its superclass or interface";
246         }
247       }
248       if (method.hasModifierProperty(PsiModifier.STATIC)) {
249         return "Class cannot be inlined because it has static methods";
250       }
251     }
252
253     final PsiClass[] innerClasses = psiClass.getInnerClasses();
254     for(PsiClass innerClass: innerClasses) {
255       PsiModifierList classModifiers = innerClass.getModifierList();
256       if (classModifiers != null && classModifiers.hasModifierProperty(PsiModifier.STATIC)) {
257         return "Class cannot be inlined because it has static inner classes";
258       }
259       if (!ReferencesSearch.search(innerClass, searchScope).forEach(new AllowedUsagesProcessor(psiClass))) {
260         return "Class cannot be inlined because it has usages of its inner classes";
261       }
262     }
263
264     final PsiField[] fields = psiClass.getFields();
265     for(PsiField field: fields) {
266       final PsiModifierList fieldModifiers = field.getModifierList();
267       if (fieldModifiers != null && fieldModifiers.hasModifierProperty(PsiModifier.STATIC)) {
268         if (!fieldModifiers.hasModifierProperty(PsiModifier.FINAL)) {
269           return "Class cannot be inlined because it has static non-final fields";
270         }
271         Object initValue = null;
272         final PsiExpression initializer = field.getInitializer();
273         if (initializer != null) {
274           initValue = JavaPsiFacade.getInstance(psiClass.getProject()).getConstantEvaluationHelper().computeConstantExpression(initializer);
275         }
276         if (initValue == null) {
277           return "Class cannot be inlined because it has static fields with non-constant initializers";
278         }
279       }
280       if (!ReferencesSearch.search(field, searchScope).forEach(new AllowedUsagesProcessor(psiClass))) {
281         return "Class cannot be inlined because it has usages of fields not inherited from its superclass";
282       }
283     }
284
285     final PsiClassInitializer[] initializers = psiClass.getInitializers();
286     for(PsiClassInitializer initializer: initializers) {
287       final PsiModifierList modifiers = initializer.getModifierList();
288       if (modifiers != null && modifiers.hasModifierProperty(PsiModifier.STATIC)) {
289         return "Class cannot be inlined because it has static initializers";
290       }
291     }
292
293     return getCannotInlineDueToUsagesMessage(psiClass);
294   }
295
296   static boolean isRedundantImplements(@NotNull final PsiClass superClass, final PsiClassType interfaceType) {
297     boolean redundantImplements = false;
298     PsiClassType[] superClassInterfaces = superClass.getImplementsListTypes();
299     for(PsiClassType superClassInterface: superClassInterfaces) {
300       if (superClassInterface.equals(interfaceType)) {
301         redundantImplements = true;
302         break;
303       }
304     }
305     return redundantImplements;
306   }
307
308   @Nullable
309   private static String getCannotInlineDueToUsagesMessage(final PsiClass aClass) {
310     boolean hasUsages = false;
311     for(PsiReference reference : ReferencesSearch.search(aClass, GlobalSearchScope.projectScope(aClass.getProject()))) {
312       final PsiElement element = reference.getElement();
313       if (!PsiTreeUtil.isAncestor(aClass, element, false)) {
314         hasUsages = true;
315       }
316       final PsiElement parentElement = element.getParent();
317       if (parentElement != null) {
318         final PsiElement grandPa = parentElement.getParent();
319         if (grandPa instanceof PsiClassObjectAccessExpression) {
320           return "Class cannot be inlined because it has usages of its class literal";
321         }
322         if (ourCatchClausePattern.accepts(parentElement)) {
323           return "Class cannot be inlined because it is used in a 'catch' clause";
324         }
325       }
326       if (ourThrowsClausePattern.accepts(element)) {
327         return "Class cannot be inlined because it is used in a 'throws' clause";
328       }
329       if (parentElement instanceof PsiThisExpression) {
330         return "Class cannot be inlined because it is used as a 'this' qualifier";
331       }
332       if (parentElement instanceof PsiNewExpression) {
333         final PsiNewExpression newExpression = (PsiNewExpression)parentElement;
334         final PsiMethod[] constructors = aClass.getConstructors();
335         if (constructors.length == 0) {
336           PsiExpressionList newArgumentList = newExpression.getArgumentList();
337           if (newArgumentList != null && !newArgumentList.isEmpty()) {
338             return "Class cannot be inlined because a call to its constructor is unresolved";
339           }
340         }
341         else {
342           final JavaResolveResult resolveResult = newExpression.resolveMethodGenerics();
343           if (!resolveResult.isValidResult()) {
344             return "Class cannot be inlined because a call to its constructor is unresolved";
345           }
346         }
347       }
348     }
349     if (!hasUsages) {
350       return RefactoringBundle.message("class.is.never.used");
351     }
352     return null;
353   }
354
355   private static class AllowedUsagesProcessor implements Processor<PsiReference> {
356     private final PsiElement myPsiElement;
357
358     AllowedUsagesProcessor(final PsiElement psiElement) {
359       myPsiElement = psiElement;
360     }
361
362     @Override
363     public boolean process(final PsiReference psiReference) {
364       PsiElement element = psiReference.getElement();
365       if (PsiTreeUtil.isAncestor(myPsiElement, element.getNavigationElement(), false)) {
366         return true;
367       }
368       if (element instanceof PsiReferenceExpression) {
369         PsiExpression qualifier = ((PsiReferenceExpression)element).getQualifierExpression();
370         while (qualifier instanceof PsiParenthesizedExpression) {
371           qualifier = ((PsiParenthesizedExpression) qualifier).getExpression();
372         }
373         if (qualifier instanceof PsiNewExpression) {
374           PsiNewExpression newExpr = (PsiNewExpression) qualifier;
375           PsiJavaCodeReferenceElement classRef = newExpr.getClassReference();
376           if (classRef != null && myPsiElement.isEquivalentTo(classRef.resolve())) {
377             return true;
378           }
379         }
380       }
381       return false;
382     }
383   }
384
385   @Nullable
386   @Override
387   public String getActionName(PsiElement element) {
388     return "Inline to Anonymous Class";
389   }
390 }