summaryrefslogtreecommitdiff
path: root/java/java-impl/src/com/intellij/refactoring/inline/InlineToAnonymousClassHandler.java
blob: e1732684424932e11cb2f4b9d86a2c01e0e599bf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
/*
 * Copyright 2000-2013 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.refactoring.inline;

import com.intellij.codeInsight.TargetElementUtilBase;
import com.intellij.lang.StdLanguages;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.patterns.ElementPattern;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.search.searches.FunctionalExpressionSearch;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.refactoring.util.RefactoringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Processor;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;

/**
 * @author yole
 */
public class InlineToAnonymousClassHandler extends JavaInlineActionHandler {
  static final ElementPattern ourCatchClausePattern = PlatformPatterns.psiElement(PsiTypeElement.class).withParent(PlatformPatterns.psiElement(PsiParameter.class).withParent(
  PlatformPatterns.psiElement(PsiCatchSection.class)));
  static final ElementPattern ourThrowsClausePattern = PlatformPatterns.psiElement().withParent(PlatformPatterns.psiElement(PsiReferenceList.class).withFirstChild(
    PlatformPatterns.psiElement().withText(PsiKeyword.THROWS)));

  @Override
  public boolean isEnabledOnElement(PsiElement element) {
    return element instanceof PsiMethod || element instanceof PsiClass;
  }

  @Override
  public boolean canInlineElement(final PsiElement element) {
    if (element.getLanguage() != StdLanguages.JAVA) return false;
    if (element instanceof PsiMethod) {
      PsiMethod method = (PsiMethod)element;
      if (method.isConstructor() && !InlineMethodHandler.isChainingConstructor(method)) {
        final PsiClass containingClass = method.getContainingClass();
        if (containingClass == null) return false;
        return findClassInheritors(containingClass);
      }
    }
    if (!(element instanceof PsiClass)) return false;
    if (element instanceof PsiAnonymousClass) return false;
    return findClassInheritors((PsiClass)element);
  }

  private static boolean findClassInheritors(final PsiClass element) {
    final Collection<PsiElement> inheritors = new ArrayList<PsiElement>();
    if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable(){
      @Override
      public void run() {
        ApplicationManager.getApplication().runReadAction(new Runnable() {
          @Override
          public void run() {
            final PsiClass inheritor = ClassInheritorsSearch.search(element).findFirst();
            if (inheritor != null) {
              inheritors.add(inheritor);
            } else {
              final PsiFunctionalExpression functionalExpression = FunctionalExpressionSearch.search(element).findFirst();
              if (functionalExpression != null) {
                inheritors.add(functionalExpression);
              }
            }
          }
        });
      }
    }, "Searching for class \"" + element.getQualifiedName() + "\" inheritors ...", true, element.getProject())) return false;
    return inheritors.size() == 0;
  }

  @Override
  public boolean canInlineElementInEditor(PsiElement element, Editor editor) {
    if (canInlineElement(element)) {
      PsiReference reference = editor != null ? TargetElementUtilBase.findReference(editor, editor.getCaretModel().getOffset()) : null;
      if (!InlineMethodHandler.isThisReference(reference)) {
        if (element instanceof PsiMethod && reference != null) {
          final PsiElement referenceElement = reference.getElement();
          return referenceElement != null && !PsiTreeUtil.isAncestor(((PsiMethod)element).getContainingClass(), referenceElement, false);
        }
        return true;
      }
    }
    return false;
  }

  @Override
  public void inlineElement(final Project project, final Editor editor, final PsiElement psiElement) {
    final PsiClass psiClass = psiElement instanceof PsiMethod ? ((PsiMethod) psiElement).getContainingClass() : (PsiClass) psiElement;
    PsiCall callToInline = findCallToInline(editor);

    final PsiClassType superType = InlineToAnonymousClassProcessor.getSuperType(psiClass);
    if (superType == null) {
      CommonRefactoringUtil.showErrorHint(project, editor, "java.lang.Object is not found", RefactoringBundle.message("inline.to.anonymous.refactoring"), null);
      return;
    }

    final Ref<String> errorMessage = new Ref<String>();
    if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable(){
      @Override
      public void run() {
        errorMessage.set(getCannotInlineMessage(psiClass));
      }
    }, "Check if inline is possible...", true, project)) return;
    if (errorMessage.get() != null) {
      CommonRefactoringUtil.showErrorHint(project, editor, errorMessage.get(), RefactoringBundle.message("inline.to.anonymous.refactoring"), null);
      return;
    }

    new InlineToAnonymousClassDialog(project, psiClass, callToInline, canBeInvokedOnReference(callToInline, superType)).show();
  }

  public static boolean canBeInvokedOnReference(PsiCall callToInline, PsiType superType) {
    if (callToInline != null) {
      final PsiElement parent = callToInline.getParent();
      if (parent instanceof PsiExpressionStatement || parent instanceof PsiSynchronizedStatement) {
        return true;
      }
      else if (parent instanceof PsiReferenceExpression) {
        return true;
      }
      else if (parent instanceof PsiExpressionList) {
        final PsiMethodCallExpression methodCallExpression = PsiTreeUtil.getParentOfType(parent, PsiMethodCallExpression.class);
        if (methodCallExpression != null) {
          int paramIdx = ArrayUtil.find(methodCallExpression.getArgumentList().getExpressions(), callToInline);
          if (paramIdx != -1) {
            final JavaResolveResult resolveResult = methodCallExpression.resolveMethodGenerics();
            final PsiElement resolvedMethod = resolveResult.getElement();
            if (resolvedMethod instanceof PsiMethod) {
              PsiType paramType;
              final PsiParameter[] parameters = ((PsiMethod)resolvedMethod).getParameterList().getParameters();
              if (paramIdx >= parameters.length) {
                final PsiParameter varargParameter = parameters[parameters.length - 1];
                paramType = varargParameter.getType();
              }
              else {
                paramType = parameters[paramIdx].getType();
              }
              if (paramType instanceof PsiEllipsisType) {
                paramType = ((PsiEllipsisType)paramType).getComponentType();
              }
              paramType = resolveResult.getSubstitutor().substitute(paramType);

              final PsiJavaCodeReferenceElement classReference = ((PsiNewExpression)callToInline).getClassOrAnonymousClassReference();
              if (classReference != null) {
                superType = classReference.advancedResolve(false).getSubstitutor().substitute(superType);
                if (TypeConversionUtil.isAssignable(paramType, superType)) {
                  return true;
                }
              }
            }
          }
        }
      }
    }
    return false;
  }


  @Nullable
  public static PsiCall findCallToInline(final Editor editor) {
    PsiCall callToInline = null;
    PsiReference reference = editor != null ? TargetElementUtilBase.findReference(editor) : null;
    if (reference != null) {
      final PsiElement element = reference.getElement();
      if (element instanceof PsiJavaCodeReferenceElement) {
        callToInline = RefactoringUtil.getEnclosingConstructorCall((PsiJavaCodeReferenceElement)element);
      }
    }
    return callToInline;
  }

  @Nullable
  public static String getCannotInlineMessage(final PsiClass psiClass) {
    if (psiClass.isAnnotationType()) {
      return "Annotation types cannot be inlined";
    }
    if (psiClass.isInterface()) {
      return "Interfaces cannot be inlined";
    }
    if (psiClass.isEnum()) {
      return "Enums cannot be inlined";
    }
    if (psiClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
      return RefactoringBundle.message("inline.to.anonymous.no.abstract");
    }
    if (!psiClass.getManager().isInProject(psiClass)) {
      return "Library classes cannot be inlined";
    }

    PsiClassType[] classTypes = psiClass.getExtendsListTypes();
    for(PsiClassType classType: classTypes) {
      PsiClass superClass = classType.resolve();
      if (superClass == null) {
        return "Class cannot be inlined because its superclass cannot be resolved";
      }
    }

    final PsiClassType[] interfaces = psiClass.getImplementsListTypes();
    if (interfaces.length > 1) {
      return RefactoringBundle.message("inline.to.anonymous.no.multiple.interfaces");
    }
    if (interfaces.length == 1) {
      if (interfaces [0].resolve() == null) {
        return "Class cannot be inlined because an interface implemented by it cannot be resolved";
      }
      final PsiClass superClass = psiClass.getSuperClass();
      if (superClass != null && !CommonClassNames.JAVA_LANG_OBJECT.equals(superClass.getQualifiedName())) {
        PsiClassType interfaceType = interfaces[0];
        if (!isRedundantImplements(superClass, interfaceType)) {
          return RefactoringBundle.message("inline.to.anonymous.no.superclass.and.interface");
        }
      }
    }

    final PsiMethod[] methods = psiClass.getMethods();
    for(PsiMethod method: methods) {
      if (method.isConstructor()) {
        PsiReturnStatement stmt = findReturnStatement(method);
        if (stmt != null) {
          return "Class cannot be inlined because its constructor contains 'return' statements";
        }
      }
      else if (method.findSuperMethods().length == 0) {
        if (!ReferencesSearch.search(method).forEach(new AllowedUsagesProcessor(psiClass))) {
          return "Class cannot be inlined because it has usages of methods not inherited from its superclass or interface";
        }
      }
      if (method.hasModifierProperty(PsiModifier.STATIC)) {
        return "Class cannot be inlined because it has static methods";
      }
    }

    final PsiClass[] innerClasses = psiClass.getInnerClasses();
    for(PsiClass innerClass: innerClasses) {
      PsiModifierList classModifiers = innerClass.getModifierList();
      if (classModifiers.hasModifierProperty(PsiModifier.STATIC)) {
        return "Class cannot be inlined because it has static inner classes";
      }
      if (!ReferencesSearch.search(innerClass).forEach(new AllowedUsagesProcessor(psiClass))) {
        return "Class cannot be inlined because it has usages of its inner classes";
      }
    }

    final PsiField[] fields = psiClass.getFields();
    for(PsiField field: fields) {
      final PsiModifierList fieldModifiers = field.getModifierList();
      if (fieldModifiers != null && fieldModifiers.hasModifierProperty(PsiModifier.STATIC)) {
        if (!fieldModifiers.hasModifierProperty(PsiModifier.FINAL)) {
          return "Class cannot be inlined because it has static non-final fields";
        }
        Object initValue = null;
        final PsiExpression initializer = field.getInitializer();
        if (initializer != null) {
          initValue = JavaPsiFacade.getInstance(psiClass.getProject()).getConstantEvaluationHelper().computeConstantExpression(initializer);
        }
        if (initValue == null) {
          return "Class cannot be inlined because it has static fields with non-constant initializers";
        }
      }
      if (!ReferencesSearch.search(field).forEach(new AllowedUsagesProcessor(psiClass))) {
        return "Class cannot be inlined because it has usages of fields not inherited from its superclass";
      }
    }

    final PsiClassInitializer[] initializers = psiClass.getInitializers();
    for(PsiClassInitializer initializer: initializers) {
      final PsiModifierList modifiers = initializer.getModifierList();
      if (modifiers != null && modifiers.hasModifierProperty(PsiModifier.STATIC)) {
        return "Class cannot be inlined because it has static initializers";
      }
    }

    return getCannotInlineDueToUsagesMessage(psiClass);
  }

  static boolean isRedundantImplements(final PsiClass superClass, final PsiClassType interfaceType) {
    boolean redundantImplements = false;
    PsiClassType[] superClassInterfaces = superClass.getImplementsListTypes();
    for(PsiClassType superClassInterface: superClassInterfaces) {
      if (superClassInterface.equals(interfaceType)) {
        redundantImplements = true;
        break;
      }
    }
    return redundantImplements;
  }

  private static PsiReturnStatement findReturnStatement(final PsiMethod method) {
    final Ref<PsiReturnStatement> stmt = Ref.create(null);
    method.accept(new JavaRecursiveElementWalkingVisitor() {
      @Override public void visitReturnStatement(final PsiReturnStatement statement) {
        super.visitReturnStatement(statement);
        stmt.set(statement);
      }
    });
    return stmt.get();
  }

  @Nullable
  private static String getCannotInlineDueToUsagesMessage(final PsiClass aClass) {
    boolean hasUsages = false;
    for(PsiReference reference : ReferencesSearch.search(aClass)) {
      final PsiElement element = reference.getElement();
      if (element == null) continue;
      if (!PsiTreeUtil.isAncestor(aClass, element, false)) {
        hasUsages = true;
      }
      final PsiElement parentElement = element.getParent();
      if (parentElement != null) {
        final PsiElement grandPa = parentElement.getParent();
        if (grandPa instanceof PsiClassObjectAccessExpression) {
          return "Class cannot be inlined because it has usages of its class literal";
        }
        if (ourCatchClausePattern.accepts(parentElement)) {
          return "Class cannot be inlined because it is used in a 'catch' clause";
        }
      }
      if (ourThrowsClausePattern.accepts(element)) {
        return "Class cannot be inlined because it is used in a 'throws' clause";
      }
      if (parentElement instanceof PsiThisExpression) {
        return "Class cannot be inlined because it is used as a 'this' qualifier";
      }
      if (parentElement instanceof PsiNewExpression) {
        final PsiNewExpression newExpression = (PsiNewExpression)parentElement;
        final PsiMethod[] constructors = aClass.getConstructors();
        if (constructors.length == 0) {
          PsiExpressionList newArgumentList = newExpression.getArgumentList();
          if (newArgumentList != null && newArgumentList.getExpressions().length > 0) {
            return "Class cannot be inlined because a call to its constructor is unresolved";
          }
        }
        else {
          final JavaResolveResult resolveResult = newExpression.resolveMethodGenerics();
          if (!resolveResult.isValidResult()) {
            return "Class cannot be inlined because a call to its constructor is unresolved";
          }
        }
      }
    }
    if (!hasUsages) {
      return RefactoringBundle.message("class.is.never.used");
    }
    return null;
  }

  private static class AllowedUsagesProcessor implements Processor<PsiReference> {
    private final PsiElement myPsiElement;

    public AllowedUsagesProcessor(final PsiElement psiElement) {
      myPsiElement = psiElement;
    }

    @Override
    public boolean process(final PsiReference psiReference) {
      if (PsiTreeUtil.isAncestor(myPsiElement, psiReference.getElement(), false)) {
        return true;
      }
      PsiElement element = psiReference.getElement();
      if (element instanceof PsiReferenceExpression) {
        PsiExpression qualifier = ((PsiReferenceExpression)element).getQualifierExpression();
        while (qualifier instanceof PsiParenthesizedExpression) {
          qualifier = ((PsiParenthesizedExpression) qualifier).getExpression();
        }
        if (qualifier instanceof PsiNewExpression) {
          PsiNewExpression newExpr = (PsiNewExpression) qualifier;
          PsiJavaCodeReferenceElement classRef = newExpr.getClassReference();
          if (classRef != null && myPsiElement.equals(classRef.resolve())) {
            return true;
          }
        }
      }
      return false;
    }
  }
}