Cleanup: NotNull/Nullable
[idea/community.git] / java / java-impl / src / com / intellij / codeInspection / OptionalIsPresentInspection.java
1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.codeInspection;
3
4 import com.intellij.codeInsight.Nullability;
5 import com.intellij.codeInspection.dataFlow.NullabilityUtil;
6 import com.intellij.codeInspection.util.LambdaGenerationUtil;
7 import com.intellij.codeInspection.util.OptionalRefactoringUtil;
8 import com.intellij.codeInspection.util.OptionalUtil;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.project.Project;
11 import com.intellij.openapi.util.Ref;
12 import com.intellij.psi.*;
13 import com.intellij.psi.codeStyle.CodeStyleManager;
14 import com.intellij.psi.codeStyle.VariableKind;
15 import com.intellij.psi.util.PsiTreeUtil;
16 import com.intellij.psi.util.PsiTypesUtil;
17 import com.intellij.psi.util.PsiUtil;
18 import com.intellij.util.ObjectUtils;
19 import com.siyeh.ig.psiutils.*;
20 import org.jetbrains.annotations.Contract;
21 import org.jetbrains.annotations.Nls;
22 import org.jetbrains.annotations.NotNull;
23 import org.jetbrains.annotations.Nullable;
24
25 import java.util.Objects;
26
27 import static com.intellij.codeInsight.PsiEquivalenceUtil.areElementsEquivalent;
28
29 public class OptionalIsPresentInspection extends AbstractBaseJavaLocalInspectionTool {
30   private static final Logger LOG = Logger.getInstance(OptionalIsPresentInspection.class);
31
32   private static final OptionalIsPresentCase[] CASES = {
33     new ReturnCase(),
34     new AssignmentCase(),
35     new ConsumerCase(),
36     new TernaryCase()
37   };
38
39   private enum ProblemType {
40     WARNING, INFO, NONE;
41
42     void registerProblem(@NotNull ProblemsHolder holder, @NotNull PsiExpression condition, OptionalIsPresentCase scenario) {
43       if(this != NONE) {
44         if (this == INFO && !holder.isOnTheFly()) {
45           return; //don't register fixes in batch mode
46         }
47         holder.registerProblem(condition, "Can be replaced with single expression in functional style",
48                                this == INFO ? ProblemHighlightType.INFORMATION : ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
49                                new OptionalIsPresentFix(scenario));
50       }
51     }
52   }
53
54   @NotNull
55   @Override
56   public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
57     if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) {
58       return PsiElementVisitor.EMPTY_VISITOR;
59     }
60     return new JavaElementVisitor() {
61       @Override
62       public void visitConditionalExpression(@NotNull PsiConditionalExpression expression) {
63         super.visitConditionalExpression(expression);
64         PsiExpression condition = PsiUtil.skipParenthesizedExprDown(expression.getCondition());
65         if (condition == null) return;
66         boolean invert = false;
67         PsiExpression strippedCondition = condition;
68         if (BoolUtils.isNegation(condition)) {
69           strippedCondition = BoolUtils.getNegated(condition);
70           invert = true;
71         }
72         PsiReferenceExpression optionalRef = extractOptionalFromIsPresentCheck(strippedCondition);
73         if (optionalRef == null) return;
74         PsiExpression thenExpression = invert ? expression.getElseExpression() : expression.getThenExpression();
75         PsiExpression elseExpression = invert ? expression.getThenExpression() : expression.getElseExpression();
76         check(condition, optionalRef, thenExpression, elseExpression);
77       }
78
79       @Override
80       public void visitIfStatement(@NotNull PsiIfStatement statement) {
81         super.visitIfStatement(statement);
82         PsiExpression condition = PsiUtil.skipParenthesizedExprDown(statement.getCondition());
83         if (condition == null) return;
84         boolean invert = false;
85         PsiExpression strippedCondition = condition;
86         if (BoolUtils.isNegation(condition)) {
87           strippedCondition = BoolUtils.getNegated(condition);
88           invert = true;
89         }
90         PsiReferenceExpression optionalRef = extractOptionalFromIsPresentCheck(strippedCondition);
91         if (optionalRef == null) return;
92         PsiStatement thenStatement = extractThenStatement(statement, invert);
93         PsiStatement elseStatement = extractElseStatement(statement, invert);
94         check(condition, optionalRef, thenStatement, elseStatement);
95       }
96
97       void check(@NotNull PsiExpression condition, PsiReferenceExpression optionalRef, PsiElement thenElement, PsiElement elseElement) {
98         for (OptionalIsPresentCase scenario : CASES) {
99           scenario.getProblemType(optionalRef, thenElement, elseElement).registerProblem(holder, condition, scenario);
100         }
101       }
102     };
103   }
104
105   private static boolean isRaw(@NotNull PsiVariable variable) {
106     PsiType type = variable.getType();
107     return type instanceof PsiClassType && ((PsiClassType)type).isRaw();
108   }
109
110   @Nullable
111   private static PsiStatement extractThenStatement(@NotNull PsiIfStatement ifStatement, boolean invert) {
112     if (invert) return extractElseStatement(ifStatement, false);
113     return ControlFlowUtils.stripBraces(ifStatement.getThenBranch());
114   }
115
116   @Nullable
117   private static PsiStatement extractElseStatement(@NotNull PsiIfStatement ifStatement, boolean invert) {
118     if (invert) return extractThenStatement(ifStatement, false);
119     PsiStatement statement = ControlFlowUtils.stripBraces(ifStatement.getElseBranch());
120     if (statement == null) {
121       PsiStatement thenStatement = extractThenStatement(ifStatement, false);
122       if (thenStatement instanceof PsiReturnStatement) {
123         PsiElement nextElement = PsiTreeUtil.skipWhitespacesAndCommentsForward(ifStatement);
124         if (nextElement instanceof PsiStatement) {
125           statement = ControlFlowUtils.stripBraces((PsiStatement)nextElement);
126         }
127       }
128     }
129     return statement;
130   }
131
132   @Nullable
133   @Contract("null -> null")
134   static PsiReferenceExpression extractOptionalFromIsPresentCheck(PsiExpression expression) {
135     if (!(expression instanceof PsiMethodCallExpression)) return null;
136     PsiMethodCallExpression call = (PsiMethodCallExpression)expression;
137     if (!call.getArgumentList().isEmpty()) return null;
138     if (!"isPresent".equals(call.getMethodExpression().getReferenceName())) return null;
139     PsiMethod method = call.resolveMethod();
140     if (method == null) return null;
141     PsiClass containingClass = method.getContainingClass();
142     if (containingClass == null || !CommonClassNames.JAVA_UTIL_OPTIONAL.equals(containingClass.getQualifiedName())) return null;
143     PsiReferenceExpression qualifier =
144       ObjectUtils.tryCast(call.getMethodExpression().getQualifierExpression(), PsiReferenceExpression.class);
145     if (qualifier == null) return null;
146     PsiElement element = qualifier.resolve();
147     if (!(element instanceof PsiVariable) || isRaw((PsiVariable)element)) return null;
148     return qualifier;
149   }
150
151   @Contract("null, _ -> false")
152   static boolean isOptionalGetCall(PsiElement element, @NotNull PsiReferenceExpression optionalRef) {
153     if (!(element instanceof PsiMethodCallExpression)) return false;
154     PsiMethodCallExpression call = (PsiMethodCallExpression)element;
155     if (!call.getArgumentList().isEmpty()) return false;
156     PsiReferenceExpression methodExpression = call.getMethodExpression();
157     if ("get".equals(methodExpression.getReferenceName())) {
158       PsiExpression qualifier = ExpressionUtils.getEffectiveQualifier(methodExpression);
159       return qualifier != null && areElementsEquivalent(qualifier, optionalRef);
160     }
161     return false;
162   }
163
164   @NotNull
165   static ProblemType getTypeByLambdaCandidate(@NotNull PsiReferenceExpression optionalRef,
166                                               @Nullable PsiElement lambdaCandidate,
167                                               @Nullable PsiExpression falseExpression) {
168     if (lambdaCandidate == null) return ProblemType.NONE;
169     if (lambdaCandidate instanceof PsiReferenceExpression &&
170         areElementsEquivalent(lambdaCandidate, optionalRef) && OptionalUtil.isOptionalEmptyCall(falseExpression)) {
171       return ProblemType.WARNING;
172     }
173     if (!LambdaGenerationUtil.canBeUncheckedLambda(lambdaCandidate, optionalRef::isReferenceTo)) return ProblemType.NONE;
174     Ref<Boolean> hasOptionalReference = new Ref<>(Boolean.FALSE);
175     boolean hasNoBadRefs = PsiTreeUtil.processElements(lambdaCandidate, e -> {
176       if (!(e instanceof PsiReferenceExpression)) return true;
177       if (!areElementsEquivalent(e, optionalRef)) return true;
178       // Check that Optional variable is referenced only in context of get() call
179       hasOptionalReference.set(Boolean.TRUE);
180       return isOptionalGetCall(e.getParent().getParent(), optionalRef);
181     });
182     if (!hasNoBadRefs) return ProblemType.NONE;
183     if (!hasOptionalReference.get() || !(lambdaCandidate instanceof PsiExpression)) return ProblemType.INFO;
184     PsiExpression expression = (PsiExpression)lambdaCandidate;
185     if (falseExpression != null) {
186       // falseExpression == null is "consumer" case (to be replaced with ifPresent())
187       if (!ExpressionUtils.isNullLiteral(falseExpression) &&
188           NullabilityUtil.getExpressionNullability(expression, true) != Nullability.NOT_NULL) {
189         // if falseExpression is null literal, then semantics is preserved
190         return ProblemType.INFO;
191       }
192       PsiType falseType = falseExpression.getType();
193       PsiType trueType = expression.getType();
194       // like x ? double_expression : integer_expression; support only if integer_expression is simple literal,
195       // so could be converted explicitly to double
196       if (falseType instanceof PsiPrimitiveType && trueType instanceof PsiPrimitiveType &&
197           !falseType.equals(trueType) && JavaPsiMathUtil.getNumberFromLiteral(falseExpression) == null) {
198         return ProblemType.NONE;
199       }
200     }
201     return ProblemType.WARNING;
202   }
203
204   @NotNull
205   static String generateOptionalLambda(@NotNull PsiElementFactory factory,
206                                        @NotNull CommentTracker ct,
207                                        PsiReferenceExpression optionalRef,
208                                        PsiElement trueValue) {
209     PsiType type = optionalRef.getType();
210     String paramName = new VariableNameGenerator(trueValue, VariableKind.PARAMETER)
211       .byType(OptionalUtil.getOptionalElementType(type)).byName("value").generate(true);
212     if(trueValue instanceof PsiExpressionStatement) {
213       trueValue = ((PsiExpressionStatement)trueValue).getExpression();
214     }
215     ct.markUnchanged(trueValue);
216     PsiElement copy = trueValue.copy();
217     for (PsiElement getCall : PsiTreeUtil.collectElements(copy, e -> isOptionalGetCall(e, optionalRef))) {
218       PsiElement result = getCall.replace(factory.createIdentifier(paramName));
219       if (copy == getCall) copy = result;
220     }
221     if(copy instanceof PsiStatement && !(copy instanceof PsiBlockStatement)) {
222       return paramName + "->{" + copy.getText()+"}";
223     }
224     return paramName + "->" + copy.getText();
225   }
226
227   static String generateOptionalUnwrap(@NotNull PsiElementFactory factory,
228                                        @NotNull CommentTracker ct,
229                                        @NotNull PsiReferenceExpression optionalRef,
230                                        @NotNull PsiExpression trueValue,
231                                        @NotNull PsiExpression falseValue,
232                                        PsiType targetType) {
233     if (areElementsEquivalent(trueValue, optionalRef) && OptionalUtil.isOptionalEmptyCall(falseValue)) {
234       trueValue =
235         factory.createExpressionFromText(CommonClassNames.JAVA_UTIL_OPTIONAL + ".of(" + optionalRef.getText() + ".get())", trueValue);
236     }
237     if (areElementsEquivalent(falseValue, optionalRef)) {
238       falseValue = factory.createExpressionFromText(CommonClassNames.JAVA_UTIL_OPTIONAL + ".empty()", falseValue);
239     }
240     String lambdaText = generateOptionalLambda(factory, ct, optionalRef, trueValue);
241     PsiLambdaExpression lambda = (PsiLambdaExpression)factory.createExpressionFromText(lambdaText, trueValue);
242     PsiExpression body = Objects.requireNonNull((PsiExpression)lambda.getBody());
243     return OptionalRefactoringUtil.generateOptionalUnwrap(optionalRef.getText(), lambda.getParameterList().getParameters()[0],
244                                                           body, ct.markUnchanged(falseValue), targetType, true);
245   }
246
247   static boolean isSimpleOrUnchecked(PsiExpression expression) {
248     return ExpressionUtils.isSafelyRecomputableExpression(expression) || LambdaGenerationUtil.canBeUncheckedLambda(expression);
249   }
250
251   static class OptionalIsPresentFix implements LocalQuickFix {
252     private final OptionalIsPresentCase myScenario;
253
254     OptionalIsPresentFix(OptionalIsPresentCase scenario) {
255       myScenario = scenario;
256     }
257
258     @Nls
259     @NotNull
260     @Override
261     public String getFamilyName() {
262       return "Replace Optional.isPresent() condition with functional style expression";
263     }
264
265     @Override
266     public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
267       PsiElement element = descriptor.getStartElement();
268       if (!(element instanceof PsiExpression)) return;
269       PsiExpression condition = (PsiExpression)element;
270       boolean invert = false;
271       if (BoolUtils.isNegation(condition)) {
272         condition = BoolUtils.getNegated(condition);
273         invert = true;
274       }
275       PsiReferenceExpression optionalRef = extractOptionalFromIsPresentCheck(condition);
276       if (optionalRef == null) return;
277       PsiElement cond = PsiTreeUtil.getParentOfType(element, PsiIfStatement.class, PsiConditionalExpression.class);
278       PsiElement thenElement;
279       PsiElement elseElement;
280       if(cond instanceof PsiIfStatement) {
281         thenElement = extractThenStatement((PsiIfStatement)cond, invert);
282         elseElement = extractElseStatement((PsiIfStatement)cond, invert);
283       } else if(cond instanceof PsiConditionalExpression) {
284         thenElement = invert ? ((PsiConditionalExpression)cond).getElseExpression() : ((PsiConditionalExpression)cond).getThenExpression();
285         elseElement = invert ? ((PsiConditionalExpression)cond).getThenExpression() : ((PsiConditionalExpression)cond).getElseExpression();
286       } else return;
287       if (myScenario.getProblemType(optionalRef, thenElement, elseElement) == ProblemType.NONE) return;
288       PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
289       CommentTracker ct = new CommentTracker();
290       String replacementText = myScenario.generateReplacement(factory, ct, optionalRef, thenElement, elseElement);
291       if (thenElement != null && !PsiTreeUtil.isAncestor(cond, thenElement, true)) ct.delete(thenElement);
292       if (elseElement != null && !PsiTreeUtil.isAncestor(cond, elseElement, true)) ct.delete(elseElement);
293       PsiElement result = ct.replaceAndRestoreComments(cond, replacementText);
294       LambdaCanBeMethodReferenceInspection.replaceAllLambdasWithMethodReferences(result);
295       RemoveRedundantTypeArgumentsUtil.removeRedundantTypeArguments(result);
296       CodeStyleManager.getInstance(project).reformat(result);
297     }
298   }
299
300   interface OptionalIsPresentCase {
301     @NotNull
302     ProblemType getProblemType(@NotNull PsiReferenceExpression optionalVariable,
303                                @Nullable PsiElement trueElement,
304                                @Nullable PsiElement falseElement);
305
306     @NotNull
307     String generateReplacement(@NotNull PsiElementFactory factory,
308                                @NotNull CommentTracker ct,
309                                @NotNull PsiReferenceExpression optionalVariable,
310                                PsiElement trueElement,
311                                PsiElement falseElement);
312   }
313
314   static class ReturnCase implements OptionalIsPresentCase {
315     @NotNull
316     @Override
317     public ProblemType getProblemType(@NotNull PsiReferenceExpression optionalRef,
318                                       @Nullable PsiElement trueElement,
319                                       @Nullable PsiElement falseElement) {
320       if (!(trueElement instanceof PsiReturnStatement) || !(falseElement instanceof PsiReturnStatement)) return ProblemType.NONE;
321       PsiExpression falseValue = ((PsiReturnStatement)falseElement).getReturnValue();
322       PsiExpression trueValue = ((PsiReturnStatement)trueElement).getReturnValue();
323       if (!isSimpleOrUnchecked(falseValue)) return ProblemType.NONE;
324       return getTypeByLambdaCandidate(optionalRef, trueValue, falseValue);
325     }
326
327     @NotNull
328     @Override
329     public String generateReplacement(@NotNull PsiElementFactory factory,
330                                       @NotNull CommentTracker ct, @NotNull PsiReferenceExpression optionalVariable,
331                                       PsiElement trueElement,
332                                       PsiElement falseElement) {
333       PsiExpression trueValue = ((PsiReturnStatement)trueElement).getReturnValue();
334       PsiExpression falseValue = ((PsiReturnStatement)falseElement).getReturnValue();
335       LOG.assertTrue(trueValue != null);
336       LOG.assertTrue(falseValue != null);
337       return "return " +
338              generateOptionalUnwrap(factory, ct, optionalVariable, trueValue, falseValue, PsiTypesUtil.getMethodReturnType(trueElement)) +
339              ";";
340     }
341   }
342
343   static class AssignmentCase implements OptionalIsPresentCase {
344     @NotNull
345     @Override
346     public ProblemType getProblemType(@NotNull PsiReferenceExpression optionalVariable,
347                                       @Nullable PsiElement trueElement,
348                                       @Nullable PsiElement falseElement) {
349       PsiAssignmentExpression trueAssignment = ExpressionUtils.getAssignment(trueElement);
350       PsiAssignmentExpression falseAssignment = ExpressionUtils.getAssignment(falseElement);
351       if (trueAssignment == null || falseAssignment == null) return ProblemType.NONE;
352       PsiExpression falseVal = falseAssignment.getRExpression();
353       PsiExpression trueVal = trueAssignment.getRExpression();
354       if (areElementsEquivalent(trueAssignment.getLExpression(), falseAssignment.getLExpression()) &&
355           isSimpleOrUnchecked(falseVal)) {
356         return getTypeByLambdaCandidate(optionalVariable, trueVal, falseVal);
357       }
358       return ProblemType.NONE;
359     }
360
361     @NotNull
362     @Override
363     public String generateReplacement(@NotNull PsiElementFactory factory,
364                                       @NotNull CommentTracker ct,
365                                       @NotNull PsiReferenceExpression optionalRef,
366                                       PsiElement trueElement,
367                                       PsiElement falseElement) {
368       PsiAssignmentExpression trueAssignment = ExpressionUtils.getAssignment(trueElement);
369       PsiAssignmentExpression falseAssignment = ExpressionUtils.getAssignment(falseElement);
370       LOG.assertTrue(trueAssignment != null);
371       LOG.assertTrue(falseAssignment != null);
372       PsiExpression lValue = trueAssignment.getLExpression();
373       PsiExpression trueValue = trueAssignment.getRExpression();
374       PsiExpression falseValue = falseAssignment.getRExpression();
375       LOG.assertTrue(trueValue != null);
376       LOG.assertTrue(falseValue != null);
377       return lValue.getText() + " = " + generateOptionalUnwrap(factory, ct, optionalRef, trueValue, falseValue, lValue.getType()) + ";";
378     }
379   }
380
381   static class TernaryCase implements OptionalIsPresentCase {
382     @NotNull
383     @Override
384     public ProblemType getProblemType(@NotNull PsiReferenceExpression optionalVariable,
385                                       @Nullable PsiElement trueElement,
386                                       @Nullable PsiElement falseElement) {
387       if(!(trueElement instanceof PsiExpression) || !(falseElement instanceof PsiExpression)) return ProblemType.NONE;
388       PsiExpression trueExpression = (PsiExpression)trueElement;
389       PsiExpression falseExpression = (PsiExpression)falseElement;
390       PsiType trueType = trueExpression.getType();
391       PsiType falseType = falseExpression.getType();
392       if (trueType == null || falseType == null || !trueType.isAssignableFrom(falseType) || !isSimpleOrUnchecked(falseExpression)) {
393         return ProblemType.NONE;
394       }
395       return getTypeByLambdaCandidate(optionalVariable, trueExpression, falseExpression);
396     }
397
398     @NotNull
399     @Override
400     public String generateReplacement(@NotNull PsiElementFactory factory,
401                                       @NotNull CommentTracker ct,
402                                       @NotNull PsiReferenceExpression optionalVariable,
403                                       PsiElement trueElement,
404                                       PsiElement falseElement) {
405       PsiExpression ternary = PsiTreeUtil.getParentOfType(trueElement, PsiConditionalExpression.class);
406       LOG.assertTrue(ternary != null);
407       PsiExpression trueExpression = (PsiExpression)trueElement;
408       PsiExpression falseExpression = (PsiExpression)falseElement;
409       return generateOptionalUnwrap(factory, ct, optionalVariable, trueExpression, falseExpression, ternary.getType());
410     }
411   }
412
413   static class ConsumerCase implements OptionalIsPresentCase {
414     @NotNull
415     @Override
416     public ProblemType getProblemType(@NotNull PsiReferenceExpression optionalRef,
417                                       @Nullable PsiElement trueElement,
418                                       @Nullable PsiElement falseElement) {
419       if (falseElement != null && !(falseElement instanceof PsiEmptyStatement)) return ProblemType.NONE;
420       if (!(trueElement instanceof PsiStatement)) return ProblemType.NONE;
421       if (trueElement instanceof PsiExpressionStatement) {
422         PsiExpression expression = ((PsiExpressionStatement)trueElement).getExpression();
423         if (isOptionalGetCall(expression, optionalRef)) return ProblemType.NONE;
424         trueElement = expression;
425       }
426       return getTypeByLambdaCandidate(optionalRef, trueElement, null);
427     }
428
429     @NotNull
430     @Override
431     public String generateReplacement(@NotNull PsiElementFactory factory,
432                                       @NotNull CommentTracker ct,
433                                       @NotNull PsiReferenceExpression optionalRef,
434                                       PsiElement trueElement,
435                                       PsiElement falseElement) {
436       return optionalRef.getText() + ".ifPresent(" + generateOptionalLambda(factory, ct, optionalRef, trueElement) + ");";
437     }
438   }
439 }