From 9e9aaa26b2f55b886abc902eaf9ab7510b80b148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Merlin=20B=C3=B6gershausen?= Date: Tue, 16 Jun 2026 13:50:05 +0200 Subject: [PATCH 1/3] Split PowerMockWhiteboxToJavaReflection into per-API recipes Restructure the monolithic `PowerMockWhiteboxToJavaReflection` recipe into six focused, independently-usable per-API recipes that share a configurable, package-private `WhiteboxToReflectionVisitor`. The released `org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection` name is preserved as a declarative YAML composite listing the six, so by-name usage and the `ReplacePowerMockito` reference are unaffected. This also expands Whitebox coverage: the `Class where` overloads of `set`/`getInternalState`, static `invokeMethod`, and `getField`/`getMethod`/ `invokeConstructor`. Each recipe gates its visitor with `UsesMethod` on its own matchers, and the shared visitor is configured per recipe (matchers + target reflect import) rather than via inheritance hooks. --- ...rMockWhiteboxGetFieldToJavaReflection.java | 70 +++ ...teboxGetInternalStateToJavaReflection.java | 91 +++ ...MockWhiteboxGetMethodToJavaReflection.java | 80 +++ ...eboxInvokeConstructorToJavaReflection.java | 209 +++++++ ...kWhiteboxInvokeMethodToJavaReflection.java | 183 ++++++ ...teboxSetInternalStateToJavaReflection.java | 78 +++ .../PowerMockWhiteboxToJavaReflection.java | 391 ------------- .../mockito/WhiteboxToReflectionVisitor.java | 396 +++++++++++++ .../META-INF/rewrite/powermockito.yml | 20 + .../resources/META-INF/rewrite/recipes.csv | 8 +- ...kWhiteboxGetFieldToJavaReflectionTest.java | 102 ++++ ...xGetInternalStateToJavaReflectionTest.java | 240 ++++++++ ...WhiteboxGetMethodToJavaReflectionTest.java | 148 +++++ ...InvokeConstructorToJavaReflectionTest.java | 221 ++++++++ ...teboxInvokeMethodToJavaReflectionTest.java | 536 ++++++++++++++++++ ...xSetInternalStateToJavaReflectionTest.java | 322 +++++++++++ ...PowerMockWhiteboxToJavaReflectionTest.java | 432 +------------- 17 files changed, 2726 insertions(+), 801 deletions(-) create mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java create mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java create mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java create mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java create mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java create mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java delete mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java create mode 100644 src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java create mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java create mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java create mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java create mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java create mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java create mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java new file mode 100644 index 000000000..4a1662a65 --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java @@ -0,0 +1,70 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.List; + +public class PowerMockWhiteboxGetFieldToJavaReflection extends Recipe { + + private static final MethodMatcher GET_FIELD = + new MethodMatcher("org.powermock.reflect.Whitebox getField(java.lang.Class, java.lang.String)"); + + @Getter + final String displayName = "Replace PowerMock `Whitebox.getField()` with Java reflection"; + + @Getter + final String description = "Replace `Whitebox.getField(Class, String)` with `Class.getDeclaredField(String)` " + + "plus `setAccessible(true)`. Unlike PowerMock, `getDeclaredField` does not traverse the class " + + "hierarchy for fields inherited from a superclass."; + + @Override + public TreeVisitor getVisitor() { + return new GetFieldVisitor().withPrecondition(); + } + + private static class GetFieldVisitor extends WhiteboxToReflectionVisitor { + + GetFieldVisitor() { + super("java.lang.reflect.Field", GET_FIELD); + } + + @Override + String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + String varName = resultLocalName(sink, mi.getArguments().get(1), scope, true); + return "Field " + varName + " = #{any(java.lang.Class)}.getDeclaredField(#{any(java.lang.String)});\n" + + varName + ".setAccessible(true);"; + } + + @Override + Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + // declaringClass, fieldName + return new Object[]{args.get(0), args.get(1)}; + } + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java new file mode 100644 index 000000000..19f2a08de --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java @@ -0,0 +1,91 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.List; + +public class PowerMockWhiteboxGetInternalStateToJavaReflection extends Recipe { + + private static final MethodMatcher GET_INTERNAL_STATE = + new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String)"); + private static final MethodMatcher GET_INTERNAL_STATE_WHERE = + new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String, java.lang.Class)"); + + @Getter + final String displayName = "Replace PowerMock `Whitebox.getInternalState()` with Java reflection"; + + @Getter + final String description = "Replace `Whitebox.getInternalState(Object, String)` and the `Class where` overload " + + "with `java.lang.reflect.Field` access, casting to the declared result type where needed. The field " + + "lookup uses `getDeclaredField` on the target object's class (or the `where` class), which differs " + + "from PowerMock's class-hierarchy traversal for fields inherited from a superclass."; + + @Override + public TreeVisitor getVisitor() { + return new GetInternalStateVisitor().withPrecondition(); + } + + private static class GetInternalStateVisitor extends WhiteboxToReflectionVisitor { + + GetInternalStateVisitor() { + super("java.lang.reflect.Field", GET_INTERNAL_STATE, GET_INTERNAL_STATE_WHERE); + } + + @Override + String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + String varName = fieldVarName(mi.getArguments().get(1), scope); + String receiver = GET_INTERNAL_STATE_WHERE.matches(mi) ? "#{any(java.lang.Class)}" : "#{any(java.lang.Object)}.getClass()"; + return fieldLookupPrefix(varName, receiver) + fieldGetTail(varName, sink); + } + + @Override + Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + if (GET_INTERNAL_STATE_WHERE.matches(mi)) { + // where, fieldName, target + return new Object[]{args.get(2), args.get(1), args.get(0)}; + } + // target, fieldName, target + return new Object[]{args.get(0), args.get(1), args.get(0)}; + } + + /** + * Build the trailing {@code Field.get(...)} statement, casting to the result type when the + * result is stored in a variable. + */ + private String fieldGetTail(String varName, ResultSink sink) { + if (sink.varName != null) { + if (isNonObjectCast(sink.castType)) { + return sink.castType + " " + sink.varName + " = (" + boxedCastType(sink.castType) + ") " + varName + ".get(#{any(java.lang.Object)});"; + } + return "Object " + sink.varName + " = " + varName + ".get(#{any(java.lang.Object)});"; + } + return varName + ".get(#{any(java.lang.Object)});"; + } + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java new file mode 100644 index 000000000..02d356d3b --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java @@ -0,0 +1,80 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.List; + +public class PowerMockWhiteboxGetMethodToJavaReflection extends Recipe { + + private static final MethodMatcher GET_METHOD = + new MethodMatcher("org.powermock.reflect.Whitebox getMethod(java.lang.Class, java.lang.String, ..)"); + + @Getter + final String displayName = "Replace PowerMock `Whitebox.getMethod()` with Java reflection"; + + @Getter + final String description = "Replace `Whitebox.getMethod(Class, String, Class...)` with " + + "`Class.getDeclaredMethod(String, Class...)` plus `setAccessible(true)`. Unlike PowerMock, " + + "`getDeclaredMethod` does not traverse the class hierarchy; calls passing an explicit `Class[]` " + + "array are left unchanged for manual migration."; + + @Override + public TreeVisitor getVisitor() { + return new GetMethodVisitor().withPrecondition(); + } + + private static class GetMethodVisitor extends WhiteboxToReflectionVisitor { + + GetMethodVisitor() { + super("java.lang.reflect.Method", GET_METHOD); + } + + @Override + @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + if (hasArrayArg(args, 2)) { + // Explicit Class[] varargs array is not supported; leave for manual migration (flagged downstream) + return null; + } + String varName = resultLocalName(sink, args.get(1), scope, false); + StringBuilder sb = new StringBuilder("Method ").append(varName) + .append(" = #{any(java.lang.Class)}.getDeclaredMethod(#{any(java.lang.String)}"); + for (int i = 2; i < args.size(); i++) { + sb.append(", #{any(java.lang.Class)}"); + } + sb.append(");\n").append(varName).append(".setAccessible(true);"); + return sb.toString(); + } + + @Override + Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { + // declaringClass, methodName, paramType0, paramType1, ... + return mi.getArguments().toArray(); + } + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java new file mode 100644 index 000000000..25d319874 --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java @@ -0,0 +1,209 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.ArrayList; +import java.util.List; + +import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; +import static org.openrewrite.java.VariableNameUtils.generateVariableName; + +public class PowerMockWhiteboxInvokeConstructorToJavaReflection extends Recipe { + + private static final MethodMatcher INVOKE_CONSTRUCTOR_ARGS = + new MethodMatcher("org.powermock.reflect.Whitebox invokeConstructor(java.lang.Class, java.lang.Object[])"); + private static final MethodMatcher INVOKE_CONSTRUCTOR_EXPLICIT = + new MethodMatcher("org.powermock.reflect.Whitebox invokeConstructor(java.lang.Class, java.lang.Class[], java.lang.Object[])"); + + @Getter + final String displayName = "Replace PowerMock `Whitebox.invokeConstructor()` with Java reflection"; + + @Getter + final String description = "Replace `Whitebox.invokeConstructor(..)` with `java.lang.reflect.Constructor` " + + "lookup and `newInstance()` on the named class. Constructor parameter types are taken from the " + + "unambiguously resolved constructor, falling back to each argument's compile-time class; arrays " + + "passed to the `Object...` varargs overload are left unchanged for manual migration."; + + @Override + public TreeVisitor getVisitor() { + return new InvokeConstructorVisitor().withPrecondition(); + } + + private static class InvokeConstructorVisitor extends WhiteboxToReflectionVisitor { + + InvokeConstructorVisitor() { + super("java.lang.reflect.Constructor", INVOKE_CONSTRUCTOR_ARGS, INVOKE_CONSTRUCTOR_EXPLICIT); + } + + @Override + JavaType.@Nullable Method resolve(J.MethodInvocation mi) { + if (INVOKE_CONSTRUCTOR_ARGS.matches(mi)) { + return resolveTargetConstructor(mi.getArguments()); + } + return null; + } + + @Override + @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + boolean explicit = INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi); + if (!explicit && hasArrayArg(args, 1)) { + // An array passed to the `Object...` varargs overload is ambiguous (spread vs single arg); + // leave for manual migration (flagged downstream). + return null; + } + JavaType.FullyQualified elem = classLiteralElementType(args.get(0)); + String genericType = elem != null ? elem.getClassName() : "?"; + String varName = constructorVarName(elem, scope); + + StringBuilder sb = new StringBuilder("Constructor<").append(genericType).append("> ").append(varName) + .append(" = #{any(java.lang.Class)}.getDeclaredConstructor("); + + int newInstanceArgCount; + if (explicit) { + List paramTypeExprs = arrayElements(args.get(1)); + List ctorArgExprs = arrayElements(args.get(2)); + if (paramTypeExprs == null || ctorArgExprs == null) { + // Cannot unwrap the explicit Class[]/Object[] arrays; leave for manual migration (flagged downstream) + return null; + } + for (int i = 0; i < paramTypeExprs.size(); i++) { + sb.append(i > 0 ? ", " : "").append("#{any(java.lang.Class)}"); + } + newInstanceArgCount = ctorArgExprs.size(); + } else { + for (int i = 1; i < args.size(); i++) { + sb.append(i > 1 ? ", " : ""); + String classLiteral = getParamClassLiteral(args, i, resolvedMethod, 1); + sb.append(classLiteral != null ? classLiteral : "#{any(java.lang.Object)}.getClass()"); + } + newInstanceArgCount = args.size() - 1; + } + sb.append(");\n"); + sb.append(varName).append(".setAccessible(true);\n"); + sb.append(constructorNewInstanceTail(varName, sink, elem != null, newInstanceArgCount)); + return sb.toString(); + } + + @Override + Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + List result = new ArrayList<>(); + result.add(args.get(0)); // getDeclaredConstructor receiver (Class) + if (INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi)) { + List paramTypeExprs = arrayElements(args.get(1)); + List ctorArgExprs = arrayElements(args.get(2)); + if (paramTypeExprs != null) { + result.addAll(paramTypeExprs); // one per #{any(java.lang.Class)} + } + if (ctorArgExprs != null) { + result.addAll(ctorArgExprs); // newInstance args + } + } else { + for (int i = 1; i < args.size(); i++) { + if (getParamClassLiteral(args, i, resolvedMethod, 1) == null) { + result.add(args.get(i)); // arg.getClass() fallback receiver + } + } + for (int i = 1; i < args.size(); i++) { + result.add(args.get(i)); // newInstance args + } + } + return result.toArray(); + } + + private String constructorNewInstanceTail(String varName, ResultSink sink, boolean elemKnown, int argCount) { + String invokeArgs = repeatObjectPlaceholders(argCount); + if (sink.varName != null) { + String castType = sink.castType != null ? sink.castType : "Object"; + // When the Class element type is known, newInstance() returns it directly (no cast needed); + // otherwise we have a raw Constructor returning Object that must be cast. + if (!elemKnown && isNonObjectCast(castType)) { + return castType + " " + sink.varName + " = (" + castType + ") " + varName + ".newInstance(" + invokeArgs + ");"; + } + return castType + " " + sink.varName + " = " + varName + ".newInstance(" + invokeArgs + ");"; + } + return varName + ".newInstance(" + invokeArgs + ");"; + } + + private String repeatObjectPlaceholders(int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(i > 0 ? ", " : "").append("#{any(java.lang.Object)}"); + } + return sb.toString(); + } + + private @Nullable List arrayElements(Expression expr) { + if (expr instanceof J.NewArray) { + return ((J.NewArray) expr).getInitializer(); + } + return null; + } + + private String constructorVarName(JavaType.@Nullable FullyQualified elem, Cursor scope) { + String base; + if (elem != null) { + String simple = elem.getClassName(); + int dot = simple.lastIndexOf('.'); + if (dot >= 0) { + simple = simple.substring(dot + 1); + } + base = Character.toLowerCase(simple.charAt(0)) + simple.substring(1) + "Constructor"; + } else { + base = "reflectConstructor"; + } + return generateVariableName(base, scope, INCREMENT_NUMBER); + } + + /** + * Resolve the target constructor from the {@code Class} literal's element type, by parameter count. + * Returns null if not unambiguously resolvable (so parameter types fall back to {@code arg.getClass()}). + */ + private JavaType.@Nullable Method resolveTargetConstructor(List args) { + if (args.size() <= 1) { + return null; + } + JavaType.FullyQualified type = classLiteralElementType(args.get(0)); + if (type == null) { + return null; + } + int expectedParamCount = args.size() - 1; + JavaType.Method match = null; + for (JavaType.Method method : type.getMethods()) { + if (method.isConstructor() && method.getParameterTypes().size() == expectedParamCount) { + if (match != null) { + return null; // ambiguous overload + } + match = method; + } + } + return match; + } + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java new file mode 100644 index 000000000..6084d0d26 --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java @@ -0,0 +1,183 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class PowerMockWhiteboxInvokeMethodToJavaReflection extends Recipe { + + private static final MethodMatcher INVOKE_METHOD = + new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Object, java.lang.String, ..)"); + private static final MethodMatcher INVOKE_METHOD_STATIC = + new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Class, java.lang.String, ..)"); + + @Getter + final String displayName = "Replace PowerMock `Whitebox.invokeMethod()` with Java reflection"; + + @Getter + final String description = "Replace instance and static `Whitebox.invokeMethod(..)` calls with " + + "`java.lang.reflect.Method` lookup and `invoke()`. Parameter types are taken from the unambiguously " + + "resolved target method, falling back to each argument's compile-time class; calls passing an " + + "explicit `Class[]` array are left unchanged for manual migration."; + + @Override + public TreeVisitor getVisitor() { + return new InvokeMethodVisitor().withPrecondition(); + } + + private static class InvokeMethodVisitor extends WhiteboxToReflectionVisitor { + + InvokeMethodVisitor() { + super("java.lang.reflect.Method", INVOKE_METHOD, INVOKE_METHOD_STATIC); + } + + @Override + JavaType.@Nullable Method resolve(J.MethodInvocation mi) { + if (INVOKE_METHOD.matches(mi)) { + return resolveTargetMethod(mi.getArguments()); + } + if (INVOKE_METHOD_STATIC.matches(mi)) { + return resolveStaticTargetMethod(mi.getArguments()); + } + return null; + } + + @Override + @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + if (hasArrayArg(args, 2)) { + // Explicit `Class[]` parameter-type overload (or an array passed as varargs) cannot be + // mechanically expanded; leave for manual migration (flagged downstream). + return null; + } + boolean isStatic = INVOKE_METHOD_STATIC.matches(mi); + String varName = methodVarName(args.get(1), scope); + // Static calls take a Class target; instance calls take an Object whose class we look up. + String declaredMethodReceiver = isStatic ? "#{any(java.lang.Class)}" : "#{any(java.lang.Object)}.getClass()"; + String invokeTarget = isStatic ? "null" : "#{any(java.lang.Object)}"; + + // getDeclaredMethod line + StringBuilder sb = new StringBuilder(); + sb.append("Method ").append(varName).append(" = ").append(declaredMethodReceiver) + .append(".getDeclaredMethod(#{any(java.lang.String)}"); + for (int i = 2; i < args.size(); i++) { + String classLiteral = getParamClassLiteral(args, i, resolvedMethod); + if (classLiteral != null) { + sb.append(", ").append(classLiteral); + } else { + sb.append(", #{any(java.lang.Object)}.getClass()"); + } + } + sb.append(");\n"); + + // setAccessible line + sb.append(varName).append(".setAccessible(true);\n"); + + // invoke line + if (sink.varName != null) { + if (isNonObjectCast(sink.castType)) { + sb.append(sink.castType).append(" ").append(sink.varName).append(" = (").append(boxedCastType(sink.castType)).append(") "); + } else { + sb.append("Object ").append(sink.varName).append(" = "); + } + } + sb.append(varName).append(".invoke(").append(invokeTarget); + for (int i = 2; i < args.size(); i++) { + sb.append(", #{any(java.lang.Object)}"); + } + sb.append(");"); + + return sb.toString(); + } + + @Override + Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + List result = new ArrayList<>(); + result.add(args.get(0)); // receiver for getDeclaredMethod (Class for static, Object for instance) + result.add(args.get(1)); // methodName + for (int i = 2; i < args.size(); i++) { + if (getParamClassLiteral(args, i, resolvedMethod) == null) { + result.add(args.get(i)); // arg.getClass() fallback for getDeclaredMethod + } + } + if (!INVOKE_METHOD_STATIC.matches(mi)) { + result.add(args.get(0)); // target for invoke (static uses the null literal, no placeholder) + } + for (int i = 2; i < args.size(); i++) { + result.add(args.get(i)); // arg for invoke + } + return result.toArray(); + } + + /** + * Resolve the target method from the first argument's type and the method name. + * Returns null if the method cannot be unambiguously resolved (not found, overloaded, + * or missing type information). + */ + private JavaType.@Nullable Method resolveTargetMethod(List args) { + if (args.size() <= 2) { + return null; + } + JavaType targetType = args.get(0).getType(); + JavaType.FullyQualified fq = targetType instanceof JavaType.FullyQualified ? (JavaType.FullyQualified) targetType : null; + return findUniqueMethod(fq, extractStringLiteral(args.get(1)), args.size() - 2); + } + + /** + * Resolve the static target method from the {@code Class} literal's element type and the method name. + */ + private JavaType.@Nullable Method resolveStaticTargetMethod(List args) { + if (args.size() <= 2) { + return null; + } + return findUniqueMethod(classLiteralElementType(args.get(0)), extractStringLiteral(args.get(1)), args.size() - 2); + } + + private JavaType.@Nullable Method findUniqueMethod(JavaType.@Nullable FullyQualified targetType, + @Nullable String methodName, int expectedParamCount) { + if (targetType == null || methodName == null) { + return null; + } + JavaType.Method match = null; + for (Iterator it = targetType.getVisibleMethods(); it.hasNext(); ) { + JavaType.Method method = it.next(); + if (method.getName().equals(methodName) && + method.getParameterTypes().size() == expectedParamCount) { + if (match != null) { + return null; // ambiguous overload + } + match = method; + } + } + return match; + } + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java new file mode 100644 index 000000000..79312a8b0 --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.List; + +public class PowerMockWhiteboxSetInternalStateToJavaReflection extends Recipe { + + private static final MethodMatcher SET_INTERNAL_STATE = + new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object)"); + private static final MethodMatcher SET_INTERNAL_STATE_WHERE = + new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object, java.lang.Class)"); + + @Getter + final String displayName = "Replace PowerMock `Whitebox.setInternalState()` with Java reflection"; + + @Getter + final String description = "Replace `Whitebox.setInternalState(Object, String, Object)` and the `Class where` " + + "overload with `java.lang.reflect.Field` access. The field lookup uses `getDeclaredField` on the target " + + "object's class (or the `where` class), which differs from PowerMock's class-hierarchy traversal for " + + "fields inherited from a superclass."; + + @Override + public TreeVisitor getVisitor() { + return new SetInternalStateVisitor().withPrecondition(); + } + + private static class SetInternalStateVisitor extends WhiteboxToReflectionVisitor { + + SetInternalStateVisitor() { + super("java.lang.reflect.Field", SET_INTERNAL_STATE, SET_INTERNAL_STATE_WHERE); + } + + @Override + String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + String varName = fieldVarName(mi.getArguments().get(1), scope); + String receiver = SET_INTERNAL_STATE_WHERE.matches(mi) ? "#{any(java.lang.Class)}" : "#{any(java.lang.Object)}.getClass()"; + return fieldLookupPrefix(varName, receiver) + + varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});"; + } + + @Override + Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { + List args = mi.getArguments(); + if (SET_INTERNAL_STATE_WHERE.matches(mi)) { + // where, fieldName, target, value + return new Object[]{args.get(3), args.get(1), args.get(0), args.get(2)}; + } + // target, fieldName, target, value + return new Object[]{args.get(0), args.get(1), args.get(0), args.get(2)}; + } + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java deleted file mode 100644 index f181125d6..000000000 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java +++ /dev/null @@ -1,391 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import lombok.Getter; -import org.jspecify.annotations.Nullable; -import org.openrewrite.*; -import org.openrewrite.internal.ListUtils; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.JavaParser; -import org.openrewrite.java.JavaTemplate; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.search.UsesType; -import org.openrewrite.java.tree.*; -import org.openrewrite.marker.Markers; - -import java.util.ArrayList; -import java.util.List; - -import static java.util.Collections.emptyList; -import static org.openrewrite.Tree.randomId; -import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; -import static org.openrewrite.java.VariableNameUtils.generateVariableName; - -public class PowerMockWhiteboxToJavaReflection extends Recipe { - - private static final String WHITEBOX_FQN = "org.powermock.reflect.Whitebox"; - private static final MethodMatcher SET_INTERNAL_STATE = - new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object)"); - private static final MethodMatcher GET_INTERNAL_STATE = - new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String)"); - private static final MethodMatcher INVOKE_METHOD = - new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Object, java.lang.String, ..)"); - - @Getter - final String displayName = "Replace PowerMock `Whitebox` with Java reflection"; - - @Getter - final String description = "Replace `org.powermock.reflect.Whitebox` calls " + - "(`setInternalState`, `getInternalState`, `invokeMethod`) with plain Java reflection using " + - "`java.lang.reflect.Field` and `java.lang.reflect.Method`."; - - @Override - public TreeVisitor getVisitor() { - return Preconditions.check( - new UsesType<>(WHITEBOX_FQN, false), - new JavaIsoVisitor() { - - private static final String WHITEBOX_REPLACED = "whiteboxReplaced"; - private static final String NEEDS_FIELD_IMPORT = "needsFieldImport"; - private static final String NEEDS_METHOD_IMPORT = "needsMethodImport"; - - @Override - public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { - J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); - if (getCursor().getMessage(WHITEBOX_REPLACED, false)) { - md = addThrowsExceptionIfAbsent(md); - maybeRemoveImport(WHITEBOX_FQN); - if (getCursor().getMessage(NEEDS_FIELD_IMPORT, false)) { - maybeAddImport("java.lang.reflect.Field", false); - } - if (getCursor().getMessage(NEEDS_METHOD_IMPORT, false)) { - maybeAddImport("java.lang.reflect.Method", false); - } - return maybeAutoFormat(method, md, ctx); - } - return md; - } - - @Override - public J.Block visitBlock(J.Block block, ExecutionContext ctx) { - J.Block b = super.visitBlock(block, ctx); - - List statements = b.getStatements(); - // Process in reverse so that coordinate positions remain valid after each replacement - for (int i = statements.size() - 1; i >= 0; i--) { - Statement stmt = statements.get(i); - J.MethodInvocation mi = extractWhiteboxInvocation(stmt); - if (mi == null) { - continue; - } - Cursor blockCursor = new Cursor(getCursor().getParentOrThrow(), b); - JavaType.Method resolvedMethod = INVOKE_METHOD.matches(mi) ? - resolveTargetMethod(mi.getArguments()) : null; - String template = buildReplacementTemplate(stmt, mi, blockCursor, resolvedMethod); - if (template != null) { - Object[] templateArgs = buildTemplateArgs(mi, resolvedMethod); - - List templateImports = new ArrayList<>(); - templateImports.add("java.lang.reflect.Field"); - templateImports.add("java.lang.reflect.Method"); - if (resolvedMethod != null) { - for (JavaType paramType : resolvedMethod.getParameterTypes()) { - if (paramType instanceof JavaType.FullyQualified) { - JavaType.FullyQualified fq = (JavaType.FullyQualified) paramType; - if (!"java.lang".equals(fq.getPackageName())) { - templateImports.add(fq.getFullyQualifiedName()); - maybeAddImport(fq.getFullyQualifiedName()); - } - } - } - } - - b = JavaTemplate.builder(template) - .contextSensitive() - .javaParser(JavaParser.fromJavaVersion()) - .imports(templateImports.toArray(new String[0])) - .build() - .apply( - new Cursor(getCursor().getParentOrThrow(), b), - stmt.getCoordinates().replace(), - templateArgs - ); - getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, WHITEBOX_REPLACED, true); - if (SET_INTERNAL_STATE.matches(mi) || GET_INTERNAL_STATE.matches(mi)) { - getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_FIELD_IMPORT, true); - } - if (INVOKE_METHOD.matches(mi)) { - getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_METHOD_IMPORT, true); - } - // Re-read statements list since the block has been rebuilt - statements = b.getStatements(); - } - } - - return b; - } - - private @Nullable String buildReplacementTemplate(Statement statement, J.MethodInvocation mi, - Cursor scope, JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - - if (SET_INTERNAL_STATE.matches(mi) && args.size() == 3) { - return buildSetInternalStateTemplate(args, scope); - } - if (GET_INTERNAL_STATE.matches(mi) && args.size() == 2) { - return buildGetInternalStateTemplate(args, statement, scope); - } - if (INVOKE_METHOD.matches(mi) && args.size() >= 2) { - return buildInvokeMethodTemplate(args, statement, scope, resolvedMethod); - } - return null; - } - - private Object[] buildTemplateArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - - if (SET_INTERNAL_STATE.matches(mi) && args.size() == 3) { - // target, fieldName, target, value - return new Object[]{args.get(0), args.get(1), args.get(0), args.get(2)}; - } - if (GET_INTERNAL_STATE.matches(mi) && args.size() == 2) { - // target, fieldName, target - return new Object[]{args.get(0), args.get(1), args.get(0)}; - } - if (INVOKE_METHOD.matches(mi) && args.size() >= 2) { - return buildInvokeMethodArgs(args, resolvedMethod); - } - return new Object[0]; - } - - private @Nullable String buildSetInternalStateTemplate(List args, Cursor scope) { - String fieldName = extractStringLiteral(args.get(1)); - if (fieldName == null) { - return null; - } - String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER); - return "Field " + varName + " = #{any(java.lang.Object)}.getClass().getDeclaredField(#{any(java.lang.String)});\n" + - varName + ".setAccessible(true);\n" + - varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});"; - } - - private @Nullable String buildGetInternalStateTemplate(List args, Statement statement, Cursor scope) { - String fieldName = extractStringLiteral(args.get(1)); - if (fieldName == null) { - return null; - } - String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER); - String prefix = "Field " + varName + " = #{any(java.lang.Object)}.getClass().getDeclaredField(#{any(java.lang.String)});\n" + - varName + ".setAccessible(true);\n"; - - if (statement instanceof J.VariableDeclarations) { - J.VariableDeclarations varDecls = (J.VariableDeclarations) statement; - String assignToVar = varDecls.getVariables().get(0).getSimpleName(); - String castType = getCastType(varDecls.getType()); - if (castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType)) { - return prefix + castType + " " + assignToVar + " = (" + castType + ") " + varName + ".get(#{any(java.lang.Object)});"; - } - return prefix + "Object " + assignToVar + " = " + varName + ".get(#{any(java.lang.Object)});"; - } - return prefix + varName + ".get(#{any(java.lang.Object)});"; - } - - private @Nullable String buildInvokeMethodTemplate(List args, Statement statement, - Cursor scope, JavaType.@Nullable Method resolvedMethod) { - String methodName = extractStringLiteral(args.get(1)); - if (methodName == null) { - return null; - } - String varName = generateVariableName(methodName + "Method", scope, INCREMENT_NUMBER); - - // getDeclaredMethod line - StringBuilder sb = new StringBuilder(); - sb.append("Method ").append(varName).append(" = #{any(java.lang.Object)}.getClass().getDeclaredMethod(#{any(java.lang.String)}"); - for (int i = 2; i < args.size(); i++) { - String classLiteral = getParamClassLiteral(args, i, resolvedMethod); - if (classLiteral != null) { - sb.append(", ").append(classLiteral); - } else { - sb.append(", #{any(java.lang.Object)}.getClass()"); - } - } - sb.append(");\n"); - - // setAccessible line - sb.append(varName).append(".setAccessible(true);\n"); - - // invoke line - if (statement instanceof J.VariableDeclarations) { - J.VariableDeclarations varDecls = (J.VariableDeclarations) statement; - String assignToVar = varDecls.getVariables().get(0).getSimpleName(); - String castType = getCastType(varDecls.getType()); - if (castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType)) { - sb.append(castType).append(" ").append(assignToVar).append(" = (").append(castType).append(") "); - } else { - sb.append("Object ").append(assignToVar).append(" = "); - } - } - sb.append(varName).append(".invoke(#{any(java.lang.Object)}"); - for (int i = 2; i < args.size(); i++) { - sb.append(", #{any(java.lang.Object)}"); - } - sb.append(");"); - - return sb.toString(); - } - - private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable Method resolvedMethod) { - int extraArgs = args.size() - 2; - int unresolvedCount = 0; - for (int i = 2; i < args.size(); i++) { - if (getParamClassLiteral(args, i, resolvedMethod) == null) { - unresolvedCount++; - } - } - Object[] result = new Object[2 + unresolvedCount + 1 + extraArgs]; - int idx = 0; - result[idx++] = args.get(0); // target for getDeclaredMethod - result[idx++] = args.get(1); // methodName - for (int i = 2; i < args.size(); i++) { - if (getParamClassLiteral(args, i, resolvedMethod) == null) { - result[idx++] = args.get(i); // arg.getClass() fallback for getDeclaredMethod - } - } - result[idx++] = args.get(0); // target for invoke - for (int i = 2; i < args.size(); i++) { - result[idx++] = args.get(i); // arg for invoke - } - return result; - } - - /** - * Get the class literal for a parameter at the given argument index. - * Prefers the resolved method's declared parameter type, falls back to the argument's - * compile-time type, and returns null if neither is available. - */ - private @Nullable String getParamClassLiteral(List args, int argIndex, - JavaType.@Nullable Method resolvedMethod) { - if (resolvedMethod != null) { - String literal = classLiteralFromType(resolvedMethod.getParameterTypes().get(argIndex - 2)); - if (literal != null) { - return literal; - } - } - return getClassLiteral(args.get(argIndex)); - } - - /** - * Resolve the target method from the first argument's type and the method name. - * Returns null if the method cannot be unambiguously resolved (not found, overloaded, - * or missing type information). - */ - private JavaType.@Nullable Method resolveTargetMethod(List args) { - if (args.size() <= 2) { - return null; - } - String methodName = extractStringLiteral(args.get(1)); - if (methodName == null) { - return null; - } - JavaType targetType = args.get(0).getType(); - if (!(targetType instanceof JavaType.FullyQualified)) { - return null; - } - int expectedParamCount = args.size() - 2; - JavaType.Method match = null; - for (JavaType.FullyQualified current = (JavaType.FullyQualified) targetType; - current != null; current = current.getSupertype()) { - for (JavaType.Method method : current.getMethods()) { - if (method.getName().equals(methodName) && - method.getParameterTypes().size() == expectedParamCount) { - if (match != null) { - return null; // ambiguous overload - } - match = method; - } - } - } - return match; - } - - private J.@Nullable MethodInvocation extractWhiteboxInvocation(Statement statement) { - if (statement instanceof J.MethodInvocation) { - J.MethodInvocation mi = (J.MethodInvocation) statement; - if (SET_INTERNAL_STATE.matches(mi) || GET_INTERNAL_STATE.matches(mi) || INVOKE_METHOD.matches(mi)) { - return mi; - } - } - if (statement instanceof J.VariableDeclarations) { - J.VariableDeclarations varDecls = (J.VariableDeclarations) statement; - if (varDecls.getVariables().size() == 1) { - Expression init = varDecls.getVariables().get(0).getInitializer(); - if (init instanceof J.MethodInvocation) { - J.MethodInvocation mi = (J.MethodInvocation) init; - if (GET_INTERNAL_STATE.matches(mi) || INVOKE_METHOD.matches(mi)) { - return mi; - } - } - } - } - return null; - } - - private @Nullable String extractStringLiteral(Expression expr) { - if (expr instanceof J.Literal && ((J.Literal) expr).getValue() instanceof String) { - return (String) ((J.Literal) expr).getValue(); - } - return null; - } - - private @Nullable String getClassLiteral(Expression expr) { - return classLiteralFromType(expr.getType()); - } - - private @Nullable String classLiteralFromType(@Nullable JavaType type) { - if (type instanceof JavaType.Primitive) { - return ((JavaType.Primitive) type).getKeyword() + ".class"; - } - if (type instanceof JavaType.FullyQualified) { - return ((JavaType.FullyQualified) type).getClassName() + ".class"; - } - return null; - } - - private @Nullable String getCastType(@Nullable JavaType type) { - if (type instanceof JavaType.FullyQualified) { - return ((JavaType.FullyQualified) type).getClassName(); - } - if (type instanceof JavaType.Primitive) { - return ((JavaType.Primitive) type).getKeyword(); - } - return null; - } - - private J.MethodDeclaration addThrowsExceptionIfAbsent(J.MethodDeclaration md) { - if (md.getThrows() != null && md.getThrows().stream() - .anyMatch(j -> TypeUtils.isOfClassType(j.getType(), "java.lang.Exception") || - TypeUtils.isOfClassType(j.getType(), "java.lang.Throwable"))) { - return md; - } - JavaType.Class exceptionType = JavaType.ShallowClass.build("java.lang.Exception"); - return md.withThrows(ListUtils.concat(md.getThrows(), - new J.Identifier(randomId(), Space.SINGLE_SPACE, Markers.EMPTY, emptyList(), - exceptionType.getClassName(), exceptionType, null))); - } - }); - } -} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java b/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java new file mode 100644 index 000000000..7401d7d0b --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java @@ -0,0 +1,396 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static org.openrewrite.Tree.randomId; +import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; +import static org.openrewrite.java.VariableNameUtils.generateVariableName; + +/** + * Shared machinery for replacing a single {@code org.powermock.reflect.Whitebox} API family with + * plain Java reflection: extracts migratable Whitebox calls from block statements, applies the + * family's replacement template, and maintains the surrounding method declaration + * ({@code throws Exception}, imports, formatting). Subclasses configure the family's matchers and + * required {@code java.lang.reflect} import via the constructor and supply the template and its + * arguments. + */ +abstract class WhiteboxToReflectionVisitor extends JavaIsoVisitor { + + static final String WHITEBOX_FQN = "org.powermock.reflect.Whitebox"; + + private static final String WHITEBOX_REPLACED = "whiteboxReplaced"; + + private static final Map BOXED_TYPES = new HashMap<>(); + + static { + BOXED_TYPES.put("int", "Integer"); + BOXED_TYPES.put("long", "Long"); + BOXED_TYPES.put("double", "Double"); + BOXED_TYPES.put("float", "Float"); + BOXED_TYPES.put("boolean", "Boolean"); + BOXED_TYPES.put("byte", "Byte"); + BOXED_TYPES.put("short", "Short"); + BOXED_TYPES.put("char", "Character"); + } + + private final String reflectiveImport; + private final List matchers; + + WhiteboxToReflectionVisitor(String reflectiveImport, MethodMatcher... matchers) { + this.reflectiveImport = reflectiveImport; + this.matchers = Arrays.asList(matchers); + } + + /** + * Where a result-producing reflection call stores its result. {@code varName} is the declared + * variable receiving the value, or null when the result is discarded (the call was a bare + * statement); {@code castType} is that variable's declared type. + */ + static final class ResultSink { + final @Nullable String varName; + final @Nullable String castType; + + private ResultSink(@Nullable String varName, @Nullable String castType) { + this.varName = varName; + this.castType = castType; + } + } + + /** + * The replacement template for the matched call, or null when the call cannot be mechanically + * migrated and must be left unchanged. + */ + abstract @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod); + + /** + * The template arguments matching {@link #buildTemplate}'s placeholders. + */ + abstract Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod); + + /** + * The target method/constructor the call reflects on, when it can be unambiguously resolved; + * used to derive declared parameter types for class literals. + */ + JavaType.@Nullable Method resolve(J.MethodInvocation mi) { + return null; + } + + /** + * This visitor gated so that only source files calling one of the configured Whitebox methods + * are traversed. + */ + TreeVisitor withPrecondition() { + TreeVisitor precondition = new UsesMethod<>(matchers.get(0)); + for (int i = 1; i < matchers.size(); i++) { + precondition = Preconditions.or(precondition, new UsesMethod<>(matchers.get(i))); + } + return Preconditions.check(precondition, this); + } + + private boolean matches(J.MethodInvocation mi) { + for (MethodMatcher matcher : matchers) { + if (matcher.matches(mi)) { + return true; + } + } + return false; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); + if (getCursor().getMessage(WHITEBOX_REPLACED, false)) { + md = addThrowsExceptionIfAbsent(md); + maybeRemoveImport(WHITEBOX_FQN); + maybeAddImport(reflectiveImport, false); + return maybeAutoFormat(method, md, ctx); + } + return md; + } + + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + J.Block b = super.visitBlock(block, ctx); + + // Replace Whitebox calls that are a statement or single-variable declaration initializer, in + // reverse so coordinates stay valid: each JavaTemplate.apply rebuilds the block, letting + // generateVariableName see prior locals (ListUtils.flatMap would need manual name dedup). + List statements = b.getStatements(); + for (int i = statements.size() - 1; i >= 0; i--) { + Statement stmt = statements.get(i); + J.MethodInvocation mi = extractWhiteboxInvocation(stmt); + if (mi == null) { + continue; + } + Cursor blockCursor = new Cursor(getCursor().getParentOrThrow(), b); + JavaType.Method resolvedMethod = resolve(mi); + String template = buildTemplate(mi, sinkFromStatement(stmt), blockCursor, resolvedMethod); + if (template != null) { + b = JavaTemplate.builder(template) + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion()) + .imports(templateImports(resolvedMethod).toArray(new String[0])) + .build() + .apply( + new Cursor(getCursor().getParentOrThrow(), b), + stmt.getCoordinates().replace(), + buildArgs(mi, resolvedMethod) + ); + recordReplacement(resolvedMethod); + // Re-read statements list since the block has been rebuilt + statements = b.getStatements(); + } + } + + return b; + } + + private void recordReplacement(JavaType.@Nullable Method resolvedMethod) { + getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, WHITEBOX_REPLACED, true); + for (String paramImport : resolvedParamImports(resolvedMethod)) { + maybeAddImport(paramImport); + } + } + + private List templateImports(JavaType.@Nullable Method resolvedMethod) { + List imports = new ArrayList<>(); + imports.add(reflectiveImport); + imports.addAll(resolvedParamImports(resolvedMethod)); + return imports; + } + + // Non-java.lang fully-qualified parameter types of the resolved method/constructor that the + // generated class literals (e.g. `List.class`) need imported. + private List resolvedParamImports(JavaType.@Nullable Method resolvedMethod) { + if (resolvedMethod == null) { + return emptyList(); + } + List imports = new ArrayList<>(); + for (JavaType paramType : resolvedMethod.getParameterTypes()) { + JavaType.FullyQualified fq = TypeUtils.asFullyQualified(paramType); + if (fq != null && !"java.lang".equals(fq.getPackageName())) { + imports.add(fq.getFullyQualifiedName()); + } + } + return imports; + } + + private ResultSink sinkFromStatement(Statement statement) { + if (statement instanceof J.VariableDeclarations) { + J.VariableDeclarations vd = (J.VariableDeclarations) statement; + return new ResultSink(vd.getVariables().get(0).getSimpleName(), getCastType(vd.getType())); + } + return new ResultSink(null, null); + } + + private J.@Nullable MethodInvocation extractWhiteboxInvocation(Statement statement) { + if (statement instanceof J.MethodInvocation) { + J.MethodInvocation mi = (J.MethodInvocation) statement; + if (matches(mi)) { + return mi; + } + } + if (statement instanceof J.VariableDeclarations) { + J.VariableDeclarations varDecls = (J.VariableDeclarations) statement; + if (varDecls.getVariables().size() == 1) { + Expression init = varDecls.getVariables().get(0).getInitializer(); + if (init instanceof J.MethodInvocation) { + J.MethodInvocation mi = (J.MethodInvocation) init; + if (matches(mi) && !returnsVoid(mi)) { + return mi; + } + } + } + } + return null; + } + + // A void Whitebox call (e.g. setInternalState) cannot produce a variable declaration initializer. + private boolean returnsVoid(J.MethodInvocation mi) { + return mi.getMethodType() != null && JavaType.Primitive.Void == mi.getMethodType().getReturnType(); + } + + // `Field = .getDeclaredField(); .setAccessible(true);` — shared by the + // instance (receiver `obj.getClass()`) and `Class where` (receiver the where-class) get/set variants. + String fieldLookupPrefix(String varName, String fieldReceiver) { + return "Field " + varName + " = " + fieldReceiver + ".getDeclaredField(#{any(java.lang.String)});\n" + + varName + ".setAccessible(true);\n"; + } + + // True when castType denotes a meaningful type to cast to (i.e. not null and not Object). + boolean isNonObjectCast(@Nullable String castType) { + return castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType); + } + + /** + * For calls whose result type IS the reflective object ({@code getField}/{@code getMethod}), + * reuse the result variable as the local; otherwise generate one. + */ + String resultLocalName(ResultSink sink, Expression nameExpr, Cursor scope, boolean field) { + if (sink.varName != null) { + return sink.varName; + } + return field ? fieldVarName(nameExpr, scope) : methodVarName(nameExpr, scope); + } + + boolean hasArrayArg(List args, int fromIndex) { + for (int i = fromIndex; i < args.size(); i++) { + if (TypeUtils.asArray(args.get(i).getType()) != null) { + return true; + } + } + return false; + } + + /** + * Get the class literal for a parameter at the given argument index, where the first declared + * parameter is at argument index 2 (target, name, params...). + */ + @Nullable String getParamClassLiteral(List args, int argIndex, + JavaType.@Nullable Method resolvedMethod) { + return getParamClassLiteral(args, argIndex, resolvedMethod, 2); + } + + /** + * Get the class literal for a parameter, where {@code firstParamArgIndex} is the argument index of + * the first declared parameter (2 for {@code invokeMethod}: target, name, params...; 1 for + * {@code invokeConstructor}: class, params...). Prefers the resolved method's declared parameter + * type, falls back to the argument's compile-time type, and returns null if neither is available. + */ + @Nullable String getParamClassLiteral(List args, int argIndex, + JavaType.@Nullable Method resolvedMethod, int firstParamArgIndex) { + if (resolvedMethod != null) { + int paramIdx = argIndex - firstParamArgIndex; + List paramTypes = resolvedMethod.getParameterTypes(); + if (paramIdx >= 0 && paramIdx < paramTypes.size()) { + String literal = classLiteralFromType(paramTypes.get(paramIdx)); + if (literal != null) { + return literal; + } + } + } + return getClassLiteral(args.get(argIndex)); + } + + /** + * Extract the element type {@code X} from a {@code Class}-typed expression (e.g. {@code MyService.class}). + */ + JavaType.@Nullable FullyQualified classLiteralElementType(Expression classExpr) { + JavaType.Parameterized parameterized = TypeUtils.asParameterized(classExpr.getType()); + if (parameterized != null && !parameterized.getTypeParameters().isEmpty()) { + return TypeUtils.asFullyQualified(parameterized.getTypeParameters().get(0)); + } + return null; + } + + /** + * Generate the local variable name for a reflective {@code Field}. When the field name is a + * String literal we derive a readable name (e.g. {@code nameField}); otherwise we fall back to + * a generic {@code reflectField} base. Uniqueness within scope is guaranteed by INCREMENT_NUMBER. + */ + String fieldVarName(Expression nameExpr, Cursor scope) { + return reflectVarName(nameExpr, "Field", "reflectField", scope); + } + + /** + * Generate the local variable name for a reflective {@code Method}. See {@link #fieldVarName}. + */ + String methodVarName(Expression nameExpr, Cursor scope) { + return reflectVarName(nameExpr, "Method", "reflectMethod", scope); + } + + // Derive a unique local name from a String-literal name (`name` + suffix, e.g. `nameField`), + // falling back to a generic base when the name is not a literal. + private String reflectVarName(Expression nameExpr, String suffix, String fallbackBase, Cursor scope) { + String literal = extractStringLiteral(nameExpr); + String base = literal != null ? literal + suffix : fallbackBase; + return generateVariableName(base, scope, INCREMENT_NUMBER); + } + + @Nullable String extractStringLiteral(Expression expr) { + if (expr instanceof J.Literal && ((J.Literal) expr).getValue() instanceof String) { + return (String) ((J.Literal) expr).getValue(); + } + return null; + } + + private @Nullable String getClassLiteral(Expression expr) { + return classLiteralFromType(expr.getType()); + } + + private @Nullable String classLiteralFromType(@Nullable JavaType type) { + if (type instanceof JavaType.Primitive) { + return ((JavaType.Primitive) type).getKeyword() + ".class"; + } + if (type instanceof JavaType.FullyQualified) { + return ((JavaType.FullyQualified) type).getClassName() + ".class"; + } + return null; + } + + private @Nullable String getCastType(@Nullable JavaType type) { + if (type instanceof JavaType.FullyQualified) { + return ((JavaType.FullyQualified) type).getClassName(); + } + if (type instanceof JavaType.Primitive) { + return ((JavaType.Primitive) type).getKeyword(); + } + return null; + } + + /** + * {@code Field.get}/{@code Method.invoke} return {@code Object} (boxing primitives), so a primitive + * declared type must be cast to its wrapper (a direct {@code (int) object} cast does not compile); + * the surrounding assignment then auto-unboxes. + */ + String boxedCastType(String castType) { + return BOXED_TYPES.getOrDefault(castType, castType); + } + + private J.MethodDeclaration addThrowsExceptionIfAbsent(J.MethodDeclaration md) { + if (md.getThrows() != null && md.getThrows().stream() + .anyMatch(j -> TypeUtils.isOfClassType(j.getType(), "java.lang.Exception") || + TypeUtils.isOfClassType(j.getType(), "java.lang.Throwable"))) { + return md; + } + JavaType.Class exceptionType = JavaType.ShallowClass.build("java.lang.Exception"); + return md.withThrows(ListUtils.concat(md.getThrows(), + new J.Identifier(randomId(), Space.SINGLE_SPACE, Markers.EMPTY, emptyList(), + exceptionType.getClassName(), exceptionType, null))); + } +} diff --git a/src/main/resources/META-INF/rewrite/powermockito.yml b/src/main/resources/META-INF/rewrite/powermockito.yml index c45e62ff7..07df7ed02 100644 --- a/src/main/resources/META-INF/rewrite/powermockito.yml +++ b/src/main/resources/META-INF/rewrite/powermockito.yml @@ -59,3 +59,23 @@ recipeList: - org.openrewrite.maven.RemoveManagedDependency: groupId: org.powermock artifactId: powermock* +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection +displayName: Replace PowerMock `Whitebox` with Java reflection +description: >- + Replace `org.powermock.reflect.Whitebox` calls (`setInternalState`, `getInternalState`, `invokeMethod`, + static `invokeMethod`, `invokeConstructor`, `getField` and `getMethod`, including the `Class where` + overloads) with plain Java reflection using `java.lang.reflect.Field`, `Method` and `Constructor`. + Field/constructor lookups use `getDeclaredField`/`getDeclaredConstructor` on the named class, which + differs from PowerMock for members inherited from a superclass. +tags: + - testing + - mockito +recipeList: + - org.openrewrite.java.testing.mockito.PowerMockWhiteboxSetInternalStateToJavaReflection + - org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetInternalStateToJavaReflection + - org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetFieldToJavaReflection + - org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetMethodToJavaReflection + - org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeMethodToJavaReflection + - org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeConstructorToJavaReflection diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index c31a59384..6a2142939 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -201,7 +201,13 @@ maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.tes 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. Note that the `@Mock` fields will then be initialized by the strict mocking session of the extension or runner; tests that relied on the lenient mocks created by an explicit `openMocks(this)` call inside `@BeforeEach` may surface `UnnecessaryStubbingException`. Add `@MockitoSettings(strictness = Strictness.LENIENT)` to opt out.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.AddMockitoSettingsWithWarnStrictness,Add `@MockitoSettings(strictness = Strictness.WARN)` to `@ExtendWith(MockitoExtension.class)` classes,Adds `@MockitoSettings(strictness = Strictness.WARN)` to test classes that have `@ExtendWith(MockitoExtension.class)` but do not already have a `@MockitoSettings` annotation. This preserves the lenient stubbing behavior from Mockito 1.x/2.x migrations and prevents `UnnecessaryStubbingException` from strict stubbing defaults.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.VerifyZeroToNoMoreInteractions,Replace `verifyZeroInteractions()` with `verifyNoMoreInteractions()`,Replaces `verifyZeroInteractions()` with `verifyNoMoreInteractions()` in Mockito tests when migration when using a Mockito version < 3.x.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -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.,, +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`, static `invokeMethod`, `invokeConstructor`, `getField` and `getMethod`, including the `Class where` overloads) with plain Java reflection using `java.lang.reflect.Field`, `Method` and `Constructor`. Field/constructor lookups use `getDeclaredField`/`getDeclaredConstructor` on the named class, which differs from PowerMock for members inherited from a superclass.",7,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxSetInternalStateToJavaReflection,Replace PowerMock `Whitebox.setInternalState()` with Java reflection,"Replace `Whitebox.setInternalState(Object, String, Object)` and the `Class where` overload with `java.lang.reflect.Field` access. The field lookup uses `getDeclaredField` on the target object's class (or the `where` class), which differs from PowerMock's class-hierarchy traversal for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetInternalStateToJavaReflection,Replace PowerMock `Whitebox.getInternalState()` with Java reflection,"Replace `Whitebox.getInternalState(Object, String)` and the `Class where` overload with `java.lang.reflect.Field` access, casting to the declared result type where needed. The field lookup uses `getDeclaredField` on the target object's class (or the `where` class), which differs from PowerMock's class-hierarchy traversal for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetFieldToJavaReflection,Replace PowerMock `Whitebox.getField()` with Java reflection,"Replace `Whitebox.getField(Class, String)` with `Class.getDeclaredField(String)` plus `setAccessible(true)`. Unlike PowerMock, `getDeclaredField` does not traverse the class hierarchy for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetMethodToJavaReflection,Replace PowerMock `Whitebox.getMethod()` with Java reflection,"Replace `Whitebox.getMethod(Class, String, Class...)` with `Class.getDeclaredMethod(String, Class...)` plus `setAccessible(true)`. Unlike PowerMock, `getDeclaredMethod` does not traverse the class hierarchy; calls passing an explicit `Class[]` array are left unchanged for manual migration.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeMethodToJavaReflection,Replace PowerMock `Whitebox.invokeMethod()` with Java reflection,"Replace instance and static `Whitebox.invokeMethod(..)` calls with `java.lang.reflect.Method` lookup and `invoke()`. Parameter types are taken from the unambiguously resolved target method, falling back to each argument's compile-time class; calls passing an explicit `Class[]` array are left unchanged for manual migration.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeConstructorToJavaReflection,Replace PowerMock `Whitebox.invokeConstructor()` with Java reflection,"Replace `Whitebox.invokeConstructor(..)` with `java.lang.reflect.Constructor` lookup and `newInstance()` on the named class. Constructor parameter types are taken from the unambiguously resolved constructor, falling back to each argument's compile-time class; arrays passed to the `Object...` varargs overload are left unchanged for manual migration.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.ArgumentMatcherToLambda,Convert `ArgumentMatcher` anonymous class to lambda,"Converts anonymous `ArgumentMatcher` implementations with `matches(Object)` to lambda expressions with the correct parameter type. In Mockito 1.x, `ArgumentMatcher` extended Hamcrest's `BaseMatcher` and `matches` always took `Object`. In Mockito 2+, `ArgumentMatcher` is a functional interface where `matches` takes `T`.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.MockConstructionToTryWithResources,Wrap `MockedConstruction` in try-with-resources,"Wraps `MockedConstruction` variable declarations that have explicit `.close()` calls into try-with-resources blocks, removing the explicit close call. This ensures proper resource management and makes the code cleaner.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.RemoveDoNothingForDefaultMocks,Remove `doNothing()` for void methods on `@Mock` fields,"Remove unnecessary `doNothing()` stubbings for void methods on `@Mock` fields. Mockito mocks already do nothing for void methods by default, making these stubbings redundant and triggering strict stubbing violations in Mockito 3+.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java new file mode 100644 index 000000000..beff0a14f --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class PowerMockWhiteboxGetFieldToJavaReflectionTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpathFromResources(new InMemoryExecutionContext(), + "powermock-core-1", + "powermock-reflect-1" + )) + .recipe(new PowerMockWhiteboxGetFieldToJavaReflection()); + } + + @DocumentExample + @Test + void getFieldReturnsFieldVariable() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import java.lang.reflect.Field; + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Field f = Whitebox.getField(MyService.class, "name"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void test() throws Exception { + Field f = MyService.class.getDeclaredField("name"); + f.setAccessible(true); + } + } + """ + ) + ); + } + + @Test + void noChangeForOtherWhiteboxApis() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + MyService service = new MyService(); + Whitebox.setInternalState(service, "name", "expectedValue"); + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java new file mode 100644 index 000000000..1b1877839 --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java @@ -0,0 +1,240 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class PowerMockWhiteboxGetInternalStateToJavaReflectionTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpathFromResources(new InMemoryExecutionContext(), + "powermock-core-1", + "powermock-reflect-1" + )) + .recipe(new PowerMockWhiteboxGetInternalStateToJavaReflection()); + } + + @DocumentExample + @Test + void getInternalStateWithAssignment() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name = "hello"; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testGetField() { + MyService service = new MyService(); + String result = Whitebox.getInternalState(service, "name"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testGetField() throws Exception { + MyService service = new MyService(); + Field nameField = service.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + String result = (String) nameField.get(service); + } + } + """ + ) + ); + } + + @Test + void getInternalStateNonLiteralFieldName() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name = "hello"; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testGetField(String fieldName) { + MyService service = new MyService(); + String result = Whitebox.getInternalState(service, fieldName); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testGetField(String fieldName) throws Exception { + MyService service = new MyService(); + Field reflectField = service.getClass().getDeclaredField(fieldName); + reflectField.setAccessible(true); + String result = (String) reflectField.get(service); + } + } + """ + ) + ); + } + + @Test + void twoNonLiteralGetInternalStateInSameBlock() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name = "hello"; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test(String f1, String f2) { + MyService service = new MyService(); + String a = Whitebox.getInternalState(service, f1); + String b = Whitebox.getInternalState(service, f2); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void test(String f1, String f2) throws Exception { + MyService service = new MyService(); + Field reflectField1 = service.getClass().getDeclaredField(f1); + reflectField1.setAccessible(true); + String a = (String) reflectField1.get(service); + Field reflectField = service.getClass().getDeclaredField(f2); + reflectField.setAccessible(true); + String b = (String) reflectField.get(service); + } + } + """ + ) + ); + } + + @Test + void getInternalStateWithWhereClass() { + //language=java + rewriteRun( + java( + """ + class Parent { + private String name = "hello"; + } + """ + ), + java( + """ + class Child extends Parent { + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Child child = new Child(); + String n = Whitebox.getInternalState(child, "name", Parent.class); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void test() throws Exception { + Child child = new Child(); + Field nameField = Parent.class.getDeclaredField("name"); + nameField.setAccessible(true); + String n = (String) nameField.get(child); + } + } + """ + ) + ); + } + + @Test + void getInternalStatePrimitiveResultUsesBoxedCast() { + //language=java + rewriteRun( + java( + """ + class MyService { + private int count = 3; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + MyService service = new MyService(); + int count = Whitebox.getInternalState(service, "count"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void test() throws Exception { + MyService service = new MyService(); + Field countField = service.getClass().getDeclaredField("count"); + countField.setAccessible(true); + int count = (Integer) countField.get(service); + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java new file mode 100644 index 000000000..d34572c0d --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class PowerMockWhiteboxGetMethodToJavaReflectionTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpathFromResources(new InMemoryExecutionContext(), + "powermock-core-1", + "powermock-reflect-1" + )) + .recipe(new PowerMockWhiteboxGetMethodToJavaReflection()); + } + + @DocumentExample + @Test + void getMethodWithParamType() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String greet(String name) { return "Hello " + name; } + } + """ + ), + java( + """ + import java.lang.reflect.Method; + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Method m = Whitebox.getMethod(MyService.class, "greet", String.class); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + Method m = MyService.class.getDeclaredMethod("greet", String.class); + m.setAccessible(true); + } + } + """ + ) + ); + } + + @Test + void getMethodNoParamTypes() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String compute() { return "result"; } + } + """ + ), + java( + """ + import java.lang.reflect.Method; + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Method m = Whitebox.getMethod(MyService.class, "compute"); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + Method m = MyService.class.getDeclaredMethod("compute"); + m.setAccessible(true); + } + } + """ + ) + ); + } + + @Test + void getMethodPrimitiveParamType() { + //language=java + rewriteRun( + java( + """ + class MyService { + private int doubleIt(int value) { return value * 2; } + } + """ + ), + java( + """ + import java.lang.reflect.Method; + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Method m = Whitebox.getMethod(MyService.class, "doubleIt", int.class); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + Method m = MyService.class.getDeclaredMethod("doubleIt", int.class); + m.setAccessible(true); + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java new file mode 100644 index 000000000..3e7b8180f --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java @@ -0,0 +1,221 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; + +import static org.openrewrite.java.Assertions.java; + +class PowerMockWhiteboxInvokeConstructorToJavaReflectionTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpathFromResources(new InMemoryExecutionContext(), + "powermock-core-1", + "powermock-reflect-1" + )) + .recipe(new PowerMockWhiteboxInvokeConstructorToJavaReflection()); + } + + @DocumentExample + @Test + void invokeConstructorNoArgs() { + //language=java + rewriteRun( + // The user type used as a generic/result type is generated by the template and cannot be + // attributed by the isolated template parser (a test-only artifact); the source is valid Java. + spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), + java( + """ + class MyService { + private MyService() { + } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + MyService s = Whitebox.invokeConstructor(MyService.class); + } + } + """, + """ + import java.lang.reflect.Constructor; + + class MyServiceTest { + void test() throws Exception { + Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(); + myServiceConstructor.setAccessible(true); + MyService s = myServiceConstructor.newInstance(); + } + } + """ + ) + ); + } + + @Test + void invokeConstructorWithResolvedParamTypes() { + //language=java + rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), + java( + """ + class MyService { + private MyService(String name, int age) { + } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + MyService s = Whitebox.invokeConstructor(MyService.class, "Alice", 42); + } + } + """, + """ + import java.lang.reflect.Constructor; + + class MyServiceTest { + void test() throws Exception { + Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(String.class, int.class); + myServiceConstructor.setAccessible(true); + MyService s = myServiceConstructor.newInstance("Alice", 42); + } + } + """ + ) + ); + } + + @Test + void invokeConstructorExplicitParamTypes() { + //language=java + rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), + java( + """ + class MyService { + private MyService(String name, int age) { + } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + MyService s = Whitebox.invokeConstructor(MyService.class, new Class[]{String.class, int.class}, new Object[]{"Alice", 42}); + } + } + """, + """ + import java.lang.reflect.Constructor; + + class MyServiceTest { + void test() throws Exception { + Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(String.class, int.class); + myServiceConstructor.setAccessible(true); + MyService s = myServiceConstructor.newInstance("Alice", 42); + } + } + """ + ) + ); + } + + @Test + void invokeConstructorAsStatement() { + //language=java + rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), + java( + """ + class MyService { + private MyService(String name) { + } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Whitebox.invokeConstructor(MyService.class, "Alice"); + } + } + """, + """ + import java.lang.reflect.Constructor; + + class MyServiceTest { + void test() throws Exception { + Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(String.class); + myServiceConstructor.setAccessible(true); + myServiceConstructor.newInstance("Alice"); + } + } + """ + ) + ); + } + + @Test + void invokeConstructorVarargsArrayNotMigrated() { + //language=java + rewriteRun( + java( + """ + class MyService { + private MyService(String a, String b) { + } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() throws Exception { + MyService s = Whitebox.invokeConstructor(MyService.class, new Object[]{"a", "b"}); + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java new file mode 100644 index 000000000..e9b4650b9 --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java @@ -0,0 +1,536 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class PowerMockWhiteboxInvokeMethodToJavaReflectionTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpathFromResources(new InMemoryExecutionContext(), + "powermock-core-1", + "powermock-reflect-1" + )) + .recipe(new PowerMockWhiteboxInvokeMethodToJavaReflection()); + } + + @DocumentExample + @Test + void invokeMethodNoArgs() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String compute() { return "result"; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testInvoke() { + MyService service = new MyService(); + String result = Whitebox.invokeMethod(service, "compute"); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void testInvoke() throws Exception { + MyService service = new MyService(); + Method computeMethod = service.getClass().getDeclaredMethod("compute"); + computeMethod.setAccessible(true); + String result = (String) computeMethod.invoke(service); + } + } + """ + ) + ); + } + + @Test + void invokeMethodWithArgs() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String greet(String name) { return "Hello " + name; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testInvokeWithArgs() { + MyService service = new MyService(); + String result = Whitebox.invokeMethod(service, "greet", "World"); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void testInvokeWithArgs() throws Exception { + MyService service = new MyService(); + Method greetMethod = service.getClass().getDeclaredMethod("greet", String.class); + greetMethod.setAccessible(true); + String result = (String) greetMethod.invoke(service, "World"); + } + } + """ + ) + ); + } + + @Test + void invokeMethodWithMultipleArgs() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String combine(String a, String b) { return a + b; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testInvokeWithMultipleArgs() { + MyService service = new MyService(); + String result = Whitebox.invokeMethod(service, "combine", "Hello", "World"); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void testInvokeWithMultipleArgs() throws Exception { + MyService service = new MyService(); + Method combineMethod = service.getClass().getDeclaredMethod("combine", String.class, String.class); + combineMethod.setAccessible(true); + String result = (String) combineMethod.invoke(service, "Hello", "World"); + } + } + """ + ) + ); + } + + @Test + void invokeMethodWithPrimitiveArg() { + //language=java + rewriteRun( + java( + """ + class MyService { + private int doubleIt(int value) { return value * 2; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testInvokeWithPrimitive() { + MyService service = new MyService(); + Whitebox.invokeMethod(service, "doubleIt", 5); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void testInvokeWithPrimitive() throws Exception { + MyService service = new MyService(); + Method doubleItMethod = service.getClass().getDeclaredMethod("doubleIt", int.class); + doubleItMethod.setAccessible(true); + doubleItMethod.invoke(service, 5); + } + } + """ + ) + ); + } + + @Test + void invokeMethodWithConcreteArgButInterfaceParam() { + //language=java + rewriteRun( + java( + """ + import java.util.List; + + class MyService { + private String process(List items) { return items.toString(); } + } + """ + ), + java( + """ + import java.util.ArrayList; + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testInvokeWithConcreteArg() { + MyService service = new MyService(); + ArrayList items = new ArrayList<>(); + String result = Whitebox.invokeMethod(service, "process", items); + } + } + """, + """ + import java.lang.reflect.Method; + import java.util.ArrayList; + import java.util.List; + + class MyServiceTest { + void testInvokeWithConcreteArg() throws Exception { + MyService service = new MyService(); + ArrayList items = new ArrayList<>(); + Method processMethod = service.getClass().getDeclaredMethod("process", List.class); + processMethod.setAccessible(true); + String result = (String) processMethod.invoke(service, items); + } + } + """ + ) + ); + } + + @Test + void invokeMethodWithInterfaceTypedArg() { + //language=java + rewriteRun( + java( + """ + import java.util.List; + + class MyService { + private String process(List items) { return items.toString(); } + } + """ + ), + java( + """ + import java.util.ArrayList; + import java.util.List; + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testInvokeWithInterfaceArg() { + MyService service = new MyService(); + List items = new ArrayList<>(); + String result = Whitebox.invokeMethod(service, "process", items); + } + } + """, + """ + import java.lang.reflect.Method; + import java.util.ArrayList; + import java.util.List; + + class MyServiceTest { + void testInvokeWithInterfaceArg() throws Exception { + MyService service = new MyService(); + List items = new ArrayList<>(); + Method processMethod = service.getClass().getDeclaredMethod("process", List.class); + processMethod.setAccessible(true); + String result = (String) processMethod.invoke(service, items); + } + } + """ + ) + ); + } + + @Test + void invokeMethodNonLiteralMethodName() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String compute() { return "result"; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testInvoke(String methodName) { + MyService service = new MyService(); + Whitebox.invokeMethod(service, methodName); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void testInvoke(String methodName) throws Exception { + MyService service = new MyService(); + Method reflectMethod = service.getClass().getDeclaredMethod(methodName); + reflectMethod.setAccessible(true); + reflectMethod.invoke(service); + } + } + """ + ) + ); + } + + @Test + void invokeStaticMethodNoArgs() { + //language=java + rewriteRun( + java( + """ + class MyService { + private static String compute() { return "result"; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + String r = Whitebox.invokeMethod(MyService.class, "compute"); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + Method computeMethod = MyService.class.getDeclaredMethod("compute"); + computeMethod.setAccessible(true); + String r = (String) computeMethod.invoke(null); + } + } + """ + ) + ); + } + + @Test + void invokeStaticMethodWithPrimitiveArg() { + //language=java + rewriteRun( + java( + """ + class MyService { + private static String compute(int value) { return "" + value; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + String r = Whitebox.invokeMethod(MyService.class, "compute", 5); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + Method computeMethod = MyService.class.getDeclaredMethod("compute", int.class); + computeMethod.setAccessible(true); + String r = (String) computeMethod.invoke(null, 5); + } + } + """ + ) + ); + } + + @Test + void invokeStaticMethodAsStatement() { + //language=java + rewriteRun( + java( + """ + class MyService { + private static void doStuff(int value) { } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Whitebox.invokeMethod(MyService.class, "doStuff", 5); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + Method doStuffMethod = MyService.class.getDeclaredMethod("doStuff", int.class); + doStuffMethod.setAccessible(true); + doStuffMethod.invoke(null, 5); + } + } + """ + ) + ); + } + + @Test + void instanceAndStaticInvokeMethodInSameBlock() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String instanceCompute() { return "i"; } + private static String staticCompute() { return "s"; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + MyService service = new MyService(); + String a = Whitebox.invokeMethod(service, "instanceCompute"); + String b = Whitebox.invokeMethod(MyService.class, "staticCompute"); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + MyService service = new MyService(); + Method instanceComputeMethod = service.getClass().getDeclaredMethod("instanceCompute"); + instanceComputeMethod.setAccessible(true); + String a = (String) instanceComputeMethod.invoke(service); + Method staticComputeMethod = MyService.class.getDeclaredMethod("staticCompute"); + staticComputeMethod.setAccessible(true); + String b = (String) staticComputeMethod.invoke(null); + } + } + """ + ) + ); + } + + @Test + void invokeMethodPrimitiveResultUsesBoxedCast() { + //language=java + rewriteRun( + java( + """ + class MyService { + private int compute() { return 42; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + MyService service = new MyService(); + int r = Whitebox.invokeMethod(service, "compute"); + } + } + """, + """ + import java.lang.reflect.Method; + + class MyServiceTest { + void test() throws Exception { + MyService service = new MyService(); + Method computeMethod = service.getClass().getDeclaredMethod("compute"); + computeMethod.setAccessible(true); + int r = (Integer) computeMethod.invoke(service); + } + } + """ + ) + ); + } + + @Test + void invokeMethodExplicitParamTypesNotMigrated() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String greet(String name) { return name; } + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() throws Exception { + MyService service = new MyService(); + String r = Whitebox.invokeMethod(service, "greet", new Class[]{String.class}, new Object[]{"World"}); + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java new file mode 100644 index 000000000..614611144 --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java @@ -0,0 +1,322 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class PowerMockWhiteboxSetInternalStateToJavaReflectionTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(true) + .classpathFromResources(new InMemoryExecutionContext(), + "powermock-core-1", + "powermock-reflect-1" + )) + .recipe(new PowerMockWhiteboxSetInternalStateToJavaReflection()); + } + + @DocumentExample + @Test + void setInternalStateReplacedWithReflection() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testSetField() { + MyService service = new MyService(); + Whitebox.setInternalState(service, "name", "expectedValue"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testSetField() throws Exception { + MyService service = new MyService(); + Field nameField = service.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(service, "expectedValue"); + } + } + """ + ) + ); + } + + @Test + void throwsExceptionNotDuplicatedWhenAlreadyPresent() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testSetField() throws Exception { + MyService service = new MyService(); + Whitebox.setInternalState(service, "name", "expectedValue"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testSetField() throws Exception { + MyService service = new MyService(); + Field nameField = service.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(service, "expectedValue"); + } + } + """ + ) + ); + } + + @Test + void whiteboxInsideIfBlock() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testSetFieldConditionally(boolean condition) { + MyService service = new MyService(); + if (condition) { + Whitebox.setInternalState(service, "name", "expectedValue"); + } + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testSetFieldConditionally(boolean condition) throws Exception { + MyService service = new MyService(); + if (condition) { + Field nameField = service.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(service, "expectedValue"); + } + } + } + """ + ) + ); + } + + @Test + void multipleWhiteboxCallsSameFieldName() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testSetFieldTwice() { + MyService svc1 = new MyService(); + MyService svc2 = new MyService(); + Whitebox.setInternalState(svc1, "name", "first"); + Whitebox.setInternalState(svc2, "name", "second"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testSetFieldTwice() throws Exception { + MyService svc1 = new MyService(); + MyService svc2 = new MyService(); + Field nameField1 = svc1.getClass().getDeclaredField("name"); + nameField1.setAccessible(true); + nameField1.set(svc1, "first"); + Field nameField = svc2.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(svc2, "second"); + } + } + """ + ) + ); + } + + @Test + void throwsNotAddedWhenThrowableAlreadyPresent() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testSetField() throws Throwable { + MyService service = new MyService(); + Whitebox.setInternalState(service, "name", "expectedValue"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testSetField() throws Throwable { + MyService service = new MyService(); + Field nameField = service.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(service, "expectedValue"); + } + } + """ + ) + ); + } + + @Test + void setInternalStateNonLiteralFieldName() { + //language=java + rewriteRun( + java( + """ + class MyService { + private String name; + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void testSetField(String fieldName) { + MyService service = new MyService(); + Whitebox.setInternalState(service, fieldName, "expectedValue"); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void testSetField(String fieldName) throws Exception { + MyService service = new MyService(); + Field reflectField = service.getClass().getDeclaredField(fieldName); + reflectField.setAccessible(true); + reflectField.set(service, "expectedValue"); + } + } + """ + ) + ); + } + + @Test + void setInternalStateWithWhereClass() { + //language=java + rewriteRun( + java( + """ + class Parent { + private String name = "hello"; + } + """ + ), + java( + """ + class Child extends Parent { + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Child child = new Child(); + Whitebox.setInternalState(child, "name", "newValue", Parent.class); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void test() throws Exception { + Child child = new Child(); + Field nameField = Parent.class.getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(child, "newValue"); + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java index 711343365..d8a2260a7 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java @@ -21,6 +21,7 @@ import org.openrewrite.java.JavaParser; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import static org.openrewrite.java.Assertions.java; @@ -34,7 +35,7 @@ public void defaults(RecipeSpec spec) { "powermock-core-1", "powermock-reflect-1" )) - .recipe(new PowerMockWhiteboxToJavaReflection()); + .recipeFromResources("org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection"); } @DocumentExample @@ -77,13 +78,17 @@ void testSetField() throws Exception { } @Test - void getInternalStateWithAssignment() { + void mixedWhiteboxApisInSameMethod() { //language=java rewriteRun( + // The user type used as a generic/result type is generated by the template and cannot be + // attributed by the isolated template parser (a test-only artifact); the source is valid Java. + spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), java( """ class MyService { - private String name = "hello"; + private String name; + private String compute() { return name; } } """ ), @@ -92,426 +97,35 @@ class MyService { import org.powermock.reflect.Whitebox; class MyServiceTest { - void testGetField() { + void test() { MyService service = new MyService(); - String result = Whitebox.getInternalState(service, "name"); + Whitebox.setInternalState(service, "name", "newValue"); + String name = Whitebox.getInternalState(service, "name"); + String result = Whitebox.invokeMethod(service, "compute"); + MyService copy = Whitebox.invokeConstructor(MyService.class); } } """, """ + import java.lang.reflect.Constructor; import java.lang.reflect.Field; + import java.lang.reflect.Method; class MyServiceTest { - void testGetField() throws Exception { + void test() throws Exception { MyService service = new MyService(); Field nameField = service.getClass().getDeclaredField("name"); nameField.setAccessible(true); - String result = (String) nameField.get(service); - } - } - """ - ) - ); - } - - @Test - void invokeMethodNoArgs() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String compute() { return "result"; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testInvoke() { - MyService service = new MyService(); - String result = Whitebox.invokeMethod(service, "compute"); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void testInvoke() throws Exception { - MyService service = new MyService(); + nameField.set(service, "newValue"); + Field nameField1 = service.getClass().getDeclaredField("name"); + nameField1.setAccessible(true); + String name = (String) nameField1.get(service); Method computeMethod = service.getClass().getDeclaredMethod("compute"); computeMethod.setAccessible(true); String result = (String) computeMethod.invoke(service); - } - } - """ - ) - ); - } - - @Test - void invokeMethodWithArgs() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String greet(String name) { return "Hello " + name; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testInvokeWithArgs() { - MyService service = new MyService(); - String result = Whitebox.invokeMethod(service, "greet", "World"); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void testInvokeWithArgs() throws Exception { - MyService service = new MyService(); - Method greetMethod = service.getClass().getDeclaredMethod("greet", String.class); - greetMethod.setAccessible(true); - String result = (String) greetMethod.invoke(service, "World"); - } - } - """ - ) - ); - } - - @Test - void invokeMethodWithMultipleArgs() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String combine(String a, String b) { return a + b; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testInvokeWithMultipleArgs() { - MyService service = new MyService(); - String result = Whitebox.invokeMethod(service, "combine", "Hello", "World"); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void testInvokeWithMultipleArgs() throws Exception { - MyService service = new MyService(); - Method combineMethod = service.getClass().getDeclaredMethod("combine", String.class, String.class); - combineMethod.setAccessible(true); - String result = (String) combineMethod.invoke(service, "Hello", "World"); - } - } - """ - ) - ); - } - - @Test - void invokeMethodWithPrimitiveArg() { - //language=java - rewriteRun( - java( - """ - class MyService { - private int doubleIt(int value) { return value * 2; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testInvokeWithPrimitive() { - MyService service = new MyService(); - Whitebox.invokeMethod(service, "doubleIt", 5); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void testInvokeWithPrimitive() throws Exception { - MyService service = new MyService(); - Method doubleItMethod = service.getClass().getDeclaredMethod("doubleIt", int.class); - doubleItMethod.setAccessible(true); - doubleItMethod.invoke(service, 5); - } - } - """ - ) - ); - } - - @Test - void invokeMethodWithConcreteArgButInterfaceParam() { - //language=java - rewriteRun( - java( - """ - import java.util.List; - - class MyService { - private String process(List items) { return items.toString(); } - } - """ - ), - java( - """ - import java.util.ArrayList; - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testInvokeWithConcreteArg() { - MyService service = new MyService(); - ArrayList items = new ArrayList<>(); - String result = Whitebox.invokeMethod(service, "process", items); - } - } - """, - """ - import java.lang.reflect.Method; - import java.util.ArrayList; - import java.util.List; - - class MyServiceTest { - void testInvokeWithConcreteArg() throws Exception { - MyService service = new MyService(); - ArrayList items = new ArrayList<>(); - Method processMethod = service.getClass().getDeclaredMethod("process", List.class); - processMethod.setAccessible(true); - String result = (String) processMethod.invoke(service, items); - } - } - """ - ) - ); - } - - @Test - void invokeMethodWithInterfaceTypedArg() { - //language=java - rewriteRun( - java( - """ - import java.util.List; - - class MyService { - private String process(List items) { return items.toString(); } - } - """ - ), - java( - """ - import java.util.ArrayList; - import java.util.List; - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testInvokeWithInterfaceArg() { - MyService service = new MyService(); - List items = new ArrayList<>(); - String result = Whitebox.invokeMethod(service, "process", items); - } - } - """, - """ - import java.lang.reflect.Method; - import java.util.ArrayList; - import java.util.List; - - class MyServiceTest { - void testInvokeWithInterfaceArg() throws Exception { - MyService service = new MyService(); - List items = new ArrayList<>(); - Method processMethod = service.getClass().getDeclaredMethod("process", List.class); - processMethod.setAccessible(true); - String result = (String) processMethod.invoke(service, items); - } - } - """ - ) - ); - } - - @Test - void throwsExceptionNotDuplicatedWhenAlreadyPresent() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testSetField() throws Exception { - MyService service = new MyService(); - Whitebox.setInternalState(service, "name", "expectedValue"); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void testSetField() throws Exception { - MyService service = new MyService(); - Field nameField = service.getClass().getDeclaredField("name"); - nameField.setAccessible(true); - nameField.set(service, "expectedValue"); - } - } - """ - ) - ); - } - - @Test - void whiteboxInsideIfBlock() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testSetFieldConditionally(boolean condition) { - MyService service = new MyService(); - if (condition) { - Whitebox.setInternalState(service, "name", "expectedValue"); - } - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void testSetFieldConditionally(boolean condition) throws Exception { - MyService service = new MyService(); - if (condition) { - Field nameField = service.getClass().getDeclaredField("name"); - nameField.setAccessible(true); - nameField.set(service, "expectedValue"); - } - } - } - """ - ) - ); - } - - @Test - void multipleWhiteboxCallsSameFieldName() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testSetFieldTwice() { - MyService svc1 = new MyService(); - MyService svc2 = new MyService(); - Whitebox.setInternalState(svc1, "name", "first"); - Whitebox.setInternalState(svc2, "name", "second"); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void testSetFieldTwice() throws Exception { - MyService svc1 = new MyService(); - MyService svc2 = new MyService(); - Field nameField1 = svc1.getClass().getDeclaredField("name"); - nameField1.setAccessible(true); - nameField1.set(svc1, "first"); - Field nameField = svc2.getClass().getDeclaredField("name"); - nameField.setAccessible(true); - nameField.set(svc2, "second"); - } - } - """ - ) - ); - } - - @Test - void throwsNotAddedWhenThrowableAlreadyPresent() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testSetField() throws Throwable { - MyService service = new MyService(); - Whitebox.setInternalState(service, "name", "expectedValue"); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void testSetField() throws Throwable { - MyService service = new MyService(); - Field nameField = service.getClass().getDeclaredField("name"); - nameField.setAccessible(true); - nameField.set(service, "expectedValue"); + Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(); + myServiceConstructor.setAccessible(true); + MyService copy = myServiceConstructor.newInstance(); } } """ From 9250c5020af23483b6e3d975563ce0e584b6abe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Merlin=20B=C3=B6gershausen?= Date: Tue, 16 Jun 2026 14:02:34 +0200 Subject: [PATCH 2/3] Scope Whitebox split to the recipe's released APIs Drop the getField, getMethod and invokeConstructor recipes and the where-overload / static-invokeMethod handling, so this PR is a pure refactor of the released PowerMockWhiteboxToJavaReflection recipe (setInternalState, getInternalState, invokeMethod) into three per-API recipes plus the shared WhiteboxToReflectionVisitor, with no behavior change versus main. The broader API coverage will follow in a separate PR. --- ...rMockWhiteboxGetFieldToJavaReflection.java | 70 ----- ...teboxGetInternalStateToJavaReflection.java | 53 ++-- ...MockWhiteboxGetMethodToJavaReflection.java | 80 ------ ...eboxInvokeConstructorToJavaReflection.java | 209 --------------- ...kWhiteboxInvokeMethodToJavaReflection.java | 112 ++++---- ...teboxSetInternalStateToJavaReflection.java | 31 ++- .../mockito/WhiteboxToReflectionVisitor.java | 145 +--------- .../META-INF/rewrite/powermockito.yml | 10 +- .../resources/META-INF/rewrite/recipes.csv | 11 +- ...kWhiteboxGetFieldToJavaReflectionTest.java | 102 ------- ...xGetInternalStateToJavaReflectionTest.java | 162 ----------- ...WhiteboxGetMethodToJavaReflectionTest.java | 148 ---------- ...InvokeConstructorToJavaReflectionTest.java | 221 --------------- ...teboxInvokeMethodToJavaReflectionTest.java | 253 ------------------ ...xSetInternalStateToJavaReflectionTest.java | 82 ------ ...PowerMockWhiteboxToJavaReflectionTest.java | 20 +- 16 files changed, 117 insertions(+), 1592 deletions(-) delete mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java delete mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java delete mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java delete mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java delete mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java delete mode 100644 src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java deleted file mode 100644 index 4a1662a65..000000000 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflection.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import lombok.Getter; -import org.jspecify.annotations.Nullable; -import org.openrewrite.Cursor; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; - -import java.util.List; - -public class PowerMockWhiteboxGetFieldToJavaReflection extends Recipe { - - private static final MethodMatcher GET_FIELD = - new MethodMatcher("org.powermock.reflect.Whitebox getField(java.lang.Class, java.lang.String)"); - - @Getter - final String displayName = "Replace PowerMock `Whitebox.getField()` with Java reflection"; - - @Getter - final String description = "Replace `Whitebox.getField(Class, String)` with `Class.getDeclaredField(String)` " + - "plus `setAccessible(true)`. Unlike PowerMock, `getDeclaredField` does not traverse the class " + - "hierarchy for fields inherited from a superclass."; - - @Override - public TreeVisitor getVisitor() { - return new GetFieldVisitor().withPrecondition(); - } - - private static class GetFieldVisitor extends WhiteboxToReflectionVisitor { - - GetFieldVisitor() { - super("java.lang.reflect.Field", GET_FIELD); - } - - @Override - String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, - JavaType.@Nullable Method resolvedMethod) { - String varName = resultLocalName(sink, mi.getArguments().get(1), scope, true); - return "Field " + varName + " = #{any(java.lang.Class)}.getDeclaredField(#{any(java.lang.String)});\n" + - varName + ".setAccessible(true);"; - } - - @Override - Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - // declaringClass, fieldName - return new Object[]{args.get(0), args.get(1)}; - } - } -} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java index 19f2a08de..0ab3363da 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflection.java @@ -28,21 +28,22 @@ import java.util.List; +import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; +import static org.openrewrite.java.VariableNameUtils.generateVariableName; + public class PowerMockWhiteboxGetInternalStateToJavaReflection extends Recipe { private static final MethodMatcher GET_INTERNAL_STATE = new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String)"); - private static final MethodMatcher GET_INTERNAL_STATE_WHERE = - new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String, java.lang.Class)"); @Getter final String displayName = "Replace PowerMock `Whitebox.getInternalState()` with Java reflection"; @Getter - final String description = "Replace `Whitebox.getInternalState(Object, String)` and the `Class where` overload " + - "with `java.lang.reflect.Field` access, casting to the declared result type where needed. The field " + - "lookup uses `getDeclaredField` on the target object's class (or the `where` class), which differs " + - "from PowerMock's class-hierarchy traversal for fields inherited from a superclass."; + final String description = "Replace `Whitebox.getInternalState(Object, String)` with `java.lang.reflect.Field` " + + "access, casting to the declared result type where needed. The field lookup uses `getDeclaredField` on " + + "the target object's class, which differs from PowerMock's class-hierarchy traversal for fields " + + "inherited from a superclass."; @Override public TreeVisitor getVisitor() { @@ -52,40 +53,32 @@ public TreeVisitor getVisitor() { private static class GetInternalStateVisitor extends WhiteboxToReflectionVisitor { GetInternalStateVisitor() { - super("java.lang.reflect.Field", GET_INTERNAL_STATE, GET_INTERNAL_STATE_WHERE); + super("java.lang.reflect.Field", GET_INTERNAL_STATE); } @Override - String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, - JavaType.@Nullable Method resolvedMethod) { - String varName = fieldVarName(mi.getArguments().get(1), scope); - String receiver = GET_INTERNAL_STATE_WHERE.matches(mi) ? "#{any(java.lang.Class)}" : "#{any(java.lang.Object)}.getClass()"; - return fieldLookupPrefix(varName, receiver) + fieldGetTail(varName, sink); + @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + String fieldName = extractStringLiteral(mi.getArguments().get(1)); + if (fieldName == null) { + return null; + } + String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER); + String prefix = fieldLookupPrefix(varName); + if (sink.varName != null) { + if (isNonObjectCast(sink.castType)) { + return prefix + sink.castType + " " + sink.varName + " = (" + sink.castType + ") " + varName + ".get(#{any(java.lang.Object)});"; + } + return prefix + "Object " + sink.varName + " = " + varName + ".get(#{any(java.lang.Object)});"; + } + return prefix + varName + ".get(#{any(java.lang.Object)});"; } @Override Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { List args = mi.getArguments(); - if (GET_INTERNAL_STATE_WHERE.matches(mi)) { - // where, fieldName, target - return new Object[]{args.get(2), args.get(1), args.get(0)}; - } // target, fieldName, target return new Object[]{args.get(0), args.get(1), args.get(0)}; } - - /** - * Build the trailing {@code Field.get(...)} statement, casting to the result type when the - * result is stored in a variable. - */ - private String fieldGetTail(String varName, ResultSink sink) { - if (sink.varName != null) { - if (isNonObjectCast(sink.castType)) { - return sink.castType + " " + sink.varName + " = (" + boxedCastType(sink.castType) + ") " + varName + ".get(#{any(java.lang.Object)});"; - } - return "Object " + sink.varName + " = " + varName + ".get(#{any(java.lang.Object)});"; - } - return varName + ".get(#{any(java.lang.Object)});"; - } } } diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java deleted file mode 100644 index 02d356d3b..000000000 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflection.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import lombok.Getter; -import org.jspecify.annotations.Nullable; -import org.openrewrite.Cursor; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; - -import java.util.List; - -public class PowerMockWhiteboxGetMethodToJavaReflection extends Recipe { - - private static final MethodMatcher GET_METHOD = - new MethodMatcher("org.powermock.reflect.Whitebox getMethod(java.lang.Class, java.lang.String, ..)"); - - @Getter - final String displayName = "Replace PowerMock `Whitebox.getMethod()` with Java reflection"; - - @Getter - final String description = "Replace `Whitebox.getMethod(Class, String, Class...)` with " + - "`Class.getDeclaredMethod(String, Class...)` plus `setAccessible(true)`. Unlike PowerMock, " + - "`getDeclaredMethod` does not traverse the class hierarchy; calls passing an explicit `Class[]` " + - "array are left unchanged for manual migration."; - - @Override - public TreeVisitor getVisitor() { - return new GetMethodVisitor().withPrecondition(); - } - - private static class GetMethodVisitor extends WhiteboxToReflectionVisitor { - - GetMethodVisitor() { - super("java.lang.reflect.Method", GET_METHOD); - } - - @Override - @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, - JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - if (hasArrayArg(args, 2)) { - // Explicit Class[] varargs array is not supported; leave for manual migration (flagged downstream) - return null; - } - String varName = resultLocalName(sink, args.get(1), scope, false); - StringBuilder sb = new StringBuilder("Method ").append(varName) - .append(" = #{any(java.lang.Class)}.getDeclaredMethod(#{any(java.lang.String)}"); - for (int i = 2; i < args.size(); i++) { - sb.append(", #{any(java.lang.Class)}"); - } - sb.append(");\n").append(varName).append(".setAccessible(true);"); - return sb.toString(); - } - - @Override - Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { - // declaringClass, methodName, paramType0, paramType1, ... - return mi.getArguments().toArray(); - } - } -} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java deleted file mode 100644 index 25d319874..000000000 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflection.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import lombok.Getter; -import org.jspecify.annotations.Nullable; -import org.openrewrite.Cursor; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; - -import java.util.ArrayList; -import java.util.List; - -import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; -import static org.openrewrite.java.VariableNameUtils.generateVariableName; - -public class PowerMockWhiteboxInvokeConstructorToJavaReflection extends Recipe { - - private static final MethodMatcher INVOKE_CONSTRUCTOR_ARGS = - new MethodMatcher("org.powermock.reflect.Whitebox invokeConstructor(java.lang.Class, java.lang.Object[])"); - private static final MethodMatcher INVOKE_CONSTRUCTOR_EXPLICIT = - new MethodMatcher("org.powermock.reflect.Whitebox invokeConstructor(java.lang.Class, java.lang.Class[], java.lang.Object[])"); - - @Getter - final String displayName = "Replace PowerMock `Whitebox.invokeConstructor()` with Java reflection"; - - @Getter - final String description = "Replace `Whitebox.invokeConstructor(..)` with `java.lang.reflect.Constructor` " + - "lookup and `newInstance()` on the named class. Constructor parameter types are taken from the " + - "unambiguously resolved constructor, falling back to each argument's compile-time class; arrays " + - "passed to the `Object...` varargs overload are left unchanged for manual migration."; - - @Override - public TreeVisitor getVisitor() { - return new InvokeConstructorVisitor().withPrecondition(); - } - - private static class InvokeConstructorVisitor extends WhiteboxToReflectionVisitor { - - InvokeConstructorVisitor() { - super("java.lang.reflect.Constructor", INVOKE_CONSTRUCTOR_ARGS, INVOKE_CONSTRUCTOR_EXPLICIT); - } - - @Override - JavaType.@Nullable Method resolve(J.MethodInvocation mi) { - if (INVOKE_CONSTRUCTOR_ARGS.matches(mi)) { - return resolveTargetConstructor(mi.getArguments()); - } - return null; - } - - @Override - @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, - JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - boolean explicit = INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi); - if (!explicit && hasArrayArg(args, 1)) { - // An array passed to the `Object...` varargs overload is ambiguous (spread vs single arg); - // leave for manual migration (flagged downstream). - return null; - } - JavaType.FullyQualified elem = classLiteralElementType(args.get(0)); - String genericType = elem != null ? elem.getClassName() : "?"; - String varName = constructorVarName(elem, scope); - - StringBuilder sb = new StringBuilder("Constructor<").append(genericType).append("> ").append(varName) - .append(" = #{any(java.lang.Class)}.getDeclaredConstructor("); - - int newInstanceArgCount; - if (explicit) { - List paramTypeExprs = arrayElements(args.get(1)); - List ctorArgExprs = arrayElements(args.get(2)); - if (paramTypeExprs == null || ctorArgExprs == null) { - // Cannot unwrap the explicit Class[]/Object[] arrays; leave for manual migration (flagged downstream) - return null; - } - for (int i = 0; i < paramTypeExprs.size(); i++) { - sb.append(i > 0 ? ", " : "").append("#{any(java.lang.Class)}"); - } - newInstanceArgCount = ctorArgExprs.size(); - } else { - for (int i = 1; i < args.size(); i++) { - sb.append(i > 1 ? ", " : ""); - String classLiteral = getParamClassLiteral(args, i, resolvedMethod, 1); - sb.append(classLiteral != null ? classLiteral : "#{any(java.lang.Object)}.getClass()"); - } - newInstanceArgCount = args.size() - 1; - } - sb.append(");\n"); - sb.append(varName).append(".setAccessible(true);\n"); - sb.append(constructorNewInstanceTail(varName, sink, elem != null, newInstanceArgCount)); - return sb.toString(); - } - - @Override - Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - List result = new ArrayList<>(); - result.add(args.get(0)); // getDeclaredConstructor receiver (Class) - if (INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi)) { - List paramTypeExprs = arrayElements(args.get(1)); - List ctorArgExprs = arrayElements(args.get(2)); - if (paramTypeExprs != null) { - result.addAll(paramTypeExprs); // one per #{any(java.lang.Class)} - } - if (ctorArgExprs != null) { - result.addAll(ctorArgExprs); // newInstance args - } - } else { - for (int i = 1; i < args.size(); i++) { - if (getParamClassLiteral(args, i, resolvedMethod, 1) == null) { - result.add(args.get(i)); // arg.getClass() fallback receiver - } - } - for (int i = 1; i < args.size(); i++) { - result.add(args.get(i)); // newInstance args - } - } - return result.toArray(); - } - - private String constructorNewInstanceTail(String varName, ResultSink sink, boolean elemKnown, int argCount) { - String invokeArgs = repeatObjectPlaceholders(argCount); - if (sink.varName != null) { - String castType = sink.castType != null ? sink.castType : "Object"; - // When the Class element type is known, newInstance() returns it directly (no cast needed); - // otherwise we have a raw Constructor returning Object that must be cast. - if (!elemKnown && isNonObjectCast(castType)) { - return castType + " " + sink.varName + " = (" + castType + ") " + varName + ".newInstance(" + invokeArgs + ");"; - } - return castType + " " + sink.varName + " = " + varName + ".newInstance(" + invokeArgs + ");"; - } - return varName + ".newInstance(" + invokeArgs + ");"; - } - - private String repeatObjectPlaceholders(int count) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < count; i++) { - sb.append(i > 0 ? ", " : "").append("#{any(java.lang.Object)}"); - } - return sb.toString(); - } - - private @Nullable List arrayElements(Expression expr) { - if (expr instanceof J.NewArray) { - return ((J.NewArray) expr).getInitializer(); - } - return null; - } - - private String constructorVarName(JavaType.@Nullable FullyQualified elem, Cursor scope) { - String base; - if (elem != null) { - String simple = elem.getClassName(); - int dot = simple.lastIndexOf('.'); - if (dot >= 0) { - simple = simple.substring(dot + 1); - } - base = Character.toLowerCase(simple.charAt(0)) + simple.substring(1) + "Constructor"; - } else { - base = "reflectConstructor"; - } - return generateVariableName(base, scope, INCREMENT_NUMBER); - } - - /** - * Resolve the target constructor from the {@code Class} literal's element type, by parameter count. - * Returns null if not unambiguously resolvable (so parameter types fall back to {@code arg.getClass()}). - */ - private JavaType.@Nullable Method resolveTargetConstructor(List args) { - if (args.size() <= 1) { - return null; - } - JavaType.FullyQualified type = classLiteralElementType(args.get(0)); - if (type == null) { - return null; - } - int expectedParamCount = args.size() - 1; - JavaType.Method match = null; - for (JavaType.Method method : type.getMethods()) { - if (method.isConstructor() && method.getParameterTypes().size() == expectedParamCount) { - if (match != null) { - return null; // ambiguous overload - } - match = method; - } - } - return match; - } - } -} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java index 6084d0d26..4e7b679a5 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflection.java @@ -27,24 +27,23 @@ import org.openrewrite.java.tree.JavaType; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; +import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; +import static org.openrewrite.java.VariableNameUtils.generateVariableName; + public class PowerMockWhiteboxInvokeMethodToJavaReflection extends Recipe { private static final MethodMatcher INVOKE_METHOD = new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Object, java.lang.String, ..)"); - private static final MethodMatcher INVOKE_METHOD_STATIC = - new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Class, java.lang.String, ..)"); @Getter final String displayName = "Replace PowerMock `Whitebox.invokeMethod()` with Java reflection"; @Getter - final String description = "Replace instance and static `Whitebox.invokeMethod(..)` calls with " + - "`java.lang.reflect.Method` lookup and `invoke()`. Parameter types are taken from the unambiguously " + - "resolved target method, falling back to each argument's compile-time class; calls passing an " + - "explicit `Class[]` array are left unchanged for manual migration."; + final String description = "Replace `Whitebox.invokeMethod(Object, String, ..)` with `java.lang.reflect.Method` " + + "lookup and `invoke()`. Parameter types are taken from the unambiguously resolved target method, " + + "falling back to each argument's compile-time class."; @Override public TreeVisitor getVisitor() { @@ -54,39 +53,27 @@ public TreeVisitor getVisitor() { private static class InvokeMethodVisitor extends WhiteboxToReflectionVisitor { InvokeMethodVisitor() { - super("java.lang.reflect.Method", INVOKE_METHOD, INVOKE_METHOD_STATIC); + super("java.lang.reflect.Method", INVOKE_METHOD); } @Override JavaType.@Nullable Method resolve(J.MethodInvocation mi) { - if (INVOKE_METHOD.matches(mi)) { - return resolveTargetMethod(mi.getArguments()); - } - if (INVOKE_METHOD_STATIC.matches(mi)) { - return resolveStaticTargetMethod(mi.getArguments()); - } - return null; + return resolveTargetMethod(mi.getArguments()); } @Override @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, JavaType.@Nullable Method resolvedMethod) { List args = mi.getArguments(); - if (hasArrayArg(args, 2)) { - // Explicit `Class[]` parameter-type overload (or an array passed as varargs) cannot be - // mechanically expanded; leave for manual migration (flagged downstream). + String methodName = extractStringLiteral(args.get(1)); + if (methodName == null) { return null; } - boolean isStatic = INVOKE_METHOD_STATIC.matches(mi); - String varName = methodVarName(args.get(1), scope); - // Static calls take a Class target; instance calls take an Object whose class we look up. - String declaredMethodReceiver = isStatic ? "#{any(java.lang.Class)}" : "#{any(java.lang.Object)}.getClass()"; - String invokeTarget = isStatic ? "null" : "#{any(java.lang.Object)}"; + String varName = generateVariableName(methodName + "Method", scope, INCREMENT_NUMBER); // getDeclaredMethod line StringBuilder sb = new StringBuilder(); - sb.append("Method ").append(varName).append(" = ").append(declaredMethodReceiver) - .append(".getDeclaredMethod(#{any(java.lang.String)}"); + sb.append("Method ").append(varName).append(" = #{any(java.lang.Object)}.getClass().getDeclaredMethod(#{any(java.lang.String)}"); for (int i = 2; i < args.size(); i++) { String classLiteral = getParamClassLiteral(args, i, resolvedMethod); if (classLiteral != null) { @@ -103,12 +90,12 @@ private static class InvokeMethodVisitor extends WhiteboxToReflectionVisitor { // invoke line if (sink.varName != null) { if (isNonObjectCast(sink.castType)) { - sb.append(sink.castType).append(" ").append(sink.varName).append(" = (").append(boxedCastType(sink.castType)).append(") "); + sb.append(sink.castType).append(" ").append(sink.varName).append(" = (").append(sink.castType).append(") "); } else { sb.append("Object ").append(sink.varName).append(" = "); } } - sb.append(varName).append(".invoke(").append(invokeTarget); + sb.append(varName).append(".invoke(#{any(java.lang.Object)}"); for (int i = 2; i < args.size(); i++) { sb.append(", #{any(java.lang.Object)}"); } @@ -121,16 +108,14 @@ private static class InvokeMethodVisitor extends WhiteboxToReflectionVisitor { Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { List args = mi.getArguments(); List result = new ArrayList<>(); - result.add(args.get(0)); // receiver for getDeclaredMethod (Class for static, Object for instance) + result.add(args.get(0)); // target for getDeclaredMethod result.add(args.get(1)); // methodName for (int i = 2; i < args.size(); i++) { if (getParamClassLiteral(args, i, resolvedMethod) == null) { result.add(args.get(i)); // arg.getClass() fallback for getDeclaredMethod } } - if (!INVOKE_METHOD_STATIC.matches(mi)) { - result.add(args.get(0)); // target for invoke (static uses the null literal, no placeholder) - } + result.add(args.get(0)); // target for invoke for (int i = 2; i < args.size(); i++) { result.add(args.get(i)); // arg for invoke } @@ -138,46 +123,63 @@ Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMeth } /** - * Resolve the target method from the first argument's type and the method name. - * Returns null if the method cannot be unambiguously resolved (not found, overloaded, - * or missing type information). + * Get the class literal for a parameter at the given argument index. Prefers the resolved + * method's declared parameter type, falls back to the argument's compile-time type, and + * returns null if neither is available. */ - private JavaType.@Nullable Method resolveTargetMethod(List args) { - if (args.size() <= 2) { - return null; + private @Nullable String getParamClassLiteral(List args, int argIndex, + JavaType.@Nullable Method resolvedMethod) { + if (resolvedMethod != null) { + String literal = classLiteralFromType(resolvedMethod.getParameterTypes().get(argIndex - 2)); + if (literal != null) { + return literal; + } } - JavaType targetType = args.get(0).getType(); - JavaType.FullyQualified fq = targetType instanceof JavaType.FullyQualified ? (JavaType.FullyQualified) targetType : null; - return findUniqueMethod(fq, extractStringLiteral(args.get(1)), args.size() - 2); + return classLiteralFromType(args.get(argIndex).getType()); } /** - * Resolve the static target method from the {@code Class} literal's element type and the method name. + * Resolve the target method from the first argument's type and the method name, walking the + * supertype chain. Returns null if the method cannot be unambiguously resolved (not found, + * overloaded, or missing type information). */ - private JavaType.@Nullable Method resolveStaticTargetMethod(List args) { + private JavaType.@Nullable Method resolveTargetMethod(List args) { if (args.size() <= 2) { return null; } - return findUniqueMethod(classLiteralElementType(args.get(0)), extractStringLiteral(args.get(1)), args.size() - 2); - } - - private JavaType.@Nullable Method findUniqueMethod(JavaType.@Nullable FullyQualified targetType, - @Nullable String methodName, int expectedParamCount) { - if (targetType == null || methodName == null) { + String methodName = extractStringLiteral(args.get(1)); + if (methodName == null) { + return null; + } + JavaType targetType = args.get(0).getType(); + if (!(targetType instanceof JavaType.FullyQualified)) { return null; } + int expectedParamCount = args.size() - 2; JavaType.Method match = null; - for (Iterator it = targetType.getVisibleMethods(); it.hasNext(); ) { - JavaType.Method method = it.next(); - if (method.getName().equals(methodName) && - method.getParameterTypes().size() == expectedParamCount) { - if (match != null) { - return null; // ambiguous overload + for (JavaType.FullyQualified current = (JavaType.FullyQualified) targetType; + current != null; current = current.getSupertype()) { + for (JavaType.Method method : current.getMethods()) { + if (method.getName().equals(methodName) && + method.getParameterTypes().size() == expectedParamCount) { + if (match != null) { + return null; // ambiguous overload + } + match = method; } - match = method; } } return match; } + + private @Nullable String classLiteralFromType(@Nullable JavaType type) { + if (type instanceof JavaType.Primitive) { + return ((JavaType.Primitive) type).getKeyword() + ".class"; + } + if (type instanceof JavaType.FullyQualified) { + return ((JavaType.FullyQualified) type).getClassName() + ".class"; + } + return null; + } } } diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java index 79312a8b0..fe8111992 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java @@ -28,21 +28,21 @@ import java.util.List; +import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; +import static org.openrewrite.java.VariableNameUtils.generateVariableName; + public class PowerMockWhiteboxSetInternalStateToJavaReflection extends Recipe { private static final MethodMatcher SET_INTERNAL_STATE = new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object)"); - private static final MethodMatcher SET_INTERNAL_STATE_WHERE = - new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object, java.lang.Class)"); @Getter final String displayName = "Replace PowerMock `Whitebox.setInternalState()` with Java reflection"; @Getter - final String description = "Replace `Whitebox.setInternalState(Object, String, Object)` and the `Class where` " + - "overload with `java.lang.reflect.Field` access. The field lookup uses `getDeclaredField` on the target " + - "object's class (or the `where` class), which differs from PowerMock's class-hierarchy traversal for " + - "fields inherited from a superclass."; + final String description = "Replace `Whitebox.setInternalState(Object, String, Object)` with `java.lang.reflect.Field` " + + "access. The field lookup uses `getDeclaredField` on the target object's class, which differs from " + + "PowerMock's class-hierarchy traversal for fields inherited from a superclass."; @Override public TreeVisitor getVisitor() { @@ -52,25 +52,24 @@ public TreeVisitor getVisitor() { private static class SetInternalStateVisitor extends WhiteboxToReflectionVisitor { SetInternalStateVisitor() { - super("java.lang.reflect.Field", SET_INTERNAL_STATE, SET_INTERNAL_STATE_WHERE); + super("java.lang.reflect.Field", SET_INTERNAL_STATE); } @Override - String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, - JavaType.@Nullable Method resolvedMethod) { - String varName = fieldVarName(mi.getArguments().get(1), scope); - String receiver = SET_INTERNAL_STATE_WHERE.matches(mi) ? "#{any(java.lang.Class)}" : "#{any(java.lang.Object)}.getClass()"; - return fieldLookupPrefix(varName, receiver) + + @Nullable String buildTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedMethod) { + String fieldName = extractStringLiteral(mi.getArguments().get(1)); + if (fieldName == null) { + return null; + } + String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER); + return fieldLookupPrefix(varName) + varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});"; } @Override Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { List args = mi.getArguments(); - if (SET_INTERNAL_STATE_WHERE.matches(mi)) { - // where, fieldName, target, value - return new Object[]{args.get(3), args.get(1), args.get(0), args.get(2)}; - } // target, fieldName, target, value return new Object[]{args.get(0), args.get(1), args.get(0), args.get(2)}; } diff --git a/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java b/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java index 7401d7d0b..2e648d8fe 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java @@ -31,14 +31,10 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; -import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; -import static org.openrewrite.java.VariableNameUtils.generateVariableName; /** * Shared machinery for replacing a single {@code org.powermock.reflect.Whitebox} API family with @@ -54,19 +50,6 @@ abstract class WhiteboxToReflectionVisitor extends JavaIsoVisitor BOXED_TYPES = new HashMap<>(); - - static { - BOXED_TYPES.put("int", "Integer"); - BOXED_TYPES.put("long", "Long"); - BOXED_TYPES.put("double", "Double"); - BOXED_TYPES.put("float", "Float"); - BOXED_TYPES.put("boolean", "Boolean"); - BOXED_TYPES.put("byte", "Byte"); - BOXED_TYPES.put("short", "Short"); - BOXED_TYPES.put("char", "Character"); - } - private final String reflectiveImport; private final List matchers; @@ -103,8 +86,8 @@ private ResultSink(@Nullable String varName, @Nullable String castType) { abstract Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod); /** - * The target method/constructor the call reflects on, when it can be unambiguously resolved; - * used to derive declared parameter types for class literals. + * The target method the call reflects on, when it can be unambiguously resolved; used to derive + * declared parameter types for class literals. */ JavaType.@Nullable Method resolve(J.MethodInvocation mi) { return null; @@ -194,8 +177,8 @@ private List templateImports(JavaType.@Nullable Method resolvedMethod) { return imports; } - // Non-java.lang fully-qualified parameter types of the resolved method/constructor that the - // generated class literals (e.g. `List.class`) need imported. + // Non-java.lang fully-qualified parameter types of the resolved method that the generated class + // literals (e.g. `List.class`) need imported. private List resolvedParamImports(JavaType.@Nullable Method resolvedMethod) { if (resolvedMethod == null) { return emptyList(); @@ -231,6 +214,7 @@ private ResultSink sinkFromStatement(Statement statement) { Expression init = varDecls.getVariables().get(0).getInitializer(); if (init instanceof J.MethodInvocation) { J.MethodInvocation mi = (J.MethodInvocation) init; + // A void call (setInternalState) cannot initialize a variable declaration. if (matches(mi) && !returnsVoid(mi)) { return mi; } @@ -240,15 +224,14 @@ private ResultSink sinkFromStatement(Statement statement) { return null; } - // A void Whitebox call (e.g. setInternalState) cannot produce a variable declaration initializer. private boolean returnsVoid(J.MethodInvocation mi) { return mi.getMethodType() != null && JavaType.Primitive.Void == mi.getMethodType().getReturnType(); } - // `Field = .getDeclaredField(); .setAccessible(true);` — shared by the - // instance (receiver `obj.getClass()`) and `Class where` (receiver the where-class) get/set variants. - String fieldLookupPrefix(String varName, String fieldReceiver) { - return "Field " + varName + " = " + fieldReceiver + ".getDeclaredField(#{any(java.lang.String)});\n" + + // `Field = .getClass().getDeclaredField(); .setAccessible(true);` — + // shared by the get/set field variants. + String fieldLookupPrefix(String varName) { + return "Field " + varName + " = #{any(java.lang.Object)}.getClass().getDeclaredField(#{any(java.lang.String)});\n" + varName + ".setAccessible(true);\n"; } @@ -257,91 +240,6 @@ boolean isNonObjectCast(@Nullable String castType) { return castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType); } - /** - * For calls whose result type IS the reflective object ({@code getField}/{@code getMethod}), - * reuse the result variable as the local; otherwise generate one. - */ - String resultLocalName(ResultSink sink, Expression nameExpr, Cursor scope, boolean field) { - if (sink.varName != null) { - return sink.varName; - } - return field ? fieldVarName(nameExpr, scope) : methodVarName(nameExpr, scope); - } - - boolean hasArrayArg(List args, int fromIndex) { - for (int i = fromIndex; i < args.size(); i++) { - if (TypeUtils.asArray(args.get(i).getType()) != null) { - return true; - } - } - return false; - } - - /** - * Get the class literal for a parameter at the given argument index, where the first declared - * parameter is at argument index 2 (target, name, params...). - */ - @Nullable String getParamClassLiteral(List args, int argIndex, - JavaType.@Nullable Method resolvedMethod) { - return getParamClassLiteral(args, argIndex, resolvedMethod, 2); - } - - /** - * Get the class literal for a parameter, where {@code firstParamArgIndex} is the argument index of - * the first declared parameter (2 for {@code invokeMethod}: target, name, params...; 1 for - * {@code invokeConstructor}: class, params...). Prefers the resolved method's declared parameter - * type, falls back to the argument's compile-time type, and returns null if neither is available. - */ - @Nullable String getParamClassLiteral(List args, int argIndex, - JavaType.@Nullable Method resolvedMethod, int firstParamArgIndex) { - if (resolvedMethod != null) { - int paramIdx = argIndex - firstParamArgIndex; - List paramTypes = resolvedMethod.getParameterTypes(); - if (paramIdx >= 0 && paramIdx < paramTypes.size()) { - String literal = classLiteralFromType(paramTypes.get(paramIdx)); - if (literal != null) { - return literal; - } - } - } - return getClassLiteral(args.get(argIndex)); - } - - /** - * Extract the element type {@code X} from a {@code Class}-typed expression (e.g. {@code MyService.class}). - */ - JavaType.@Nullable FullyQualified classLiteralElementType(Expression classExpr) { - JavaType.Parameterized parameterized = TypeUtils.asParameterized(classExpr.getType()); - if (parameterized != null && !parameterized.getTypeParameters().isEmpty()) { - return TypeUtils.asFullyQualified(parameterized.getTypeParameters().get(0)); - } - return null; - } - - /** - * Generate the local variable name for a reflective {@code Field}. When the field name is a - * String literal we derive a readable name (e.g. {@code nameField}); otherwise we fall back to - * a generic {@code reflectField} base. Uniqueness within scope is guaranteed by INCREMENT_NUMBER. - */ - String fieldVarName(Expression nameExpr, Cursor scope) { - return reflectVarName(nameExpr, "Field", "reflectField", scope); - } - - /** - * Generate the local variable name for a reflective {@code Method}. See {@link #fieldVarName}. - */ - String methodVarName(Expression nameExpr, Cursor scope) { - return reflectVarName(nameExpr, "Method", "reflectMethod", scope); - } - - // Derive a unique local name from a String-literal name (`name` + suffix, e.g. `nameField`), - // falling back to a generic base when the name is not a literal. - private String reflectVarName(Expression nameExpr, String suffix, String fallbackBase, Cursor scope) { - String literal = extractStringLiteral(nameExpr); - String base = literal != null ? literal + suffix : fallbackBase; - return generateVariableName(base, scope, INCREMENT_NUMBER); - } - @Nullable String extractStringLiteral(Expression expr) { if (expr instanceof J.Literal && ((J.Literal) expr).getValue() instanceof String) { return (String) ((J.Literal) expr).getValue(); @@ -349,21 +247,7 @@ private String reflectVarName(Expression nameExpr, String suffix, String fallbac return null; } - private @Nullable String getClassLiteral(Expression expr) { - return classLiteralFromType(expr.getType()); - } - - private @Nullable String classLiteralFromType(@Nullable JavaType type) { - if (type instanceof JavaType.Primitive) { - return ((JavaType.Primitive) type).getKeyword() + ".class"; - } - if (type instanceof JavaType.FullyQualified) { - return ((JavaType.FullyQualified) type).getClassName() + ".class"; - } - return null; - } - - private @Nullable String getCastType(@Nullable JavaType type) { + @Nullable String getCastType(@Nullable JavaType type) { if (type instanceof JavaType.FullyQualified) { return ((JavaType.FullyQualified) type).getClassName(); } @@ -373,15 +257,6 @@ private String reflectVarName(Expression nameExpr, String suffix, String fallbac return null; } - /** - * {@code Field.get}/{@code Method.invoke} return {@code Object} (boxing primitives), so a primitive - * declared type must be cast to its wrapper (a direct {@code (int) object} cast does not compile); - * the surrounding assignment then auto-unboxes. - */ - String boxedCastType(String castType) { - return BOXED_TYPES.getOrDefault(castType, castType); - } - private J.MethodDeclaration addThrowsExceptionIfAbsent(J.MethodDeclaration md) { if (md.getThrows() != null && md.getThrows().stream() .anyMatch(j -> TypeUtils.isOfClassType(j.getType(), "java.lang.Exception") || diff --git a/src/main/resources/META-INF/rewrite/powermockito.yml b/src/main/resources/META-INF/rewrite/powermockito.yml index 07df7ed02..69b7a695d 100644 --- a/src/main/resources/META-INF/rewrite/powermockito.yml +++ b/src/main/resources/META-INF/rewrite/powermockito.yml @@ -64,18 +64,12 @@ type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection displayName: Replace PowerMock `Whitebox` with Java reflection description: >- - Replace `org.powermock.reflect.Whitebox` calls (`setInternalState`, `getInternalState`, `invokeMethod`, - static `invokeMethod`, `invokeConstructor`, `getField` and `getMethod`, including the `Class where` - overloads) with plain Java reflection using `java.lang.reflect.Field`, `Method` and `Constructor`. - Field/constructor lookups use `getDeclaredField`/`getDeclaredConstructor` on the named class, which - differs from PowerMock for members inherited from a superclass. + Replace `org.powermock.reflect.Whitebox` calls (`setInternalState`, `getInternalState`, `invokeMethod`) + with plain Java reflection using `java.lang.reflect.Field` and `java.lang.reflect.Method`. tags: - testing - mockito recipeList: - org.openrewrite.java.testing.mockito.PowerMockWhiteboxSetInternalStateToJavaReflection - org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetInternalStateToJavaReflection - - org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetFieldToJavaReflection - - org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetMethodToJavaReflection - org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeMethodToJavaReflection - - org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeConstructorToJavaReflection diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 6a2142939..704708aa1 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -201,13 +201,10 @@ maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.tes 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. Note that the `@Mock` fields will then be initialized by the strict mocking session of the extension or runner; tests that relied on the lenient mocks created by an explicit `openMocks(this)` call inside `@BeforeEach` may surface `UnnecessaryStubbingException`. Add `@MockitoSettings(strictness = Strictness.LENIENT)` to opt out.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.AddMockitoSettingsWithWarnStrictness,Add `@MockitoSettings(strictness = Strictness.WARN)` to `@ExtendWith(MockitoExtension.class)` classes,Adds `@MockitoSettings(strictness = Strictness.WARN)` to test classes that have `@ExtendWith(MockitoExtension.class)` but do not already have a `@MockitoSettings` annotation. This preserves the lenient stubbing behavior from Mockito 1.x/2.x migrations and prevents `UnnecessaryStubbingException` from strict stubbing defaults.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.VerifyZeroToNoMoreInteractions,Replace `verifyZeroInteractions()` with `verifyNoMoreInteractions()`,Replaces `verifyZeroInteractions()` with `verifyNoMoreInteractions()` in Mockito tests when migration when using a Mockito version < 3.x.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -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`, static `invokeMethod`, `invokeConstructor`, `getField` and `getMethod`, including the `Class where` overloads) with plain Java reflection using `java.lang.reflect.Field`, `Method` and `Constructor`. Field/constructor lookups use `getDeclaredField`/`getDeclaredConstructor` on the named class, which differs from PowerMock for members inherited from a superclass.",7,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxSetInternalStateToJavaReflection,Replace PowerMock `Whitebox.setInternalState()` with Java reflection,"Replace `Whitebox.setInternalState(Object, String, Object)` and the `Class where` overload with `java.lang.reflect.Field` access. The field lookup uses `getDeclaredField` on the target object's class (or the `where` class), which differs from PowerMock's class-hierarchy traversal for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetInternalStateToJavaReflection,Replace PowerMock `Whitebox.getInternalState()` with Java reflection,"Replace `Whitebox.getInternalState(Object, String)` and the `Class where` overload with `java.lang.reflect.Field` access, casting to the declared result type where needed. The field lookup uses `getDeclaredField` on the target object's class (or the `where` class), which differs from PowerMock's class-hierarchy traversal for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetFieldToJavaReflection,Replace PowerMock `Whitebox.getField()` with Java reflection,"Replace `Whitebox.getField(Class, String)` with `Class.getDeclaredField(String)` plus `setAccessible(true)`. Unlike PowerMock, `getDeclaredField` does not traverse the class hierarchy for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetMethodToJavaReflection,Replace PowerMock `Whitebox.getMethod()` with Java reflection,"Replace `Whitebox.getMethod(Class, String, Class...)` with `Class.getDeclaredMethod(String, Class...)` plus `setAccessible(true)`. Unlike PowerMock, `getDeclaredMethod` does not traverse the class hierarchy; calls passing an explicit `Class[]` array are left unchanged for manual migration.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeMethodToJavaReflection,Replace PowerMock `Whitebox.invokeMethod()` with Java reflection,"Replace instance and static `Whitebox.invokeMethod(..)` calls with `java.lang.reflect.Method` lookup and `invoke()`. Parameter types are taken from the unambiguously resolved target method, falling back to each argument's compile-time class; calls passing an explicit `Class[]` array are left unchanged for manual migration.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeConstructorToJavaReflection,Replace PowerMock `Whitebox.invokeConstructor()` with Java reflection,"Replace `Whitebox.invokeConstructor(..)` with `java.lang.reflect.Constructor` lookup and `newInstance()` on the named class. Constructor parameter types are taken from the unambiguously resolved constructor, falling back to each argument's compile-time class; arrays passed to the `Object...` varargs overload are left unchanged for manual migration.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +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`.",4,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxSetInternalStateToJavaReflection,Replace PowerMock `Whitebox.setInternalState()` with Java reflection,"Replace `Whitebox.setInternalState(Object, String, Object)` with `java.lang.reflect.Field` access. The field lookup uses `getDeclaredField` on the target object's class, which differs from PowerMock's class-hierarchy traversal for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxGetInternalStateToJavaReflection,Replace PowerMock `Whitebox.getInternalState()` with Java reflection,"Replace `Whitebox.getInternalState(Object, String)` with `java.lang.reflect.Field` access, casting to the declared result type where needed. The field lookup uses `getDeclaredField` on the target object's class, which differs from PowerMock's class-hierarchy traversal for fields inherited from a superclass.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxInvokeMethodToJavaReflection,Replace PowerMock `Whitebox.invokeMethod()` with Java reflection,"Replace `Whitebox.invokeMethod(Object, String, ..)` with `java.lang.reflect.Method` lookup and `invoke()`. Parameter types are taken from the unambiguously resolved target method, falling back to each argument's compile-time class.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.ArgumentMatcherToLambda,Convert `ArgumentMatcher` anonymous class to lambda,"Converts anonymous `ArgumentMatcher` implementations with `matches(Object)` to lambda expressions with the correct parameter type. In Mockito 1.x, `ArgumentMatcher` extended Hamcrest's `BaseMatcher` and `matches` always took `Object`. In Mockito 2+, `ArgumentMatcher` is a functional interface where `matches` takes `T`.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.MockConstructionToTryWithResources,Wrap `MockedConstruction` in try-with-resources,"Wraps `MockedConstruction` variable declarations that have explicit `.close()` calls into try-with-resources blocks, removing the explicit close call. This ensures proper resource management and makes the code cleaner.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.RemoveDoNothingForDefaultMocks,Remove `doNothing()` for void methods on `@Mock` fields,"Remove unnecessary `doNothing()` stubbings for void methods on `@Mock` fields. Mockito mocks already do nothing for void methods by default, making these stubbings redundant and triggering strict stubbing violations in Mockito 3+.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java deleted file mode 100644 index beff0a14f..000000000 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetFieldToJavaReflectionTest.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.InMemoryExecutionContext; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -class PowerMockWhiteboxGetFieldToJavaReflectionTest implements RewriteTest { - @Override - public void defaults(RecipeSpec spec) { - spec - .parser(JavaParser.fromJavaVersion() - .logCompilationWarningsAndErrors(true) - .classpathFromResources(new InMemoryExecutionContext(), - "powermock-core-1", - "powermock-reflect-1" - )) - .recipe(new PowerMockWhiteboxGetFieldToJavaReflection()); - } - - @DocumentExample - @Test - void getFieldReturnsFieldVariable() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name; - } - """ - ), - java( - """ - import java.lang.reflect.Field; - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Field f = Whitebox.getField(MyService.class, "name"); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void test() throws Exception { - Field f = MyService.class.getDeclaredField("name"); - f.setAccessible(true); - } - } - """ - ) - ); - } - - @Test - void noChangeForOtherWhiteboxApis() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - MyService service = new MyService(); - Whitebox.setInternalState(service, "name", "expectedValue"); - } - } - """ - ) - ); - } -} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java index 1b1877839..c1721c06f 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetInternalStateToJavaReflectionTest.java @@ -75,166 +75,4 @@ void testGetField() throws Exception { ) ); } - - @Test - void getInternalStateNonLiteralFieldName() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name = "hello"; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testGetField(String fieldName) { - MyService service = new MyService(); - String result = Whitebox.getInternalState(service, fieldName); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void testGetField(String fieldName) throws Exception { - MyService service = new MyService(); - Field reflectField = service.getClass().getDeclaredField(fieldName); - reflectField.setAccessible(true); - String result = (String) reflectField.get(service); - } - } - """ - ) - ); - } - - @Test - void twoNonLiteralGetInternalStateInSameBlock() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name = "hello"; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test(String f1, String f2) { - MyService service = new MyService(); - String a = Whitebox.getInternalState(service, f1); - String b = Whitebox.getInternalState(service, f2); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void test(String f1, String f2) throws Exception { - MyService service = new MyService(); - Field reflectField1 = service.getClass().getDeclaredField(f1); - reflectField1.setAccessible(true); - String a = (String) reflectField1.get(service); - Field reflectField = service.getClass().getDeclaredField(f2); - reflectField.setAccessible(true); - String b = (String) reflectField.get(service); - } - } - """ - ) - ); - } - - @Test - void getInternalStateWithWhereClass() { - //language=java - rewriteRun( - java( - """ - class Parent { - private String name = "hello"; - } - """ - ), - java( - """ - class Child extends Parent { - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Child child = new Child(); - String n = Whitebox.getInternalState(child, "name", Parent.class); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void test() throws Exception { - Child child = new Child(); - Field nameField = Parent.class.getDeclaredField("name"); - nameField.setAccessible(true); - String n = (String) nameField.get(child); - } - } - """ - ) - ); - } - - @Test - void getInternalStatePrimitiveResultUsesBoxedCast() { - //language=java - rewriteRun( - java( - """ - class MyService { - private int count = 3; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - MyService service = new MyService(); - int count = Whitebox.getInternalState(service, "count"); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void test() throws Exception { - MyService service = new MyService(); - Field countField = service.getClass().getDeclaredField("count"); - countField.setAccessible(true); - int count = (Integer) countField.get(service); - } - } - """ - ) - ); - } } diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java deleted file mode 100644 index d34572c0d..000000000 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxGetMethodToJavaReflectionTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.InMemoryExecutionContext; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -class PowerMockWhiteboxGetMethodToJavaReflectionTest implements RewriteTest { - @Override - public void defaults(RecipeSpec spec) { - spec - .parser(JavaParser.fromJavaVersion() - .logCompilationWarningsAndErrors(true) - .classpathFromResources(new InMemoryExecutionContext(), - "powermock-core-1", - "powermock-reflect-1" - )) - .recipe(new PowerMockWhiteboxGetMethodToJavaReflection()); - } - - @DocumentExample - @Test - void getMethodWithParamType() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String greet(String name) { return "Hello " + name; } - } - """ - ), - java( - """ - import java.lang.reflect.Method; - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Method m = Whitebox.getMethod(MyService.class, "greet", String.class); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - Method m = MyService.class.getDeclaredMethod("greet", String.class); - m.setAccessible(true); - } - } - """ - ) - ); - } - - @Test - void getMethodNoParamTypes() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String compute() { return "result"; } - } - """ - ), - java( - """ - import java.lang.reflect.Method; - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Method m = Whitebox.getMethod(MyService.class, "compute"); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - Method m = MyService.class.getDeclaredMethod("compute"); - m.setAccessible(true); - } - } - """ - ) - ); - } - - @Test - void getMethodPrimitiveParamType() { - //language=java - rewriteRun( - java( - """ - class MyService { - private int doubleIt(int value) { return value * 2; } - } - """ - ), - java( - """ - import java.lang.reflect.Method; - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Method m = Whitebox.getMethod(MyService.class, "doubleIt", int.class); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - Method m = MyService.class.getDeclaredMethod("doubleIt", int.class); - m.setAccessible(true); - } - } - """ - ) - ); - } -} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java deleted file mode 100644 index 3e7b8180f..000000000 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeConstructorToJavaReflectionTest.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.InMemoryExecutionContext; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; -import org.openrewrite.test.TypeValidation; - -import static org.openrewrite.java.Assertions.java; - -class PowerMockWhiteboxInvokeConstructorToJavaReflectionTest implements RewriteTest { - @Override - public void defaults(RecipeSpec spec) { - spec - .parser(JavaParser.fromJavaVersion() - .logCompilationWarningsAndErrors(true) - .classpathFromResources(new InMemoryExecutionContext(), - "powermock-core-1", - "powermock-reflect-1" - )) - .recipe(new PowerMockWhiteboxInvokeConstructorToJavaReflection()); - } - - @DocumentExample - @Test - void invokeConstructorNoArgs() { - //language=java - rewriteRun( - // The user type used as a generic/result type is generated by the template and cannot be - // attributed by the isolated template parser (a test-only artifact); the source is valid Java. - spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), - java( - """ - class MyService { - private MyService() { - } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - MyService s = Whitebox.invokeConstructor(MyService.class); - } - } - """, - """ - import java.lang.reflect.Constructor; - - class MyServiceTest { - void test() throws Exception { - Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(); - myServiceConstructor.setAccessible(true); - MyService s = myServiceConstructor.newInstance(); - } - } - """ - ) - ); - } - - @Test - void invokeConstructorWithResolvedParamTypes() { - //language=java - rewriteRun( - spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), - java( - """ - class MyService { - private MyService(String name, int age) { - } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - MyService s = Whitebox.invokeConstructor(MyService.class, "Alice", 42); - } - } - """, - """ - import java.lang.reflect.Constructor; - - class MyServiceTest { - void test() throws Exception { - Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(String.class, int.class); - myServiceConstructor.setAccessible(true); - MyService s = myServiceConstructor.newInstance("Alice", 42); - } - } - """ - ) - ); - } - - @Test - void invokeConstructorExplicitParamTypes() { - //language=java - rewriteRun( - spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), - java( - """ - class MyService { - private MyService(String name, int age) { - } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - MyService s = Whitebox.invokeConstructor(MyService.class, new Class[]{String.class, int.class}, new Object[]{"Alice", 42}); - } - } - """, - """ - import java.lang.reflect.Constructor; - - class MyServiceTest { - void test() throws Exception { - Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(String.class, int.class); - myServiceConstructor.setAccessible(true); - MyService s = myServiceConstructor.newInstance("Alice", 42); - } - } - """ - ) - ); - } - - @Test - void invokeConstructorAsStatement() { - //language=java - rewriteRun( - spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), - java( - """ - class MyService { - private MyService(String name) { - } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Whitebox.invokeConstructor(MyService.class, "Alice"); - } - } - """, - """ - import java.lang.reflect.Constructor; - - class MyServiceTest { - void test() throws Exception { - Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(String.class); - myServiceConstructor.setAccessible(true); - myServiceConstructor.newInstance("Alice"); - } - } - """ - ) - ); - } - - @Test - void invokeConstructorVarargsArrayNotMigrated() { - //language=java - rewriteRun( - java( - """ - class MyService { - private MyService(String a, String b) { - } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() throws Exception { - MyService s = Whitebox.invokeConstructor(MyService.class, new Object[]{"a", "b"}); - } - } - """ - ) - ); - } -} diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java index e9b4650b9..8e1da38c3 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxInvokeMethodToJavaReflectionTest.java @@ -280,257 +280,4 @@ void testInvokeWithInterfaceArg() throws Exception { ) ); } - - @Test - void invokeMethodNonLiteralMethodName() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String compute() { return "result"; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testInvoke(String methodName) { - MyService service = new MyService(); - Whitebox.invokeMethod(service, methodName); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void testInvoke(String methodName) throws Exception { - MyService service = new MyService(); - Method reflectMethod = service.getClass().getDeclaredMethod(methodName); - reflectMethod.setAccessible(true); - reflectMethod.invoke(service); - } - } - """ - ) - ); - } - - @Test - void invokeStaticMethodNoArgs() { - //language=java - rewriteRun( - java( - """ - class MyService { - private static String compute() { return "result"; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - String r = Whitebox.invokeMethod(MyService.class, "compute"); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - Method computeMethod = MyService.class.getDeclaredMethod("compute"); - computeMethod.setAccessible(true); - String r = (String) computeMethod.invoke(null); - } - } - """ - ) - ); - } - - @Test - void invokeStaticMethodWithPrimitiveArg() { - //language=java - rewriteRun( - java( - """ - class MyService { - private static String compute(int value) { return "" + value; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - String r = Whitebox.invokeMethod(MyService.class, "compute", 5); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - Method computeMethod = MyService.class.getDeclaredMethod("compute", int.class); - computeMethod.setAccessible(true); - String r = (String) computeMethod.invoke(null, 5); - } - } - """ - ) - ); - } - - @Test - void invokeStaticMethodAsStatement() { - //language=java - rewriteRun( - java( - """ - class MyService { - private static void doStuff(int value) { } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Whitebox.invokeMethod(MyService.class, "doStuff", 5); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - Method doStuffMethod = MyService.class.getDeclaredMethod("doStuff", int.class); - doStuffMethod.setAccessible(true); - doStuffMethod.invoke(null, 5); - } - } - """ - ) - ); - } - - @Test - void instanceAndStaticInvokeMethodInSameBlock() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String instanceCompute() { return "i"; } - private static String staticCompute() { return "s"; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - MyService service = new MyService(); - String a = Whitebox.invokeMethod(service, "instanceCompute"); - String b = Whitebox.invokeMethod(MyService.class, "staticCompute"); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - MyService service = new MyService(); - Method instanceComputeMethod = service.getClass().getDeclaredMethod("instanceCompute"); - instanceComputeMethod.setAccessible(true); - String a = (String) instanceComputeMethod.invoke(service); - Method staticComputeMethod = MyService.class.getDeclaredMethod("staticCompute"); - staticComputeMethod.setAccessible(true); - String b = (String) staticComputeMethod.invoke(null); - } - } - """ - ) - ); - } - - @Test - void invokeMethodPrimitiveResultUsesBoxedCast() { - //language=java - rewriteRun( - java( - """ - class MyService { - private int compute() { return 42; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - MyService service = new MyService(); - int r = Whitebox.invokeMethod(service, "compute"); - } - } - """, - """ - import java.lang.reflect.Method; - - class MyServiceTest { - void test() throws Exception { - MyService service = new MyService(); - Method computeMethod = service.getClass().getDeclaredMethod("compute"); - computeMethod.setAccessible(true); - int r = (Integer) computeMethod.invoke(service); - } - } - """ - ) - ); - } - - @Test - void invokeMethodExplicitParamTypesNotMigrated() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String greet(String name) { return name; } - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() throws Exception { - MyService service = new MyService(); - String r = Whitebox.invokeMethod(service, "greet", new Class[]{String.class}, new Object[]{"World"}); - } - } - """ - ) - ); - } } diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java index 614611144..90da77c7f 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java @@ -237,86 +237,4 @@ void testSetField() throws Throwable { ) ); } - - @Test - void setInternalStateNonLiteralFieldName() { - //language=java - rewriteRun( - java( - """ - class MyService { - private String name; - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void testSetField(String fieldName) { - MyService service = new MyService(); - Whitebox.setInternalState(service, fieldName, "expectedValue"); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void testSetField(String fieldName) throws Exception { - MyService service = new MyService(); - Field reflectField = service.getClass().getDeclaredField(fieldName); - reflectField.setAccessible(true); - reflectField.set(service, "expectedValue"); - } - } - """ - ) - ); - } - - @Test - void setInternalStateWithWhereClass() { - //language=java - rewriteRun( - java( - """ - class Parent { - private String name = "hello"; - } - """ - ), - java( - """ - class Child extends Parent { - } - """ - ), - java( - """ - import org.powermock.reflect.Whitebox; - - class MyServiceTest { - void test() { - Child child = new Child(); - Whitebox.setInternalState(child, "name", "newValue", Parent.class); - } - } - """, - """ - import java.lang.reflect.Field; - - class MyServiceTest { - void test() throws Exception { - Child child = new Child(); - Field nameField = Parent.class.getDeclaredField("name"); - nameField.setAccessible(true); - nameField.set(child, "newValue"); - } - } - """ - ) - ); - } } diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java index d8a2260a7..c58b48f4a 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java @@ -21,7 +21,6 @@ import org.openrewrite.java.JavaParser; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; -import org.openrewrite.test.TypeValidation; import static org.openrewrite.java.Assertions.java; @@ -78,16 +77,14 @@ void testSetField() throws Exception { } @Test - void mixedWhiteboxApisInSameMethod() { + void migratesSetGetAndInvokeInOneMethod() { //language=java rewriteRun( - // The user type used as a generic/result type is generated by the template and cannot be - // attributed by the isolated template parser (a test-only artifact); the source is valid Java. - spec -> spec.typeValidationOptions(TypeValidation.builder().identifiers(false).methodInvocations(false).build()), java( """ class MyService { private String name; + private String description = "d"; private String compute() { return name; } } """ @@ -100,14 +97,12 @@ class MyServiceTest { void test() { MyService service = new MyService(); Whitebox.setInternalState(service, "name", "newValue"); - String name = Whitebox.getInternalState(service, "name"); + String desc = Whitebox.getInternalState(service, "description"); String result = Whitebox.invokeMethod(service, "compute"); - MyService copy = Whitebox.invokeConstructor(MyService.class); } } """, """ - import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -117,15 +112,12 @@ void test() throws Exception { Field nameField = service.getClass().getDeclaredField("name"); nameField.setAccessible(true); nameField.set(service, "newValue"); - Field nameField1 = service.getClass().getDeclaredField("name"); - nameField1.setAccessible(true); - String name = (String) nameField1.get(service); + Field descriptionField = service.getClass().getDeclaredField("description"); + descriptionField.setAccessible(true); + String desc = (String) descriptionField.get(service); Method computeMethod = service.getClass().getDeclaredMethod("compute"); computeMethod.setAccessible(true); String result = (String) computeMethod.invoke(service); - Constructor myServiceConstructor = MyService.class.getDeclaredConstructor(); - myServiceConstructor.setAccessible(true); - MyService copy = myServiceConstructor.newInstance(); } } """ From 3b58f8f5542ea3fddeaa9427260a81cebf5409b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:30:08 +0000 Subject: [PATCH 3/3] Resolve merge conflicts: add 4-arg setInternalState support from main --- ...teboxSetInternalStateToJavaReflection.java | 20 +- .../PowerMockWhiteboxToJavaReflection.java | 404 ------------------ .../mockito/WhiteboxToReflectionVisitor.java | 7 + ...xSetInternalStateToJavaReflectionTest.java | 90 ++++ ...PowerMockWhiteboxToJavaReflectionTest.java | 6 +- 5 files changed, 113 insertions(+), 414 deletions(-) delete mode 100644 src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java index fe8111992..cc3d8035c 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflection.java @@ -35,14 +35,17 @@ public class PowerMockWhiteboxSetInternalStateToJavaReflection extends Recipe { private static final MethodMatcher SET_INTERNAL_STATE = new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object)"); + private static final MethodMatcher SET_INTERNAL_STATE_WHERE = + new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object, java.lang.Class)"); @Getter final String displayName = "Replace PowerMock `Whitebox.setInternalState()` with Java reflection"; @Getter - final String description = "Replace `Whitebox.setInternalState(Object, String, Object)` with `java.lang.reflect.Field` " + - "access. The field lookup uses `getDeclaredField` on the target object's class, which differs from " + - "PowerMock's class-hierarchy traversal for fields inherited from a superclass."; + final String description = "Replace `Whitebox.setInternalState(Object, String, Object)` and " + + "`Whitebox.setInternalState(Object, String, Object, Class)` with `java.lang.reflect.Field` access. " + + "The 3-arg overload looks up the field on the target's class; the 4-arg where-overload uses the " + + "supplied Class to resolve fields declared on a superclass."; @Override public TreeVisitor getVisitor() { @@ -52,7 +55,7 @@ public TreeVisitor getVisitor() { private static class SetInternalStateVisitor extends WhiteboxToReflectionVisitor { SetInternalStateVisitor() { - super("java.lang.reflect.Field", SET_INTERNAL_STATE); + super("java.lang.reflect.Field", SET_INTERNAL_STATE, SET_INTERNAL_STATE_WHERE); } @Override @@ -63,13 +66,20 @@ private static class SetInternalStateVisitor extends WhiteboxToReflectionVisitor return null; } String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER); - return fieldLookupPrefix(varName) + + String prefix = mi.getArguments().size() == 4 + ? fieldLookupPrefixWhere(varName) + : fieldLookupPrefix(varName); + return prefix + varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});"; } @Override Object[] buildArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { List args = mi.getArguments(); + if (args.size() == 4) { + // whereClass, fieldName, target, value + return new Object[]{args.get(3), args.get(1), args.get(0), args.get(2)}; + } // target, fieldName, target, value return new Object[]{args.get(0), args.get(1), args.get(0), args.get(2)}; } diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java deleted file mode 100644 index a267d144b..000000000 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.testing.mockito; - -import lombok.Getter; -import org.jspecify.annotations.Nullable; -import org.openrewrite.*; -import org.openrewrite.internal.ListUtils; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.JavaParser; -import org.openrewrite.java.JavaTemplate; -import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.search.UsesType; -import org.openrewrite.java.tree.*; -import org.openrewrite.marker.Markers; - -import java.util.ArrayList; -import java.util.List; - -import static java.util.Collections.emptyList; -import static org.openrewrite.Tree.randomId; -import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; -import static org.openrewrite.java.VariableNameUtils.generateVariableName; - -public class PowerMockWhiteboxToJavaReflection extends Recipe { - - private static final String WHITEBOX_FQN = "org.powermock.reflect.Whitebox"; - private static final MethodMatcher SET_INTERNAL_STATE = - new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object)"); - private static final MethodMatcher SET_INTERNAL_STATE_WHERE = - new MethodMatcher("org.powermock.reflect.Whitebox setInternalState(java.lang.Object, java.lang.String, java.lang.Object, java.lang.Class)"); - private static final MethodMatcher GET_INTERNAL_STATE = - new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String)"); - private static final MethodMatcher INVOKE_METHOD = - new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Object, java.lang.String, ..)"); - - @Getter - final String displayName = "Replace PowerMock `Whitebox` with Java reflection"; - - @Getter - final String description = "Replace `org.powermock.reflect.Whitebox` calls " + - "(`setInternalState`, `getInternalState`, `invokeMethod`) with plain Java reflection using " + - "`java.lang.reflect.Field` and `java.lang.reflect.Method`."; - - @Override - public TreeVisitor getVisitor() { - return Preconditions.check( - new UsesType<>(WHITEBOX_FQN, false), - new JavaIsoVisitor() { - - private static final String WHITEBOX_REPLACED = "whiteboxReplaced"; - private static final String NEEDS_FIELD_IMPORT = "needsFieldImport"; - private static final String NEEDS_METHOD_IMPORT = "needsMethodImport"; - - @Override - public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { - J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); - if (getCursor().getMessage(WHITEBOX_REPLACED, false)) { - md = addThrowsExceptionIfAbsent(md); - maybeRemoveImport(WHITEBOX_FQN); - if (getCursor().getMessage(NEEDS_FIELD_IMPORT, false)) { - maybeAddImport("java.lang.reflect.Field", false); - } - if (getCursor().getMessage(NEEDS_METHOD_IMPORT, false)) { - maybeAddImport("java.lang.reflect.Method", false); - } - return maybeAutoFormat(method, md, ctx); - } - return md; - } - - @Override - public J.Block visitBlock(J.Block block, ExecutionContext ctx) { - J.Block b = super.visitBlock(block, ctx); - - List statements = b.getStatements(); - // Process in reverse so that coordinate positions remain valid after each replacement - for (int i = statements.size() - 1; i >= 0; i--) { - Statement stmt = statements.get(i); - J.MethodInvocation mi = extractWhiteboxInvocation(stmt); - if (mi == null) { - continue; - } - Cursor blockCursor = new Cursor(getCursor().getParentOrThrow(), b); - JavaType.Method resolvedMethod = INVOKE_METHOD.matches(mi) ? - resolveTargetMethod(mi.getArguments()) : null; - String template = buildReplacementTemplate(stmt, mi, blockCursor, resolvedMethod); - if (template != null) { - Object[] templateArgs = buildTemplateArgs(mi, resolvedMethod); - - List templateImports = new ArrayList<>(); - templateImports.add("java.lang.reflect.Field"); - templateImports.add("java.lang.reflect.Method"); - if (resolvedMethod != null) { - for (JavaType paramType : resolvedMethod.getParameterTypes()) { - if (paramType instanceof JavaType.FullyQualified) { - JavaType.FullyQualified fq = (JavaType.FullyQualified) paramType; - if (!"java.lang".equals(fq.getPackageName())) { - templateImports.add(fq.getFullyQualifiedName()); - maybeAddImport(fq.getFullyQualifiedName()); - } - } - } - } - - b = JavaTemplate.builder(template) - .contextSensitive() - .javaParser(JavaParser.fromJavaVersion()) - .imports(templateImports.toArray(new String[0])) - .build() - .apply( - new Cursor(getCursor().getParentOrThrow(), b), - stmt.getCoordinates().replace(), - templateArgs - ); - getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, WHITEBOX_REPLACED, true); - if (SET_INTERNAL_STATE.matches(mi) || SET_INTERNAL_STATE_WHERE.matches(mi) || GET_INTERNAL_STATE.matches(mi)) { - getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_FIELD_IMPORT, true); - } - if (INVOKE_METHOD.matches(mi)) { - getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_METHOD_IMPORT, true); - } - // Re-read statements list since the block has been rebuilt - statements = b.getStatements(); - } - } - - return b; - } - - private @Nullable String buildReplacementTemplate(Statement statement, J.MethodInvocation mi, - Cursor scope, JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - - if (SET_INTERNAL_STATE.matches(mi) && args.size() == 3) { - return buildSetInternalStateTemplate(args, scope, false); - } - if (SET_INTERNAL_STATE_WHERE.matches(mi) && args.size() == 4) { - return buildSetInternalStateTemplate(args, scope, true); - } - if (GET_INTERNAL_STATE.matches(mi) && args.size() == 2) { - return buildGetInternalStateTemplate(args, statement, scope); - } - if (INVOKE_METHOD.matches(mi) && args.size() >= 2) { - return buildInvokeMethodTemplate(args, statement, scope, resolvedMethod); - } - return null; - } - - private Object[] buildTemplateArgs(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { - List args = mi.getArguments(); - - if (SET_INTERNAL_STATE.matches(mi) && args.size() == 3) { - // target, fieldName, target, value - return new Object[]{args.get(0), args.get(1), args.get(0), args.get(2)}; - } - if (SET_INTERNAL_STATE_WHERE.matches(mi) && args.size() == 4) { - // whereClass, fieldName, target, value - return new Object[]{args.get(3), args.get(1), args.get(0), args.get(2)}; - } - if (GET_INTERNAL_STATE.matches(mi) && args.size() == 2) { - // target, fieldName, target - return new Object[]{args.get(0), args.get(1), args.get(0)}; - } - if (INVOKE_METHOD.matches(mi) && args.size() >= 2) { - return buildInvokeMethodArgs(args, resolvedMethod); - } - return new Object[0]; - } - - private @Nullable String buildSetInternalStateTemplate(List args, Cursor scope, boolean where) { - String fieldName = extractStringLiteral(args.get(1)); - if (fieldName == null) { - return null; - } - String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER); - String lookup = where ? - "Field " + varName + " = #{any(java.lang.Class)}.getDeclaredField(#{any(java.lang.String)});\n" : - "Field " + varName + " = #{any(java.lang.Object)}.getClass().getDeclaredField(#{any(java.lang.String)});\n"; - return lookup + - varName + ".setAccessible(true);\n" + - varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});"; - } - - private @Nullable String buildGetInternalStateTemplate(List args, Statement statement, Cursor scope) { - String fieldName = extractStringLiteral(args.get(1)); - if (fieldName == null) { - return null; - } - String varName = generateVariableName(fieldName + "Field", scope, INCREMENT_NUMBER); - String prefix = "Field " + varName + " = #{any(java.lang.Object)}.getClass().getDeclaredField(#{any(java.lang.String)});\n" + - varName + ".setAccessible(true);\n"; - - if (statement instanceof J.VariableDeclarations) { - J.VariableDeclarations varDecls = (J.VariableDeclarations) statement; - String assignToVar = varDecls.getVariables().get(0).getSimpleName(); - String castType = getCastType(varDecls.getType()); - if (castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType)) { - return prefix + castType + " " + assignToVar + " = (" + castType + ") " + varName + ".get(#{any(java.lang.Object)});"; - } - return prefix + "Object " + assignToVar + " = " + varName + ".get(#{any(java.lang.Object)});"; - } - return prefix + varName + ".get(#{any(java.lang.Object)});"; - } - - private @Nullable String buildInvokeMethodTemplate(List args, Statement statement, - Cursor scope, JavaType.@Nullable Method resolvedMethod) { - String methodName = extractStringLiteral(args.get(1)); - if (methodName == null) { - return null; - } - String varName = generateVariableName(methodName + "Method", scope, INCREMENT_NUMBER); - - // getDeclaredMethod line - StringBuilder sb = new StringBuilder(); - sb.append("Method ").append(varName).append(" = #{any(java.lang.Object)}.getClass().getDeclaredMethod(#{any(java.lang.String)}"); - for (int i = 2; i < args.size(); i++) { - String classLiteral = getParamClassLiteral(args, i, resolvedMethod); - if (classLiteral != null) { - sb.append(", ").append(classLiteral); - } else { - sb.append(", #{any(java.lang.Object)}.getClass()"); - } - } - sb.append(");\n"); - - // setAccessible line - sb.append(varName).append(".setAccessible(true);\n"); - - // invoke line - if (statement instanceof J.VariableDeclarations) { - J.VariableDeclarations varDecls = (J.VariableDeclarations) statement; - String assignToVar = varDecls.getVariables().get(0).getSimpleName(); - String castType = getCastType(varDecls.getType()); - if (castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType)) { - sb.append(castType).append(" ").append(assignToVar).append(" = (").append(castType).append(") "); - } else { - sb.append("Object ").append(assignToVar).append(" = "); - } - } - sb.append(varName).append(".invoke(#{any(java.lang.Object)}"); - for (int i = 2; i < args.size(); i++) { - sb.append(", #{any(java.lang.Object)}"); - } - sb.append(");"); - - return sb.toString(); - } - - private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable Method resolvedMethod) { - int extraArgs = args.size() - 2; - int unresolvedCount = 0; - for (int i = 2; i < args.size(); i++) { - if (getParamClassLiteral(args, i, resolvedMethod) == null) { - unresolvedCount++; - } - } - Object[] result = new Object[2 + unresolvedCount + 1 + extraArgs]; - int idx = 0; - result[idx++] = args.get(0); // target for getDeclaredMethod - result[idx++] = args.get(1); // methodName - for (int i = 2; i < args.size(); i++) { - if (getParamClassLiteral(args, i, resolvedMethod) == null) { - result[idx++] = args.get(i); // arg.getClass() fallback for getDeclaredMethod - } - } - result[idx++] = args.get(0); // target for invoke - for (int i = 2; i < args.size(); i++) { - result[idx++] = args.get(i); // arg for invoke - } - return result; - } - - /** - * Get the class literal for a parameter at the given argument index. - * Prefers the resolved method's declared parameter type, falls back to the argument's - * compile-time type, and returns null if neither is available. - */ - private @Nullable String getParamClassLiteral(List args, int argIndex, - JavaType.@Nullable Method resolvedMethod) { - if (resolvedMethod != null) { - String literal = classLiteralFromType(resolvedMethod.getParameterTypes().get(argIndex - 2)); - if (literal != null) { - return literal; - } - } - return getClassLiteral(args.get(argIndex)); - } - - /** - * Resolve the target method from the first argument's type and the method name. - * Returns null if the method cannot be unambiguously resolved (not found, overloaded, - * or missing type information). - */ - private JavaType.@Nullable Method resolveTargetMethod(List args) { - if (args.size() <= 2) { - return null; - } - String methodName = extractStringLiteral(args.get(1)); - if (methodName == null) { - return null; - } - JavaType targetType = args.get(0).getType(); - if (!(targetType instanceof JavaType.FullyQualified)) { - return null; - } - int expectedParamCount = args.size() - 2; - JavaType.Method match = null; - for (JavaType.FullyQualified current = (JavaType.FullyQualified) targetType; - current != null; current = current.getSupertype()) { - for (JavaType.Method method : current.getMethods()) { - if (method.getName().equals(methodName) && - method.getParameterTypes().size() == expectedParamCount) { - if (match != null) { - return null; // ambiguous overload - } - match = method; - } - } - } - return match; - } - - private J.@Nullable MethodInvocation extractWhiteboxInvocation(Statement statement) { - if (statement instanceof J.MethodInvocation) { - J.MethodInvocation mi = (J.MethodInvocation) statement; - if (SET_INTERNAL_STATE.matches(mi) || SET_INTERNAL_STATE_WHERE.matches(mi) || - GET_INTERNAL_STATE.matches(mi) || INVOKE_METHOD.matches(mi)) { - return mi; - } - } - if (statement instanceof J.VariableDeclarations) { - J.VariableDeclarations varDecls = (J.VariableDeclarations) statement; - if (varDecls.getVariables().size() == 1) { - Expression init = varDecls.getVariables().get(0).getInitializer(); - if (init instanceof J.MethodInvocation) { - J.MethodInvocation mi = (J.MethodInvocation) init; - if (GET_INTERNAL_STATE.matches(mi) || INVOKE_METHOD.matches(mi)) { - return mi; - } - } - } - } - return null; - } - - private @Nullable String extractStringLiteral(Expression expr) { - if (expr instanceof J.Literal && ((J.Literal) expr).getValue() instanceof String) { - return (String) ((J.Literal) expr).getValue(); - } - return null; - } - - private @Nullable String getClassLiteral(Expression expr) { - return classLiteralFromType(expr.getType()); - } - - private @Nullable String classLiteralFromType(@Nullable JavaType type) { - if (type instanceof JavaType.Primitive) { - return ((JavaType.Primitive) type).getKeyword() + ".class"; - } - if (type instanceof JavaType.FullyQualified) { - return ((JavaType.FullyQualified) type).getClassName() + ".class"; - } - return null; - } - - private @Nullable String getCastType(@Nullable JavaType type) { - if (type instanceof JavaType.FullyQualified) { - return ((JavaType.FullyQualified) type).getClassName(); - } - if (type instanceof JavaType.Primitive) { - return ((JavaType.Primitive) type).getKeyword(); - } - return null; - } - - private J.MethodDeclaration addThrowsExceptionIfAbsent(J.MethodDeclaration md) { - if (md.getThrows() != null && md.getThrows().stream() - .anyMatch(j -> TypeUtils.isOfClassType(j.getType(), "java.lang.Exception") || - TypeUtils.isOfClassType(j.getType(), "java.lang.Throwable"))) { - return md; - } - JavaType.Class exceptionType = JavaType.ShallowClass.build("java.lang.Exception"); - return md.withThrows(ListUtils.concat(md.getThrows(), - new J.Identifier(randomId(), Space.SINGLE_SPACE, Markers.EMPTY, emptyList(), - exceptionType.getClassName(), exceptionType, null))); - } - }); - } -} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java b/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java index 2e648d8fe..1f121f85d 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/WhiteboxToReflectionVisitor.java @@ -235,6 +235,13 @@ String fieldLookupPrefix(String varName) { varName + ".setAccessible(true);\n"; } + // `Field = .getDeclaredField(); .setAccessible(true);` — + // used by the 4-arg setInternalState(target, field, value, Class) where-overload. + String fieldLookupPrefixWhere(String varName) { + return "Field " + varName + " = #{any(java.lang.Class)}.getDeclaredField(#{any(java.lang.String)});\n" + + varName + ".setAccessible(true);\n"; + } + // True when castType denotes a meaningful type to cast to (i.e. not null and not Object). boolean isNonObjectCast(@Nullable String castType) { return castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType); diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java index 90da77c7f..9405ad775 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxSetInternalStateToJavaReflectionTest.java @@ -76,6 +76,96 @@ void testSetField() throws Exception { ); } + @Test + void setInternalStateWithWhereClass() { + //language=java + rewriteRun( + java( + """ + class Parent { + private String name; + } + """ + ), + java( + """ + class Child extends Parent { + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Child child = new Child(); + Whitebox.setInternalState(child, "name", "newValue", Parent.class); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void test() throws Exception { + Child child = new Child(); + Field nameField = Parent.class.getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(child, "newValue"); + } + } + """ + ) + ); + } + + @Test + void setInternalStateWithWhereClassVariable() { + //language=java + rewriteRun( + java( + """ + class Parent { + private String name; + } + """ + ), + java( + """ + class Child extends Parent { + } + """ + ), + java( + """ + import org.powermock.reflect.Whitebox; + + class MyServiceTest { + void test() { + Child child = new Child(); + Class where = Parent.class; + Whitebox.setInternalState(child, "name", "newValue", where); + } + } + """, + """ + import java.lang.reflect.Field; + + class MyServiceTest { + void test() throws Exception { + Child child = new Child(); + Class where = Parent.class; + Field nameField = where.getDeclaredField("name"); + nameField.setAccessible(true); + nameField.set(child, "newValue"); + } + } + """ + ) + ); + } + @Test void throwsExceptionNotDuplicatedWhenAlreadyPresent() { //language=java diff --git a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java index cf0f70698..8f3d75895 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java @@ -77,9 +77,6 @@ void testSetField() throws Exception { } @Test -<<<<<<< HEAD - void migratesSetGetAndInvokeInOneMethod() { -======= void setInternalStateWithWhereClass() { //language=java rewriteRun( @@ -170,8 +167,7 @@ void test() throws Exception { } @Test - void getInternalStateWithAssignment() { ->>>>>>> origin/main + void migratesSetGetAndInvokeInOneMethod() { //language=java rewriteRun( java(