What version of OpenRewrite are you using?
I am using
- rewrite-testing-frameworks 3.34.0, 3.35.2
What is the smallest, simplest way to reproduce the problem?
Copilot-assisted test case:
import org.junit.jupiter.api.Test
import org.openrewrite.java.Assertions.java
import org.openrewrite.java.JavaParser
import org.openrewrite.java.testing.mockito.PowerMockitoMockStaticToMockito
import org.openrewrite.test.RewriteTest
import org.openrewrite.test.TypeValidation
/*
Description: PowerMockitoMockStaticToMockito recipe throws ClassCastException when @PrepareForTest uses the fullyQualifiedNames attribute with a string literal instead of the value attribute with a Class array. The extractPrepareForTestClasses method at line 124 casts the J.Assignment's right-hand side to J.NewArray without an instanceof check, crashing when the value is a J.Literal (string).
Expected behavior: The recipe should gracefully handle @PrepareForTest(fullyQualifiedNames = "...") without crashing. Since fullyQualifiedNames uses strings rather than Class references, the recipe should skip this attribute (it cannot produce MockedStatic fields from string names) and leave the source unchanged.
Actual behavior: java.lang.ClassCastException: class org.openrewrite.java.tree.J$Literal cannot be cast to class org.openrewrite.java.tree.J$NewArray at PowerMockitoMockStaticToMockito$PowerMockitoToMockitoVisitor.lambda$extractPrepareForTestClasses$0(PowerMockitoMockStaticToMockito.java:124). The recipe crashes after running for ~1 hour on a large repo, failing while processing a test file that uses @PrepareForTest(fullyQualifiedNames = "...").
Tested versions: rewrite-recipe-bom 3.28.0 (rewrite-testing-frameworks 3.34.0), confirmed bug still present on main branch as of 2026-04-30. Also affects rewrite-testing-frameworks 3.35.2 (latest release).
External references: Recipe source: https://github.com/openrewrite/rewrite-testing-frameworks/blob/main/src/main/java/org/openrewrite/java/testing/mockito/PowerMockitoMockStaticToMockito.java - The bug is in the extractPrepareForTestClasses method. The condition `if (a instanceof J.Assignment && ((J.NewArray) ((J.Assignment) a).getAssignment()).getInitializer() != null)` performs an unchecked cast. Fix: add `((J.Assignment) a).getAssignment() instanceof J.NewArray &&` before the cast. No existing issue found as of 2026-04-30.
*/
class PowerMockitoPrepareForTestFullyQualifiedNamesCrashTest : RewriteTest {
companion object {
// Stub types for PowerMock annotations and runner, so the parser can resolve them
//language=java
private val PREPARE_FOR_TEST_STUB = """
package org.powermock.core.classloader.annotations;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrepareForTest {
Class<?>[] value() default {};
String[] fullyQualifiedNames() default {};
}
""".trimIndent()
//language=java
private val POWER_MOCK_RUNNER_STUB = """
package org.powermock.modules.junit4;
public class PowerMockRunner extends org.junit.runner.Runner {
public PowerMockRunner(Class<?> clazz) {}
public org.junit.runner.Description getDescription() { return null; }
public void run(org.junit.runner.notification.RunNotifier notifier) {}
}
""".trimIndent()
//language=java
private val POWER_MOCKITO_STUB = """
package org.powermock.api.mockito;
public class PowerMockito {
public static <T> org.mockito.stubbing.OngoingStubbing<T> when(T methodCall) { return null; }
public static void mockStatic(Class<?>... classes) {}
}
""".trimIndent()
//language=java
private val MOCKITO_STUB = """
package org.mockito;
public class Mockito {
public static <T> org.mockito.MockedStatic<T> mockStatic(Class<T> classToMock) { return null; }
public static <T> org.mockito.stubbing.OngoingStubbing<T> when(T methodCall) { return null; }
}
""".trimIndent()
//language=java
private val MOCKED_STATIC_STUB = """
package org.mockito;
public interface MockedStatic<T> extends org.mockito.ScopedMock {
void close();
void closeOnDemand();
}
""".trimIndent()
//language=java
private val SCOPED_MOCK_STUB = """
package org.mockito;
public interface ScopedMock extends AutoCloseable {
void close();
void closeOnDemand();
}
""".trimIndent()
//language=java
private val ONGOING_STUBBING_STUB = """
package org.mockito.stubbing;
public interface OngoingStubbing<T> {
OngoingStubbing<T> thenReturn(T value);
}
""".trimIndent()
//language=java
private val JUNIT_TEST_STUB = """
package org.junit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
Class<? extends Throwable> expected() default Test.None.class;
class None extends Throwable {}
}
""".trimIndent()
//language=java
private val JUNIT_RUNNER_STUB = """
package org.junit.runner;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunWith {
Class<? extends Runner> value();
}
""".trimIndent()
//language=java
private val RUNNER_STUB = """
package org.junit.runner;
public abstract class Runner {
public abstract Description getDescription();
public abstract void run(org.junit.runner.notification.RunNotifier notifier);
}
""".trimIndent()
//language=java
private val DESCRIPTION_STUB = """
package org.junit.runner;
public class Description implements java.io.Serializable {}
""".trimIndent()
//language=java
private val RUN_NOTIFIER_STUB = """
package org.junit.runner.notification;
public class RunNotifier {}
""".trimIndent()
}
override fun defaults(spec: org.openrewrite.test.RecipeSpec) {
spec.recipe(PowerMockitoMockStaticToMockito())
.parser(
JavaParser.fromJavaVersion()
.dependsOn(
PREPARE_FOR_TEST_STUB, POWER_MOCK_RUNNER_STUB, POWER_MOCKITO_STUB,
MOCKITO_STUB, MOCKED_STATIC_STUB, SCOPED_MOCK_STUB, ONGOING_STUBBING_STUB,
JUNIT_TEST_STUB, JUNIT_RUNNER_STUB, RUNNER_STUB, DESCRIPTION_STUB, RUN_NOTIFIER_STUB
)
)
.typeValidationOptions(
TypeValidation.builder()
.identifiers(false)
.methodInvocations(false)
.cursorAcyclic(false)
.build()
)
}
/**
* Reproduces ClassCastException when @PrepareForTest uses fullyQualifiedNames attribute
* with a single string literal.
*
* Root cause: In extractPrepareForTestClasses(), when processing annotation arguments:
* if (a instanceof J.Assignment &&
* ((J.NewArray) ((J.Assignment) a).getAssignment()).getInitializer() != null)
*
* The code checks `a instanceof J.Assignment` (true for `fullyQualifiedNames = "..."`),
* then immediately casts the assignment value to J.NewArray. But with a string literal,
* the assignment value is a J.Literal, causing:
* ClassCastException: J$Literal cannot be cast to J$NewArray
*
*/
@Test
fun prepareForTestWithFullyQualifiedNamesStringLiteralShouldNotCrash() {
rewriteRun(
java(
"""
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest(fullyQualifiedNames = "com.example.SomeClass")
public class MyTest {
@Test
public void testSomething() {
}
}
""".trimIndent()
)
)
}
/**
* With fullyQualifiedNames as an array of strings, the J.Assignment value IS a J.NewArray
* (of J.Literal strings), so the cast doesn't crash. However, the recipe removes the
* @PrepareForTest annotation without creating MockedStatic fields since string literals
* don't carry Class type information. This is a secondary concern.
*/
@Test
fun prepareForTestWithFullyQualifiedNamesStringArrayRemovesAnnotation() {
rewriteRun(
java(
"""
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest(fullyQualifiedNames = {"com.example.Foo", "com.example.Bar"})
public class MyTest {
@Test
public void testSomething() {
}
}
""".trimIndent(),
"""
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
public class MyTest {
@Test
public void testSomething() {
}
}
""".trimIndent()
)
)
}
/**
* Verifies the normal case: @PrepareForTest with value attribute using a single class
* (no curly braces) is handled correctly. This uses J.FieldAccess for the argument.
* The recipe removes the annotation (which is its purpose).
*/
@Test
fun prepareForTestWithSingleClassValueDoesNotCrash() {
rewriteRun(
java(
"""
import java.util.Calendar;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest(Calendar.class)
public class MyTest {
@Test
public void testSomething() {
}
}
""".trimIndent(),
"""
import java.util.Calendar;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
public class MyTest {
@Test
public void testSomething() {
}
}
""".trimIndent()
)
)
}
}
What version of OpenRewrite are you using?
I am using
What is the smallest, simplest way to reproduce the problem?
Copilot-assisted test case: