[java-dfa] First class support for Collection.removeIf()

Fixes IDEA-286737 Side-effects from removeIf predicate are not taken into account

GitOrigin-RevId: 6efc71f98d10824a288e4510a209da34ad381bbb
This commit is contained in:
Tagir Valeev
2022-01-18 15:44:01 +00:00
committed by intellij-monorepo-bot
parent 7d070fec8f
commit d2e2a474ad
5 changed files with 102 additions and 6 deletions
@@ -2394,7 +2394,7 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
}
private static final CallInliner[] INLINERS = {
new OptionalChainInliner(), new LambdaInliner(),
new OptionalChainInliner(), new LambdaInliner(), new CollectionUpdateInliner(),
new StreamChainInliner(), new MapUpdateInliner(), new AssumeInliner(), new ClassMethodsInliner(),
new AssertAllInliner(), new BoxingInliner(), new SimpleMethodInliner(),
new TransformInliner(), new EnumCompareInliner(), new IndexOfInliner()
@@ -0,0 +1,62 @@
/*
* Copyright 2000-2017 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.inliner;
import com.intellij.codeInspection.dataFlow.DfaPsiUtil;
import com.intellij.codeInspection.dataFlow.Mutability;
import com.intellij.codeInspection.dataFlow.NullabilityUtil;
import com.intellij.codeInspection.dataFlow.java.CFGBuilder;
import com.intellij.codeInspection.dataFlow.jvm.SpecialField;
import com.intellij.codeInspection.dataFlow.jvm.problems.MutabilityProblem;
import com.intellij.codeInspection.dataFlow.types.DfType;
import com.intellij.codeInspection.dataFlow.types.DfTypes;
import com.intellij.codeInspection.dataFlow.value.DfaVariableValue;
import com.intellij.codeInspection.dataFlow.value.RelationType;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import org.jetbrains.annotations.NotNull;
public class CollectionUpdateInliner implements CallInliner {
private static final CallMatcher COLLECTION_REMOVEIF = CallMatcher.instanceCall(
CommonClassNames.JAVA_UTIL_COLLECTION, "removeIf").parameterTypes(CommonClassNames.JAVA_UTIL_FUNCTION_PREDICATE);
@Override
public boolean tryInlineCall(@NotNull CFGBuilder builder, @NotNull PsiMethodCallExpression call) {
if (COLLECTION_REMOVEIF.test(call)) {
PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
if (qualifier == null) return false;
PsiType elementType = PsiUtil.substituteTypeParameter(qualifier.getType(), CommonClassNames.JAVA_UTIL_COLLECTION, 0, true);
DfType elementDfType = DfTypes.typedObject(elementType, DfaPsiUtil.getTypeNullability(elementType));
PsiExpression predicate = call.getArgumentList().getExpressions()[0];
DfaVariableValue result = builder.createTempVariable(PsiType.BOOLEAN);
builder
.assignAndPop(result, DfTypes.FALSE)
.pushExpression(qualifier) // stack: qualifier
.ensure(RelationType.IS, Mutability.MUTABLE.asDfType(), new MutabilityProblem(call, true), null)
.evaluateFunction(predicate)
.unwrap(SpecialField.COLLECTION_SIZE) // stack: qualifier.size
.dup() // stack: qualifier.size qualifier.size
.push(DfTypes.intValue(0)) // stack: qualifier.size qualifier.size 0
.ifCondition(RelationType.GT)
.doWhileUnknown()
.push(elementDfType)
.invokeFunction(1, predicate)
.ifConditionIs(true)
.pushUnknown()
.assign()
.assignAndPop(result, DfTypes.TRUE)
.end()
.end()
.end()
.pop()
.push(result);
return true;
}
return false;
}
}
@@ -0,0 +1,36 @@
import java.util.Collection;
import java.util.Collections;
class RemoveIf
{
void unmodifiable(Collection<String> c) {
c = Collections.unmodifiableCollection(c);
c.<warning descr="Immutable object is modified">removeIf</warning>(x -> true);
}
void falsePredicate(Collection<String> c) {
int size = c.size();
c.removeIf(x -> false);
if (<warning descr="Condition 'size == c.size()' is always 'true'">size == c.size()</warning>) {}
c.removeIf(String::isEmpty);
if (size == c.size()) {}
}
void empty(Collection<String> c) {
if (!c.isEmpty()) return;
c.removeIf(x -> c.add(x));
if (<warning descr="Condition 'c.isEmpty()' is always 'true'">c.isEmpty()</warning>) {}
}
int x;
void returnValueAndSideEffect(Collection<String> c) {
x = 0;
boolean ret = c.removeIf(v -> {
x = 1;
return v.isEmpty();
});
if (<warning descr="Condition 'x == 0 && ret' is always 'false'">x == 0 && <warning descr="Condition 'ret' is always 'false' when reached">ret</warning></warning>) {}
if (x == 1 && ret) {}
}
}
@@ -78,6 +78,9 @@ public class DataFlowRangeAnalysisTest extends DataFlowInspectionTestCase {
public void testWidenMismatch() { doTest(); }
public void testDontWidenPlusInLoop() { doTest(); }
public void testCollectionAddRemove() { doTest(); }
public void testCollectionRemoveIf() { doTest(); }
public void testRelationsOnAddition() { doTest(); }
public void testModSpecialCase() { doTest(); }
public void testArrayAccessWithCastInCountedLoop() { doTest(); }
@@ -1899,11 +1899,6 @@
<item name="java.util.Collection boolean removeAll(java.util.Collection&lt;?&gt;) 0">
<annotation name="org.jetbrains.annotations.NotNull" />
</item>
<item name='java.util.Collection boolean removeIf(java.util.function.Predicate&lt;? super E&gt;)'>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>
</annotation>
</item>
<item name="java.util.Collection boolean retainAll(java.util.Collection&lt;?&gt;)">
<annotation name='org.jetbrains.annotations.Contract'>
<val name="mutates" val="&quot;this&quot;"/>