mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-191937 Contract checker: more checks
This commit is contained in:
+101
-6
@@ -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(); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user