Skip to content

Commit c33bba2

Browse files
bmuschkotimtebeek
andauthored
Add ThenThrowCheckedExceptionToRuntimeException recipe (#954)
* Add ThenThrowCheckedExceptionToRuntimeException recipe Mockito 3+ validates that checked exceptions passed to `thenThrow()` are declared in the mocked method's `throws` clause. This recipe replaces undeclared checked exception class literals with `RuntimeException.class` and cleans up unused imports. * Regenerate recipes.csv * Simplify ThenThrowCheckedExceptionToRuntimeException - Replace manual method name/type checks with MethodMatcher - Use ListUtils.map instead of boolean changed + manual loop - Inline local variables and combine nested conditionals * Remove unnecessary PowerMockRunner stubs in ReplacePowerMockitoIntegrationTest The inline stub classes didn't extend Runner, causing compilation errors on CI. The real PowerMockRunner is already on the classpath via classpathFromResources, so the stubs are unnecessary. --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent a5c1ffd commit c33bba2

5 files changed

Lines changed: 297 additions & 26 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.testing.mockito;
17+
18+
import lombok.Getter;
19+
import org.openrewrite.ExecutionContext;
20+
import org.openrewrite.Preconditions;
21+
import org.openrewrite.Recipe;
22+
import org.openrewrite.TreeVisitor;
23+
import org.openrewrite.java.JavaIsoVisitor;
24+
import org.openrewrite.java.MethodMatcher;
25+
import org.openrewrite.java.search.UsesType;
26+
import org.openrewrite.java.tree.Expression;
27+
import org.openrewrite.java.tree.J;
28+
import org.openrewrite.java.tree.JavaType;
29+
import org.openrewrite.java.tree.TypeUtils;
30+
31+
import org.openrewrite.internal.ListUtils;
32+
33+
34+
public class ThenThrowCheckedExceptionToRuntimeException extends Recipe {
35+
@Getter
36+
final String displayName = "Replace undeclared checked exceptions in `thenThrow` with `RuntimeException`";
37+
38+
@Getter
39+
final String description = "In Mockito 3+, `thenThrow()` validates that checked exceptions are declared " +
40+
"in the mocked method's `throws` clause. This recipe replaces checked exception class " +
41+
"literals in `thenThrow()` calls with `RuntimeException.class` when the mocked method " +
42+
"does not declare the exception.";
43+
44+
private static final MethodMatcher WHEN_MATCHER = new MethodMatcher("org.mockito.Mockito when(..)");
45+
private static final MethodMatcher THEN_THROW_MATCHER = new MethodMatcher("org.mockito.stubbing.OngoingStubbing thenThrow(..)");
46+
47+
@Override
48+
public TreeVisitor<?, ExecutionContext> getVisitor() {
49+
return Preconditions.check(
50+
new UsesType<>("org.mockito.stubbing.OngoingStubbing", true),
51+
new JavaIsoVisitor<ExecutionContext>() {
52+
@Override
53+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
54+
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
55+
56+
if (!THEN_THROW_MATCHER.matches(mi)) {
57+
return mi;
58+
}
59+
60+
JavaType.Method mockedMethodType = findMockedMethodType(mi);
61+
if (mockedMethodType == null) {
62+
return mi;
63+
}
64+
65+
return mi.withArguments(ListUtils.map(mi.getArguments(), arg -> {
66+
if (arg instanceof J.FieldAccess) {
67+
J.FieldAccess fa = (J.FieldAccess) arg;
68+
if ("class".equals(fa.getName().getSimpleName())) {
69+
JavaType exceptionType = fa.getTarget().getType();
70+
if (isUndeclaredCheckedException(exceptionType, mockedMethodType) &&
71+
fa.getTarget() instanceof J.Identifier) {
72+
maybeRemoveImport(((JavaType.FullyQualified) exceptionType).getFullyQualifiedName());
73+
return fa.withTarget(((J.Identifier) fa.getTarget())
74+
.withSimpleName("RuntimeException")
75+
.withType(JavaType.ShallowClass.build("java.lang.RuntimeException")));
76+
}
77+
}
78+
}
79+
return arg;
80+
}));
81+
}
82+
}
83+
);
84+
}
85+
86+
private static JavaType.Method findMockedMethodType(J.MethodInvocation thenThrowCall) {
87+
Expression select = thenThrowCall.getSelect();
88+
if (select instanceof J.MethodInvocation) {
89+
J.MethodInvocation possibleWhenCall = (J.MethodInvocation) select;
90+
if (WHEN_MATCHER.matches(possibleWhenCall) && !possibleWhenCall.getArguments().isEmpty()) {
91+
Expression mockedCall = possibleWhenCall.getArguments().get(0);
92+
if (mockedCall instanceof J.MethodInvocation) {
93+
return ((J.MethodInvocation) mockedCall).getMethodType();
94+
}
95+
}
96+
}
97+
return null;
98+
}
99+
100+
private static boolean isUndeclaredCheckedException(JavaType exceptionType, JavaType.Method mockedMethodType) {
101+
if (!(exceptionType instanceof JavaType.FullyQualified)) {
102+
return false;
103+
}
104+
if (!TypeUtils.isAssignableTo("java.lang.Exception", exceptionType) ||
105+
TypeUtils.isAssignableTo("java.lang.RuntimeException", exceptionType)) {
106+
return false;
107+
}
108+
for (JavaType thrown : mockedMethodType.getThrownExceptions()) {
109+
if (thrown instanceof JavaType.FullyQualified &&
110+
TypeUtils.isAssignableTo(((JavaType.FullyQualified) thrown).getFullyQualifiedName(), exceptionType)) {
111+
return false;
112+
}
113+
}
114+
return true;
115+
}
116+
}

src/main/resources/META-INF/rewrite/mockito.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ recipeList:
178178
- org.openrewrite.java.ChangeType:
179179
oldFullyQualifiedTypeName: org.mockito.runners.MockitoJUnitRunner
180180
newFullyQualifiedTypeName: org.mockito.junit.MockitoJUnitRunner
181+
- org.openrewrite.java.testing.mockito.ThenThrowCheckedExceptionToRuntimeException
181182
- org.openrewrite.java.testing.mockito.CleanupMockitoImports
182183
- org.openrewrite.java.testing.mockito.MockUtilsToStatic
183184
- org.openrewrite.java.testing.mockito.ReplacePowerMockito

0 commit comments

Comments
 (0)