Skip to content

PowerMockitoMockStaticToMockito throws when @PrepareForTest uses fullyQualifiedNames with String literal #982

@nmck257

Description

@nmck257

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()
            )
        )
    }
}

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions