IDEA-191937 Contract checker: more checks

This commit is contained in:
Tagir Valeev
2018-05-15 10:37:07 +07:00
parent befdc18f2a
commit dc36822f8a
4 changed files with 131 additions and 7 deletions
@@ -4,14 +4,22 @@ package com.intellij.codeInspection.dataFlow;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.dataFlow.StandardMethodContract.ValueConstraint;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static com.intellij.codeInspection.dataFlow.StandardMethodContract.ParseException;
import static com.intellij.codeInspection.dataFlow.StandardMethodContract.parseContract;
/**
* @author peter
@@ -80,22 +88,109 @@ public class ContractInspection extends AbstractBaseJavaLocalInspectionTool {
public static String checkContract(PsiMethod method, String text) {
List<StandardMethodContract> contracts;
try {
contracts = StandardMethodContract.parseContract(text);
contracts = parseContract(text);
}
catch (StandardMethodContract.ParseException e) {
catch (ParseException e) {
return e.getMessage();
}
int paramCount = method.getParameterList().getParametersCount();
for (int i = 0; i < contracts.size(); i++) {
StandardMethodContract contract = contracts.get(i);
PsiParameter[] parameters = method.getParameterList().getParameters();
int paramCount = parameters.length;
List<Conditions> possibleConditions = Collections.singletonList(new Conditions(paramCount));
for (StandardMethodContract contract : contracts) {
if (contract.getParameterCount() != paramCount) {
return "Method takes " + paramCount + " parameters, while contract clause number " + (i + 1) + " expects " + contract.getParameterCount();
return "Method takes " + paramCount + " parameters, " +
"while contract clause '" + contract + "' expects " + contract.getParameterCount();
}
for (int i = 0; i < parameters.length; i++) {
ValueConstraint constraint = contract.getParameterConstraint(i);
PsiType type = parameters[i].getType();
switch (constraint) {
case ANY_VALUE:
break;
case NULL_VALUE:
case NOT_NULL_VALUE:
if (type instanceof PsiPrimitiveType) {
return "Contract clause '"+contract+"': parameter #"+(i+1)+" has primitive type '"+type.getPresentableText()+"'";
}
break;
case TRUE_VALUE:
case FALSE_VALUE:
if (!PsiType.BOOLEAN.equals(type) && !type.equalsToText(CommonClassNames.JAVA_LANG_BOOLEAN)) {
return "Contract clause '"+contract+"': parameter #"+(i+1)+" has '"+type.getPresentableText()+"' type (expected boolean)";
}
break;
}
}
String problem = contract.getReturnValue().getMethodCompatibilityProblem(method);
if (problem != null) {
return problem;
}
if (possibleConditions != null) {
if (possibleConditions.isEmpty()) {
return "Contract clause '" + contract + "' is unreachable: previous contracts cover all possible cases";
}
if (StreamEx.of(possibleConditions).allMatch(c -> c.fitContract(contract) == null)) {
return "Contract clause '" + contract + "' is never satisfied as its conditions are covered by previous contracts";
}
possibleConditions = StreamEx.of(possibleConditions).flatMap(c -> c.misfitContract(contract))
.limit(DataFlowRunner.MAX_STATES_PER_BRANCH).toList();
if (possibleConditions.size() >= DataFlowRunner.MAX_STATES_PER_BRANCH) {
possibleConditions = null;
}
}
}
return null;
}
private static final class Conditions {
private final List<ValueConstraint> myParameters;
Conditions(int paramCount) {
myParameters = StreamEx.constant(ValueConstraint.ANY_VALUE, paramCount).toList();
}
private Conditions(List<ValueConstraint> parameters) {
myParameters = parameters;
}
@Nullable
Conditions fitContract(StandardMethodContract contract) {
List<ValueConstraint> result = new ArrayList<>(myParameters);
assert contract.getParameterCount() == result.size();
for (int i = 0; i < result.size(); i++) {
ValueConstraint condition = result.get(i);
ValueConstraint constraint = contract.getParameterConstraint(i);
if (condition == constraint || condition == ValueConstraint.ANY_VALUE) {
result.set(i, constraint);
} else if (constraint == ValueConstraint.ANY_VALUE) {
result.set(i, condition);
}
else {
return null;
}
}
return new Conditions(result);
}
@NotNull
Stream<Conditions> misfitContract(StandardMethodContract contract) {
assert contract.getParameterCount() == myParameters.size();
List<ValueConstraint> constraints = contract.getConstraints();
List<ValueConstraint> template = StreamEx.constant(ValueConstraint.ANY_VALUE, myParameters.size()).toList();
List<StandardMethodContract> antiContracts = new ArrayList<>();
for (int i = 0; i < constraints.size(); i++) {
ValueConstraint constraint = constraints.get(i);
if (constraint == ValueConstraint.ANY_VALUE) continue;
template.set(i, constraint.negate());
antiContracts.add(new StandardMethodContract(template.toArray(new ValueConstraint[0]), ContractReturnValue.returnAny()));
template.set(i, constraint);
}
return StreamEx.of(antiContracts).map(this::fitContract).nonNull();
}
@Override
public String toString() {
return myParameters.toString();
}
}
}
@@ -0,0 +1,22 @@
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
class Foo {
@Contract(<warning descr="Contract clause 'null -> null' is unreachable: previous contracts cover all possible cases">"_ -> !null; null -> null"</warning>)
public native String nonTrivialAfterTrivial(String x);
@Contract(<warning descr="Contract clause '!null -> null' is never satisfied as its conditions are covered by previous contracts">"!null -> !null; !null -> null"</warning>)
public native String repeating(String x);
@Contract(<warning descr="Contract clause 'true, _, _ -> fail' is never satisfied as its conditions are covered by previous contracts">"true, false, _ -> !null; true, true, _ -> null; true, _, _ -> fail"</warning>)
public native String booleanProblem(boolean x, boolean y, String z);
@Contract("true, false, _ -> !null; true, true, _ -> null; false, _, _ -> fail")
public native String booleanOk(boolean x, boolean y, String z);
@Contract("true, false, _ -> !null; false, _, _ -> fail; true, true, _ -> null")
public native String booleanOk2(boolean x, boolean y, String z);
@Contract(<warning descr="Contract clause 'null, null, _, null, !null -> fail' is never satisfied as its conditions are covered by previous contracts">"null, null, null, null, null -> null; null, null, !null, null, _ -> null; null, null, null, null, !null -> !null; null, null, _, null, !null -> fail"</warning>)
public native String test(String a, String b, String c, String d, String e);
}
@@ -5,7 +5,7 @@ class Foo {
@Contract(<warning descr="A contract clause must be in form arg1, ..., argN -> return-value">"a"</warning>)
void malformedContract() {}
@Contract(<warning descr="Method takes 2 parameters, while contract clause number 1 expects 1">"null -> _"</warning>)
@Contract(<warning descr="Method takes 2 parameters, while contract clause 'null -> _' expects 1">"null -> _"</warning>)
void wrongParameterCount(Object a, boolean b) {}
@Contract(pure=true)
@@ -48,4 +48,10 @@ class Foo {
@Contract(<warning descr="Return value should be one of: null, !null, true, false, this, new, paramN, fail, _. Found: foo">"->foo"</warning>)
public native void invalidReturn();
@Contract(<warning descr="Contract clause 'true -> fail': parameter #1 has 'String' type (expected boolean)">"true -> fail"</warning>)
public native void invalidType(String s);
@Contract(<warning descr="Contract clause 'null -> fail': parameter #1 has primitive type 'int'">"null -> fail"</warning>)
public native void invalidType(int s);
}
@@ -52,4 +52,5 @@ public class ContractCheckTest extends LightCodeInsightFixtureTestCase {
public void testCallingNotNullMethod() { doTest(); }
public void testMutationSignatureProblems() { doTest(); }
public void testNewThisParam() { doTest(); }
public void testConditionsConflict() { doTest(); }
}