|
| 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