guava inspection: migrate Optional.fromNullable chains (IDEA-160227)
[idea/community.git] / java / typeMigration / src / com / intellij / refactoring / typeMigration / rules / guava / GuavaFluentIterableConversionRule.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.rules.guava;
17
18 import com.intellij.codeInspection.java18StreamApi.PseudoLambdaReplaceTemplate;
19 import com.intellij.codeInspection.java18StreamApi.StreamApiConstants;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.util.NotNullLazyValue;
22 import com.intellij.psi.*;
23 import com.intellij.psi.codeStyle.JavaCodeStyleManager;
24 import com.intellij.psi.codeStyle.SuggestedNameInfo;
25 import com.intellij.psi.codeStyle.VariableKind;
26 import com.intellij.psi.util.InheritanceUtil;
27 import com.intellij.psi.util.PsiTypesUtil;
28 import com.intellij.refactoring.typeMigration.TypeConversionDescriptor;
29 import com.intellij.refactoring.typeMigration.TypeConversionDescriptorBase;
30 import com.intellij.refactoring.typeMigration.TypeEvaluator;
31 import com.intellij.refactoring.typeMigration.TypeMigrationLabeler;
32 import com.intellij.refactoring.typeMigration.rules.TypeConversionRule;
33 import com.intellij.util.IncorrectOperationException;
34 import com.intellij.util.SmartList;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.intellij.util.containers.Stack;
37 import com.intellij.util.containers.hash.HashMap;
38 import com.siyeh.ig.controlflow.DoubleNegationInspection;
39 import com.siyeh.ig.psiutils.ParenthesesUtils;
40 import org.jetbrains.annotations.NonNls;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import java.util.*;
45
46 /**
47  * @author Dmitry Batkovich
48  */
49 public class GuavaFluentIterableConversionRule extends BaseGuavaTypeConversionRule {
50   private static final Logger LOG = Logger.getInstance(GuavaFluentIterableConversionRule.class);
51   private static final Map<String, TypeConversionDescriptorFactory> DESCRIPTORS_MAP =
52     new HashMap<>();
53
54   public static final Set<String> CHAIN_HEAD_METHODS = ContainerUtil.newHashSet("from", "of", "fromNullable");
55   public static final String FLUENT_ITERABLE = "com.google.common.collect.FluentIterable";
56   public static final String STREAM_COLLECT_TO_LIST = "$it$.collect(java.util.stream.Collectors.toList())";
57
58   static class TypeConversionDescriptorFactory {
59     private final String myStringToReplace;
60     private final String myReplaceByString;
61     private final boolean myWithLambdaParameter;
62     private final boolean myChainedMethod;
63     private final boolean myFluentIterableReturnType;
64
65     public TypeConversionDescriptorFactory(String stringToReplace, String replaceByString, boolean withLambdaParameter) {
66       this(stringToReplace, replaceByString, withLambdaParameter, false, false);
67     }
68
69     public TypeConversionDescriptorFactory(@NonNls final String stringToReplace,
70                                            @NonNls final String replaceByString,
71                                            boolean withLambdaParameter,
72                                            boolean chainedMethod,
73                                            boolean fluentIterableReturnType) {
74       myStringToReplace = stringToReplace;
75       myReplaceByString = replaceByString;
76       myWithLambdaParameter = withLambdaParameter;
77       myChainedMethod = chainedMethod;
78       myFluentIterableReturnType = fluentIterableReturnType;
79     }
80
81     public TypeConversionDescriptor create() {
82       GuavaTypeConversionDescriptor descriptor = new GuavaTypeConversionDescriptor(myStringToReplace, myReplaceByString);
83       if (!myWithLambdaParameter) {
84         descriptor = descriptor.setConvertParameterAsLambda(false);
85       }
86       return descriptor;
87     }
88
89     public boolean isChainedMethod() {
90       return myChainedMethod;
91     }
92
93     public boolean isFluentIterableReturnType() {
94       return myFluentIterableReturnType;
95     }
96   }
97
98   static {
99     DESCRIPTORS_MAP.put("skip", new TypeConversionDescriptorFactory("$q$.skip($p$)", "$q$.skip($p$)", false, true, true));
100     DESCRIPTORS_MAP.put("limit", new TypeConversionDescriptorFactory("$q$.limit($p$)", "$q$.limit($p$)", false, true, true));
101     DESCRIPTORS_MAP.put("first", new TypeConversionDescriptorFactory("$q$.first()", "$q$.findFirst()", false, true, false));
102     DESCRIPTORS_MAP.put("transform", new TypeConversionDescriptorFactory("$q$.transform($params$)", "$q$.map($params$)", true, true, true));
103
104     DESCRIPTORS_MAP.put("allMatch", new TypeConversionDescriptorFactory("$it$.allMatch($c$)", "$it$." + StreamApiConstants.ALL_MATCH + "($c$)", true));
105     DESCRIPTORS_MAP.put("anyMatch", new TypeConversionDescriptorFactory("$it$.anyMatch($c$)", "$it$." + StreamApiConstants.ANY_MATCH + "($c$)", true));
106     DESCRIPTORS_MAP.put("firstMatch", new TypeConversionDescriptorFactory("$it$.firstMatch($p$)", "$it$.filter($p$).findFirst()", true, true, false));
107     DESCRIPTORS_MAP.put("size", new TypeConversionDescriptorFactory("$it$.size()", "(int) $it$.count()", false));
108   }
109
110   @Override
111   protected boolean isValidMethodQualifierToConvert(PsiClass aClass) {
112     return super.isValidMethodQualifierToConvert(aClass) ||
113            (aClass != null && GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(aClass.getQualifiedName()));
114   }
115
116   @Nullable
117   @Override
118   protected TypeConversionDescriptorBase findConversionForMethod(@NotNull PsiType from,
119                                                                  @NotNull PsiType to,
120                                                                  @NotNull PsiMethod method,
121                                                                  @NotNull String methodName,
122                                                                  PsiExpression context,
123                                                                  TypeMigrationLabeler labeler) {
124     if (context instanceof PsiMethodCallExpression) {
125       return buildCompoundDescriptor((PsiMethodCallExpression)context, to, labeler);
126     }
127
128     return getOneMethodDescriptor(methodName, method, from, to, context);
129   }
130
131   @Nullable
132   private static TypeConversionDescriptorBase getOneMethodDescriptor(@NotNull String methodName,
133                                                                      @NotNull PsiMethod method,
134                                                                      @NotNull PsiType from,
135                                                                      @Nullable PsiType to,
136                                                                      @Nullable PsiExpression context) {
137     TypeConversionDescriptor descriptorBase = null;
138     PsiType conversionType = null;
139     boolean needSpecifyType = true;
140     if (methodName.equals("of")) {
141       descriptorBase = new TypeConversionDescriptor("'FluentIterable*.of($arr$)", "java.util.Arrays.stream($arr$)");
142     } else if (methodName.equals("from")) {
143       descriptorBase = new TypeConversionDescriptor("'FluentIterable*.from($it$)", null) {
144         @Override
145         public PsiExpression replace(PsiExpression expression, TypeEvaluator evaluator) {
146           final PsiMethodCallExpression methodCall = (PsiMethodCallExpression)expression;
147           PsiExpression argument =
148             PseudoLambdaReplaceTemplate.replaceTypeParameters(methodCall.getArgumentList().getExpressions()[0]);
149           if (argument == null) {
150             return expression;
151           }
152           boolean isCollection =
153             InheritanceUtil.isInheritor(PsiTypesUtil.getPsiClass(argument.getType()), CommonClassNames.JAVA_UTIL_COLLECTION);
154           setReplaceByString(isCollection ? "($it$).stream()" : "java.util.stream.StreamSupport.stream(($it$).spliterator(), false)");
155           final PsiExpression replaced = super.replace(expression, evaluator);
156           ParenthesesUtils.removeParentheses(replaced, false);
157           return replaced;
158         }
159       };
160     } else if (methodName.equals("filter")) {
161       descriptorBase = FluentIterableConversionUtil.getFilterDescriptor(method);
162     } else if (methodName.equals("isEmpty")) {
163       descriptorBase = new TypeConversionDescriptor("$q$.isEmpty()", null) {
164         @Override
165         public PsiExpression replace(PsiExpression expression, TypeEvaluator evaluator) {
166           final PsiElement parent = expression.getParent();
167           boolean isDoubleNegation = false;
168           if (parent instanceof PsiExpression && DoubleNegationInspection.isNegation((PsiExpression)parent)) {
169             isDoubleNegation = true;
170             expression = (PsiExpression)parent.replace(expression);
171           }
172           setReplaceByString((isDoubleNegation ? "" : "!") + "$q$.findAny().isPresent()");
173           return super.replace(expression, evaluator);
174         }
175       };
176       needSpecifyType = false;
177     }
178     else if (methodName.equals("transformAndConcat")) {
179       descriptorBase = new FluentIterableConversionUtil.TransformAndConcatConversionRule();
180     } else if (methodName.equals("toArray")) {
181       descriptorBase = FluentIterableConversionUtil.getToArrayDescriptor(from, context);
182       needSpecifyType = false;
183     }
184     else if (methodName.equals("copyInto")) {
185       descriptorBase = new FluentIterableConversionUtil.CopyIntoConversionDescriptor();
186       needSpecifyType = false;
187     }
188     else if (methodName.equals("append")) {
189       descriptorBase = createDescriptorForAppend(method, context);
190     }
191     else if (methodName.equals("get")) {
192       descriptorBase = new TypeConversionDescriptor("$it$.get($p$)", null) {
193         @Override
194         public PsiExpression replace(PsiExpression expression, TypeEvaluator evaluator) {
195           PsiMethodCallExpression methodCall = (PsiMethodCallExpression)expression;
196           final PsiExpression[] arguments = methodCall.getArgumentList().getExpressions();
197           setReplaceByString("$it$.skip($p$).findFirst().get()");
198           if (arguments.length == 1 && arguments[0] instanceof PsiLiteralExpression) {
199             final Object value = ((PsiLiteralExpression)arguments[0]).getValue();
200             if (value != null && value.equals(0)) {
201               setReplaceByString("$it$.findFirst().get()");
202             }
203           }
204           return super.replace(expression, evaluator);
205         }
206       };
207       needSpecifyType = false;
208     }
209     else if (methodName.equals("contains")) {
210       descriptorBase = new TypeConversionDescriptor("$it$.contains($o$)", null) {
211         @Override
212         public PsiExpression replace(PsiExpression expression, TypeEvaluator evaluator) {
213           final PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression)expression;
214           final PsiExpression qualifier = methodCallExpression.getMethodExpression().getQualifierExpression();
215           LOG.assertTrue(qualifier != null);
216           final PsiClassType qualifierType = (PsiClassType)qualifier.getType();
217           LOG.assertTrue(qualifierType != null);
218           final PsiType[] parameters = qualifierType.getParameters();
219
220           final JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(expression.getProject());
221           final SuggestedNameInfo suggestedNameInfo = codeStyleManager
222             .suggestVariableName(VariableKind.LOCAL_VARIABLE, null, null, parameters.length == 1 ? parameters[0] : null, false);
223           final String suggestedName = codeStyleManager.suggestUniqueVariableName(suggestedNameInfo, expression, false).names[0];
224
225           setReplaceByString("$it$.anyMatch(" + suggestedName + " -> java.util.Objects.equals(" + suggestedName + ", $o$))");
226
227           return super.replace(expression, evaluator);
228         }
229       };
230       needSpecifyType = false;
231     }
232     else if (methodName.equals("last")) {
233       descriptorBase = new TypeConversionDescriptor("$it$.last()", null) {
234         @Override
235         public PsiExpression replace(PsiExpression expression, TypeEvaluator evaluator) {
236           final JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(expression.getProject());
237           String varA = suggestName("a", codeStyleManager, expression);
238           String varB = suggestName("b", codeStyleManager, expression);
239           setReplaceByString("$it$.reduce((" + varA + ", " + varB + ") -> " + varB + ")");
240           return super.replace(expression, evaluator);
241         }
242
243         private  String suggestName(String baseName, JavaCodeStyleManager codeStyleManager, PsiElement place) {
244           final SuggestedNameInfo suggestedNameInfo = codeStyleManager
245             .suggestVariableName(VariableKind.LOCAL_VARIABLE, baseName, null, null, false);
246           return codeStyleManager.suggestUniqueVariableName(suggestedNameInfo, place, false).names[0];
247         }
248       };
249     }
250     else {
251       final TypeConversionDescriptorFactory base = DESCRIPTORS_MAP.get(methodName);
252       if (base != null) {
253         final TypeConversionDescriptor descriptor = base.create();
254         needSpecifyType = base.isChainedMethod();
255         if (needSpecifyType && !base.isFluentIterableReturnType()) {
256           conversionType = GuavaConversionUtil.addTypeParameters(GuavaOptionalConversionRule.JAVA_OPTIONAL, context.getType(), context);
257         }
258         descriptorBase = descriptor;
259       }
260     }
261     if (descriptorBase == null) {
262       return FluentIterableConversionUtil.createToCollectionDescriptor(methodName, context);
263     }
264     if (needSpecifyType) {
265       if (conversionType == null) {
266         PsiMethodCallExpression methodCall = (PsiMethodCallExpression) (context instanceof PsiMethodCallExpression ? context : context.getParent());
267         conversionType = GuavaConversionUtil.addTypeParameters(GuavaTypeConversionDescriptor.isIterable(methodCall) ? CommonClassNames.JAVA_LANG_ITERABLE : StreamApiConstants.JAVA_UTIL_STREAM_STREAM, context.getType(), context);
268       }
269       descriptorBase.withConversionType(conversionType);
270     }
271     return descriptorBase;
272   }
273
274   @Nullable
275   private static TypeConversionDescriptor createDescriptorForAppend(PsiMethod method, PsiExpression context) {
276     LOG.assertTrue("append".equals(method.getName()));
277     final PsiParameterList list = method.getParameterList();
278     if (list.getParametersCount() != 1) return null;
279     final PsiType parameterType = list.getParameters()[0].getType();
280     if (parameterType instanceof PsiEllipsisType) {
281       return new TypeConversionDescriptor("$q$.append('params*)", "java.util.stream.Stream.concat($q$, java.util.Arrays.asList($params$).stream())");
282     }
283     else if (parameterType instanceof PsiClassType) {
284       final PsiClass psiClass = PsiTypesUtil.getPsiClass(parameterType);
285       if (psiClass != null && CommonClassNames.JAVA_LANG_ITERABLE.equals(psiClass.getQualifiedName())) {
286         PsiMethodCallExpression methodCall =
287           (PsiMethodCallExpression)(context instanceof PsiMethodCallExpression ? context : context.getParent());
288         final PsiExpression expression = methodCall.getArgumentList().getExpressions()[0];
289         boolean isCollection =
290           InheritanceUtil.isInheritor(PsiTypesUtil.getPsiClass(expression.getType()), CommonClassNames.JAVA_UTIL_COLLECTION);
291         final String argTemplate = isCollection ? "$arg$.stream()" : "java.util.stream.StreamSupport.stream($arg$.spliterator(), false)";
292         return new TypeConversionDescriptor("$q$.append($arg$)", "java.util.stream.Stream.concat($q$," + argTemplate + ")");
293       }
294     }
295     return null;
296   }
297
298   @Nullable
299   public static GuavaChainedConversionDescriptor buildCompoundDescriptor(PsiMethodCallExpression expression,
300                                                                           PsiType to,
301                                                                           TypeMigrationLabeler labeler) {
302     List<TypeConversionDescriptorBase> methodDescriptors = new SmartList<>();
303
304     NotNullLazyValue<TypeConversionRule> optionalDescriptor = new NotNullLazyValue<TypeConversionRule>() {
305       @NotNull
306       @Override
307       protected TypeConversionRule compute() {
308         for (TypeConversionRule rule : TypeConversionRule.EP_NAME.getExtensions()) {
309           if (rule instanceof GuavaOptionalConversionRule) {
310             return rule;
311           }
312         }
313         throw new RuntimeException("GuavaOptionalConversionRule extension is not found");
314       }
315     };
316
317     PsiMethodCallExpression current = expression;
318     while (true) {
319       final PsiMethod method = current.resolveMethod();
320       if (method == null) {
321         break;
322       }
323       final String methodName = method.getName();
324       final PsiClass containingClass = method.getContainingClass();
325       if (containingClass == null) {
326         break;
327       }
328       TypeConversionDescriptorBase descriptor = null;
329       if (FLUENT_ITERABLE.equals(containingClass.getQualifiedName())) {
330         descriptor = getOneMethodDescriptor(methodName, method, current.getType(), null, current);
331         if (descriptor == null) {
332           return null;
333         }
334       }
335       else if (GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(containingClass.getQualifiedName())) {
336         descriptor = optionalDescriptor.getValue().findConversion(null, null, method, current.getMethodExpression(), labeler);
337         if (descriptor == null) {
338           return null;
339         }
340       }
341       if (descriptor == null) {
342         addToMigrateChainQualifier(labeler, current);
343         break;
344       }
345       methodDescriptors.add(descriptor);
346       final PsiExpression qualifier = current.getMethodExpression().getQualifierExpression();
347       if (qualifier instanceof PsiMethodCallExpression) {
348         current = (PsiMethodCallExpression)qualifier;
349       }
350       else if (method.hasModifierProperty(PsiModifier.STATIC)) {
351         if (!CHAIN_HEAD_METHODS.contains(methodName)) {
352           return null;
353         }
354         final PsiClass aClass = method.getContainingClass();
355         if (aClass == null || !(FLUENT_ITERABLE.equals(aClass.getQualifiedName()) ||
356                                 GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(aClass.getQualifiedName()))) {
357           return null;
358         }
359         break;
360       }
361       else if (qualifier instanceof PsiReferenceExpression && ((PsiReferenceExpression)qualifier).resolve() instanceof PsiVariable) {
362         addToMigrateChainQualifier(labeler, qualifier);
363         break;
364       }
365       else {
366         return null;
367       }
368     }
369
370     return new GuavaChainedConversionDescriptor(methodDescriptors, to);
371   }
372
373   private static void addToMigrateChainQualifier(TypeMigrationLabeler labeler, PsiExpression qualifier) {
374     final PsiClass qClass = PsiTypesUtil.getPsiClass(qualifier.getType());
375     final boolean isFluentIterable;
376     if (qClass != null && ((isFluentIterable = GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(qClass.getQualifiedName())) ||
377                            GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(qClass.getQualifiedName()))) {
378       labeler.migrateExpressionType(qualifier,
379                                     GuavaConversionUtil.addTypeParameters(isFluentIterable ? StreamApiConstants.JAVA_UTIL_STREAM_STREAM :
380                                                                           GuavaOptionalConversionRule.JAVA_OPTIONAL, qualifier.getType(), qualifier),
381                                     qualifier.getParent(),
382                                     false,
383                                     false);
384     }
385   }
386
387   private static class GuavaChainedConversionDescriptor extends TypeConversionDescriptorBase {
388     private final List<TypeConversionDescriptorBase> myMethodDescriptors;
389     private final PsiType myToType;
390
391     private GuavaChainedConversionDescriptor(List<TypeConversionDescriptorBase> descriptors, PsiType to) {
392       myMethodDescriptors = new ArrayList<>(descriptors);
393       Collections.reverse(myMethodDescriptors);
394       myToType = to;
395     }
396
397     @Override
398     public PsiExpression replace(@NotNull PsiExpression expression, TypeEvaluator evaluator) throws IncorrectOperationException {
399       Stack<PsiMethodCallExpression> methodChainStack = new Stack<>();
400       PsiMethodCallExpression current = (PsiMethodCallExpression) expression;
401       while (current != null) {
402         methodChainStack.add(current);
403         final PsiExpression qualifier = current.getMethodExpression().getQualifierExpression();
404         current = qualifier instanceof PsiMethodCallExpression ? (PsiMethodCallExpression)qualifier : null;
405       }
406
407       if (methodChainStack.size() != myMethodDescriptors.size()) {
408         return expression;
409       }
410
411       PsiExpression converted = null;
412
413       for (TypeConversionDescriptorBase descriptor : myMethodDescriptors) {
414         final PsiMethodCallExpression toConvert = methodChainStack.pop();
415         converted = descriptor.replace(toConvert, evaluator);
416       }
417
418       return converted;
419     }
420
421     @Nullable
422     @Override
423     public PsiType conversionType() {
424       return myToType;
425     }
426   }
427
428   @NotNull
429   @Override
430   public String ruleFromClass() {
431     return FLUENT_ITERABLE;
432   }
433
434   @NotNull
435   @Override
436   public String ruleToClass() {
437     return StreamApiConstants.JAVA_UTIL_STREAM_STREAM;
438   }
439 }