add PsiDocumentManagerImplTest.testPerformLaterWhenAllCommittedFromCommitHandler...
[idea/community.git] / java / typeMigration / src / com / intellij / refactoring / typeMigration / inspections / GuavaInspection.java
1 /*
2  * Copyright 2000-2015 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.typeMigration.inspections;
17
18 import com.intellij.codeInsight.FileModificationService;
19 import com.intellij.codeInspection.*;
20 import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel;
21 import com.intellij.openapi.command.undo.UndoUtil;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.util.AtomicNotNullLazyValue;
26 import com.intellij.psi.*;
27 import com.intellij.psi.search.GlobalSearchScope;
28 import com.intellij.psi.search.GlobalSearchScopesCore;
29 import com.intellij.psi.util.PsiTreeUtil;
30 import com.intellij.psi.util.PsiTypesUtil;
31 import com.intellij.psi.util.PsiUtil;
32 import com.intellij.refactoring.typeMigration.TypeMigrationProcessor;
33 import com.intellij.refactoring.typeMigration.TypeMigrationRules;
34 import com.intellij.refactoring.typeMigration.rules.TypeConversionRule;
35 import com.intellij.refactoring.typeMigration.rules.guava.*;
36 import com.intellij.reference.SoftLazyValue;
37 import com.intellij.util.Function;
38 import com.intellij.util.IncorrectOperationException;
39 import com.intellij.util.containers.ContainerUtil;
40 import com.intellij.util.containers.hash.HashMap;
41 import org.jetbrains.annotations.Nls;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44
45 import javax.swing.*;
46 import java.util.*;
47
48 /**
49  * @author Dmitry Batkovich
50  */
51 @SuppressWarnings("DialogTitleCapitalization")
52 public class GuavaInspection extends BaseJavaLocalInspectionTool {
53   //public class GuavaInspection extends BaseJavaBatchLocalInspectionTool {
54   private final static Logger LOG = Logger.getInstance(GuavaInspection.class);
55
56   public final static String PROBLEM_DESCRIPTION = "Guava's functional primitives can be replaced by Java API";
57
58   private final static SoftLazyValue<Set<String>> FLUENT_ITERABLE_STOP_METHODS = new SoftLazyValue<Set<String>>() {
59     @NotNull
60     @Override
61     protected Set<String> compute() {
62       return ContainerUtil.newHashSet("append", "cycle", "uniqueIndex", "index");
63     }
64   };
65
66   public boolean checkVariables = true;
67   public boolean checkChains = true;
68   public boolean checkReturnTypes = true;
69   public boolean ignoreJavaxNullable = true;
70
71   @SuppressWarnings("Duplicates")
72   @Override
73   public JComponent createOptionsPanel() {
74     final MultipleCheckboxOptionsPanel panel = new MultipleCheckboxOptionsPanel(this);
75     panel.addCheckbox("Report variables", "checkVariables");
76     panel.addCheckbox("Report method chains", "checkChains");
77     panel.addCheckbox("Report return types", "checkReturnTypes");
78     panel.addCheckbox("Erase @javax.annotations.Nullable from converted functions", "ignoreJavaxNullable");
79     return panel;
80   }
81
82   @NotNull
83   @Override
84   public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
85     if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) {
86       return PsiElementVisitor.EMPTY_VISITOR;
87     }
88     return new JavaElementVisitor() {
89       private final AtomicNotNullLazyValue<Map<String, PsiClass>> myGuavaClassConversions =
90         new AtomicNotNullLazyValue<Map<String, PsiClass>>() {
91           @NotNull
92           @Override
93           protected Map<String, PsiClass> compute() {
94             Map<String, PsiClass> map = new HashMap<String, PsiClass>();
95             for (TypeConversionRule rule : TypeConversionRule.EP_NAME.getExtensions()) {
96               if (rule instanceof BaseGuavaTypeConversionRule) {
97                 final String fromClass = ((BaseGuavaTypeConversionRule)rule).ruleFromClass();
98                 final String toClass = ((BaseGuavaTypeConversionRule)rule).ruleToClass();
99
100                 final Project project = holder.getProject();
101                 final JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(project);
102                 final PsiClass targetClass = javaPsiFacade.findClass(toClass, GlobalSearchScope.allScope(project));
103
104                 if (targetClass != null) {
105                   map.put(fromClass, targetClass);
106                 }
107               }
108             }
109             return map;
110           }
111         };
112
113       @Override
114       public void visitVariable(PsiVariable variable) {
115         if (!checkVariables) return;
116         final PsiType type = variable.getType();
117         PsiType targetType = getConversionClassType(type);
118         if (targetType != null) {
119           holder.registerProblem(variable.getNameIdentifier(),
120                                  PROBLEM_DESCRIPTION,
121                                  new MigrateGuavaTypeFix(variable, targetType));
122         }
123       }
124
125       @Override
126       public void visitMethod(PsiMethod method) {
127         super.visitMethod(method);
128         if (!checkReturnTypes) return;
129         final PsiType targetType = getConversionClassType(method.getReturnType());
130         if (targetType != null) {
131           final PsiTypeElement typeElement = method.getReturnTypeElement();
132           if (typeElement != null) {
133             holder.registerProblem(typeElement,
134                                    PROBLEM_DESCRIPTION,
135                                    new MigrateGuavaTypeFix(method, targetType));
136           }
137         }
138       }
139
140       @Override
141       public void visitMethodCallExpression(PsiMethodCallExpression expression) {
142         checkFluentIterableGenerationMethod(expression);
143         checkPredicatesUtilityMethod(expression);
144       }
145
146       private void checkPredicatesUtilityMethod(PsiMethodCallExpression expression) {
147         if (GuavaPredicateConversionRule.isPredicates(expression)) {
148           final PsiClassType initialType = (PsiClassType)expression.getType();
149           PsiClassType targetType = createTargetType(initialType);
150           if (targetType == null) return;
151           holder.registerProblem(expression.getMethodExpression().getReferenceNameElement(),
152                                  PROBLEM_DESCRIPTION,
153                                  new MigrateGuavaTypeFix(expression, targetType));
154         }
155       }
156
157       private void checkFluentIterableGenerationMethod(PsiMethodCallExpression expression) {
158         if (!checkChains) return;
159         if (!isFluentIterableFromCall(expression)) return;
160
161         final PsiMethodCallExpression chain = findGuavaMethodChain(expression);
162         if (chain == null) {
163           return;
164         }
165
166         PsiClassType initialType = (PsiClassType)chain.getType();
167         LOG.assertTrue(initialType != null);
168         PsiClassType targetType = createTargetType(initialType);
169         if (targetType == null) return;
170
171         PsiElement highlightedElement = chain;
172         if (chain.getParent() instanceof PsiReferenceExpression && chain.getParent().getParent() instanceof PsiMethodCallExpression) {
173           highlightedElement = chain.getParent().getParent();
174         }
175         holder.registerProblem(highlightedElement, PROBLEM_DESCRIPTION, new MigrateGuavaTypeFix(chain, targetType));
176       }
177
178       @Nullable
179       private PsiClassType createTargetType(PsiClassType initialType) {
180         PsiClass resolvedClass = initialType.resolve();
181         PsiClass target;
182         if (resolvedClass == null || (target = myGuavaClassConversions.getValue().get(resolvedClass.getQualifiedName())) == null) {
183           return null;
184         }
185         return addTypeParameters(initialType, initialType.resolveGenerics(), target);
186       }
187
188       private PsiType getConversionClassType(PsiType initialType) {
189         if (initialType == null) return null;
190         final PsiType type = initialType.getDeepComponentType();
191         if (type instanceof PsiClassType) {
192           final PsiClassType.ClassResolveResult resolveResult = ((PsiClassType)type).resolveGenerics();
193           final PsiClass psiClass = resolveResult.getElement();
194           if (psiClass != null) {
195             final String qName = psiClass.getQualifiedName();
196             final PsiClass targetClass = myGuavaClassConversions.getValue().get(qName);
197             if (targetClass != null) {
198               final PsiClassType createdType = addTypeParameters(type, resolveResult, targetClass);
199               return initialType instanceof PsiArrayType ? wrapAsArray((PsiArrayType)initialType, createdType) : createdType;
200             }
201           }
202         }
203         return null;
204       }
205
206       ;
207
208       private PsiType wrapAsArray(PsiArrayType initial, PsiType created) {
209         PsiArrayType result = new PsiArrayType(created);
210         while (initial.getComponentType() instanceof PsiArrayType) {
211           initial = (PsiArrayType)initial.getComponentType();
212           result = new PsiArrayType(result);
213         }
214         return result;
215       }
216
217       private boolean isFluentIterableFromCall(PsiMethodCallExpression expression) {
218         PsiMethod method = expression.resolveMethod();
219         if (method == null || !GuavaFluentIterableConversionRule.CHAIN_HEAD_METHODS.contains(method.getName())) {
220           return false;
221         }
222         PsiClass aClass = method.getContainingClass();
223         return aClass != null && (GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(aClass.getQualifiedName()) ||
224                                   GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(aClass.getQualifiedName()));
225       }
226
227       private PsiMethodCallExpression findGuavaMethodChain(PsiMethodCallExpression expression) {
228         PsiMethodCallExpression chain = expression;
229         while (true) {
230           final PsiMethodCallExpression current = PsiTreeUtil.getParentOfType(chain, PsiMethodCallExpression.class);
231           if (current != null && current.getMethodExpression().getQualifierExpression() == chain) {
232             final PsiMethod method = current.resolveMethod();
233             if (method == null) {
234               return chain;
235             }
236             if (FLUENT_ITERABLE_STOP_METHODS.getValue().contains(method.getName())) {
237               return null;
238             }
239             final PsiClass containingClass = method.getContainingClass();
240             if (containingClass == null || !(GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(containingClass.getQualifiedName())
241                                              || GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(containingClass.getQualifiedName()))) {
242               return chain;
243             }
244             final PsiType returnType = method.getReturnType();
245             final PsiClass returnClass = PsiTypesUtil.getPsiClass(returnType);
246             if (returnClass == null || !(GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(returnClass.getQualifiedName())
247                                     || GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(returnClass.getQualifiedName()))) {
248               return chain;
249             }
250             if (GuavaTypeConversionDescriptor.isIterable(current)) {
251               return chain;
252             }
253           }
254           else {
255             return chain;
256           }
257           chain = current;
258         }
259       }
260
261       @NotNull
262       private PsiClassType addTypeParameters(PsiType currentType,
263                                              PsiClassType.ClassResolveResult currentTypeResolveResult,
264                                              PsiClass targetClass) {
265         final Map<PsiTypeParameter, PsiType> substitutionMap = currentTypeResolveResult.getSubstitutor().getSubstitutionMap();
266         final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(holder.getProject());
267         if (substitutionMap.size() == 1) {
268           return elementFactory.createType(targetClass, ContainerUtil.getFirstItem(substitutionMap.values()));
269         }
270         else {
271           LOG.assertTrue(substitutionMap.size() == 2);
272           LOG.assertTrue(GuavaLambda.FUNCTION.getJavaAnalogueClassQName().equals(targetClass.getQualifiedName()));
273           final PsiType returnType = LambdaUtil.getFunctionalInterfaceReturnType(currentType);
274           final List<PsiType> types = new ArrayList<PsiType>(substitutionMap.values());
275           types.remove(returnType);
276           final PsiType parameterType = types.get(0);
277           return elementFactory.createType(targetClass, parameterType, returnType);
278         }
279       }
280     };
281   }
282
283   public class MigrateGuavaTypeFix extends LocalQuickFixAndIntentionActionOnPsiElement implements BatchQuickFix<ProblemDescriptor> {
284     private final PsiType myTargetType;
285
286     private MigrateGuavaTypeFix(@NotNull PsiElement element, PsiType targetType) {
287       super(element);
288       myTargetType = targetType;
289     }
290
291     @Override
292     public void invoke(@NotNull Project project,
293                        @NotNull PsiFile file,
294                        @Nullable("is null when called from inspection") Editor editor,
295                        @NotNull PsiElement startElement,
296                        @NotNull PsiElement endElement) {
297       performTypeMigration(Collections.singletonList(startElement), Collections.singletonList(myTargetType));
298     }
299
300     @Override
301     protected boolean isAvailable() {
302       return super.isAvailable() && myTargetType.isValid();
303     }
304
305     @NotNull
306     @Override
307     public String getText() {
308       final PsiElement element = getStartElement();
309       if (!myTargetType.isValid() || !element.isValid()) {
310         return getFamilyName();
311       }
312       final String presentation;
313       if (element instanceof PsiMethodCallExpression) {
314         presentation = "method chain";
315       }
316       else {
317         presentation = TypeMigrationProcessor.getPresentation(element);
318       }
319       return "Migrate " + presentation + " type to '" + myTargetType.getCanonicalText(false) + "'";
320     }
321
322     @Nls
323     @NotNull
324     @Override
325     public String getFamilyName() {
326       return "Migrate Guava's type to Java";
327     }
328
329     @Override
330     public boolean startInWriteAction() {
331       return false;
332     }
333
334     @Override
335     public void applyFix(@NotNull final Project project,
336                          @NotNull ProblemDescriptor[] descriptors,
337                          @NotNull List<PsiElement> psiElementsToIgnore,
338                          @Nullable Runnable refreshViews) {
339       final List<PsiElement> elementsToFix = new ArrayList<PsiElement>();
340       final List<PsiType> migrationTypes = new ArrayList<PsiType>();
341
342       for (ProblemDescriptor descriptor : descriptors) {
343         final MigrateGuavaTypeFix fix = getFix(descriptor);
344         elementsToFix.add(fix.getStartElement());
345         migrationTypes.add(fix.myTargetType);
346       }
347
348       if (!elementsToFix.isEmpty()) performTypeMigration(elementsToFix, migrationTypes);
349     }
350
351     private MigrateGuavaTypeFix getFix(ProblemDescriptor descriptor) {
352       final QuickFix[] fixes = descriptor.getFixes();
353       LOG.assertTrue(fixes != null);
354       for (QuickFix fix : fixes) {
355         if (fix instanceof MigrateGuavaTypeFix) {
356           return (MigrateGuavaTypeFix)fix;
357         }
358       }
359       throw new AssertionError();
360     }
361
362     private boolean performTypeMigration(List<PsiElement> elements, List<PsiType> types) {
363       PsiFile containingFile = null;
364       for (PsiElement element : elements) {
365         final PsiFile currentContainingFile = element.getContainingFile();
366         if (containingFile == null) {
367           containingFile = currentContainingFile;
368         }
369         else {
370           LOG.assertTrue(containingFile.isEquivalentTo(currentContainingFile));
371         }
372       }
373       LOG.assertTrue(containingFile != null);
374       if (!FileModificationService.getInstance().prepareFileForWrite(containingFile)) return false;
375       try {
376         final TypeMigrationRules rules = new TypeMigrationRules();
377         rules.setBoundScope(GlobalSearchScopesCore.projectProductionScope(containingFile.getProject())
378                               .union(GlobalSearchScopesCore.projectTestScope(containingFile.getProject())));
379         rules.addConversionRuleSettings(new GuavaConversionSettings(ignoreJavaxNullable));
380         TypeMigrationProcessor.runHighlightingTypeMigration(containingFile.getProject(),
381                                                             null,
382                                                             rules,
383                                                             elements.toArray(new PsiElement[elements.size()]),
384                                                             createMigrationTypeFunction(elements, types),
385                                                             true);
386         UndoUtil.markPsiFileForUndo(containingFile);
387       }
388       catch (IncorrectOperationException e) {
389         LOG.error(e);
390       }
391       return true;
392     }
393
394     private Function<PsiElement, PsiType> createMigrationTypeFunction(@NotNull final List<PsiElement> elements,
395                                                                              @NotNull final List<PsiType> types) {
396       LOG.assertTrue(elements.size() == types.size());
397       final Map<PsiElement, PsiType> mappings = new HashMap<PsiElement, PsiType>();
398       final Iterator<PsiType> typeIterator = types.iterator();
399       for (PsiElement element : elements) {
400         PsiType type = typeIterator.next();
401         mappings.put(element, type);
402       }
403       return element -> mappings.get(element);
404     }
405   }
406 }