Skip to content

Commit 0677ba8

Browse files
committed
Add queries for flawed try-catch testing for expected exceptions
1 parent ff70054 commit 0677ba8

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Finds test methods which use `try`-`catch` to check for an expected exception,
3+
* but where the `try` block contains a pointless assertion. For example:
4+
* ```java
5+
* try {
6+
* // assertEquals is pointless because code should have already failed with
7+
* // expected exception, and if not it would reach `fail()` call below
8+
* assertEquals(3, parseInt("invalid"));
9+
* fail();
10+
* } catch (IllegalArgumentException expected) {
11+
* }
12+
* ```
13+
*
14+
* @kind problem
15+
*/
16+
17+
import java
18+
import lib.TestsQLInterop
19+
20+
predicate isTestMethod(Callable callable) {
21+
(
22+
callable instanceof TestMethod
23+
// Check if method is inside test class, might be utility method
24+
// then which is used by test methods
25+
or exists (RefType type | type = callable.getDeclaringType() |
26+
type instanceof TestClass
27+
and type.isTopLevel()
28+
and type.getCompilationUnit() = callable.getCompilationUnit()
29+
)
30+
)
31+
// Ignore teardown methods
32+
and not callable instanceof TeardownMethod
33+
}
34+
35+
predicate isExpectingException(CatchClause catch) {
36+
// Usually block is empty when exception is expected
37+
catch.getBlock().getNumStmt() = 0
38+
// Or it performs assertions on the expected exception
39+
or exists(MethodAccess assertCall |
40+
assertCall.getEnclosingStmt() = catch
41+
and assertCall.getMethod() instanceof AssertionMethod
42+
)
43+
}
44+
45+
from TryStmt try, BlockStmt tryBlock, MethodAccess assertCall, AssertionMethod assertMethod
46+
where
47+
isTestMethod(try.getEnclosingCallable())
48+
and tryBlock = try.getBlock()
49+
and isExpectingException(try.getACatchClause())
50+
and assertCall.getAnEnclosingStmt() = tryBlock
51+
and assertMethod = assertCall.getMethod()
52+
and not assertMethod instanceof AssertFailMethod
53+
and not assertCall instanceof TestFailingCall
54+
and exists(ExprStmt assertCallStmt, ExprStmt failCallStmt |
55+
assertCallStmt.getExpr() = assertCall
56+
and assertCallStmt.getEnclosingStmt+() = tryBlock
57+
and failCallStmt.getExpr() instanceof TestFailingCall
58+
// Fail call should be last statement
59+
and tryBlock.getLastStmt() = failCallStmt
60+
// And assert call should come immediately before failing call, to reduce false positives
61+
// for cases where assert call occurs before call which is causing the expected exception
62+
and assertCallStmt.getIndex() + 1 = failCallStmt.getIndex()
63+
)
64+
select assertCall, "Assertion call is pointless because code should have already failed with expected exception"
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Finds test methods which use `try`-`catch` to test for expected exceptions, but where the
3+
* `try` block contains multiple calls which might throw the expected exception. For example:
4+
* ```java
5+
* try {
6+
* // Unclear whether doAction1 or doAction2 cause the expected exception
7+
* doAction1();
8+
* doAction2();
9+
* fail();
10+
* } catch (IOException expected) {
11+
* }
12+
* ```
13+
* Ideally only call the method which is supposed to throw the exception inside the `try` block
14+
* and perform everything else outside of it. Additionally it might also be useful to be as
15+
* specific as possible with the expected exception (e.g. `IOException` instead of just `Exception`)
16+
* and to also check the message of the exception (unless it is created by third-party code).
17+
*
18+
* See also SonarSource rules
19+
* - [RSPEC-5778](https://rules.sonarsource.com/java/RSPEC-5778)
20+
* - [RSPEC-5783](https://rules.sonarsource.com/java/RSPEC-5783)
21+
*
22+
* @kind problem
23+
*/
24+
25+
// TODO: Also cover assertThrows (or write separate query)
26+
27+
import java
28+
import lib.TestsQLInterop
29+
30+
predicate isTestMethod(Callable callable) {
31+
(
32+
callable instanceof TestMethod
33+
// Check if method is inside test class, might be utility method
34+
// then which is used by test methods
35+
or exists (RefType type | type = callable.getDeclaringType() |
36+
type instanceof TestClass
37+
and type.isTopLevel()
38+
and type.getCompilationUnit() = callable.getCompilationUnit()
39+
)
40+
)
41+
// Ignore teardown methods
42+
and not callable instanceof TeardownMethod
43+
}
44+
45+
// TODO: Avoid code duplication with queries/javadoc/documented-unchecked-thrown-exception-not-inherited.ql
46+
ThrowableType getExceptionClass(string exceptionName, CompilationUnit compilationUnit) {
47+
// Currently does not check precedence in case type is ambiguous, but should suffice for
48+
// most cases
49+
result.hasQualifiedName("java.lang", exceptionName)
50+
or result.hasQualifiedName(compilationUnit.getPackage().getName(), exceptionName)
51+
or exists(Import import_ |
52+
import_.getCompilationUnit() = compilationUnit
53+
and result.getName() = exceptionName
54+
|
55+
result = [
56+
import_.(ImportOnDemandFromPackage).getAnImport(),
57+
import_.(ImportOnDemandFromType).getAnImport(),
58+
import_.(ImportStaticOnDemand).getATypeImport(),
59+
import_.(ImportStaticTypeMember).getATypeImport()
60+
]
61+
)
62+
}
63+
64+
ThrowableType getAThrownExceptionType(Callable c) {
65+
result = c.getAThrownExceptionType()
66+
or exists(ThrowsTag throwsTag |
67+
throwsTag.getParent+().(Javadoc).getCommentedElement() = c
68+
and result = getExceptionClass(throwsTag.getExceptionName(), c.getCompilationUnit())
69+
)
70+
}
71+
72+
from TryStmt try, BlockStmt tryBlock, CatchClause catch, ThrowableType caughtExceptionType
73+
where
74+
isTestMethod(try.getEnclosingCallable())
75+
and tryBlock = try.getBlock()
76+
and tryBlock.getLastStmt().(ExprStmt).getExpr() instanceof TestFailingCall
77+
and catch = try.getACatchClause()
78+
// Usually block is empty when exception is expected; ignore if it performs assertions on the caught exception,
79+
// then it might be less likely that the wrong exception causes the test to pass
80+
and catch.getBlock().getNumStmt() = 0
81+
and caughtExceptionType = catch.getACaughtType()
82+
and count(Call c |
83+
c.(Expr).getAnEnclosingStmt() = tryBlock
84+
and getAThrownExceptionType(c.getCallee()).getASourceSupertype*() = caughtExceptionType
85+
and not c.(Expr).getParent*() instanceof TestFailingCall
86+
// Ignore if this is a (redundant) assert call, that is covered by separate query
87+
and not c.getCallee() instanceof AssertionMethod
88+
) > 1
89+
select tryBlock, "Has multiple calls which might throw " + caughtExceptionType.getName()

0 commit comments

Comments
 (0)