Skip to content

Commit 9e86af9

Browse files
authored
Refactor PowerMockWhiteboxToJavaReflection into per-API recipes (#1021)
1 parent 30c392d commit 9e86af9

11 files changed

Lines changed: 1358 additions & 814 deletions
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.Cursor;
21+
import org.openrewrite.ExecutionContext;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.java.MethodMatcher;
25+
import org.openrewrite.java.tree.Expression;
26+
import org.openrewrite.java.tree.J;
27+
import org.openrewrite.java.tree.JavaType;
28+
29+
import java.util.List;
30+
31+
import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
32+
import static org.openrewrite.java.VariableNameUtils.generateVariableName;
33+
34+
public class PowerMockWhiteboxGetInternalStateToJavaReflection extends Recipe {
35+
36+
private static final MethodMatcher GET_INTERNAL_STATE =
37+
new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String)");
38+
39+
@Getter
40+
final String displayName = "Replace PowerMock `Whitebox.getInternalState()` with Java reflection";
41+
42+
@Getter
43+
final String description = "Replace `Whitebox.getInternalState(Object, String)` with `java.lang.reflect.Field` " +
44+
"access, casting to the declared result type where needed. The field lookup uses `getDeclaredField` on " +
45+
"the target object's class, which differs from PowerMock's class-hierarchy traversal for fields " +
46+
"inherited from a superclass.";
47+
48+
@Override
49+
public TreeVisitor<?, ExecutionContext> getVisitor() {
50+
return new GetInternalStateVisitor().withPrecondition();
51+
}
52+
53+
private static class GetInternalStateVisitor extends WhiteboxToReflectionVisitor {
54+
55+
GetInternalStateVisitor() {
56+
super("java.lang.reflect.Field", GET_INTERNAL_STATE);
57+
}
58+
59+
@Override
60+
@Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope,
61+
JavaType.@Nullable Method resolvedMethod) {
62+
String fieldName = extractStringLiteral(mi.getArguments().get(1));
63+
if (fieldName == null) {
64+
return null;
65+
}
66+
String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER);
67+
String prefix = fieldLookupPrefix(varName);
68+
if (sink.varName != null) {
69+
if (isNonObjectCast(sink.castType)) {
70+
return prefix + sink.castType + " " + sink.varName + " = (" + sink.castType + ") " + varName + ".get(#{any(java.lang.Object)});";
71+
}
72+
return prefix + "Object " + sink.varName + " = " + varName + ".get(#{any(java.lang.Object)});";
73+
}
74+
return prefix + varName + ".get(#{any(java.lang.Object)});";
75+
}
76+
77+
@Override
78+
Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) {
79+
List<Expression> args = mi.getArguments();
80+
// target, fieldName, target
81+
return new Object[]{args.get(0), args.get(1), args.get(0)};
82+
}
83+
}
84+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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.Cursor;
21+
import org.openrewrite.ExecutionContext;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.java.MethodMatcher;
25+
import org.openrewrite.java.tree.Expression;
26+
import org.openrewrite.java.tree.J;
27+
import org.openrewrite.java.tree.JavaType;
28+
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
32+
import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
33+
import static org.openrewrite.java.VariableNameUtils.generateVariableName;
34+
35+
public class PowerMockWhiteboxInvokeMethodToJavaReflection extends Recipe {
36+
37+
private static final MethodMatcher INVOKE_METHOD =
38+
new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Object, java.lang.String, ..)");
39+
40+
@Getter
41+
final String displayName = "Replace PowerMock `Whitebox.invokeMethod()` with Java reflection";
42+
43+
@Getter
44+
final String description = "Replace `Whitebox.invokeMethod(Object, String, ..)` with `java.lang.reflect.Method` " +
45+
"lookup and `invoke()`. Parameter types are taken from the unambiguously resolved target method, " +
46+
"falling back to each argument's compile-time class.";
47+
48+
@Override
49+
public TreeVisitor<?, ExecutionContext> getVisitor() {
50+
return new InvokeMethodVisitor().withPrecondition();
51+
}
52+
53+
private static class InvokeMethodVisitor extends WhiteboxToReflectionVisitor {
54+
55+
InvokeMethodVisitor() {
56+
super("java.lang.reflect.Method", INVOKE_METHOD);
57+
}
58+
59+
@Override
60+
JavaType.@Nullable Method resolve(J.MethodInvocation mi) {
61+
return resolveTargetMethod(mi.getArguments());
62+
}
63+
64+
@Override
65+
@Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope,
66+
JavaType.@Nullable Method resolvedMethod) {
67+
List<Expression> args = mi.getArguments();
68+
String methodName = extractStringLiteral(args.get(1));
69+
if (methodName == null) {
70+
return null;
71+
}
72+
String varName = generateVariableName(methodName + "Method", scope, INCREMENT_NUMBER);
73+
74+
// getDeclaredMethod line
75+
StringBuilder sb = new StringBuilder();
76+
sb.append("Method ").append(varName).append(" = #{any(java.lang.Object)}.getClass().getDeclaredMethod(#{any(java.lang.String)}");
77+
for (int i = 2; i < args.size(); i++) {
78+
String classLiteral = getParamClassLiteral(args, i, resolvedMethod);
79+
if (classLiteral != null) {
80+
sb.append(", ").append(classLiteral);
81+
} else {
82+
sb.append(", #{any(java.lang.Object)}.getClass()");
83+
}
84+
}
85+
sb.append(");\n");
86+
87+
// setAccessible line
88+
sb.append(varName).append(".setAccessible(true);\n");
89+
90+
// invoke line
91+
if (sink.varName != null) {
92+
if (isNonObjectCast(sink.castType)) {
93+
sb.append(sink.castType).append(" ").append(sink.varName).append(" = (").append(sink.castType).append(") ");
94+
} else {
95+
sb.append("Object ").append(sink.varName).append(" = ");
96+
}
97+
}
98+
sb.append(varName).append(".invoke(#{any(java.lang.Object)}");
99+
for (int i = 2; i < args.size(); i++) {
100+
sb.append(", #{any(java.lang.Object)}");
101+
}
102+
sb.append(");");
103+
104+
return sb.toString();
105+
}
106+
107+
@Override
108+
Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) {
109+
List<Expression> args = mi.getArguments();
110+
List<Object> result = new ArrayList<>();
111+
result.add(args.get(0)); // target for getDeclaredMethod
112+
result.add(args.get(1)); // methodName
113+
for (int i = 2; i < args.size(); i++) {
114+
if (getParamClassLiteral(args, i, resolvedMethod) == null) {
115+
result.add(args.get(i)); // arg.getClass() fallback for getDeclaredMethod
116+
}
117+
}
118+
result.add(args.get(0)); // target for invoke
119+
for (int i = 2; i < args.size(); i++) {
120+
result.add(args.get(i)); // arg for invoke
121+
}
122+
return result.toArray();
123+
}
124+
125+
/**
126+
* Get the class literal for a parameter at the given argument index. Prefers the resolved
127+
* method's declared parameter type, falls back to the argument's compile-time type, and
128+
* returns null if neither is available.
129+
*/
130+
private @Nullable String getParamClassLiteral(List<Expression> args, int argIndex,
131+
JavaType.@Nullable Method resolvedMethod) {
132+
if (resolvedMethod != null) {
133+
String literal = classLiteralFromType(resolvedMethod.getParameterTypes().get(argIndex - 2));
134+
if (literal != null) {
135+
return literal;
136+
}
137+
}
138+
return classLiteralFromType(args.get(argIndex).getType());
139+
}
140+
141+
/**
142+
* Resolve the target method from the first argument's type and the method name, walking the
143+
* supertype chain. Returns null if the method cannot be unambiguously resolved (not found,
144+
* overloaded, or missing type information).
145+
*/
146+
private JavaType.@Nullable Method resolveTargetMethod(List<Expression> args) {
147+
if (args.size() <= 2) {
148+
return null;
149+
}
150+
String methodName = extractStringLiteral(args.get(1));
151+
if (methodName == null) {
152+
return null;
153+
}
154+
JavaType targetType = args.get(0).getType();
155+
if (!(targetType instanceof JavaType.FullyQualified)) {
156+
return null;
157+
}
158+
int expectedParamCount = args.size() - 2;
159+
JavaType.Method match = null;
160+
for (JavaType.FullyQualified current = (JavaType.FullyQualified) targetType;
161+
current != null; current = current.getSupertype()) {
162+
for (JavaType.Method method : current.getMethods()) {
163+
if (method.getName().equals(methodName) &&
164+
method.getParameterTypes().size() == expectedParamCount) {
165+
if (match != null) {
166+
return null; // ambiguous overload
167+
}
168+
match = method;
169+
}
170+
}
171+
}
172+
return match;
173+
}
174+
175+
private @Nullable String classLiteralFromType(@Nullable JavaType type) {
176+
if (type instanceof JavaType.Primitive) {
177+
return ((JavaType.Primitive) type).getKeyword() + ".class";
178+
}
179+
if (type instanceof JavaType.FullyQualified) {
180+
return ((JavaType.FullyQualified) type).getClassName() + ".class";
181+
}
182+
return null;
183+
}
184+
}
185+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.Cursor;
21+
import org.openrewrite.ExecutionContext;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.java.MethodMatcher;
25+
import org.openrewrite.java.tree.Expression;
26+
import org.openrewrite.java.tree.J;
27+
import org.openrewrite.java.tree.JavaType;
28+
29+
import java.util.List;
30+
31+
import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
32+
import static org.openrewrite.java.VariableNameUtils.generateVariableName;
33+
34+
public class PowerMockWhiteboxSetInternalStateToJavaReflection extends Recipe {
35+
36+
private static final MethodMatcher SET_INTERNAL_STATE =
37+
new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object)");
38+
private static final MethodMatcher SET_INTERNAL_STATE_WHERE =
39+
new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object, java.lang.Class)");
40+
41+
@Getter
42+
final String displayName = "Replace PowerMock `Whitebox.setInternalState()` with Java reflection";
43+
44+
@Getter
45+
final String description = "Replace `Whitebox.setInternalState(Object, String, Object)` and " +
46+
"`Whitebox.setInternalState(Object, String, Object, Class)` with `java.lang.reflect.Field` access. " +
47+
"The 3-arg overload looks up the field on the target's class; the 4-arg where-overload uses the " +
48+
"supplied Class to resolve fields declared on a superclass.";
49+
50+
@Override
51+
public TreeVisitor<?, ExecutionContext> getVisitor() {
52+
return new SetInternalStateVisitor().withPrecondition();
53+
}
54+
55+
private static class SetInternalStateVisitor extends WhiteboxToReflectionVisitor {
56+
57+
SetInternalStateVisitor() {
58+
super("java.lang.reflect.Field", SET_INTERNAL_STATE, SET_INTERNAL_STATE_WHERE);
59+
}
60+
61+
@Override
62+
@Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope,
63+
JavaType.@Nullable Method resolvedMethod) {
64+
String fieldName = extractStringLiteral(mi.getArguments().get(1));
65+
if (fieldName == null) {
66+
return null;
67+
}
68+
String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER);
69+
String prefix = mi.getArguments().size() == 4
70+
? fieldLookupPrefixWhere(varName)
71+
: fieldLookupPrefix(varName);
72+
return prefix +
73+
varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});";
74+
}
75+
76+
@Override
77+
Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) {
78+
List<Expression> args = mi.getArguments();
79+
if (args.size() == 4) {
80+
// whereClass, fieldName, target, value
81+
return new Object[]{args.get(3), args.get(1), args.get(0), args.get(2)};
82+
}
83+
// target, fieldName, target, value
84+
return new Object[]{args.get(0), args.get(1), args.get(0), args.get(2)};
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)