From dfdc2d05d4ad5e2a89c4e0c9c03919479869e139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Merlin=20B=C3=B6gershausen?= Date: Mon, 8 Jun 2026 14:59:27 +0200 Subject: [PATCH] Expand PowerMockWhiteboxToJavaReflection to cover more Whitebox APIs Adds migration of getField, getMethod, invokeConstructor, static invokeMethod(Class,..), the where-Class get/setInternalState overloads, and non-literal field/method names, alongside the existing setInternalState/getInternalState/invokeMethod support. Primitive results are cast through their wrapper type, and the explicit Class[]/varargs-array overloads of invokeMethod/invokeConstructor are deliberately left untouched (no faithful mechanical translation). --- .../PowerMockWhiteboxToJavaReflection.java | 632 +++++++++++--- .../resources/META-INF/rewrite/recipes.csv | 2 +- ...PowerMockWhiteboxToJavaReflectionTest.java | 822 ++++++++++++++++++ 3 files changed, 1349 insertions(+), 107 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java index f181125d6..2dfd05fe1 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflection.java @@ -28,7 +28,11 @@ import org.openrewrite.marker.Markers; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; import static java.util.Collections.emptyList; import static org.openrewrite.Tree.randomId; @@ -44,14 +48,62 @@ public class PowerMockWhiteboxToJavaReflection extends Recipe { 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, ..)"); + private static final MethodMatcher INVOKE_METHOD_STATIC = + new MethodMatcher("org.powermock.reflect.Whitebox invokeMethod(java.lang.Class, java.lang.String, ..)"); + 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_WHERE = + new MethodMatcher("org.powermock.reflect.Whitebox getInternalState(java.lang.Object, java.lang.String, java.lang.Class)"); + private static final MethodMatcher GET_FIELD = + new MethodMatcher("org.powermock.reflect.Whitebox getField(java.lang.Class, java.lang.String)"); + private static final MethodMatcher GET_METHOD = + new MethodMatcher("org.powermock.reflect.Whitebox getMethod(java.lang.Class, java.lang.String, ..)"); + 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[])"); + + private static final List BASE_IMPORTS = Arrays.asList( + "java.lang.reflect.Field", "java.lang.reflect.Method", "java.lang.reflect.Constructor"); + + 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"); + } @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`."; + final String 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."; + + /** + * 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. + */ + private static final class ResultSink { + private final @Nullable String varName; + private final @Nullable String castType; + + private ResultSink(@Nullable String varName, @Nullable String castType) { + this.varName = varName; + this.castType = castType; + } + } @Override public TreeVisitor getVisitor() { @@ -62,6 +114,7 @@ public TreeVisitor getVisitor() { private static final String WHITEBOX_REPLACED = "whiteboxReplaced"; private static final String NEEDS_FIELD_IMPORT = "needsFieldImport"; private static final String NEEDS_METHOD_IMPORT = "needsMethodImport"; + private static final String NEEDS_CONSTRUCTOR_IMPORT = "needsConstructorImport"; @Override public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { @@ -75,6 +128,9 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex if (getCursor().getMessage(NEEDS_METHOD_IMPORT, false)) { maybeAddImport("java.lang.reflect.Method", false); } + if (getCursor().getMessage(NEEDS_CONSTRUCTOR_IMPORT, false)) { + maybeAddImport("java.lang.reflect.Constructor", false); + } return maybeAutoFormat(method, md, ctx); } return md; @@ -84,8 +140,9 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex public J.Block visitBlock(J.Block block, ExecutionContext ctx) { J.Block b = super.visitBlock(block, ctx); + // Replace Whitebox calls that are themselves a statement or a single-variable declaration + // initializer. Process in reverse so coordinates remain valid after each replacement. 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); @@ -93,44 +150,20 @@ public J.Block visitBlock(J.Block block, ExecutionContext ctx) { 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); + JavaType.Method resolvedMethod = resolveFor(mi); + String template = buildReplacementTemplate(mi, sinkFromStatement(stmt), 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])) + .imports(templateImports(resolvedMethod).toArray(new String[0])) .build() .apply( new Cursor(getCursor().getParentOrThrow(), b), stmt.getCoordinates().replace(), - templateArgs + buildTemplateArgs(mi, resolvedMethod) ); - 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); - } + recordReplacement(mi, resolvedMethod); // Re-read statements list since the block has been rebuilt statements = b.getStatements(); } @@ -139,18 +172,98 @@ public J.Block visitBlock(J.Block block, ExecutionContext ctx) { return b; } - private @Nullable String buildReplacementTemplate(Statement statement, J.MethodInvocation mi, + private JavaType.@Nullable Method resolveFor(J.MethodInvocation mi) { + if (INVOKE_METHOD.matches(mi)) { + return resolveTargetMethod(mi.getArguments()); + } + if (INVOKE_METHOD_STATIC.matches(mi)) { + return resolveStaticTargetMethod(mi.getArguments()); + } + if (INVOKE_CONSTRUCTOR_ARGS.matches(mi)) { + return resolveTargetConstructor(mi.getArguments()); + } + return null; + } + + private void recordReplacement(J.MethodInvocation mi, JavaType.@Nullable Method resolvedMethod) { + getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, WHITEBOX_REPLACED, true); + if (needsFieldImport(mi)) { + getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_FIELD_IMPORT, true); + } + if (needsMethodImport(mi)) { + getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_METHOD_IMPORT, true); + } + if (needsConstructorImport(mi)) { + getCursor().putMessageOnFirstEnclosing(J.MethodDeclaration.class, NEEDS_CONSTRUCTOR_IMPORT, true); + } + for (String paramImport : resolvedParamImports(resolvedMethod)) { + maybeAddImport(paramImport); + } + } + + private List templateImports(JavaType.@Nullable Method resolvedMethod) { + List imports = new ArrayList<>(BASE_IMPORTS); + 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 @Nullable String buildReplacementTemplate(J.MethodInvocation mi, ResultSink sink, Cursor scope, JavaType.@Nullable Method resolvedMethod) { List args = mi.getArguments(); if (SET_INTERNAL_STATE.matches(mi) && args.size() == 3) { return buildSetInternalStateTemplate(args, scope); } + if (SET_INTERNAL_STATE_WHERE.matches(mi) && args.size() == 4) { + return buildSetInternalStateWhereTemplate(args, scope); + } if (GET_INTERNAL_STATE.matches(mi) && args.size() == 2) { - return buildGetInternalStateTemplate(args, statement, scope); + return buildGetInternalStateTemplate(args, sink, scope); + } + if (GET_INTERNAL_STATE_WHERE.matches(mi) && args.size() == 3) { + return buildGetInternalStateWhereTemplate(args, sink, scope); + } + if (GET_FIELD.matches(mi) && args.size() == 2) { + return buildGetFieldTemplate(args, sink, scope); + } + if (GET_METHOD.matches(mi) && args.size() >= 2) { + return buildGetMethodTemplate(args, sink, scope); } if (INVOKE_METHOD.matches(mi) && args.size() >= 2) { - return buildInvokeMethodTemplate(args, statement, scope, resolvedMethod); + return buildInvokeMethodTemplate(args, sink, scope, resolvedMethod, false); + } + if (INVOKE_METHOD_STATIC.matches(mi) && args.size() >= 2) { + return buildInvokeMethodTemplate(args, sink, scope, resolvedMethod, true); + } + if (INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi) && args.size() == 3) { + return buildInvokeConstructorTemplate(args, sink, scope, null, true); + } + if (INVOKE_CONSTRUCTOR_ARGS.matches(mi)) { + return buildInvokeConstructorTemplate(args, sink, scope, resolvedMethod, false); } return null; } @@ -162,59 +275,146 @@ private Object[] buildTemplateArgs(J.MethodInvocation mi, JavaType.@Nullable Met // 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) { + // where, 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 (GET_INTERNAL_STATE_WHERE.matches(mi) && args.size() == 3) { + // where, fieldName, target + return new Object[]{args.get(2), args.get(1), args.get(0)}; + } + if (GET_FIELD.matches(mi) && args.size() == 2) { + // declaringClass, fieldName + return new Object[]{args.get(0), args.get(1)}; + } + if (GET_METHOD.matches(mi) && args.size() >= 2) { + // declaringClass, methodName, paramType0, paramType1, ... + return args.toArray(); + } if (INVOKE_METHOD.matches(mi) && args.size() >= 2) { - return buildInvokeMethodArgs(args, resolvedMethod); + return buildInvokeMethodArgs(args, resolvedMethod, false); + } + if (INVOKE_METHOD_STATIC.matches(mi) && args.size() >= 2) { + return buildInvokeMethodArgs(args, resolvedMethod, true); + } + if (INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi) && args.size() == 3) { + return buildInvokeConstructorArgs(args, null, true); + } + if (INVOKE_CONSTRUCTOR_ARGS.matches(mi)) { + return buildInvokeConstructorArgs(args, resolvedMethod, false); } 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" + + private String buildSetInternalStateTemplate(List args, Cursor scope) { + String varName = fieldVarName(args.get(1), scope); + return fieldLookupPrefix(varName, "#{any(java.lang.Object)}.getClass()") + 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) { + private String buildGetInternalStateTemplate(List args, ResultSink sink, Cursor scope) { + String varName = fieldVarName(args.get(1), scope); + return fieldLookupPrefix(varName, "#{any(java.lang.Object)}.getClass()") + fieldGetTail(varName, sink); + } + + private String buildGetInternalStateWhereTemplate(List args, ResultSink sink, Cursor scope) { + String varName = fieldVarName(args.get(1), scope); + return fieldLookupPrefix(varName, "#{any(java.lang.Class)}") + fieldGetTail(varName, sink); + } + + private String buildSetInternalStateWhereTemplate(List args, Cursor scope) { + String varName = fieldVarName(args.get(1), scope); + return fieldLookupPrefix(varName, "#{any(java.lang.Class)}") + + varName + ".set(#{any(java.lang.Object)}, #{any(java.lang.Object)});"; + } + + // `Field = .getDeclaredField(); .setAccessible(true);` — shared by the + // instance (receiver `obj.getClass()`) and `Class where` (receiver the where-class) get/set variants. + private String fieldLookupPrefix(String varName, String fieldReceiver) { + return "Field " + varName + " = " + fieldReceiver + ".getDeclaredField(#{any(java.lang.String)});\n" + + varName + ".setAccessible(true);\n"; + } + + private String buildGetFieldTemplate(List args, ResultSink sink, Cursor scope) { + String varName = resultLocalName(sink, args.get(1), scope, true); + return "Field " + varName + " = #{any(java.lang.Class)}.getDeclaredField(#{any(java.lang.String)});\n" + + varName + ".setAccessible(true);"; + } + + private @Nullable String buildGetMethodTemplate(List args, ResultSink sink, Cursor scope) { + if (hasArrayArg(args, 2)) { + // Explicit Class[] varargs array is not supported; leave for manual migration (flagged downstream) 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"; + 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(); + } - 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)});"; + /** + * Build the trailing {@code Field.get(...)} statement for a {@code getInternalState} replacement, + * 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)});"; + } + + // True when castType denotes a meaningful type to cast to (i.e. not null and not Object). + private boolean isNonObjectCast(@Nullable String castType) { + return castType != null && !"Object".equals(castType) && !"java.lang.Object".equals(castType); + } + + /** + * For {@code getField}/{@code getMethod}, whose result type IS the reflective object, reuse the + * result variable as the local; otherwise generate one. + */ + private 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); + } + + private 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 prefix + "Object " + assignToVar + " = " + varName + ".get(#{any(java.lang.Object)});"; } - return prefix + varName + ".get(#{any(java.lang.Object)});"; + return false; } - private @Nullable String buildInvokeMethodTemplate(List args, Statement statement, - Cursor scope, JavaType.@Nullable Method resolvedMethod) { - String methodName = extractStringLiteral(args.get(1)); - if (methodName == null) { + private @Nullable String buildInvokeMethodTemplate(List args, ResultSink sink, + Cursor scope, JavaType.@Nullable Method resolvedMethod, boolean isStatic) { + 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; } - String varName = generateVariableName(methodName + "Method", scope, INCREMENT_NUMBER); + 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(" = #{any(java.lang.Object)}.getClass().getDeclaredMethod(#{any(java.lang.String)}"); + 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) { @@ -229,17 +429,14 @@ private Object[] buildTemplateArgs(J.MethodInvocation mi, JavaType.@Nullable Met 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(") "); + 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(assignToVar).append(" = "); + sb.append("Object ").append(sink.varName).append(" = "); } } - sb.append(varName).append(".invoke(#{any(java.lang.Object)}"); + sb.append(varName).append(".invoke(").append(invokeTarget); for (int i = 2; i < args.size(); i++) { sb.append(", #{any(java.lang.Object)}"); } @@ -248,28 +445,131 @@ private Object[] buildTemplateArgs(J.MethodInvocation mi, JavaType.@Nullable Met return sb.toString(); } - private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable Method resolvedMethod) { - int extraArgs = args.size() - 2; - int unresolvedCount = 0; + private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable Method resolvedMethod, boolean isStatic) { + 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) { - unresolvedCount++; + result.add(args.get(i)); // arg.getClass() fallback for getDeclaredMethod } } - 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 + if (!isStatic) { + result.add(args.get(0)); // target for invoke (static uses the null literal, no placeholder) + } 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.add(args.get(i)); // arg for invoke + } + return result.toArray(); + } + + private @Nullable String buildInvokeConstructorTemplate(List args, ResultSink sink, Cursor scope, + JavaType.@Nullable Method resolvedCtor, boolean explicit) { + 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, resolvedCtor, 1); + sb.append(classLiteral != null ? classLiteral : "#{any(java.lang.Object)}.getClass()"); + } + newInstanceArgCount = args.size() - 1; } - result[idx++] = args.get(0); // target for invoke - for (int i = 2; i < args.size(); i++) { - result[idx++] = args.get(i); // arg for invoke + sb.append(");\n"); + sb.append(varName).append(".setAccessible(true);\n"); + sb.append(constructorNewInstanceTail(varName, sink, elem != null, newInstanceArgCount)); + return sb.toString(); + } + + private Object[] buildInvokeConstructorArgs(List args, JavaType.@Nullable Method resolvedCtor, boolean explicit) { + List result = new ArrayList<>(); + result.add(args.get(0)); // getDeclaredConstructor receiver (Class) + if (explicit) { + 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, resolvedCtor, 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 result; + 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); } /** @@ -279,10 +579,24 @@ private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable */ private @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...). + */ + private @Nullable String getParamClassLiteral(List args, int argIndex, + JavaType.@Nullable Method resolvedMethod, int firstParamArgIndex) { if (resolvedMethod != null) { - String literal = classLiteralFromType(resolvedMethod.getParameterTypes().get(argIndex - 2)); - if (literal != null) { - return literal; + 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)); @@ -297,35 +611,80 @@ private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable if (args.size() <= 2) { return null; } - String methodName = extractStringLiteral(args.get(1)); - if (methodName == 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; } - JavaType targetType = args.get(0).getType(); - if (!(targetType instanceof JavaType.FullyQualified)) { + 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; } - 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; + 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; + } + + /** + * 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; } + /** + * Extract the element type {@code X} from a {@code Class}-typed expression (e.g. {@code MyService.class}). + */ + private 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; + } + 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)) { + if (isMigratableWhitebox(mi)) { return mi; } } @@ -335,7 +694,7 @@ private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable 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)) { + if (isMigratableWhiteboxResult(mi)) { return mi; } } @@ -344,6 +703,58 @@ private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable return null; } + private boolean isMigratableWhitebox(J.MethodInvocation mi) { + return SET_INTERNAL_STATE.matches(mi) || SET_INTERNAL_STATE_WHERE.matches(mi) || + GET_INTERNAL_STATE.matches(mi) || GET_INTERNAL_STATE_WHERE.matches(mi) || + GET_FIELD.matches(mi) || GET_METHOD.matches(mi) || + INVOKE_METHOD.matches(mi) || INVOKE_METHOD_STATIC.matches(mi) || + INVOKE_CONSTRUCTOR_ARGS.matches(mi) || INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi); + } + + // Whitebox calls whose result can initialize a variable declaration (i.e. not the void setters). + private boolean isMigratableWhiteboxResult(J.MethodInvocation mi) { + return isMigratableWhitebox(mi) && + !SET_INTERNAL_STATE.matches(mi) && !SET_INTERNAL_STATE_WHERE.matches(mi); + } + + private boolean needsFieldImport(J.MethodInvocation mi) { + return SET_INTERNAL_STATE.matches(mi) || SET_INTERNAL_STATE_WHERE.matches(mi) || + GET_INTERNAL_STATE.matches(mi) || GET_INTERNAL_STATE_WHERE.matches(mi) || + GET_FIELD.matches(mi); + } + + private boolean needsMethodImport(J.MethodInvocation mi) { + return INVOKE_METHOD.matches(mi) || INVOKE_METHOD_STATIC.matches(mi) || GET_METHOD.matches(mi); + } + + private boolean needsConstructorImport(J.MethodInvocation mi) { + return INVOKE_CONSTRUCTOR_ARGS.matches(mi) || INVOKE_CONSTRUCTOR_EXPLICIT.matches(mi); + } + + /** + * 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. + */ + private 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}. + */ + private 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); + } + private @Nullable String extractStringLiteral(Expression expr) { if (expr instanceof J.Literal && ((J.Literal) expr).getValue() instanceof String) { return (String) ((J.Literal) expr).getValue(); @@ -375,6 +786,15 @@ private Object[] buildInvokeMethodArgs(List args, JavaType.@Nullable 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. + */ + private 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/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 4ec584044..2b9ee4b3d 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -201,7 +201,7 @@ 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.",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/PowerMockWhiteboxToJavaReflectionTest.java b/src/test/java/org/openrewrite/java/testing/mockito/PowerMockWhiteboxToJavaReflectionTest.java index 711343365..78e3fa9d4 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; @@ -519,6 +520,827 @@ 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 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 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 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 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 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); + } + } + """ + ) + ); + } + + @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 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"); + } + } + """ + ) + ); + } + + @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 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 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); + } + } + """ + ) + ); + } + + @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"}); + } + } + """ + ) + ); + } + + @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"}); + } + } + """ + ) + ); + } + @Test void noChangeWhenWhiteboxNotUsed() { //language=java