Skip to content

Commit b700d7b

Browse files
authored
Add PowerMockWhiteboxInvokeConstructorToJavaReflection recipe (#1027)
Whitebox.invokeConstructor(Class, args...) -> Constructor lookup + newInstance() on the named class: Constructor<MyService> c = MyService.class.getDeclaredConstructor(String.class); c.setAccessible(true); MyService s = c.newInstance("Alice"); Handles both the Object... varargs overload (param types resolved from the unambiguously matched constructor, else arg.getClass()) and the explicit Class[]/Object[] overload. Arrays passed to the Object... varargs form are ambiguous and left unchanged. Registered in the composite. Reuses the base hasArrayArg helper; the constructor resolution and class-literal helpers are kept private to the recipe, matching the existing InvokeMethod recipe (no shared-base changes).
1 parent 20880b9 commit b700d7b

4 files changed

Lines changed: 473 additions & 3 deletions

File tree

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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+
import org.openrewrite.java.tree.TypeUtils;
29+
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
33+
import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
34+
import static org.openrewrite.java.VariableNameUtils.generateVariableName;
35+
36+
public class PowerMockWhiteboxInvokeConstructorToJavaReflection extends Recipe {
37+
38+
private static final MethodMatcher INVOKE_CONSTRUCTOR_ARGS =
39+
new MethodMatcher("org.powermock.reflect.Whitebox invokeConstructor(java.lang.Class, java.lang.Object[])");
40+
private static final MethodMatcher INVOKE_CONSTRUCTOR_EXPLICIT =
41+
new MethodMatcher("org.powermock.reflect.Whitebox invokeConstructor(java.lang.Class, java.lang.Class[], java.lang.Object[])");
42+
43+
@Getter
44+
final String displayName = "Replace PowerMock `Whitebox.invokeConstructor()` with Java reflection";
45+
46+
@Getter
47+
final String description = "Replace `Whitebox.invokeConstructor(..)` with `java.lang.reflect.Constructor` " +
48+
"lookup and `newInstance()` on the named class. Constructor parameter types are taken from the " +
49+
"unambiguously resolved constructor, falling back to each argument's compile-time class; arrays " +
50+
"passed to the `Object...` varargs overload are left unchanged for manual migration.";
51+
52+
@Override
53+
public TreeVisitor<?, ExecutionContext> getVisitor() {
54+
return new InvokeConstructorVisitor().withPrecondition();
55+
}
56+
57+
private static class InvokeConstructorVisitor extends WhiteboxToReflectionVisitor {
58+
59+
InvokeConstructorVisitor() {
60+
super("java.lang.reflect.Constructor", INVOKE_CONSTRUCTOR_ARGS, INVOKE_CONSTRUCTOR_EXPLICIT);
61+
}
62+
63+
@Override
64+
JavaType.@Nullable Method resolve(J.MethodInvocation mi) {
65+
if (INVOKE_CONSTRUCTOR_ARGS.matches(mi)) {
66+
return resolveTargetConstructor(mi.getArguments());
67+
}
68+
return null;
69+
}
70+
71+
@Override
72+
@Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope,
73+
JavaType.@Nullable Method resolvedMethod) {
74+
List<Expression> args = mi.getArguments();
75+
boolean explicit = INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi);
76+
if (!explicit && hasArrayArg(args, 1)) {
77+
// An array passed to the `Object...` varargs overload is ambiguous (spread vs single arg);
78+
// leave for manual migration (flagged downstream).
79+
return null;
80+
}
81+
JavaType.FullyQualified elem = classLiteralElementType(args.get(0));
82+
String genericType = elem != null ? elem.getClassName() : "?";
83+
String varName = constructorVarName(elem, scope);
84+
85+
StringBuilder sb = new StringBuilder("Constructor<").append(genericType).append("> ").append(varName)
86+
.append(" = #{any(java.lang.Class)}.getDeclaredConstructor(");
87+
88+
int newInstanceArgCount;
89+
if (explicit) {
90+
List<Expression> paramTypeExprs = arrayElements(args.get(1));
91+
List<Expression> ctorArgExprs = arrayElements(args.get(2));
92+
if (paramTypeExprs == null || ctorArgExprs == null) {
93+
// Cannot unwrap the explicit Class[]/Object[] arrays; leave for manual migration (flagged downstream)
94+
return null;
95+
}
96+
for (int i = 0; i < paramTypeExprs.size(); i++) {
97+
sb.append(i > 0 ? ", " : "").append("#{any(java.lang.Class)}");
98+
}
99+
newInstanceArgCount = ctorArgExprs.size();
100+
} else {
101+
for (int i = 1; i < args.size(); i++) {
102+
sb.append(i > 1 ? ", " : "");
103+
String classLiteral = getParamClassLiteral(args, i, resolvedMethod, 1);
104+
sb.append(classLiteral != null ? classLiteral : "#{any(java.lang.Object)}.getClass()");
105+
}
106+
newInstanceArgCount = args.size() - 1;
107+
}
108+
sb.append(");\n");
109+
sb.append(varName).append(".setAccessible(true);\n");
110+
sb.append(constructorNewInstanceTail(varName, sink, elem != null, newInstanceArgCount));
111+
return sb.toString();
112+
}
113+
114+
@Override
115+
Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) {
116+
List<Expression> args = mi.getArguments();
117+
List<Object> result = new ArrayList<>();
118+
result.add(args.get(0)); // getDeclaredConstructor receiver (Class)
119+
if (INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi)) {
120+
List<Expression> paramTypeExprs = arrayElements(args.get(1));
121+
List<Expression> ctorArgExprs = arrayElements(args.get(2));
122+
if (paramTypeExprs != null) {
123+
result.addAll(paramTypeExprs); // one per #{any(java.lang.Class)}
124+
}
125+
if (ctorArgExprs != null) {
126+
result.addAll(ctorArgExprs); // newInstance args
127+
}
128+
} else {
129+
for (int i = 1; i < args.size(); i++) {
130+
if (getParamClassLiteral(args, i, resolvedMethod, 1) == null) {
131+
result.add(args.get(i)); // arg.getClass() fallback receiver
132+
}
133+
}
134+
for (int i = 1; i < args.size(); i++) {
135+
result.add(args.get(i)); // newInstance args
136+
}
137+
}
138+
return result.toArray();
139+
}
140+
141+
private String constructorNewInstanceTail(String varName, ResultSink sink, boolean elemKnown, int argCount) {
142+
String invokeArgs = repeatObjectPlaceholders(argCount);
143+
if (sink.varName != null) {
144+
String castType = sink.castType != null ? sink.castType : "Object";
145+
// When the Class element type is known, newInstance() returns it directly (no cast needed);
146+
// otherwise we have a raw Constructor<?> returning Object that must be cast.
147+
if (!elemKnown && isNonObjectCast(castType)) {
148+
return castType + " " + sink.varName + " = (" + castType + ") " + varName + ".newInstance(" + invokeArgs + ");";
149+
}
150+
return castType + " " + sink.varName + " = " + varName + ".newInstance(" + invokeArgs + ");";
151+
}
152+
return varName + ".newInstance(" + invokeArgs + ");";
153+
}
154+
155+
private String repeatObjectPlaceholders(int count) {
156+
StringBuilder sb = new StringBuilder();
157+
for (int i = 0; i < count; i++) {
158+
sb.append(i > 0 ? ", " : "").append("#{any(java.lang.Object)}");
159+
}
160+
return sb.toString();
161+
}
162+
163+
private @Nullable List<Expression> arrayElements(Expression expr) {
164+
if (expr instanceof J.NewArray) {
165+
return ((J.NewArray) expr).getInitializer();
166+
}
167+
return null;
168+
}
169+
170+
private String constructorVarName(JavaType.@Nullable FullyQualified elem, Cursor scope) {
171+
String base;
172+
if (elem != null) {
173+
String simple = elem.getClassName();
174+
int dot = simple.lastIndexOf('.');
175+
if (dot >= 0) {
176+
simple = simple.substring(dot + 1);
177+
}
178+
base = Character.toLowerCase(simple.charAt(0)) + simple.substring(1) + "Constructor";
179+
} else {
180+
base = "reflectConstructor";
181+
}
182+
return generateVariableName(base, scope, INCREMENT_NUMBER);
183+
}
184+
185+
/**
186+
* Resolve the target constructor from the {@code Class} literal's element type, by parameter count.
187+
* Returns null if not unambiguously resolvable (so parameter types fall back to {@code arg.getClass()}).
188+
*/
189+
private JavaType.@Nullable Method resolveTargetConstructor(List<Expression> args) {
190+
if (args.size() <= 1) {
191+
return null;
192+
}
193+
JavaType.FullyQualified type = classLiteralElementType(args.get(0));
194+
if (type == null) {
195+
return null;
196+
}
197+
int expectedParamCount = args.size() - 1;
198+
JavaType.Method match = null;
199+
for (JavaType.Method method : type.getMethods()) {
200+
if (method.isConstructor() && method.getParameterTypes().size() == expectedParamCount) {
201+
if (match != null) {
202+
return null; // ambiguous overload
203+
}
204+
match = method;
205+
}
206+
}
207+
return match;
208+
}
209+
210+
// Extract the element type X from a Class<X>-typed expression (e.g. MyService.class).
211+
private JavaType.@Nullable FullyQualified classLiteralElementType(Expression classExpr) {
212+
JavaType.Parameterized parameterized = TypeUtils.asParameterized(classExpr.getType());
213+
if (parameterized != null && !parameterized.getTypeParameters().isEmpty()) {
214+
return TypeUtils.asFullyQualified(parameterized.getTypeParameters().get(0));
215+
}
216+
return null;
217+
}
218+
219+
// Class literal for the parameter at argIndex, where firstParamArgIndex is the argument index of the
220+
// first declared parameter (1 for invokeConstructor: class, params...). Prefers the resolved
221+
// constructor's declared parameter type, falls back to the argument's compile-time type.
222+
private @Nullable String getParamClassLiteral(List<Expression> args, int argIndex,
223+
JavaType.@Nullable Method resolvedMethod, int firstParamArgIndex) {
224+
if (resolvedMethod != null) {
225+
int paramIdx = argIndex - firstParamArgIndex;
226+
List<JavaType> paramTypes = resolvedMethod.getParameterTypes();
227+
if (paramIdx >= 0 && paramIdx < paramTypes.size()) {
228+
String literal = classLiteralFromType(paramTypes.get(paramIdx));
229+
if (literal != null) {
230+
return literal;
231+
}
232+
}
233+
}
234+
return classLiteralFromType(args.get(argIndex).getType());
235+
}
236+
237+
private @Nullable String classLiteralFromType(@Nullable JavaType type) {
238+
if (type instanceof JavaType.Primitive) {
239+
return ((JavaType.Primitive) type).getKeyword() + ".class";
240+
}
241+
if (type instanceof JavaType.FullyQualified) {
242+
return ((JavaType.FullyQualified) type).getClassName() + ".class";
243+
}
244+
return null;
245+
}
246+
}
247+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ type: specs.openrewrite.org/v1beta/recipe
6464
name: org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection
6565
displayName: Replace PowerMock `Whitebox` with Java reflection
6666
description: >-
67-
Replace `org.powermock.reflect.Whitebox` calls (`setInternalState`, `getInternalState`, `invokeMethod`, `getField`, `getMethod`)
68-
with plain Java reflection using `java.lang.reflect.Field` and `java.lang.reflect.Method`.
67+
Replace `org.powermock.reflect.Whitebox` calls (`setInternalState`, `getInternalState`, `invokeMethod`, `getField`, `getMethod`, `invokeConstructor`)
68+
with plain Java reflection using `java.lang.reflect.Field`, `java.lang.reflect.Method`, and `java.lang.reflect.Constructor`.
6969
tags:
7070
- testing
7171
- mockito
@@ -75,3 +75,4 @@ recipeList:
7575
- org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeMethodToJavaReflection
7676
- org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetFieldToJavaReflection
7777
- org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetMethodToJavaReflection
78+
- org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeConstructorToJavaReflection

0 commit comments

Comments
 (0)