[java-dfa] Support failure handling in constant evaluator

Part of IDEA-326613 Support constant evaluation of Integer.parseInt and friends
Required for IDEA-324389 Inspection: Integer.parseInt() with a numeric String literal could be simplified
Also: added more Math methods

GitOrigin-RevId: 199a4bc5eaea9e8c2f26b6d4dfa5292ca8dd7795
This commit is contained in:
Tagir Valeev
2023-07-28 14:18:15 +00:00
committed by intellij-monorepo-bot
parent 7df312a1cd
commit 76aa489d1d
6 changed files with 54 additions and 11 deletions
@@ -41,6 +41,7 @@ dataflow.message.constant.condition.when.reached=Condition <code>#ref</code> #lo
dataflow.message.constant.condition=Condition <code>#ref</code> #loc is always <code>{0, choice, 0#false|1#true}</code>
dataflow.message.constant.method.reference=Method reference result is always ''{0}''
dataflow.message.constant.no.ref=Condition is always {0, choice, 0#false|1#true}
dataflow.message.fail=The call to '#ref' always fails with an exception
dataflow.message.contract.fail.index=The call to '#ref' always fails as an argument is out of bounds
dataflow.message.contract.fail=The call to '#ref' always fails, according to its method contracts
dataflow.message.immutable.modified=Immutable object is modified
@@ -45,7 +45,8 @@ public final class CustomMethodHandlers {
exactInstanceCall(JAVA_LANG_STRING, "contains", "indexOf", "startsWith", "endsWith", "lastIndexOf", "length", "trim",
"substring", "equals", "equalsIgnoreCase", "charAt", "codePointAt", "compareTo", "replace"),
staticCall(JAVA_LANG_STRING, "valueOf").parameterCount(1),
staticCall(JAVA_LANG_MATH, "abs", "sqrt", "min", "max"),
staticCall(JAVA_LANG_MATH, "abs", "sqrt", "min", "max", "addExact", "absExact", "subtractExact", "multiplyExact",
"incrementExact", "decrementExact", "toIntExact", "negateExact", "sin", "cos", "tan", "asin", "acos", "atan", "cbrt"),
staticCall(JAVA_LANG_INTEGER, "toString", "toBinaryString", "toHexString", "toOctalString", "toUnsignedString").parameterTypes("int"),
staticCall(JAVA_LANG_LONG, "toString", "toBinaryString", "toHexString", "toOctalString", "toUnsignedString").parameterTypes("long"),
staticCall(JAVA_LANG_DOUBLE, "toString", "toHexString").parameterTypes("double"),
@@ -220,8 +221,12 @@ public final class CustomMethodHandlers {
return handler == null ? handler2 : handler.compose(handler2);
}
/**
* @param method method to check
* @return true if method will be evaluated to constant when all arguments are constant
*/
@Contract("null -> false")
private static boolean isConstantCall(PsiMethod method) {
public static boolean isConstantCall(PsiMethod method) {
return CONSTANT_CALLS.methodMatches(method);
}
@@ -251,7 +256,13 @@ public final class CustomMethodHandlers {
try {
result = jvmMethod.invoke(qualifierValue, args.toArray());
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
catch (InvocationTargetException e) {
if (e.getCause() instanceof NumberFormatException || e.getCause() instanceof ArithmeticException) {
return DfType.FAIL;
}
return DfType.TOP;
}
catch (IllegalAccessException | IllegalArgumentException e) {
return DfType.TOP;
}
return constant(result, returnType);
@@ -265,7 +276,7 @@ public final class CustomMethodHandlers {
return Result.create(reflection, method);
}
private Class<?> toJvmType(PsiType type) {
private static Class<?> toJvmType(PsiType type) {
if (TypeUtils.isJavaLangString(type)) {
return String.class;
}
@@ -281,6 +292,12 @@ public final class CustomMethodHandlers {
if (PsiTypes.booleanType().equals(type)) {
return boolean.class;
}
if (PsiTypes.byteType().equals(type)) {
return byte.class;
}
if (PsiTypes.shortType().equals(type)) {
return short.class;
}
if (PsiTypes.charType().equals(type)) {
return char.class;
}
@@ -597,6 +597,11 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
private void reportAlwaysFailingCalls(ProblemReporter reporter, DataFlowInstructionVisitor visitor) {
visitor.alwaysFailingCalls().remove(TestUtils::isExceptionExpected).forEach(anchor -> {
List<? extends MethodContract> contracts = DataFlowInstructionVisitor.getContracts(anchor);
if (contracts != null && contracts.isEmpty()) {
PsiMethod method = anchor instanceof PsiCallExpression call ? call.resolveMethod() :
anchor instanceof PsiMethodReferenceExpression methodRef ? tryCast(methodRef.resolve(), PsiMethod.class) : null;
contracts = DfaUtil.addRangeContracts(method, List.of());
}
if (contracts == null) return;
String message = getContractMessage(contracts);
LocalQuickFix causeFix = createExplainFix(anchor, new TrackingRunner.FailingCallDfaProblemType());
@@ -605,6 +610,9 @@ public abstract class DataFlowInspectionBase extends AbstractBaseJavaLocalInspec
}
private static @NotNull @InspectionMessage String getContractMessage(List<? extends MethodContract> contracts) {
if (contracts.isEmpty()) {
return JavaAnalysisBundle.message("dataflow.message.fail");
}
if (ContainerUtil.and(contracts, mc -> ContainerUtil.and(mc.getConditions(), ContractValue::isBoundCheckingCondition))) {
return JavaAnalysisBundle.message("dataflow.message.contract.fail.index");
}
@@ -1950,7 +1950,7 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
addInstruction(new MethodCallInstruction(expression, JavaDfaValueFactory.getExpressionDfaValue(myFactory, expression), contracts));
anchor = expression;
}
processFailResult(contracts, anchor);
processFailResult(method, contracts, anchor);
addMethodThrows(method);
if (expression != null) {
@@ -1958,8 +1958,9 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
}
}
private void processFailResult(List<? extends MethodContract> contracts, PsiExpression anchor) {
if (ContainerUtil.exists(contracts, c -> c.getReturnValue().isFail())) {
private void processFailResult(@Nullable PsiMethod method, @NotNull List<? extends MethodContract> contracts, @NotNull PsiExpression anchor) {
if ((CustomMethodHandlers.isConstantCall(method) && !PsiTypes.booleanType().equals(method.getReturnType()))
|| ContainerUtil.exists(contracts, c -> c.getReturnValue().isFail())) {
DfaControlTransferValue transfer = createTransfer(JAVA_LANG_THROWABLE);
// if a contract resulted in 'fail', handle it
addInstruction(new EnsureInstruction(new ContractFailureProblem(anchor), RelationType.NE, DfType.FAIL, transfer));
@@ -2042,7 +2043,7 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
JavaMethodContractUtil.getMethodCallContracts(constructor, null);
contracts = DfaUtil.addRangeContracts(constructor, contracts);
addInstruction(new MethodCallInstruction(expression, precalculatedNewValue, contracts));
processFailResult(contracts, expression);
processFailResult(constructor, contracts, expression);
addMethodThrows(constructor);
}
@@ -140,7 +140,7 @@ class Contracts {
}
Assertions.assertThat(array2).isNotEmpty();
if (<warning descr="Condition 'array2.length == 0' is always 'false'">array2.length == 0</warning>) {}
Assertions.assertThat(array2).<warning descr="The call to 'isEmpty' always fails as an argument is out of bounds">isEmpty</warning>();
Assertions.assertThat(array2).<warning descr="The call to 'isEmpty' always fails with an exception">isEmpty</warning>();
}
void testString(String str, String str2) {
@@ -150,7 +150,7 @@ class Contracts {
}
Assertions.assertThat(str2).isNotEmpty();
if (<warning descr="Condition 'str2.length() == 0' is always 'false'">str2.length() == 0</warning>) {}
Assertions.assertThat(str2).<warning descr="The call to 'isEmpty' always fails as an argument is out of bounds">isEmpty</warning>();
Assertions.assertThat(str2).<warning descr="The call to 'isEmpty' always fails with an exception">isEmpty</warning>();
}
void testList(List<String> list, List<String> list2) {
@@ -160,7 +160,7 @@ class Contracts {
}
Assertions.assertThat(list2).isNotEmpty();
if (<warning descr="Condition 'list2.size() == 0' is always 'false'">list2.size() == 0</warning>) {}
Assertions.assertThat(list2).<warning descr="The call to 'isEmpty' always fails as an argument is out of bounds">isEmpty</warning>();
Assertions.assertThat(list2).<warning descr="The call to 'isEmpty' always fails with an exception">isEmpty</warning>();
}
void testAtomicBoolean() {
@@ -405,4 +405,20 @@ public class LongRangeKnownMethods {
int val = Integer.parseInt(s);
if (<warning descr="Condition 'val != 1234' is always 'false'">val != 1234</warning>) return;
}
void testParseIncorrect(String s) {
if (!s.equals("1234L")) return;
int val = Integer.<warning descr="The call to 'parseInt' always fails with an exception">parseInt</warning>(s);
if (val != 1234) return;
}
void testByteTooBig() {
byte b;
if (Math.random() > 0.5) {
b = Byte.<warning descr="The call to 'parseByte' always fails with an exception">parseByte</warning>("128");
} else {
b = Byte.parseByte("127");
}
if (<warning descr="Condition 'b == 127' is always 'true'">b == 127</warning>) {}
}
}