Skip to content

Commit e679ace

Browse files
bmuschkotimtebeek
andauthored
Add PowerMockWhiteboxToJavaReflection recipe (#944)
* Add PowerMockWhiteboxToJavaReflection recipe Replace `org.powermock.reflect.Whitebox` calls with plain Java reflection as part of the ReplacePowerMockito recipe chain. Handles setInternalState, getInternalState, and invokeMethod by expanding each call into getDeclaredField/getDeclaredMethod + setAccessible + get/set/invoke. Uses VariableNameUtils to avoid name collisions when multiple Whitebox calls target the same field. * Regenerate recipes.csv to include PowerMockWhiteboxToJavaReflection * Add powermock-reflect-1.6.5 classpath JAR for type resolution * Regenerate classpath type table to include powermock-reflect The classpath.tsv.gz type table is what CI uses for type resolution in tests. The previously committed JAR is unnecessary when the type table includes the artifact. * Address review feedback on PowerMockWhiteboxToJavaReflection - Inline the named WhiteboxVisitor into an anonymous class - Use immediate return for maybeAutoFormat - Remove unnecessary powermock-reflect-1 classpath from integration test * Fix template type attribution by using #{any(java.lang.Object)} Untyped #{any()} placeholders caused the template parser to infer the argument's actual type (e.g. MyService), which is not on the template parser's classpath. This broke type resolution for the entire method chain (.getClass(), .getDeclaredField(), .setAccessible(), etc.). Using #{any(java.lang.Object)} ensures the substitution always resolves against a JDK type, fixing type attribution. Also inlines the JAVA_PARSER field and removes the now-unnecessary methodInvocations(false) from the unit test. --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent 43acae3 commit e679ace

6 files changed

Lines changed: 663 additions & 0 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ recipeDependencies {
3939
parserClasspath("org.powermock:powermock-api-mockito:1.6.5")
4040
parserClasspath("org.powermock:powermock-api-support:1.6.5")
4141
parserClasspath("org.powermock:powermock-core:1.6.5")
42+
parserClasspath("org.powermock:powermock-reflect:1.6.5")
4243
parserClasspath("org.springframework:spring-test:6.1.+")
4344
parserClasspath("org.testcontainers:testcontainers:1.20.6")
4445
parserClasspath("org.testcontainers:junit-jupiter:1.20.6")
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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.jspecify.annotations.Nullable;
20+
import org.openrewrite.*;
21+
import org.openrewrite.internal.ListUtils;
22+
import org.openrewrite.java.*;
23+
import org.openrewrite.java.search.UsesType;
24+
import org.openrewrite.java.tree.*;
25+
import org.openrewrite.marker.Markers;
26+
27+
import java.util.List;
28+
29+
import static java.util.Collections.emptyList;
30+
import static org.openrewrite.Tree.randomId;
31+
import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
32+
import static org.openrewrite.java.VariableNameUtils.generateVariableName;
33+
34+
public class PowerMockWhiteboxToJavaReflection extends Recipe {
35+
36+
private static final String WHITEBOX_FQN = "org.powermock.reflect.Whitebox";
37+
private static final MethodMatcher SET_INTERNAL_STATE =
38+
new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object)");
39+
private static final MethodMatcher GET_INTERNAL_STATE =
40+
new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String)");
41+
private static final MethodMatcher INVOKE_METHOD =
42+
new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Object, java.lang.String, ..)");
43+
44+
@Getter
45+
final String displayName = "Replace PowerMock `Whitebox` with Java reflection";
46+
47+
@Getter
48+
final String description = "Replace `org.powermock.reflect.Whitebox` calls " +
49+
"(`setInternalState`, `getInternalState`, `invokeMethod`) with plain Java reflection using " +
50+
"`java.lang.reflect.Field` and `java.lang.reflect.Method`.";
51+
52+
@Override
53+
public TreeVisitor<?, ExecutionContext> getVisitor() {
54+
return Preconditions.check(
55+
new UsesType<>(WHITEBOX_FQN, false),
56+
new JavaIsoVisitor<ExecutionContext>() {
57+
58+
private static final String WHITEBOX_REPLACED = "whiteboxReplaced";
59+
private static final String NEEDS_FIELD_IMPORT = "needsFieldImport";
60+
private static final String NEEDS_METHOD_IMPORT = "needsMethodImport";
61+
62+
@Override
63+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
64+
J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx);
65+
if (getCursor().getMessage(WHITEBOX_REPLACED, false)) {
66+
md = addThrowsExceptionIfAbsent(md);
67+
maybeRemoveImport(WHITEBOX_FQN);
68+
if (getCursor().getMessage(NEEDS_FIELD_IMPORT, false)) {
69+
maybeAddImport("java.lang.reflect.Field", false);
70+
}
71+
if (getCursor().getMessage(NEEDS_METHOD_IMPORT, false)) {
72+
maybeAddImport("java.lang.reflect.Method", false);
73+
}
74+
return maybeAutoFormat(method, md, ctx);
75+
}
76+
return md;
77+
}
78+
79+
@Override
80+
public J.Block visitBlock(J.Block block, ExecutionContext ctx) {
81+
J.Block b = super.visitBlock(block, ctx);
82+
83+
List<Statement> statements = b.getStatements();
84+
// Process in reverse so that coordinate positions remain valid after each replacement
85+
for (int i = statements.size() - 1; i >= 0; i--) {
86+
Statement stmt = statements.get(i);
87+
J.MethodInvocation mi = extractWhiteboxInvocation(stmt);
88+
if (mi == null) {
89+
continue;
90+
}
91+
Cursor blockCursor = new Cursor(getCursor().getParentOrThrow(), b);
92+
String template = buildReplacementTemplate(stmt, mi, blockCursor);
93+
if (template != null) {
94+
Object[] templateArgs = buildTemplateArgs(mi);
95+
b = JavaTemplate.builder(template)
96+
.contextSensitive()
97+
.javaParser(JavaParser.fromJavaVersion())
98+
.imports("java.lang.reflect.Field", "java.lang.reflect.Method")
99+
.build()
100+
.apply(
101+
new Cursor(getCursor().getParentOrThrow(), b),
102+
stmt.getCoordinates().replace(),
103+
templateArgs
104+
);
105+
getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, WHITEBOX_REPLACED, true);
106+
if (SET_INTERNAL_STATE.matches(mi) || GET_INTERNAL_STATE.matches(mi)) {
107+
getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_FIELD_IMPORT, true);
108+
}
109+
if (INVOKE_METHOD.matches(mi)) {
110+
getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_METHOD_IMPORT, true);
111+
}
112+
// Re-read statements list since the block has been rebuilt
113+
statements = b.getStatements();
114+
}
115+
}
116+
117+
return b;
118+
}
119+
120+
private @Nullable String buildReplacementTemplate(Statement statement, J.MethodInvocation mi, Cursor scope) {
121+
List<Expression> args = mi.getArguments();
122+
123+
if (SET_INTERNAL_STATE.matches(mi) && args.size() == 3) {
124+
return buildSetInternalStateTemplate(args, scope);
125+
}
126+
if (GET_INTERNAL_STATE.matches(mi) && args.size() == 2) {
127+
return buildGetInternalStateTemplate(args, statement, scope);
128+
}
129+
if (INVOKE_METHOD.matches(mi) && args.size() >= 2) {
130+
return buildInvokeMethodTemplate(args, statement, scope);
131+
}
132+
return null;
133+
}
134+
135+
private Object[] buildTemplateArgs(J.MethodInvocation mi) {
136+
List<Expression> args = mi.getArguments();
137+
138+
if (SET_INTERNAL_STATE.matches(mi) && args.size() == 3) {
139+
// target, fieldName, target, value
140+
return new Object[]{args.get(0), args.get(1), args.get(0), args.get(2)};
141+
}
142+
if (GET_INTERNAL_STATE.matches(mi) && args.size() == 2) {
143+
// target, fieldName, target
144+
return new Object[]{args.get(0), args.get(1), args.get(0)};
145+
}
146+
if (INVOKE_METHOD.matches(mi) && args.size() >= 2) {
147+
return buildInvokeMethodArgs(args);
148+
}
149+
return new Object[0];
150+
}
151+
152+
private @Nullable String buildSetInternalStateTemplate(List<Expression> args, Cursor scope) {
153+
String fieldName = extractStringLiteral(args.get(1));
154+
if (fieldName == null) {
155+
return null;
156+
}
157+
String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER);
158+
return "Field " + varName + " = #{any(java.lang.Object)}.getClass().getDeclaredField(#{any(java.lang.String)});\n" +
159+
varName + ".setAccessible(true);\n" +
160+
varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});";
161+
}
162+
163+
private @Nullable String buildGetInternalStateTemplate(List<Expression> args, Statement statement, Cursor scope) {
164+
String fieldName = extractStringLiteral(args.get(1));
165+
if (fieldName == null) {
166+
return null;
167+
}
168+
String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER);
169+
String prefix = "Field " + varName + " = #{any(java.lang.Object)}.getClass().getDeclaredField(#{any(java.lang.String)});\n" +
170+
varName + ".setAccessible(true);\n";
171+
172+
if (statement instanceof J.VariableDeclarations) {
173+
J.VariableDeclarations varDecls = (J.VariableDeclarations) statement;
174+
String assignToVar = varDecls.getVariables().get(0).getSimpleName();
175+
String castType = getCastType(varDecls.getType());
176+
if (castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType)) {
177+
return prefix + castType + " " + assignToVar + " = (" + castType + ") " + varName + ".get(#{any(java.lang.Object)});";
178+
}
179+
return prefix + "Object " + assignToVar + " = " + varName + ".get(#{any(java.lang.Object)});";
180+
}
181+
return prefix + varName + ".get(#{any(java.lang.Object)});";
182+
}
183+
184+
private @Nullable String buildInvokeMethodTemplate(List<Expression> args, Statement statement, Cursor scope) {
185+
String methodName = extractStringLiteral(args.get(1));
186+
if (methodName == null) {
187+
return null;
188+
}
189+
String varName = generateVariableName(methodName + "Method", scope, INCREMENT_NUMBER);
190+
191+
// getDeclaredMethod line
192+
StringBuilder sb = new StringBuilder();
193+
sb.append("Method ").append(varName).append(" = #{any(java.lang.Object)}.getClass().getDeclaredMethod(#{any(java.lang.String)}");
194+
for (int i = 2; i < args.size(); i++) {
195+
sb.append(", #{any(java.lang.Object)}.getClass()");
196+
}
197+
sb.append(");\n");
198+
199+
// setAccessible line
200+
sb.append(varName).append(".setAccessible(true);\n");
201+
202+
// invoke line
203+
if (statement instanceof J.VariableDeclarations) {
204+
J.VariableDeclarations varDecls = (J.VariableDeclarations) statement;
205+
String assignToVar = varDecls.getVariables().get(0).getSimpleName();
206+
String castType = getCastType(varDecls.getType());
207+
if (castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType)) {
208+
sb.append(castType).append(" ").append(assignToVar).append(" = (").append(castType).append(") ");
209+
} else {
210+
sb.append("Object ").append(assignToVar).append(" = ");
211+
}
212+
}
213+
sb.append(varName).append(".invoke(#{any(java.lang.Object)}");
214+
for (int i = 2; i < args.size(); i++) {
215+
sb.append(", #{any(java.lang.Object)}");
216+
}
217+
sb.append(");");
218+
219+
return sb.toString();
220+
}
221+
222+
private Object[] buildInvokeMethodArgs(List<Expression> args) {
223+
int extraArgs = args.size() - 2;
224+
Object[] result = new Object[2 + extraArgs + 1 + extraArgs];
225+
int idx = 0;
226+
result[idx++] = args.get(0); // target for getDeclaredMethod
227+
result[idx++] = args.get(1); // methodName
228+
for (int i = 2; i < args.size(); i++) {
229+
result[idx++] = args.get(i); // arg.getClass() for getDeclaredMethod
230+
}
231+
result[idx++] = args.get(0); // target for invoke
232+
for (int i = 2; i < args.size(); i++) {
233+
result[idx++] = args.get(i); // arg for invoke
234+
}
235+
return result;
236+
}
237+
238+
private J.@Nullable MethodInvocation extractWhiteboxInvocation(Statement statement) {
239+
if (statement instanceof J.MethodInvocation) {
240+
J.MethodInvocation mi = (J.MethodInvocation) statement;
241+
if (SET_INTERNAL_STATE.matches(mi) || GET_INTERNAL_STATE.matches(mi) || INVOKE_METHOD.matches(mi)) {
242+
return mi;
243+
}
244+
}
245+
if (statement instanceof J.VariableDeclarations) {
246+
J.VariableDeclarations varDecls = (J.VariableDeclarations) statement;
247+
if (varDecls.getVariables().size() == 1) {
248+
Expression init = varDecls.getVariables().get(0).getInitializer();
249+
if (init instanceof J.MethodInvocation) {
250+
J.MethodInvocation mi = (J.MethodInvocation) init;
251+
if (GET_INTERNAL_STATE.matches(mi) || INVOKE_METHOD.matches(mi)) {
252+
return mi;
253+
}
254+
}
255+
}
256+
}
257+
return null;
258+
}
259+
260+
private @Nullable String extractStringLiteral(Expression expr) {
261+
if (expr instanceof J.Literal && ((J.Literal) expr).getValue() instanceof String) {
262+
return (String) ((J.Literal) expr).getValue();
263+
}
264+
return null;
265+
}
266+
267+
private @Nullable String getCastType(@Nullable JavaType type) {
268+
if (type instanceof JavaType.FullyQualified) {
269+
return ((JavaType.FullyQualified) type).getClassName();
270+
}
271+
if (type instanceof JavaType.Primitive) {
272+
return ((JavaType.Primitive) type).getKeyword();
273+
}
274+
return null;
275+
}
276+
277+
private J.MethodDeclaration addThrowsExceptionIfAbsent(J.MethodDeclaration md) {
278+
if (md.getThrows() != null && md.getThrows().stream()
279+
.anyMatch(j -> TypeUtils.isOfClassType(j.getType(), "java.lang.Exception") ||
280+
TypeUtils.isOfClassType(j.getType(), "java.lang.Throwable"))) {
281+
return md;
282+
}
283+
JavaType.Class exceptionType = JavaType.ShallowClass.build("java.lang.Exception");
284+
return md.withThrows(ListUtils.concat(md.getThrows(),
285+
new J.Identifier(randomId(), Space.SINGLE_SPACE, Markers.EMPTY, emptyList(),
286+
exceptionType.getClassName(), exceptionType, null)));
287+
}
288+
});
289+
}
290+
}
2.67 KB
Binary file not shown.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ recipeList:
4848
- org.openrewrite.java.testing.mockito.RemovePowerMockClassExtensions
4949
- org.openrewrite.java.testing.mockito.PowerMockitoMockStaticToMockito
5050
- org.openrewrite.java.testing.mockito.PowerMockitoWhenNewToMockito
51+
- org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection
5152
- org.openrewrite.java.testing.mockito.CleanupPowerMockImports
5253
- org.openrewrite.java.dependencies.RemoveDependency:
5354
groupId: org.powermock

src/main/resources/META-INF/rewrite/recipes.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.tes
195195
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.MockitoJUnitRunnerToExtension,Replace JUnit 4 MockitoJUnitRunner with junit-jupiter MockitoExtension,"Replace JUnit 4 MockitoJUnitRunner annotations with JUnit 5 `@ExtendWith(MockitoExtension.class)` using the appropriate strictness levels (LENIENT, WARN, STRICT_STUBS).",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
196196
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.MockitoWhenOnStaticToMockStatic,Replace `Mockito.when` on static (non mock) with try-with-resource with MockedStatic,"Replace `Mockito.when` on static (non mock) with try-with-resource with MockedStatic as Mockito4 no longer allows this. For JUnit 4/5 & TestNG: When `@Before*` is used, a `close` call is added to the corresponding `@After*` method. This change moves away from implicit bytecode manipulation for static method stubbing, making mocking behavior more explicit and scoped to avoid unintended side effects.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
197197
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockRunnerDelegateToRunWith,Replace PowerMock runner with standard `@RunWith`,"Replaces `@RunWith(PowerMockRunner.class)`. If `@PowerMockRunnerDelegate(X.class)` is present, promotes the delegate runner to `@RunWith(X.class)`. Otherwise, removes the `@RunWith(PowerMockRunner.class)` annotation entirely.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
198+
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection,Replace PowerMock `Whitebox` with Java reflection,"Replace `org.powermock.reflect.Whitebox` calls (`setInternalState`, `getInternalState`, `invokeMethod`) with plain Java reflection using `java.lang.reflect.Field` and `java.lang.reflect.Method`.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
198199
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockitoMockStaticToMockito,Replace `PowerMock.mockStatic()` with `Mockito.mockStatic()`,Replaces `PowerMockito.mockStatic()` by `Mockito.mockStatic()`. Removes the `@PrepareForTest` annotation.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
199200
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockitoWhenNewToMockito,Replace `PowerMockito.whenNew` with Mockito counterpart,Replaces `PowerMockito.whenNew` calls with respective `Mockito.whenConstructed` calls.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
200201
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.RemoveInitMocksIfRunnersSpecified,Remove `MockitoAnnotations.initMocks(this)` and `openMocks(this)` if JUnit runners specified,Remove `MockitoAnnotations.initMocks(this)` and `MockitoAnnotations.openMocks(this)` if class-level JUnit runners `@RunWith(MockitoJUnitRunner.class)` or `@ExtendWith(MockitoExtension.class)` are specified. These manual initialization calls are redundant when using Mockito's JUnit integration.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,

0 commit comments

Comments
 (0)