SpecialField#MAP_SIZE merged into COLLECTION_SIZE; support clear (IDEA-205726), support keySet/etc.size (IDEA-205727)

This commit is contained in:
Tagir Valeev
2019-01-21 15:16:54 +07:00
parent cd40025bd1
commit 297f7127b1
14 changed files with 174 additions and 52 deletions
@@ -219,7 +219,7 @@ public abstract class ContractValue {
@Override
public String toString() {
return myQualifier + "." + myField.getMethodName() + "()";
return myQualifier + "." + myField + "()";
}
}
@@ -1861,12 +1861,11 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
private DfaValue getPrecalculatedNewValue(PsiNewExpression expression) {
PsiType type = expression.getType();
if (type != null && ConstructionUtils.isEmptyCollectionInitializer(expression)) {
SpecialField sizeField = InheritanceUtil.isInheritor(type, JAVA_UTIL_MAP) ? SpecialField.MAP_SIZE : SpecialField.COLLECTION_SIZE;
DfaFactMap facts = DfaFactMap.EMPTY
.with(DfaFactType.TYPE_CONSTRAINT, TypeConstraint.exact(myFactory.createDfaType(type)))
.with(DfaFactType.NULLABILITY, DfaNullability.NOT_NULL)
.with(DfaFactType.LOCALITY, true)
.with(DfaFactType.SPECIAL_FIELD_VALUE, sizeField.withValue(myFactory.getInt(0)));
.with(DfaFactType.SPECIAL_FIELD_VALUE, SpecialField.COLLECTION_SIZE.withValue(myFactory.getInt(0)));
return myFactory.getFactFactory().createValue(facts);
}
return null;
@@ -2188,7 +2187,7 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
static final CallInliner[] INLINERS = {
new OptionalChainInliner(), new LambdaInliner(), new CollectionFactoryInliner(),
new StreamChainInliner(), new MapUpdateInliner(), new AssumeInliner(), new ClassMethodsInliner(),
new AssertAllInliner(), new BoxingInliner(), new SimpleMethodInliner()
new AssertAllInliner(), new BoxingInliner(), new SimpleMethodInliner(), new CollectionMethodInliner()
};
}
@@ -257,7 +257,7 @@ public abstract class DfaFactType<T> extends Key<T> {
@NotNull
@Override
public String getName(SpecialFieldValue fact) {
return fact == null ? super.getName(null) : StringUtil.wordsToBeginFromUpperCase(fact.getField().getMethodName());
return fact == null ? super.getName(null) : StringUtil.wordsToBeginFromUpperCase(fact.getField().toString());
}
@Nullable
@@ -91,24 +91,22 @@ public class HardcodedContracts {
(call, cnt) -> getSubstringContracts(cnt == 2))
.register(instanceCall(JAVA_LANG_STRING, "isEmpty").parameterCount(0),
ContractProvider.of(SpecialField.STRING_LENGTH.getEmptyContracts()))
.register(instanceCall(JAVA_UTIL_COLLECTION, "isEmpty").parameterCount(0),
.register(anyOf(instanceCall(JAVA_UTIL_COLLECTION, "isEmpty").parameterCount(0),
instanceCall(JAVA_UTIL_MAP, "isEmpty").parameterCount(0)),
ContractProvider.of(SpecialField.COLLECTION_SIZE.getEmptyContracts()))
.register(instanceCall(JAVA_UTIL_MAP, "isEmpty").parameterCount(0),
ContractProvider.of(SpecialField.MAP_SIZE.getEmptyContracts()))
.register(instanceCall(JAVA_LANG_STRING, "equalsIgnoreCase").parameterCount(1),
ContractProvider.of(SpecialField.STRING_LENGTH.getEqualsContracts()))
.register(anyOf(instanceCall(JAVA_UTIL_SET, "equals").parameterTypes(JAVA_LANG_OBJECT),
instanceCall(JAVA_UTIL_LIST, "equals").parameterTypes(JAVA_LANG_OBJECT)),
instanceCall(JAVA_UTIL_LIST, "equals").parameterTypes(JAVA_LANG_OBJECT),
instanceCall(JAVA_UTIL_MAP, "equals").parameterTypes(JAVA_LANG_OBJECT)),
ContractProvider.of(SpecialField.COLLECTION_SIZE.getEqualsContracts()))
.register(instanceCall(JAVA_UTIL_MAP, "equals").parameterTypes(JAVA_LANG_OBJECT),
ContractProvider.of(SpecialField.MAP_SIZE.getEqualsContracts()))
.register(instanceCall(JAVA_UTIL_COLLECTION, "contains").parameterCount(1),
ContractProvider.of(singleConditionContract(
ContractValue.qualifier().specialField(SpecialField.COLLECTION_SIZE), RelationType.EQ, ContractValue.zero(),
returnFalse())))
.register(instanceCall(JAVA_UTIL_MAP, "containsKey", "containsValue").parameterCount(1),
ContractProvider.of(singleConditionContract(
ContractValue.qualifier().specialField(SpecialField.MAP_SIZE), RelationType.EQ, ContractValue.zero(), returnFalse())))
ContractValue.qualifier().specialField(SpecialField.COLLECTION_SIZE), RelationType.EQ, ContractValue.zero(), returnFalse())))
.register(instanceCall(JAVA_UTIL_LIST, "get").parameterTypes("int"),
ContractProvider.of(nonnegativeArgumentContract(0),
specialFieldRangeContract(0, RelationType.LT, SpecialField.COLLECTION_SIZE)))
@@ -10,7 +10,6 @@ import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.MethodUtils;
import com.siyeh.ig.psiutils.TypeUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@@ -27,7 +26,7 @@ import static com.intellij.psi.CommonClassNames.*;
* @author Tagir Valeev
*/
public enum SpecialField implements VariableDescriptor {
ARRAY_LENGTH(null, "length", true) {
ARRAY_LENGTH("length", true) {
@Override
boolean isMyQualifierType(PsiType type) {
return type instanceof PsiArrayType;
@@ -59,20 +58,61 @@ public enum SpecialField implements VariableDescriptor {
return null;
}
},
STRING_LENGTH(JAVA_LANG_STRING, "length", true) {
STRING_LENGTH("length", true) {
@Override
DfaValue fromInitializer(DfaValueFactory factory, PsiExpression initializer) {
return fromConstant(factory, ExpressionUtils.computeConstantExpression(initializer));
}
@Override
boolean isMyQualifierType(PsiType type) {
return TypeUtils.isJavaLangString(type);
}
@Override
boolean isMyAccessor(PsiMember accessor) {
if (!(accessor instanceof PsiMethod) || !"length".equals(accessor.getName()) || !((PsiMethod)accessor).getParameterList().isEmpty()) {
return false;
}
PsiClass containingClass = accessor.getContainingClass();
return containingClass != null && JAVA_LANG_STRING.equals(containingClass.getQualifiedName());
}
@Override
public DfaValue fromConstant(DfaValueFactory factory, @Nullable Object obj) {
return obj instanceof String ? factory.getInt(((String)obj).length()) : null;
}
},
COLLECTION_SIZE(JAVA_UTIL_COLLECTION, "size", false),
MAP_SIZE(JAVA_UTIL_MAP, "size", false),
UNBOX(null, "value", true) {
COLLECTION_SIZE("size", false) {
private final CallMatcher SIZE_METHODS = CallMatcher.anyOf(CallMatcher.instanceCall(JAVA_UTIL_COLLECTION, "size").parameterCount(0),
CallMatcher.instanceCall(JAVA_UTIL_MAP, "size").parameterCount(0));
private final CallMatcher MAP_COLLECTIONS = CallMatcher.instanceCall(JAVA_UTIL_MAP, "keySet", "entrySet", "values")
.parameterCount(0);
@Override
boolean isMyQualifierType(PsiType type) {
return InheritanceUtil.isInheritor(type, JAVA_UTIL_MAP) || InheritanceUtil.isInheritor(type, JAVA_UTIL_COLLECTION);
}
@Override
boolean isMyAccessor(PsiMember accessor) {
return accessor instanceof PsiMethod && SIZE_METHODS.methodMatches((PsiMethod)accessor);
}
@NotNull
@Override
public DfaValue createValue(@NotNull DfaValueFactory factory, @Nullable DfaValue qualifier, boolean forAccessor) {
if (qualifier instanceof DfaVariableValue) {
DfaVariableValue var = (DfaVariableValue)qualifier;
PsiModifierListOwner owner = var.getPsiVariable();
if (var.getQualifier() != null && owner instanceof PsiMethod && MAP_COLLECTIONS.methodMatches((PsiMethod)owner)) {
return super.createValue(factory, var.getQualifier(), forAccessor);
}
}
return super.createValue(factory, qualifier, forAccessor);
}
},
UNBOX("value", true) {
private final CallMatcher UNBOXING_CALL = CallMatcher.anyOf(
CallMatcher.exactInstanceCall(JAVA_LANG_INTEGER, "intValue").parameterCount(0),
CallMatcher.exactInstanceCall(JAVA_LANG_LONG, "longValue").parameterCount(0),
@@ -114,7 +154,7 @@ public enum SpecialField implements VariableDescriptor {
return accessor instanceof PsiMethod && UNBOXING_CALL.methodMatches((PsiMethod)accessor);
}
},
OPTIONAL_VALUE(null, "value", true) {
OPTIONAL_VALUE("value", true) {
@Override
public PsiType getType(DfaVariableValue variableValue) {
return OptionalUtil.getOptionalElementType(variableValue.getType());
@@ -153,13 +193,11 @@ public enum SpecialField implements VariableDescriptor {
};
private static final SpecialField[] VALUES = values();
private final String myClassName;
private final String myMethodName;
private final String myTitle;
private final boolean myFinal;
SpecialField(String className, String methodName, boolean isFinal) {
myClassName = className;
myMethodName = methodName;
SpecialField(String title, boolean isFinal) {
myTitle = title;
myFinal = isFinal;
}
@@ -167,14 +205,8 @@ public enum SpecialField implements VariableDescriptor {
public boolean isStable() {
return myFinal;
}
public String getMethodName() {
return myMethodName;
}
boolean isMyQualifierType(PsiType type) {
return InheritanceUtil.isInheritor(type, myClassName);
}
abstract boolean isMyQualifierType(PsiType type);
/**
* Checks whether supplied accessor (field or method) can be used to read this special field
@@ -182,9 +214,7 @@ public enum SpecialField implements VariableDescriptor {
* @param accessor accessor to test to test
* @return true if supplied accessor can be used to read this special field
*/
boolean isMyAccessor(PsiMember accessor) {
return accessor instanceof PsiMethod && MethodUtils.methodMatches((PsiMethod)accessor, myClassName, null, myMethodName);
}
abstract boolean isMyAccessor(PsiMember accessor);
public String getPresentationText(@NotNull DfaValue value, @Nullable PsiType type) {
return value.toString();
@@ -336,6 +366,6 @@ public enum SpecialField implements VariableDescriptor {
@Override
public String toString() {
return myMethodName;
return myTitle;
}
}
@@ -26,7 +26,6 @@ import com.siyeh.ig.psiutils.MethodCallUtils;
import org.jetbrains.annotations.NotNull;
import static com.intellij.codeInspection.dataFlow.SpecialField.COLLECTION_SIZE;
import static com.intellij.codeInspection.dataFlow.SpecialField.MAP_SIZE;
import static com.intellij.psi.CommonClassNames.*;
import static com.siyeh.ig.callMatcher.CallMatcher.anyOf;
import static com.siyeh.ig.callMatcher.CallMatcher.staticCall;
@@ -35,24 +34,22 @@ public class CollectionFactoryInliner implements CallInliner {
static final class FactoryInfo {
final boolean myNotNull;
final int mySize;
final SpecialField mySizeField;
FactoryInfo(int size, SpecialField sizeField) {
this(size, sizeField, false);
FactoryInfo(int size) {
this(size, false);
}
FactoryInfo(int size, SpecialField sizeField, boolean notNull) {
FactoryInfo(int size, boolean notNull) {
mySize = size;
mySizeField = sizeField;
myNotNull = notNull;
}
}
private static final CallMapper<FactoryInfo> STATIC_FACTORIES = new CallMapper<FactoryInfo>()
.register(staticCall(JAVA_UTIL_COLLECTIONS, "emptyList", "emptySet").parameterCount(0), new FactoryInfo(0, COLLECTION_SIZE))
.register(staticCall(JAVA_UTIL_COLLECTIONS, "singletonList", "singleton").parameterCount(1), new FactoryInfo(1, COLLECTION_SIZE))
.register(staticCall(JAVA_UTIL_COLLECTIONS, "emptyMap").parameterCount(0), new FactoryInfo(0, MAP_SIZE))
.register(staticCall(JAVA_UTIL_COLLECTIONS, "singletonMap").parameterCount(2), new FactoryInfo(1, MAP_SIZE));
.register(staticCall(JAVA_UTIL_COLLECTIONS, "emptyList", "emptySet").parameterCount(0), new FactoryInfo(0))
.register(staticCall(JAVA_UTIL_COLLECTIONS, "singletonList", "singleton").parameterCount(1), new FactoryInfo(1))
.register(staticCall(JAVA_UTIL_COLLECTIONS, "emptyMap").parameterCount(0), new FactoryInfo(0))
.register(staticCall(JAVA_UTIL_COLLECTIONS, "singletonMap").parameterCount(2), new FactoryInfo(1));
private static final CallMatcher JDK9_MAP_FACTORIES =
staticCall(JAVA_UTIL_MAP, "of", "ofEntries");
@@ -73,13 +70,13 @@ public class CollectionFactoryInliner implements CallInliner {
if (JDK9_FACTORIES.test(call)) {
int size =
JDK9_ARRAY_FACTORIES.test(call) && !MethodCallUtils.isVarArgCall(call) ? -1 : call.getArgumentList().getExpressionCount();
return new FactoryInfo(size, COLLECTION_SIZE, true);
return new FactoryInfo(size, true);
}
if (JDK9_MAP_FACTORIES.test(call)) {
boolean ofEntries = "ofEntries".equals(call.getMethodExpression().getReferenceName());
int size =
ofEntries && !MethodCallUtils.isVarArgCall(call) ? -1 : call.getArgumentList().getExpressionCount() / (ofEntries ? 1 : 2);
return new FactoryInfo(size, MAP_SIZE, true);
return new FactoryInfo(size, true);
}
return null;
}
@@ -97,7 +94,7 @@ public class CollectionFactoryInliner implements CallInliner {
}
DfaValueFactory factory = builder.getFactory();
SpecialFieldValue sizeConstraint =
factoryInfo.mySize == -1 ? null : factoryInfo.mySizeField.withValue(factory.getInt(factoryInfo.mySize));
factoryInfo.mySize == -1 ? null : COLLECTION_SIZE.withValue(factory.getInt(factoryInfo.mySize));
DfaFactMap facts = DfaFactMap.EMPTY
.with(DfaFactType.TYPE_CONSTRAINT, factory.createDfaType(callType).asConstraint())
.with(DfaFactType.NULLABILITY, DfaNullability.NOT_NULL)
@@ -0,0 +1,42 @@
// Copyright 2000-2019 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.inliner;
import com.intellij.codeInspection.dataFlow.CFGBuilder;
import com.intellij.codeInspection.dataFlow.SpecialField;
import com.intellij.codeInspection.dataFlow.value.DfaValueFactory;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethodCallExpression;
import com.siyeh.ig.callMatcher.CallMatcher;
import org.jetbrains.annotations.NotNull;
import static com.intellij.psi.CommonClassNames.JAVA_UTIL_COLLECTION;
import static com.intellij.psi.CommonClassNames.JAVA_UTIL_MAP;
import static com.siyeh.ig.callMatcher.CallMatcher.anyOf;
import static com.siyeh.ig.callMatcher.CallMatcher.instanceCall;
public class CollectionMethodInliner implements CallInliner {
private static final CallMatcher CLEAR = anyOf(instanceCall(JAVA_UTIL_COLLECTION, "clear").parameterCount(0),
instanceCall(JAVA_UTIL_MAP, "clear").parameterCount(0));
@Override
public boolean tryInlineCall(@NotNull CFGBuilder builder, @NotNull PsiMethodCallExpression call) {
PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
if (qualifier == null) return false;
if (CLEAR.matches(call)) {
inlineClear(builder, qualifier);
return true;
}
return false;
}
private static void inlineClear(@NotNull CFGBuilder builder, @NotNull PsiExpression qualifier) {
DfaValueFactory factory = builder.getFactory();
builder
.pushExpression(qualifier)
.unwrap(SpecialField.COLLECTION_SIZE)
.push(factory.getInt(0))
.assign()
.pop()
.pushUnknown();
}
}
@@ -88,7 +88,7 @@ public class MapUpdateInliner implements CallInliner {
private static void flushSize(PsiExpression qualifier, CFGBuilder builder) {
DfaValueFactory factory = builder.getFactory();
DfaValue value = factory.createValue(qualifier);
DfaValue size = SpecialField.MAP_SIZE.createValue(factory, value);
DfaValue size = SpecialField.COLLECTION_SIZE.createValue(factory, value);
builder.assignAndPop(size, DfaUnknownValue.getInstance());
}
@@ -0,0 +1,20 @@
import java.util.*;
class X {
void test(Collection<String> c, List<String> l, Map<String, Integer> map) {
if(map.isEmpty()) {}
map.clear();
if(<warning descr="Condition 'map.isEmpty()' is always 'true'">map.isEmpty()</warning>) {}
if(<warning descr="Condition 'map.keySet().isEmpty()' is always 'true'">map.keySet().isEmpty()</warning>) {}
if(<warning descr="Condition 'map.values().isEmpty()' is always 'true'">map.values().isEmpty()</warning>) {}
if(<warning descr="Condition 'map.entrySet().isEmpty()' is always 'true'">map.entrySet().isEmpty()</warning>) {}
if(c.isEmpty()) {}
c.clear();
if(<warning descr="Condition 'c.isEmpty()' is always 'true'">c.isEmpty()</warning>) {}
if(l.isEmpty()) {}
l.clear();
if(<warning descr="Condition 'l.isEmpty()' is always 'true'">l.isEmpty()</warning>) {}
}
}
@@ -248,4 +248,5 @@ public class DataFlowInspection8Test extends DataFlowInspectionTestCase {
public void testLambdaAfterNullCheck() { doTest(); }
public void testFlatMapSideEffect() { doTest(); }
public void testOptionalValueTracking() { doTest(); }
public void testClearZeroesSize() { doTest(); }
}
@@ -96,7 +96,7 @@ public class RedundantOperationOnEmptyContainerInspection extends AbstractBaseJa
lengthField = SpecialField.COLLECTION_SIZE;
message = InspectionsBundle.message("inspection.redundant.operation.on.empty.collection.message");
} else if (InheritanceUtil.isInheritor(type, JAVA_UTIL_MAP)) {
lengthField = SpecialField.MAP_SIZE;
lengthField = SpecialField.COLLECTION_SIZE;
message = InspectionsBundle.message("inspection.redundant.operation.on.empty.map.message");
} else {
return null;
@@ -69,4 +69,12 @@ class ForEachOverEmpty {
System.out.println("hello");
}
}
}
}
class MapSubCollections {
void testMap(Map<String, String> map) {
if(!map.isEmpty()) return;
for(String s : <warning descr="Collection 'map.keySet()' is always empty">map.keySet()</warning>) {}
for(String s : <warning descr="Collection 'map.values()' is always empty">map.values()</warning>) {}
for(Map.Entry s : <warning descr="Collection 'map.entrySet()' is always empty">map.entrySet()</warning>) {}
}
}
@@ -11,23 +11,50 @@ class Calls {
void testCollection() {
List<String> list = Collections.emptyList();
<warning descr="Collection 'list' is always empty">list</warning>.clear();
list = Collections.emptyList();
<warning descr="Collection 'list' is always empty">list</warning>.remove("foo");
list = Collections.emptyList();
<warning descr="Collection 'list' is always empty">list</warning>.replaceAll(String::trim);
list = Collections.emptyList();
<warning descr="Collection 'list' is always empty">list</warning>.forEach(System.out::println);
list = Collections.emptyList();
<warning descr="Collection 'list' is always empty">list</warning>.iterator();
list = Collections.emptyList();
<warning descr="Collection 'list' is always empty">list</warning>.spliterator();
list = Collections.emptyList();
<warning descr="Collection 'list' is always empty">list</warning>.sort(null);
}
void testMap() {
Map<String, String> map = Collections.emptyMap();
<warning descr="Map 'map' is always empty">map</warning>.get("foo");
map = Collections.emptyMap();
<warning descr="Map 'map' is always empty">map</warning>.remove("foo");
map = Collections.emptyMap();
<warning descr="Map 'map' is always empty">map</warning>.remove("foo", "bar");
map = Collections.emptyMap();
<warning descr="Map 'map' is always empty">map</warning>.replace("foo", "bar");
map = Collections.emptyMap();
<warning descr="Map 'map' is always empty">map</warning>.replace("foo", "bar", "baz");
map = Collections.emptyMap();
<warning descr="Map 'map' is always empty">map</warning>.forEach((k, v) -> { });
}
void testClear(List<String> list) {
list.clear();
<warning descr="Collection 'list' is always empty">list</warning>.clear();
}
}
@@ -11,7 +11,7 @@ public class RedundantOperationOnEmptyContainerInspectionTest extends LightInspe
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_8;
return JAVA_8_ANNOTATED;
}
public void testIterationOverEmptyContainer() {