ContractValue: fully declarative extended contract description

This commit is contained in:
Tagir Valeev
2017-04-27 09:36:15 +07:00
parent aa4a2c69a5
commit 0bc50d0eb3
9 changed files with 264 additions and 187 deletions
@@ -0,0 +1,166 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.dataFlow;
import com.intellij.codeInspection.dataFlow.value.DfaRelationValue;
import com.intellij.codeInspection.dataFlow.value.DfaUnknownValue;
import com.intellij.codeInspection.dataFlow.value.DfaValue;
import com.intellij.codeInspection.dataFlow.value.DfaValueFactory;
import com.intellij.psi.PsiType;
import com.intellij.util.Function;
/**
* @author Tagir Valeev
*/
public abstract class ContractValue {
ContractValue() {
}
public abstract DfaValue makeDfaValue(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments);
public static ContractValue qualifier() {
return Qualifier.INSTANCE;
}
public static ContractValue argument(int index) {
return new Argument(index);
}
public static ContractValue constant(Object value, PsiType type) {
return new IndependentValue(factory -> factory.getConstFactory().createFromValue(value, type, null), String.valueOf(value));
}
public static ContractValue booleanValue(boolean value) {
return value ? IndependentValue.TRUE : IndependentValue.FALSE;
}
public static ContractValue optionalValue(boolean present) {
return present ? IndependentValue.PRESENT : IndependentValue.ABSENT;
}
public static ContractValue nullValue() {
return IndependentValue.NULL;
}
public static ContractValue specialField(ContractValue qualifier, SpecialField field) {
return new Spec(qualifier, field);
}
public static ContractValue condition(ContractValue left, DfaRelationValue.RelationType relation, ContractValue right) {
return new Condition(left, relation, right);
}
static class Qualifier extends ContractValue {
static final Qualifier INSTANCE = new Qualifier();
@Override
public DfaValue makeDfaValue(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return qualifier;
}
@Override
public String toString() {
return "this";
}
}
static class Argument extends ContractValue {
private final int myIndex;
Argument(int index) {
myIndex = index;
}
@Override
public DfaValue makeDfaValue(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return arguments.length <= myIndex ? DfaUnknownValue.getInstance() : arguments[myIndex];
}
@Override
public String toString() {
return "arg#" + myIndex;
}
}
static class IndependentValue extends ContractValue {
static final IndependentValue NULL = new IndependentValue(factory -> factory.getConstFactory().getNull(), "null");
static final IndependentValue TRUE = new IndependentValue(factory -> factory.getConstFactory().getTrue(), "true");
static final IndependentValue FALSE = new IndependentValue(factory -> factory.getConstFactory().getFalse(), "false");
static final IndependentValue PRESENT = new IndependentValue(factory -> factory.getOptionalFactory().getOptional(true), "present");
static final IndependentValue ABSENT = new IndependentValue(factory -> factory.getOptionalFactory().getOptional(false), "empty");
private final Function<DfaValueFactory, DfaValue> mySupplier;
private final String myPresentation;
IndependentValue(Function<DfaValueFactory, DfaValue> supplier, String presentation) {
mySupplier = supplier;
myPresentation = presentation;
}
@Override
public DfaValue makeDfaValue(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return mySupplier.fun(factory);
}
@Override
public String toString() {
return myPresentation;
}
}
static class Spec extends ContractValue {
private final ContractValue myQualifier;
private final SpecialField myField;
Spec(ContractValue qualifier, SpecialField field) {
myQualifier = qualifier;
myField = field;
}
@Override
public DfaValue makeDfaValue(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return myField.createValue(factory, myQualifier.makeDfaValue(factory, qualifier, arguments));
}
@Override
public String toString() {
return myQualifier + "." + myField.getMethodName() + "()";
}
}
static class Condition extends ContractValue {
private final ContractValue myLeft, myRight;
private final DfaRelationValue.RelationType myRelationType;
Condition(ContractValue left, DfaRelationValue.RelationType type, ContractValue right) {
myLeft = left;
myRight = right;
myRelationType = type;
}
@Override
public DfaValue makeDfaValue(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return factory.createCondition(myLeft.makeDfaValue(factory, qualifier, arguments), myRelationType,
myRight.makeDfaValue(factory, qualifier, arguments));
}
@Override
public String toString() {
return myLeft + " " + myRelationType + " " + myRight;
}
}
}
@@ -15,11 +15,7 @@
*/
package com.intellij.codeInspection.dataFlow;
import com.intellij.codeInspection.dataFlow.value.DfaOptionalValue;
import com.intellij.codeInspection.dataFlow.value.DfaRelationValue.RelationType;
import com.intellij.codeInspection.dataFlow.value.DfaValue;
import com.intellij.codeInspection.dataFlow.value.DfaValueFactory;
import com.intellij.codeInspection.dataFlow.value.SpecialField;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtil;
@@ -27,7 +23,6 @@ import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.MethodUtils;
import com.siyeh.ig.psiutils.TypeUtils;
import one.util.streamex.IntStreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -78,17 +73,19 @@ public class HardcodedContracts {
}
else if (CommonClassNames.JAVA_LANG_STRING.equals(className)) {
if (("charAt".equals(methodName) || "codePointAt".equals(methodName)) && paramCount == 1) {
return Arrays.asList(new NonnegativeArgumentContract(1, 0),
new SpecialFieldRangeContract(1, 0, RelationType.LT, SpecialField.STRING_LENGTH));
return Arrays.asList(nonnegativeArgumentContract(0),
specialFieldRangeContract(0, RelationType.LT, SpecialField.STRING_LENGTH));
}
else if (("substring".equals(methodName) || "subSequence".equals(methodName)) && paramCount <= 2) {
List<MethodContract> contracts = new ArrayList<>(5);
contracts.add(new NonnegativeArgumentContract(paramCount, 0));
contracts.add(new SpecialFieldRangeContract(paramCount, 0, RelationType.LE, SpecialField.STRING_LENGTH));
contracts.add(nonnegativeArgumentContract(0));
contracts.add(specialFieldRangeContract(0, RelationType.LE, SpecialField.STRING_LENGTH));
if (paramCount == 2) {
contracts.add(new NonnegativeArgumentContract(paramCount, 1));
contracts.add(new SpecialFieldRangeContract(paramCount, 1, RelationType.LE, SpecialField.STRING_LENGTH));
contracts.add(new ArgumentRelationContract(paramCount, 0, RelationType.LE, 1));
contracts.add(nonnegativeArgumentContract(1));
contracts.add(specialFieldRangeContract(1, RelationType.LE, SpecialField.STRING_LENGTH));
contracts.add(MethodContract
.singleConditionContract(ContractValue.argument(0), RelationType.LE.getNegated(), ContractValue.argument(1),
THROW_EXCEPTION));
}
return contracts;
}
@@ -100,8 +97,8 @@ public class HardcodedContracts {
return SpecialField.COLLECTION_SIZE.getEmptyContracts();
}
else if (MethodUtils.methodMatches(method, CommonClassNames.JAVA_UTIL_COLLECTION, null, "get", PsiType.INT)) {
return Arrays.asList(new NonnegativeArgumentContract(paramCount, 0),
new SpecialFieldRangeContract(paramCount, 0, RelationType.LT, SpecialField.COLLECTION_SIZE));
return Arrays.asList(nonnegativeArgumentContract(0),
specialFieldRangeContract(0, RelationType.LT, SpecialField.COLLECTION_SIZE));
}
else if ("org.apache.commons.lang.Validate".equals(className) ||
"org.apache.commons.lang3.Validate".equals(className) ||
@@ -124,135 +121,29 @@ public class HardcodedContracts {
}
else if (TypeUtils.isOptional(owner)) {
if (DfaOptionalSupport.isOptionalGetMethodName(methodName) || "orElseThrow".equals(methodName)) {
return Arrays.asList(new OptionalPresenceContract(false, THROW_EXCEPTION),
new OptionalPresenceContract(true, NOT_NULL_VALUE));
return Arrays.asList(optionalAbsentContract(THROW_EXCEPTION), MethodContract.trivialContract(NOT_NULL_VALUE));
}
else if ("isPresent".equals(methodName)) {
return Arrays.asList(new OptionalPresenceContract(false, FALSE_VALUE),
new OptionalPresenceContract(true, TRUE_VALUE));
return Arrays.asList(optionalAbsentContract(FALSE_VALUE), MethodContract.trivialContract(TRUE_VALUE));
}
}
return Collections.emptyList();
}
static class OptionalPresenceContract extends MethodContract {
private final boolean myPresent;
private final ValueConstraint myReturnValue;
public OptionalPresenceContract(boolean mustPresent, ValueConstraint returnValue) {
myReturnValue = returnValue;
myPresent = mustPresent;
}
@Override
protected List<DfaValue> getConditions(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
DfaOptionalValue optional = factory.getOptionalFactory().getOptional(myPresent);
return Collections.singletonList(factory.createCondition(qualifier, RelationType.IS, optional));
}
@Override
protected String getArgumentsPresentation() {
return "[" + (myPresent ? "present" : "absent") + "]";
}
@Override
public ValueConstraint getReturnValue() {
return myReturnValue;
}
static MethodContract optionalAbsentContract(MethodContract.ValueConstraint returnValue) {
return MethodContract
.singleConditionContract(ContractValue.qualifier(), RelationType.IS, ContractValue.optionalValue(false), returnValue);
}
static abstract class ArgumentRangeContract extends MethodContract {
final int myParamCount;
final int myIndex;
final RelationType myRelationType;
ArgumentRangeContract(int paramCount, int index, RelationType type) {
myParamCount = paramCount;
myIndex = index;
myRelationType = type;
}
@Override
protected List<DfaValue> getConditions(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
DfaValue left = arguments[myIndex];
DfaValue right = getBound(factory, qualifier, arguments);
return Collections.singletonList(factory.createCondition(left, myRelationType.getNegated(), right));
}
@NotNull
abstract DfaValue getBound(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments);
abstract String getBoundRepresentation();
@Override
protected String getArgumentsPresentation() {
return IntStreamEx.range(myParamCount)
.mapToObj(idx -> idx == myIndex ? myRelationType.getNegated() + getBoundRepresentation() : "_")
.joining(", ");
}
@Override
public ValueConstraint getReturnValue() {
return THROW_EXCEPTION;
}
static MethodContract nonnegativeArgumentContract(int argNumber) {
return MethodContract
.singleConditionContract(ContractValue.argument(argNumber), RelationType.LT, ContractValue.constant(0, PsiType.INT), THROW_EXCEPTION);
}
static class NonnegativeArgumentContract extends ArgumentRangeContract {
public NonnegativeArgumentContract(int paramCount, int nonNegativeArgumentIndex) {
super(paramCount, nonNegativeArgumentIndex, RelationType.GE);
}
@NotNull
@Override
DfaValue getBound(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return factory.getConstFactory().createFromValue(0, PsiType.INT, null);
}
@Override
String getBoundRepresentation() {
return "0";
}
}
static class ArgumentRelationContract extends ArgumentRangeContract {
private final int myIndex;
public ArgumentRelationContract(int paramCount, int leftIndex, RelationType relationType, int rightIndex) {
super(paramCount, leftIndex, relationType);
myIndex = rightIndex;
}
@NotNull
@Override
DfaValue getBound(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return arguments[myIndex];
}
@Override
String getBoundRepresentation() {
return "arg#" + myIndex;
}
}
static class SpecialFieldRangeContract extends ArgumentRangeContract {
private final SpecialField mySpecialField;
SpecialFieldRangeContract(int paramCount, int index, RelationType type, SpecialField specialField) {
super(paramCount, index, type);
mySpecialField = specialField;
}
@NotNull
@Override
DfaValue getBound(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return mySpecialField.createValue(factory, qualifier);
}
@Override
String getBoundRepresentation() {
return "this."+mySpecialField.getMethodName()+"()";
}
static MethodContract specialFieldRangeContract(int index, RelationType type, SpecialField specialField) {
return MethodContract.singleConditionContract(ContractValue.argument(index), type.getNegated(),
ContractValue.specialField(ContractValue.qualifier(), specialField), THROW_EXCEPTION);
}
private static boolean isJunit(String className) {
@@ -20,8 +20,8 @@ import com.intellij.codeInspection.dataFlow.value.DfaRelationValue.RelationType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* A method contract which states that method will have a concrete return value
@@ -31,7 +31,7 @@ import java.util.Objects;
*/
public abstract class MethodContract {
// package private to avoid uncontrolled implementations
public MethodContract() {
MethodContract() {
}
@@ -66,18 +66,60 @@ public abstract class MethodContract {
* @return true if this contract result does not depend on arguments
*/
boolean isTrivial() {
return false;
return getConditions().isEmpty();
}
protected abstract String getArgumentsPresentation();
abstract String getArgumentsPresentation();
protected abstract List<DfaValue> getConditions(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments);
abstract List<ContractValue> getConditions();
@Override
public String toString() {
return getArgumentsPresentation() + " -> " + getReturnValue();
}
public static MethodContract trivialContract(ValueConstraint value) {
return new MethodContract() {
@Override
public ValueConstraint getReturnValue() {
return value;
}
@Override
String getArgumentsPresentation() {
return "(any)";
}
@Override
List<ContractValue> getConditions() {
return Collections.emptyList();
}
};
}
public static MethodContract singleConditionContract(ContractValue left,
RelationType relationType,
ContractValue right,
ValueConstraint returnValue) {
ContractValue condition = ContractValue.condition(left, relationType, right);
return new MethodContract() {
@Override
public ValueConstraint getReturnValue() {
return returnValue;
}
@Override
String getArgumentsPresentation() {
return condition.toString();
}
@Override
List<ContractValue> getConditions() {
return Collections.singletonList(condition);
}
};
}
public enum ValueConstraint {
ANY_VALUE("_"), NULL_VALUE("null"), NOT_NULL_VALUE("!null"), TRUE_VALUE("true"), FALSE_VALUE("false"), THROW_EXCEPTION("fail");
private final String myPresentableName;
@@ -100,17 +142,21 @@ public abstract class MethodContract {
/**
* Returns a condition value which should be applied to memory state to satisfy this constraint
*
* @param factory factory to create new values
* @param argValue argument value to test
* @param argumentIndex argument number to test
* @return a condition
*/
public DfaValue getCondition(DfaValueFactory factory, DfaValue argValue) {
if (this == THROW_EXCEPTION || this == ANY_VALUE) {
return factory.getBoolean(true);
public ContractValue getCondition(int argumentIndex) {
ContractValue left;
if (this == NULL_VALUE || this == NOT_NULL_VALUE) {
left = ContractValue.nullValue();
}
DfaConstValue expectedValue = Objects.requireNonNull(getComparisonValue(factory));
return factory.createCondition(argValue, RelationType.equivalence(!shouldUseNonEqComparison()), expectedValue);
else if (this == TRUE_VALUE || this == FALSE_VALUE) {
left = ContractValue.booleanValue(true);
}
else {
return ContractValue.booleanValue(true);
}
return ContractValue.condition(left, RelationType.equivalence(!shouldUseNonEqComparison()), ContractValue.argument(argumentIndex));
}
@Override
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.dataFlow.value;
package com.intellij.codeInspection.dataFlow;
import com.intellij.codeInspection.dataFlow.MethodContract;
import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet;
import com.intellij.codeInspection.dataFlow.value.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
@@ -26,7 +26,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
@@ -120,39 +119,15 @@ public enum SpecialField {
return null;
}
/**
* @return a list of method contracts which equivalent to checking this special field for zero
*/
public List<MethodContract> getEmptyContracts() {
return Arrays.asList(new MethodContract() {
@Override
public ValueConstraint getReturnValue() {
return ValueConstraint.TRUE_VALUE;
}
@Override
protected String getArgumentsPresentation() {
return "[empty]";
}
@Override
protected List<DfaValue> getConditions(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return Collections.singletonList(factory.createCondition(createValue(factory, qualifier), DfaRelationValue.RelationType.EQ,
factory.getConstFactory().createFromValue(0, PsiType.INT, null)));
}
}, new MethodContract() {
@Override
public ValueConstraint getReturnValue() {
return ValueConstraint.FALSE_VALUE;
}
@Override
protected String getArgumentsPresentation() {
return "[non-empty]";
}
@Override
protected List<DfaValue> getConditions(DfaValueFactory factory, DfaValue qualifier, DfaValue[] arguments) {
return Collections.emptyList();
}
});
ContractValue thisValue = ContractValue.specialField(ContractValue.qualifier(), this);
ContractValue zeroValue = ContractValue.constant(0, PsiType.INT);
return Arrays.asList(MethodContract.singleConditionContract(thisValue, DfaRelationValue.RelationType.EQ, zeroValue,
MethodContract.ValueConstraint.TRUE_VALUE),
MethodContract.trivialContract(MethodContract.ValueConstraint.FALSE_VALUE));
}
@Override
@@ -467,7 +467,7 @@ public class StandardInstructionVisitor extends InstructionVisitor {
DfaValueFactory factory,
Set<DfaMemoryState> finalStates,
DfaValue returnValue) {
List<DfaValue> conditions = contract.getConditions(factory, callParameters.myQualifier, callParameters.myArguments);
List<DfaValue> conditions = ContainerUtil.map(contract.getConditions(), cv -> cv.makeDfaValue(factory, callParameters.myQualifier, callParameters.myArguments));
if (StreamEx.of(conditions).allMatch(factory.getConstFactory().getTrue()::equals)) {
for (DfaMemoryState state : states) {
state.push(returnValue);
@@ -15,11 +15,9 @@
*/
package com.intellij.codeInspection.dataFlow;
import com.intellij.codeInspection.dataFlow.value.DfaValue;
import com.intellij.codeInspection.dataFlow.value.DfaValueFactory;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import one.util.streamex.IntStreamEx;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
@@ -88,9 +86,9 @@ public final class StandardMethodContract extends MethodContract {
}
@Override
protected List<DfaValue> getConditions(DfaValueFactory factory, DfaValue qualifier, DfaValue[] argValues) {
return StreamEx.zip(arguments, argValues, (constraint, value) -> constraint.getCondition(factory, value))
.without(factory.getConstFactory().getTrue()).toList();
protected List<ContractValue> getConditions() {
return IntStreamEx.ofIndices(arguments).mapToObj(idx -> arguments[idx].getCondition(idx)).without(ContractValue.booleanValue(true))
.toList();
}
public static List<StandardMethodContract> parseContract(String text) throws ParseException {
@@ -26,7 +26,6 @@ package com.intellij.codeInspection.dataFlow.instructions;
import com.intellij.codeInspection.dataFlow.*;
import com.intellij.codeInspection.dataFlow.value.DfaValue;
import com.intellij.codeInspection.dataFlow.value.SpecialField;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
@@ -18,6 +18,7 @@ package com.intellij.codeInspection.dataFlow.value;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInspection.dataFlow.DfaPsiUtil;
import com.intellij.codeInspection.dataFlow.Nullness;
import com.intellij.codeInspection.dataFlow.SpecialField;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
@@ -28,6 +28,7 @@ import com.intellij.codeInsight.daemon.ImplicitUsageProvider;
import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil;
import com.intellij.codeInspection.dataFlow.DfaPsiUtil;
import com.intellij.codeInspection.dataFlow.Nullness;
import com.intellij.codeInspection.dataFlow.SpecialField;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Trinity;