mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[java-inspections] IDEA-194953 Optional::get without isPresent inspection doesn't work in Stream::map with method reference
GitOrigin-RevId: bb857fbf79fd224406b37ad2a40dd33445575514
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7e1c9360f6
commit
84bc8aa356
+51
-22
@@ -3,6 +3,9 @@ package com.intellij.codeInspection.dataFlow;
|
||||
|
||||
import com.intellij.codeInspection.dataFlow.interpreter.RunnerResult;
|
||||
import com.intellij.codeInspection.dataFlow.java.JavaDfaListener;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaDfaAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaExpressionAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaMethodReferenceArgumentAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.jvm.SpecialField;
|
||||
import com.intellij.codeInspection.dataFlow.jvm.descriptors.AssertionDisabledDescriptor;
|
||||
import com.intellij.codeInspection.dataFlow.jvm.problems.ContractFailureProblem;
|
||||
@@ -89,8 +92,8 @@ public final class CommonDataflow {
|
||||
* Represents the result of dataflow applied to some code fragment (usually a method)
|
||||
*/
|
||||
public static final class DataflowResult {
|
||||
private final @NotNull Map<PsiExpression, DataflowPoint> myData = new HashMap<>();
|
||||
private @NotNull Map<PsiExpression, DataflowPoint> myDataAssertionsDisabled = myData;
|
||||
private final @NotNull Map<JavaDfaAnchor, DataflowPoint> myData = new HashMap<>();
|
||||
private @NotNull Map<JavaDfaAnchor, DataflowPoint> myDataAssertionsDisabled = myData;
|
||||
private final RunnerResult myResult;
|
||||
|
||||
public DataflowResult(RunnerResult result) {
|
||||
@@ -100,45 +103,46 @@ public final class CommonDataflow {
|
||||
@NotNull
|
||||
DataflowResult copy() {
|
||||
DataflowResult copy = new DataflowResult(myResult);
|
||||
myData.forEach((expression, point) -> copy.myData.put(expression, new DataflowPoint(point)));
|
||||
myData.forEach((anchor, point) -> copy.myData.put(anchor, new DataflowPoint(point)));
|
||||
return copy;
|
||||
}
|
||||
|
||||
void add(PsiExpression expression, DfaMemoryState memState, DfaValue value) {
|
||||
void add(JavaDfaAnchor anchor, DfaMemoryState memState, DfaValue value) {
|
||||
DfaVariableValue assertionDisabled = AssertionDisabledDescriptor.getAssertionsDisabledVar(value.getFactory());
|
||||
if (assertionDisabled == null) {
|
||||
assert myData == myDataAssertionsDisabled;
|
||||
updateDataPoint(myData, expression, memState, value);
|
||||
updateDataPoint(myData, anchor, memState, value);
|
||||
} else {
|
||||
DfType type = memState.getDfType(assertionDisabled);
|
||||
if (type == DfTypes.TRUE || type == DfTypes.FALSE) {
|
||||
if (myData == myDataAssertionsDisabled) {
|
||||
myDataAssertionsDisabled = new HashMap<>(myData);
|
||||
}
|
||||
updateDataPoint(type == DfTypes.TRUE ? myDataAssertionsDisabled : myData, expression, memState, value);
|
||||
updateDataPoint(type == DfTypes.TRUE ? myDataAssertionsDisabled : myData, anchor, memState, value);
|
||||
} else {
|
||||
updateDataPoint(myData, expression, memState, value);
|
||||
updateDataPoint(myData, anchor, memState, value);
|
||||
if (myData != myDataAssertionsDisabled) {
|
||||
updateDataPoint(myDataAssertionsDisabled, expression, memState, value);
|
||||
updateDataPoint(myDataAssertionsDisabled, anchor, memState, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDataPoint(Map<PsiExpression, DataflowPoint> data,
|
||||
PsiExpression expression,
|
||||
private void updateDataPoint(Map<JavaDfaAnchor, DataflowPoint> data,
|
||||
JavaDfaAnchor anchor,
|
||||
DfaMemoryState memState,
|
||||
DfaValue value) {
|
||||
DataflowPoint point = data.computeIfAbsent(expression, e -> new DataflowPoint());
|
||||
DataflowPoint point = data.computeIfAbsent(anchor, e -> new DataflowPoint());
|
||||
if (DfaTypeValue.isContractFail(value)) {
|
||||
point.myMayFailByContract = true;
|
||||
return;
|
||||
}
|
||||
if (point.myDfType != DfType.TOP) {
|
||||
if (point.myDfType != DfType.TOP && anchor instanceof JavaExpressionAnchor) {
|
||||
PsiExpression expression = ((JavaExpressionAnchor)anchor).getExpression();
|
||||
PsiElement parent = PsiUtil.skipParenthesizedExprUp(expression.getParent());
|
||||
if (parent instanceof PsiConditionalExpression &&
|
||||
!PsiTreeUtil.isAncestor(((PsiConditionalExpression)parent).getCondition(), expression, false)) {
|
||||
add((PsiExpression)parent, memState, value);
|
||||
add(new JavaExpressionAnchor((PsiExpression)parent), memState, value);
|
||||
}
|
||||
}
|
||||
point.addFacts(memState, value);
|
||||
@@ -154,11 +158,17 @@ public final class CommonDataflow {
|
||||
* If false is returned, it's possible that the expression exists in unreachable branch or this expression is not tracked due to
|
||||
* the dataflow implementation details.
|
||||
*/
|
||||
public boolean expressionWasAnalyzed(PsiExpression expression) {
|
||||
@Contract("null -> false")
|
||||
public boolean expressionWasAnalyzed(@Nullable PsiExpression expression) {
|
||||
if (expression == null) return false;
|
||||
if (expression instanceof PsiParenthesizedExpression) {
|
||||
throw new IllegalArgumentException("Should not pass parenthesized expression");
|
||||
}
|
||||
return myData.containsKey(expression);
|
||||
return myData.containsKey(new JavaExpressionAnchor(expression));
|
||||
}
|
||||
|
||||
public boolean anchorWasAnalyzed(@NotNull JavaDfaAnchor anchor) {
|
||||
return myData.containsKey(anchor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,8 +178,10 @@ public final class CommonDataflow {
|
||||
* @param call call to check
|
||||
* @return true if it cannot fail by contract; false if unknown or can fail
|
||||
*/
|
||||
public boolean cannotFailByContract(PsiCallExpression call) {
|
||||
DataflowPoint point = myData.get(call);
|
||||
@Contract("null -> false")
|
||||
public boolean cannotFailByContract(@Nullable PsiCallExpression call) {
|
||||
if (call == null) return false;
|
||||
DataflowPoint point = myData.get(new JavaExpressionAnchor(call));
|
||||
return point != null && !point.myMayFailByContract;
|
||||
}
|
||||
|
||||
@@ -182,7 +194,8 @@ public final class CommonDataflow {
|
||||
*/
|
||||
@NotNull
|
||||
public Set<Object> getExpressionValues(@Nullable PsiExpression expression) {
|
||||
DataflowPoint point = myData.get(expression);
|
||||
if (expression == null) return Collections.emptySet();
|
||||
DataflowPoint point = myData.get(new JavaExpressionAnchor(expression));
|
||||
if (point == null) return Collections.emptySet();
|
||||
Set<Object> values = point.myPossibleValues;
|
||||
return values == null ? Collections.emptySet() : Collections.unmodifiableSet(values);
|
||||
@@ -196,7 +209,14 @@ public final class CommonDataflow {
|
||||
*/
|
||||
@NotNull
|
||||
public DfType getDfType(PsiExpression expression) {
|
||||
DataflowPoint point = myData.get(expression);
|
||||
if (expression == null) return DfType.TOP;
|
||||
DataflowPoint point = myData.get(new JavaExpressionAnchor(expression));
|
||||
return point == null ? DfType.TOP : point.myDfType;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DfType getDfType(@NotNull JavaDfaAnchor anchor) {
|
||||
DataflowPoint point = myData.get(anchor);
|
||||
return point == null ? DfType.TOP : point.myDfType;
|
||||
}
|
||||
|
||||
@@ -208,7 +228,8 @@ public final class CommonDataflow {
|
||||
*/
|
||||
@NotNull
|
||||
public DfType getDfTypeNoAssertions(PsiExpression expression) {
|
||||
DataflowPoint point = myDataAssertionsDisabled.get(expression);
|
||||
if (expression == null) return DfType.TOP;
|
||||
DataflowPoint point = myDataAssertionsDisabled.get(new JavaExpressionAnchor(expression));
|
||||
return point == null ? DfType.TOP : point.myDfType;
|
||||
}
|
||||
}
|
||||
@@ -346,7 +367,14 @@ public final class CommonDataflow {
|
||||
public void beforeExpressionPush(@NotNull DfaValue value,
|
||||
@NotNull PsiExpression expression,
|
||||
@NotNull DfaMemoryState state) {
|
||||
myResult.add(expression, state, value);
|
||||
myResult.add(new JavaExpressionAnchor(expression), state, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeMethodReferenceArgumentPush(@NotNull DfaValue value,
|
||||
@NotNull PsiMethodReferenceExpression expression,
|
||||
@NotNull DfaMemoryState state) {
|
||||
myResult.add(new JavaMethodReferenceArgumentAnchor(expression), state, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -355,7 +383,8 @@ public final class CommonDataflow {
|
||||
@NotNull ThreeState failed,
|
||||
@NotNull DfaMemoryState state) {
|
||||
if (problem instanceof ContractFailureProblem && failed != ThreeState.NO) {
|
||||
myResult.add(((ContractFailureProblem)problem).getAnchor(), state, value.getFactory().fromDfType(DfType.FAIL));
|
||||
myResult.add(new JavaExpressionAnchor(((ContractFailureProblem)problem).getAnchor()), state,
|
||||
value.getFactory().fromDfType(DfType.FAIL));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.intellij.codeInsight.Nullability;
|
||||
import com.intellij.codeInspection.dataFlow.DfaPsiUtil;
|
||||
import com.intellij.codeInspection.dataFlow.NullabilityProblemKind;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaExpressionAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaMethodReferenceArgumentAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaMethodReferenceReturnAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.inliner.CallInliner;
|
||||
import com.intellij.codeInspection.dataFlow.java.inst.*;
|
||||
@@ -737,6 +738,9 @@ public class CFGBuilder {
|
||||
JavaResolveResult resolveResult = methodRef.advancedResolve(false);
|
||||
PsiMethod method = ObjectUtils.tryCast(resolveResult.getElement(), PsiMethod.class);
|
||||
if (method != null && !method.isVarArgs()) {
|
||||
if (argCount == 1) {
|
||||
add(new ResultOfInstruction(new JavaMethodReferenceArgumentAnchor(methodRef)));
|
||||
}
|
||||
if (processKnownMethodReference(argCount, methodRef, method)) return this;
|
||||
int expectedArgCount = method.getParameterList().getParametersCount();
|
||||
boolean pushQualifier = true;
|
||||
|
||||
+21
-9
@@ -1,10 +1,7 @@
|
||||
// Copyright 2000-2021 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.
|
||||
package com.intellij.codeInspection.dataFlow.java;
|
||||
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaDfaAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaEndOfInstanceInitializerAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaExpressionAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaMethodReferenceReturnAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.*;
|
||||
import com.intellij.codeInspection.dataFlow.lang.DfaAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.lang.DfaListener;
|
||||
import com.intellij.codeInspection.dataFlow.memory.DfaMemoryState;
|
||||
@@ -37,6 +34,9 @@ public interface JavaDfaListener extends DfaListener {
|
||||
PsiExpression psiAnchor = ((JavaExpressionAnchor)anchor).getExpression();
|
||||
callBeforeExpressionPush(value, psiAnchor, state, psiAnchor);
|
||||
}
|
||||
if (anchor instanceof JavaMethodReferenceArgumentAnchor) {
|
||||
beforeMethodReferenceArgumentPush(value, ((JavaMethodReferenceArgumentAnchor)anchor).getMethodReference(), state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,14 +90,26 @@ public interface JavaDfaListener extends DfaListener {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before implicit sole argument of method reference is pushed to the stack
|
||||
* @param value value that is about to be pushed
|
||||
* @param expression corresponding method reference
|
||||
* @param state memory state
|
||||
*/
|
||||
default void beforeMethodReferenceArgumentPush(@NotNull DfaValue value,
|
||||
@NotNull PsiMethodReferenceExpression expression,
|
||||
@NotNull DfaMemoryState state) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before returning the value from specific computation scope (method, lambda, method reference).
|
||||
* Can be called many times for the same scope.
|
||||
*
|
||||
* @param value value to be returned
|
||||
* @param expression expression that resulted in a given value (can be null)
|
||||
* @param context context ({@link PsiMethod}, or {@link PsiFunctionalExpression})
|
||||
* @param state memory state
|
||||
*
|
||||
* @param value value to be returned
|
||||
* @param expression expression that resulted in a given value (can be null)
|
||||
* @param context context ({@link PsiMethod}, or {@link PsiFunctionalExpression})
|
||||
* @param state memory state
|
||||
*/
|
||||
default void beforeValueReturn(@NotNull DfaValue value,
|
||||
@Nullable PsiExpression expression,
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.codeInspection.dataFlow.java.anchor;
|
||||
|
||||
import com.intellij.psi.PsiMethodReferenceExpression;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Implicit sole argument of the method reference pushed to the stack.
|
||||
* Anchored only in some cases when this method reference is handled by inliner
|
||||
*/
|
||||
public class JavaMethodReferenceArgumentAnchor extends JavaDfaAnchor {
|
||||
private final @NotNull PsiMethodReferenceExpression myMethodRef;
|
||||
|
||||
public JavaMethodReferenceArgumentAnchor(@NotNull PsiMethodReferenceExpression ref) {
|
||||
myMethodRef = ref;
|
||||
}
|
||||
|
||||
public @NotNull PsiMethodReferenceExpression getMethodReference() {
|
||||
return myMethodRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
JavaMethodReferenceArgumentAnchor anchor = (JavaMethodReferenceArgumentAnchor)o;
|
||||
return Objects.equals(myMethodRef, anchor.myMethodRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(myMethodRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "argument of " + myMethodRef.getText();
|
||||
}
|
||||
}
|
||||
+23
-7
@@ -5,6 +5,9 @@ import com.intellij.codeInsight.PsiEquivalenceUtil;
|
||||
import com.intellij.codeInspection.*;
|
||||
import com.intellij.codeInspection.dataFlow.CommonDataflow;
|
||||
import com.intellij.codeInspection.dataFlow.DfaNullability;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaDfaAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaExpressionAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.java.anchor.JavaMethodReferenceArgumentAnchor;
|
||||
import com.intellij.codeInspection.dataFlow.jvm.SpecialField;
|
||||
import com.intellij.codeInspection.dataFlow.types.DfReferenceType;
|
||||
import com.intellij.codeInspection.dataFlow.types.DfType;
|
||||
@@ -17,6 +20,7 @@ import com.intellij.psi.codeStyle.VariableKind;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.siyeh.ig.psiutils.CommentTracker;
|
||||
import com.siyeh.ig.psiutils.ExpressionUtils;
|
||||
import com.siyeh.ig.psiutils.VariableNameGenerator;
|
||||
@@ -39,19 +43,31 @@ public class OptionalGetWithoutIsPresentInspection extends AbstractBaseJavaLocal
|
||||
if (qualifier == null) return;
|
||||
PsiClass optionalClass = PsiUtil.resolveClassInClassTypeOnly(qualifier.getType());
|
||||
if (optionalClass == null) return;
|
||||
CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(qualifier);
|
||||
if (result == null || !result.expressionWasAnalyzed(qualifier)) return;
|
||||
DfType dfType = SpecialField.OPTIONAL_VALUE.getFromQualifier(result.getDfType(qualifier));
|
||||
if (dfType != DfType.TOP && !(dfType instanceof DfReferenceType)) return;
|
||||
DfaNullability nullability = DfaNullability.fromDfType(dfType);
|
||||
if ((nullability == DfaNullability.UNKNOWN || nullability == DfaNullability.NULLABLE) &&
|
||||
!isPresentCallWithSameQualifierExists(qualifier)) {
|
||||
JavaExpressionAnchor anchor = new JavaExpressionAnchor(qualifier);
|
||||
if (isOptionalProblem(qualifier, anchor) && !isPresentCallWithSameQualifierExists(qualifier)) {
|
||||
holder.registerProblem(nameElement,
|
||||
JavaBundle.message("inspection.optional.get.without.is.present.message", optionalClass.getName()),
|
||||
tryCreateFix(call));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOptionalProblem(@NotNull PsiExpression context, @NotNull JavaDfaAnchor anchor) {
|
||||
CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(context);
|
||||
if (result == null || !result.anchorWasAnalyzed(anchor)) return false;
|
||||
DfType dfType = SpecialField.OPTIONAL_VALUE.getFromQualifier(result.getDfType(anchor));
|
||||
if (dfType != DfType.TOP && !(dfType instanceof DfReferenceType)) return false;
|
||||
DfaNullability nullability = DfaNullability.fromDfType(dfType);
|
||||
return nullability == DfaNullability.UNKNOWN || nullability == DfaNullability.NULLABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitMethodReferenceExpression(PsiMethodReferenceExpression methodRef) {
|
||||
if (!OptionalUtil.OPTIONAL_GET.methodReferenceMatches(methodRef)) return;
|
||||
if (isOptionalProblem(methodRef, new JavaMethodReferenceArgumentAnchor(methodRef))) {
|
||||
holder.registerProblem(methodRef, JavaBundle.message("inspection.optional.get.without.is.present.method.reference.message"));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPresentCallWithSameQualifierExists(PsiExpression qualifier) {
|
||||
// Conservatively skip the results of method calls if there's an isPresent() call with the same qualifier in the method
|
||||
if (qualifier instanceof PsiMethodCallExpression) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import java.util.stream.*;
|
||||
import java.util.*;
|
||||
|
||||
public class OptionalGetMethodReference {
|
||||
public static void main(String[] args) {
|
||||
Stream.of(Optional.ofNullable(Math.random() > 0.5 ? 1 : null))
|
||||
.map(<warning descr="'Optional::get' without 'isPresent()' check">Optional::get</warning>)
|
||||
.forEach(System.out::println);
|
||||
|
||||
Stream.of(Optional.ofNullable(Math.random() > 0.5 ? 1 : null))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
+1
@@ -17,6 +17,7 @@ public class OptionalGetWithoutIsPresentInspectionTest extends LightJavaCodeInsi
|
||||
public void testFinalInheritance() { doTest(); }
|
||||
public void testOptionalGet() { doTest(); }
|
||||
public void testOptionalGetInlineLambda() { doTest(); }
|
||||
public void testOptionalGetMethodReference() { doTest(); }
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
|
||||
@@ -551,6 +551,7 @@ inspection.nullable.problems.not.annotated.getters.for.annotated.fields=Report n
|
||||
inspection.nullable.problems.notnull.overrides.option=Report @NotNull ¶meters overriding non-annotated
|
||||
inspection.nullable.problems.notnull.parameters.with.null.literal.option=Report @NotNull parameters with null-literal argument usages
|
||||
inspection.optional.get.without.is.present.message=<code>{0}.#ref()</code> without ''isPresent()'' check
|
||||
inspection.optional.get.without.is.present.method.reference.message=<code>#ref</code> without 'isPresent()' check
|
||||
inspection.overflowing.loop.index.inspection.description=Loop executes zero or billions of times
|
||||
inspection.overflowing.loop.index.inspection.name=Loop executes zero or billions of times
|
||||
inspection.overwritten.key.map.message=Duplicate Map key
|
||||
|
||||
Reference in New Issue
Block a user