From 66ba4f60c3eae84eddd81a57e8902338b73daa62 Mon Sep 17 00:00:00 2001 From: Anshuman Mishra Date: Sat, 20 Dec 2025 22:41:01 -0800 Subject: [PATCH 1/9] Add JUnit 4 to JUnit 5 migration precondition recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a scanning recipe that identifies and marks JUnit 4 test classes eligible for migration to JUnit 5. This precondition recipe analyzes test classes to determine migratability based on: - Detection of supported vs. unsupported JUnit 4 rules and runners - Analysis of class inheritance hierarchies to ensure parent and child test classes use only supported features - Identification of @Parameters annotations with class-type source attributes that cannot be migrated - Validation that all ancestors in the class hierarchy are also migratable The recipe marks only those test classes that can be successfully migrated, preventing build or test failures post-migration. This ensures fully automated, safe migrations by excluding classes with unsupported features from the migration process. This precondition recipe is designed to work with declarative JUnit 4 to JUnit 5 migration recipes, ensuring the migration recipe only modifies classes that are guaranteed to migrate successfully. Note: This recipe has been successfully used at Uber for large-scale JUnit 4 to JUnit 5 migrations. Upstreaming to benefit the broader OpenRewrite community. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../junit5/JUnit4ToJunit5Precondition.java | 306 +++++++++++ .../java/testing/junit5/Junit4Utils.java | 1 + .../JUnit4ToJunit5PreconditionTest.java | 506 ++++++++++++++++++ 3 files changed, 813 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java create mode 100644 src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java new file mode 100644 index 000000000..488df7cfd --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -0,0 +1,306 @@ +/* + * Copyright 2024 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.junit5; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.trait.Annotated; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.marker.SearchResult; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A search recipe that identifies JUnit 4 test classes migratable to JUnit 5 and marks them for + * inclusion in the JUnit 4 to JUnit 5 migration recipe. + * + *

It marks classes that: + * + *

+ */ +@EqualsAndHashCode(callSuper = false) +@Value +public class JUnit4ToJunit5Precondition extends ScanningRecipe { + private static final String HAS_UNSUPPORTED_RULE = "hasUnsupportedRule"; + private static final String HAS_UNSUPPORTED_RUNNER = "hasUnsupportedRunner"; + private static final String HAS_CLASS_TYPE_SOURCE_ATTRIBUTE = "hasClassTypeSourceAttribute"; + + @Option(displayName = "Known migratable classes", + description = "A list of classes which are migratable. These are the classes for which recipes already exist. In practical scenarios, these are parent test classes for which we already have JUnit 5 versions.") + @Nullable Set knownMigratableClasses; + @Option(displayName = "Supported rules", + description = "Rules for which migration recipes exist.") + @Nullable Set supportedRules; + @Option(displayName = "Supported rule types", + description = "Recipe exist for rule types and all their inheriting rules (e.g., ExternalRules).") + @Nullable Set supportedRuleTypes; + @Option(displayName = "Supported runners", + description = "Runners for which migration recipes exist.") + @Nullable Set supportedRunners; + + @Override + public String getDisplayName() { + return "JUnit 4 to 5 Precondition"; + } + + @Override + public String getDescription() { + return "Marks JUnit 4 test classes that can be migrated to JUnit 5 with current recipe " + + "capabilities, including detection of unsupported rules, runners, and @Parameters annotations with " + + "class-type source attributes."; + } + + @Override + public MigratabilityAccumulator getInitialValue(ExecutionContext ctx) { + return new MigratabilityAccumulator(); + } + + @Override + public TreeVisitor getScanner(MigratabilityAccumulator acc) { + return new JUnit4ToJunit5PreconditionScanner(acc); + } + + @Override + public TreeVisitor getVisitor(MigratabilityAccumulator acc) { + return new JUnit4ToJunit5PreconditionVisitor(acc); + } + + + private class JUnit4ToJunit5PreconditionVisitor extends JavaIsoVisitor { + private final MigratabilityAccumulator accumulator; + + private JUnit4ToJunit5PreconditionVisitor(MigratabilityAccumulator accumulator) { + this.accumulator = accumulator; + } + + @Override + public J.ClassDeclaration visitClassDeclaration( + J.ClassDeclaration classDecl, ExecutionContext ctx) { + if (classDecl.getType() == null) { // missing type attribution, possibly parsing error. + return classDecl; + } + String fullQualifiedClassName = classDecl.getType().getFullyQualifiedName(); + return accumulator.isMigratable(fullQualifiedClassName) + ? SearchResult.found(classDecl) + : classDecl; + } + + boolean extendsSupportedJUnit4BaseTestClass(J.ClassDeclaration classDecl) { + return emptySetIfNull(knownMigratableClasses).stream(). + anyMatch(testBaseClass -> TypeUtils.isAssignableTo(testBaseClass, classDecl.getType())); + } + } + + /** + * A visitor that implements the scanning logic for identifying JUnit 4 test classes that can be + * migrated to JUnit 5. + */ + private class JUnit4ToJunit5PreconditionScanner extends JavaIsoVisitor { + + private final MigratabilityAccumulator accumulator; + + private JUnit4ToJunit5PreconditionScanner(MigratabilityAccumulator accumulator) { + this.accumulator = accumulator; + } + + @Override + public J.ClassDeclaration visitClassDeclaration( + J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + markSupportedRunner(cd, ctx); + + if (classDecl.getType() != null) { + this.accumulator.registerClass( + classDecl.getType().getFullyQualifiedName(), + getParentClassName(classDecl), + !hasUnsupportedFeatures()); + } + return classDecl; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration( + J.MethodDeclaration methodDecl, ExecutionContext ctx) { + // Flag @Parameters annotations with class-type source attributes + flagParametersAnnotationWithClassTypeSourceAttribute(methodDecl, ctx); + if (hasJunit4Rules(methodDecl) && methodDecl.getMethodType() != null) { + flagUnsupportedRule(methodDecl.getMethodType().getReturnType()); + } + return methodDecl; + } + + @Override + public J.VariableDeclarations visitVariableDeclarations( + J.VariableDeclarations variableDeclarations, ExecutionContext ctx) { + if (!hasJunit4Rules(variableDeclarations)) { + return variableDeclarations; + } + return super.visitVariableDeclarations(variableDeclarations, ctx); + } + + @Override + public J.VariableDeclarations.NamedVariable visitVariable( + J.VariableDeclarations.NamedVariable variable, ExecutionContext ctx) { + if (variable.getInitializer() != null) { + flagUnsupportedRule(variable.getInitializer().getType()); + } + return variable; + } + + // Flag @Parameters annotations with class-type source attributes + private void flagParametersAnnotationWithClassTypeSourceAttribute( + J.MethodDeclaration methodDecl, ExecutionContext ctx) { + Cursor methodCursor = getCursor(); + new Annotated.Matcher("junitparams.Parameters") + .asVisitor(a -> + (new JavaIsoVisitor() { + @Override + public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) { + Expression variable = assignment.getVariable(); + if (variable instanceof J.Identifier) { + J.Identifier identifier = (J.Identifier) variable; + if ("source".equals(identifier.getSimpleName()) + && assignment.getAssignment() instanceof J.FieldAccess + && "class".equals(((J.FieldAccess) assignment.getAssignment()).getSimpleName())) { + methodCursor.dropParentUntil(J.ClassDeclaration.class::isInstance) + .putMessage(HAS_CLASS_TYPE_SOURCE_ATTRIBUTE, true); + } + } + return assignment; + } + }).visit(a.getTree(), ctx)) + .visit(methodDecl, ctx); + } + + private void flagUnsupportedRule(JavaType javaType) { + JavaType.FullyQualified ruleType = TypeUtils.asFullyQualified(javaType); + if (ruleType != null + && !emptySetIfNull(supportedRules).contains(ruleType.getFullyQualifiedName()) + && emptySetIfNull(supportedRuleTypes).stream().noneMatch(s -> TypeUtils.isAssignableTo(s, javaType))) { + getCursor() + .dropParentUntil(J.ClassDeclaration.class::isInstance) + .putMessage(HAS_UNSUPPORTED_RULE, true); + } + } + + private void markSupportedRunner( + J.ClassDeclaration classDecl, ExecutionContext ctx) { + Cursor classCursor = getCursor(); + new Annotated.Matcher(Junit4Utils.RUN_WITH_ANNOTATION).asVisitor(a -> + (new JavaIsoVisitor() { + @Override + public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) { + JavaType.FullyQualified type = + TypeUtils.asFullyQualified(fieldAccess.getTarget().getType()); + if (type == null) { // missing type attribution, possibly parsing error + return fieldAccess; + } + if (!emptySetIfNull(supportedRunners).contains(type.getFullyQualifiedName())) { + classCursor.putMessage(HAS_UNSUPPORTED_RUNNER, true); + } + return SearchResult.found(fieldAccess); + } + }).visit(a.getTree(), ctx)) + .visit(classDecl, ctx); + } + + private boolean hasUnsupportedFeatures() { + return Boolean.TRUE.equals(getCursor().getMessage(HAS_UNSUPPORTED_RULE)) + || Boolean.TRUE.equals(getCursor().getMessage(HAS_UNSUPPORTED_RUNNER)) + || Boolean.TRUE.equals(getCursor().getMessage(HAS_CLASS_TYPE_SOURCE_ATTRIBUTE)); + } + + private @Nullable String getParentClassName(J.ClassDeclaration classDecl) { + if (classDecl.getExtends() != null) { + JavaType.FullyQualified parentClass = + TypeUtils.asFullyQualified(classDecl.getExtends().getType()); + if (parentClass != null) { + return parentClass.getFullyQualifiedName(); + } + } + return null; + } + + private boolean hasJunit4Rules(J j) { + return matchRule(j, Junit4Utils.RULE) || matchRule(j, Junit4Utils.CLASS_RULE); + } + + private boolean matchRule(J j, String rule) { + return new Annotated.Matcher(rule) + .asVisitor((a, found) -> { + found.set(true); + return a.getTree(); + }).reduce(j, new AtomicBoolean(false)).get(); + } + } + + /** Accumulator to store migratability information of classes. */ + public class MigratabilityAccumulator { + + private final Map classToParentMap = new HashMap<>(); + private final Map declaredMigratable = emptySetIfNull(knownMigratableClasses).stream() + .collect(Collectors.toMap(Function.identity(), key -> Boolean.TRUE)); + + /** Registers a class with its parent and whether it is migratable. */ + public void registerClass(String className, @Nullable String parentClassName, boolean isMigratable) { + classToParentMap.put(className, parentClassName); + if (isMigratable) { + declaredMigratable.put(className, true); + if (parentClassName != null) { + declaredMigratable.putIfAbsent(parentClassName, false); + } + } else { + // Mark this class and all its ancestors as non-migratable + String currentClass = className; + while (currentClass != null && !emptySetIfNull(knownMigratableClasses).contains(currentClass)) { + declaredMigratable.put(currentClass, false); + currentClass = classToParentMap.get(currentClass); + } + } + } + + /** Returns true if the class and all of its ancestors are declared migratable. */ + public boolean isMigratable(String className) { + String currentClass = className; + while (currentClass != null) { + Boolean isMigratable = declaredMigratable.get(currentClass); + if (isMigratable == null || !isMigratable) { + return false; + } + currentClass = classToParentMap.get(currentClass); + } + return true; + } + } + + private static Set emptySetIfNull(@Nullable Set set) { + return set == null ? Collections.emptySet() : set; + } +} diff --git a/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java b/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java index cbfa2b799..c9c43f5cd 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java @@ -37,6 +37,7 @@ public class Junit4Utils { static final String RULE = "org.junit.Rule"; static final String RUN_WITH = "org.junit.runner.RunWith"; static final String TEST = "org.junit.Test"; + static final String RUN_WITH_ANNOTATION = "@" + RUN_WITH; static Set classAnnotations() { return new HashSet<>(Arrays.asList(RUN_WITH, FIX_METHOD_ORDER, IGNORE)); diff --git a/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java b/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java new file mode 100644 index 000000000..5b8d71794 --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java @@ -0,0 +1,506 @@ +/* + * Copyright 2024 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.junit5; + +import static org.openrewrite.java.Assertions.java; + +import org.junit.jupiter.api.Test; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import java.util.Set; + +class JUnit4ToJunit5PreconditionTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new JUnit4ToJunit5Precondition( + Set.of("MigratableBaseTestClass"), + Set.of("org.junit.rules.TemporaryFolder"), + Set.of("org.junit.rules.ExternalResource"), + Set.of("org.junit.runners.Parameterized"))) + .parser( + JavaParser.fromJavaVersion() + .classpathFromResources(new InMemoryExecutionContext(),"junit-4", "junit-jupiter-api-5") + // language=java + .dependsOn( + """ + public class MigratableBaseTestClass { + } + """, + """ + public class NonMigratableBaseTestClass { + } + """, + """ + package io.grpc.testing; + + import org.junit.rules.ExternalResource; + + public class GrpcCleanupRule extends ExternalResource { + } + """, + // Stubbing for junitparams.Parameters + """ + package junitparams; + + import java.lang.annotation.*; + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + public @interface Parameters { + Class source(); + } + """)); + } + + @Test + void extendsMigratableBaseTestClass() { + rewriteRun( + // language=java + java( + """ + import com.uber.fievel.testing.base.FievelTestBase; + import org.junit.Test; + + public class Junit4Test extends MigratableBaseTestClass { + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """, + """ + import com.uber.fievel.testing.base.FievelTestBase; + import org.junit.Test; + + /*~~>*/public class Junit4Test extends MigratableBaseTestClass { + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void hasSupportedRule() { + rewriteRun( + // language=java + java( + """ + import org.junit.Rule; + import org.junit.Test; + import org.junit.rules.TemporaryFolder; + + public class Junit4Test { + @Rule TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """, + """ + import org.junit.Rule; + import org.junit.Test; + import org.junit.rules.TemporaryFolder; + + /*~~>*/public class Junit4Test { + @Rule TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void hasSupportedRunner() { + rewriteRun( + // language=java + java( + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.junit.runners.Parameterized; + + @RunWith(Parameterized.class) + public class Junit4Test { + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """, + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.junit.runners.Parameterized; + + /*~~>*/@RunWith(Parameterized.class) + public class Junit4Test { + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void extendsNonMigratableBaseTestClass() { + rewriteRun( + // language=java + java( + """ + import org.junit.Test; + + public class Junit4Test extends NonMigratableBaseTestClass { + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void abstractTestBaseClass() { + rewriteRun( + // language=java + java( + """ + import org.junit.Before; + + public abstract class AbstractTest { + @Before + public void setup() { + System.out.println("Hello, world!"); + } + } + """, + """ + import org.junit.Before; + + /*~~>*/public abstract class AbstractTest { + @Before + public void setup() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void unsupportedRunner() { + rewriteRun( + // language=java + java( + """ + import org.junit.experimental.theories.Theories; + import org.junit.runner.RunWith; + import org.junit.Test; + + @RunWith(Theories.class) + public class Junit4Test { + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void unsupportedRule() { + rewriteRun( + // language=java + java( + """ + import org.junit.Rule; + import org.junit.rules.ErrorCollector; + import org.junit.Test; + + public class Junit4Test { + @Rule public ErrorCollector rule = new ErrorCollector(); + @Test + public void test() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void abstractTestAndSupportedRunnerTest() { + // language=java + rewriteRun( + java( + """ + import org.junit.Before; + + public abstract class AbstractTest { + @Before + public void setup() { + System.out.println("Setup method"); + } + } + """, + """ + import org.junit.Before; + + /*~~>*/public abstract class AbstractTest { + @Before + public void setup() { + System.out.println("Setup method"); + } + } + """), + java( + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.junit.runners.Parameterized; + + @RunWith(Parameterized.class) + public class SupportedRunnerTest extends AbstractTest { + @Test + public void test() { + System.out.println("Test method"); + } + } + """, + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.junit.runners.Parameterized; + + /*~~>*/@RunWith(Parameterized.class) + public class SupportedRunnerTest extends AbstractTest { + @Test + public void test() { + System.out.println("Test method"); + } + } + """)); + } + + @Test + void abstractTestAndUnsupportedRuleTest() { + // language=java + rewriteRun( + java( + // no change since extended by an unmigratable class + """ + import org.junit.Before; + + public abstract class AbstractTest { + @Before + public void setup() { + System.out.println("Setup method"); + } + } + """), + java( + """ + import org.junit.Rule; + import org.junit.rules.ErrorCollector; + import org.junit.Test; + + public class UnsupportedRuleTest extends AbstractTest { + @Rule public ErrorCollector rule = new ErrorCollector(); + @Test + public void test() { + System.out.println("Test method"); + } + } + """)); + } + + @Test + void abstractTestSupportedRuleTestAndUnsupportedRunnerTest() { + // language=java + rewriteRun( + java( + // no change since one of the test class (UnSupportedRunnerTest) in group can't be migrated. + """ + import org.junit.Before; + + public abstract class AbstractTest { + @Before + public void setup() { + System.out.println("Setup method"); + } + } + """), + java( + """ + import org.junit.Rule; + import org.junit.rules.TemporaryFolder; + import org.junit.Test; + + public class SupportedRuleTest extends AbstractTest { + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void test() { + System.out.println("Test method"); + } + } + """), + java( + """ + import org.junit.experimental.theories.Theories; + import org.junit.runner.RunWith; + import org.junit.Test; + + @RunWith(Theories.class) + public class UnSupportedRunnerTest extends AbstractTest { + @Test + public void test() { + System.out.println("Test method"); + } + } + """)); + } + + @Test + void twoClassesWithSupportedAndUnsupportedRules() { + // language=java + rewriteRun( + java( + """ + import org.junit.Rule; + import org.junit.Test; + import org.junit.rules.TemporaryFolder; + + public class SupportedRulesClass { + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void test() { + System.out.println("Test with supported rule"); + } + } + """, + """ + import org.junit.Rule; + import org.junit.Test; + import org.junit.rules.TemporaryFolder; + + /*~~>*/public class SupportedRulesClass { + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void test() { + System.out.println("Test with supported rule"); + } + } + """), + java( + """ + import org.junit.Rule; + import org.junit.Test; + import org.junit.rules.ErrorCollector; + + public class UnsupportedRulesClass { + @Rule public ErrorCollector errorCollector = new ErrorCollector(); + @Test + public void test() { + System.out.println("Test with unsupported rule"); + } + } + """)); + } + + @Test + void supportedRunnerSubclassesTest() { + rewriteRun( + // language=java + java( + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.mockito.junit.MockitoJUnitRunner; + @RunWith(MockitoJUnitRunner.StrictStubs.class) + public class SupportedRunnerSubclassTest { + @Test + public void test() { + System.out.println("Test with another supported runner"); + } + } + """, + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.mockito.junit.MockitoJUnitRunner; + /*~~>*/@RunWith(MockitoJUnitRunner.StrictStubs.class) + public class SupportedRunnerSubclassTest { + @Test + public void test() { + System.out.println("Test with another supported runner"); + } + } + """)); + } + + @Test + void unsupportedParametersAnnotationWithClassTypeSourceAttribute() { + rewriteRun( + // language=java + java( + // no change since the @Parameters annotation has a class-type for its source attribute + """ + import org.junit.Rule; + import org.junit.Test; + import org.junit.rules.TemporaryFolder; + import junitparams.Parameters; + + public class Junit4Test{ + @Rule TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + @Parameters(source = String.class) + public void test() { + System.out.println("Hello, world!"); + } + } + """)); + } + + @Test + void supportedRuleTypes() { + rewriteRun( + // language=java + java( + """ + import org.junit.Rule; + import io.grpc.testing.GrpcCleanupRule; + + public class ExternalResourceRule { + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + } + """, + """ + import org.junit.Rule; + import io.grpc.testing.GrpcCleanupRule; + + /*~~>*/public class ExternalResourceRule { + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + } + """)); + } +} From 68bbcff71cbb67c7e7158eb4cd686db5b473dff4 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 10:25:21 +0100 Subject: [PATCH 2/9] Apply immediate best practices --- .../junit5/JUnit4ToJunit5Precondition.java | 53 +++++++++------- .../JUnit4ToJunit5PreconditionTest.java | 63 ++++++++++++------- 2 files changed, 72 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index 488df7cfd..eccb98bee 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -27,10 +27,14 @@ import org.openrewrite.java.tree.TypeUtils; import org.openrewrite.marker.SearchResult; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import java.util.stream.Collectors; + +import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.toMap; /** * A search recipe that identifies JUnit 4 test classes migratable to JUnit 5 and marks them for @@ -52,16 +56,19 @@ public class JUnit4ToJunit5Precondition extends ScanningRecipe knownMigratableClasses; - @Option(displayName = "Supported rules", + + @Option(example = "TODO Provide a usage example for the docs", displayName = "Supported rules", description = "Rules for which migration recipes exist.") @Nullable Set supportedRules; - @Option(displayName = "Supported rule types", + + @Option(example = "TODO Provide a usage example for the docs", displayName = "Supported rule types", description = "Recipe exist for rule types and all their inheriting rules (e.g., ExternalRules).") @Nullable Set supportedRuleTypes; - @Option(displayName = "Supported runners", + + @Option(example = "TODO Provide a usage example for the docs", displayName = "Supported runners", description = "Runners for which migration recipes exist.") @Nullable Set supportedRunners; @@ -72,9 +79,9 @@ public String getDisplayName() { @Override public String getDescription() { - return "Marks JUnit 4 test classes that can be migrated to JUnit 5 with current recipe " - + "capabilities, including detection of unsupported rules, runners, and @Parameters annotations with " - + "class-type source attributes."; + return "Marks JUnit 4 test classes that can be migrated to JUnit 5 with current recipe " + + "capabilities, including detection of unsupported rules, runners, and @Parameters annotations with " + + "class-type source attributes."; } @Override @@ -107,9 +114,9 @@ public J.ClassDeclaration visitClassDeclaration( return classDecl; } String fullQualifiedClassName = classDecl.getType().getFullyQualifiedName(); - return accumulator.isMigratable(fullQualifiedClassName) - ? SearchResult.found(classDecl) - : classDecl; + return accumulator.isMigratable(fullQualifiedClassName) ? + SearchResult.found(classDecl) : + classDecl; } boolean extendsSupportedJUnit4BaseTestClass(J.ClassDeclaration classDecl) { @@ -186,9 +193,9 @@ public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ct Expression variable = assignment.getVariable(); if (variable instanceof J.Identifier) { J.Identifier identifier = (J.Identifier) variable; - if ("source".equals(identifier.getSimpleName()) - && assignment.getAssignment() instanceof J.FieldAccess - && "class".equals(((J.FieldAccess) assignment.getAssignment()).getSimpleName())) { + if ("source".equals(identifier.getSimpleName()) && + assignment.getAssignment() instanceof J.FieldAccess && + "class".equals(((J.FieldAccess) assignment.getAssignment()).getSimpleName())) { methodCursor.dropParentUntil(J.ClassDeclaration.class::isInstance) .putMessage(HAS_CLASS_TYPE_SOURCE_ATTRIBUTE, true); } @@ -201,9 +208,9 @@ public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ct private void flagUnsupportedRule(JavaType javaType) { JavaType.FullyQualified ruleType = TypeUtils.asFullyQualified(javaType); - if (ruleType != null - && !emptySetIfNull(supportedRules).contains(ruleType.getFullyQualifiedName()) - && emptySetIfNull(supportedRuleTypes).stream().noneMatch(s -> TypeUtils.isAssignableTo(s, javaType))) { + if (ruleType != null && + !emptySetIfNull(supportedRules).contains(ruleType.getFullyQualifiedName()) && + emptySetIfNull(supportedRuleTypes).stream().noneMatch(s -> TypeUtils.isAssignableTo(s, javaType))) { getCursor() .dropParentUntil(J.ClassDeclaration.class::isInstance) .putMessage(HAS_UNSUPPORTED_RULE, true); @@ -232,9 +239,9 @@ public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContex } private boolean hasUnsupportedFeatures() { - return Boolean.TRUE.equals(getCursor().getMessage(HAS_UNSUPPORTED_RULE)) - || Boolean.TRUE.equals(getCursor().getMessage(HAS_UNSUPPORTED_RUNNER)) - || Boolean.TRUE.equals(getCursor().getMessage(HAS_CLASS_TYPE_SOURCE_ATTRIBUTE)); + return Boolean.TRUE.equals(getCursor().getMessage(HAS_UNSUPPORTED_RULE)) || + Boolean.TRUE.equals(getCursor().getMessage(HAS_UNSUPPORTED_RUNNER)) || + Boolean.TRUE.equals(getCursor().getMessage(HAS_CLASS_TYPE_SOURCE_ATTRIBUTE)); } private @Nullable String getParentClassName(J.ClassDeclaration classDecl) { @@ -266,7 +273,7 @@ public class MigratabilityAccumulator { private final Map classToParentMap = new HashMap<>(); private final Map declaredMigratable = emptySetIfNull(knownMigratableClasses).stream() - .collect(Collectors.toMap(Function.identity(), key -> Boolean.TRUE)); + .collect(toMap(Function.identity(), key -> Boolean.TRUE)); /** Registers a class with its parent and whether it is migratable. */ public void registerClass(String className, @Nullable String parentClassName, boolean isMigratable) { @@ -301,6 +308,6 @@ public boolean isMigratable(String className) { } private static Set emptySetIfNull(@Nullable Set set) { - return set == null ? Collections.emptySet() : set; + return set == null ? emptySet() : set; } } diff --git a/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java b/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java index 5b8d71794..33cff0ea4 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java @@ -15,9 +15,8 @@ */ package org.openrewrite.java.testing.junit5; -import static org.openrewrite.java.Assertions.java; - import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.java.JavaParser; import org.openrewrite.test.RecipeSpec; @@ -25,6 +24,8 @@ import java.util.Set; +import static org.openrewrite.java.Assertions.java; + class JUnit4ToJunit5PreconditionTest implements RewriteTest { @Override @@ -69,6 +70,7 @@ public class GrpcCleanupRule extends ExternalResource { """)); } + @DocumentExample @Test void extendsMigratableBaseTestClass() { rewriteRun( @@ -95,7 +97,8 @@ public void test() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -128,7 +131,8 @@ public void test() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -161,7 +165,8 @@ public void test() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -178,7 +183,8 @@ public void test() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -205,7 +211,8 @@ public void setup() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -225,7 +232,8 @@ public void test() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -245,7 +253,8 @@ public void test() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -272,7 +281,8 @@ public void setup() { System.out.println("Setup method"); } } - """), + """ + ), java( """ import org.junit.Test; @@ -299,7 +309,8 @@ public void test() { System.out.println("Test method"); } } - """)); + """ + )); } @Test @@ -317,7 +328,8 @@ public void setup() { System.out.println("Setup method"); } } - """), + """ + ), java( """ import org.junit.Rule; @@ -331,7 +343,8 @@ public void test() { System.out.println("Test method"); } } - """)); + """ + )); } @Test @@ -349,7 +362,8 @@ public void setup() { System.out.println("Setup method"); } } - """), + """ + ), java( """ import org.junit.Rule; @@ -363,7 +377,8 @@ public void test() { System.out.println("Test method"); } } - """), + """ + ), java( """ import org.junit.experimental.theories.Theories; @@ -377,7 +392,8 @@ public void test() { System.out.println("Test method"); } } - """)); + """ + )); } @Test @@ -410,7 +426,8 @@ public void test() { System.out.println("Test with supported rule"); } } - """), + """ + ), java( """ import org.junit.Rule; @@ -424,7 +441,8 @@ public void test() { System.out.println("Test with unsupported rule"); } } - """)); + """ + )); } @Test @@ -455,7 +473,8 @@ public void test() { System.out.println("Test with another supported runner"); } } - """)); + """ + )); } @Test @@ -478,7 +497,8 @@ public void test() { System.out.println("Hello, world!"); } } - """)); + """ + )); } @Test @@ -501,6 +521,7 @@ public class ExternalResourceRule { /*~~>*/public class ExternalResourceRule { @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); } - """)); + """ + )); } } From 9d468482f36e9580c2c21bb9350a3c933fdd15e3 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 10:31:21 +0100 Subject: [PATCH 3/9] Add example values from tests --- .../junit5/JUnit4ToJunit5Precondition.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index eccb98bee..b9c87a689 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -56,20 +56,25 @@ public class JUnit4ToJunit5Precondition extends ScanningRecipe knownMigratableClasses; - @Option(example = "TODO Provide a usage example for the docs", displayName = "Supported rules", - description = "Rules for which migration recipes exist.") + @Option(displayName = "Supported rules", + description = "Rules for which migration recipes exist.", + example = "org.junit.rules.TemporaryFolder") @Nullable Set supportedRules; - @Option(example = "TODO Provide a usage example for the docs", displayName = "Supported rule types", - description = "Recipe exist for rule types and all their inheriting rules (e.g., ExternalRules).") + @Option(displayName = "Supported rule types", + description = "Recipe exist for rule types and all their inheriting rules (e.g., ExternalRules).", + example = "org.junit.rules.ExternalResource") @Nullable Set supportedRuleTypes; - @Option(example = "TODO Provide a usage example for the docs", displayName = "Supported runners", - description = "Runners for which migration recipes exist.") + @Option(displayName = "Supported runners", + description = "Runners for which migration recipes exist.", + example = "org.junit.runners.Parameterized") @Nullable Set supportedRunners; @Override From ba6e5a00b81cb39226505a1cb5597f4b4eca9381 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 10:36:54 +0100 Subject: [PATCH 4/9] Remove unused method --- .../java/testing/junit5/JUnit4ToJunit5Precondition.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index b9c87a689..887103923 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -123,11 +123,6 @@ public J.ClassDeclaration visitClassDeclaration( SearchResult.found(classDecl) : classDecl; } - - boolean extendsSupportedJUnit4BaseTestClass(J.ClassDeclaration classDecl) { - return emptySetIfNull(knownMigratableClasses).stream(). - anyMatch(testBaseClass -> TypeUtils.isAssignableTo(testBaseClass, classDecl.getType())); - } } /** From 902db8cecc3ff94f293ee3565f09fca3109993cc Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 12:28:45 +0100 Subject: [PATCH 5/9] Inline annotation pattern that's unlikely to change --- .../java/testing/junit5/JUnit4ToJunit5Precondition.java | 2 +- .../java/org/openrewrite/java/testing/junit5/Junit4Utils.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index 887103923..4e8cfc7db 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -220,7 +220,7 @@ private void flagUnsupportedRule(JavaType javaType) { private void markSupportedRunner( J.ClassDeclaration classDecl, ExecutionContext ctx) { Cursor classCursor = getCursor(); - new Annotated.Matcher(Junit4Utils.RUN_WITH_ANNOTATION).asVisitor(a -> + new Annotated.Matcher("@org.junit.runner.RunWith").asVisitor(a -> (new JavaIsoVisitor() { @Override public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) { diff --git a/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java b/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java index c9c43f5cd..cbfa2b799 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/Junit4Utils.java @@ -37,7 +37,6 @@ public class Junit4Utils { static final String RULE = "org.junit.Rule"; static final String RUN_WITH = "org.junit.runner.RunWith"; static final String TEST = "org.junit.Test"; - static final String RUN_WITH_ANNOTATION = "@" + RUN_WITH; static Set classAnnotations() { return new HashSet<>(Arrays.asList(RUN_WITH, FIX_METHOD_ORDER, IGNORE)); From cb89d3af46dd26cfc9085b9c5ddc0875b0348a57 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 12:43:55 +0100 Subject: [PATCH 6/9] Adopt `AnnotationMatcher` --- .../junit5/JUnit4ToJunit5Precondition.java | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index 4e8cfc7db..dc390ed64 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -19,7 +19,9 @@ import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.*; +import org.openrewrite.java.AnnotationMatcher; import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.service.AnnotationService; import org.openrewrite.java.trait.Annotated; import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; @@ -30,7 +32,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import static java.util.Collections.emptySet; @@ -52,6 +53,9 @@ @EqualsAndHashCode(callSuper = false) @Value public class JUnit4ToJunit5Precondition extends ScanningRecipe { + + private static final AnnotationMatcher ANY_RULE_ANNOTATION_MATCHER = new AnnotationMatcher("@org.junit.*Rule", true); + private static final String HAS_UNSUPPORTED_RULE = "hasUnsupportedRule"; private static final String HAS_UNSUPPORTED_RUNNER = "hasUnsupportedRunner"; private static final String HAS_CLASS_TYPE_SOURCE_ATTRIBUTE = "hasClassTypeSourceAttribute"; @@ -85,7 +89,7 @@ public String getDisplayName() { @Override public String getDescription() { return "Marks JUnit 4 test classes that can be migrated to JUnit 5 with current recipe " + - "capabilities, including detection of unsupported rules, runners, and @Parameters annotations with " + + "capabilities, including detection of unsupported rules, runners, and `@Parameters` annotations with " + "class-type source attributes."; } @@ -101,28 +105,18 @@ public TreeVisitor getScanner(MigratabilityAccumulator acc) @Override public TreeVisitor getVisitor(MigratabilityAccumulator acc) { - return new JUnit4ToJunit5PreconditionVisitor(acc); - } - - - private class JUnit4ToJunit5PreconditionVisitor extends JavaIsoVisitor { - private final MigratabilityAccumulator accumulator; - - private JUnit4ToJunit5PreconditionVisitor(MigratabilityAccumulator accumulator) { - this.accumulator = accumulator; - } - - @Override - public J.ClassDeclaration visitClassDeclaration( - J.ClassDeclaration classDecl, ExecutionContext ctx) { - if (classDecl.getType() == null) { // missing type attribution, possibly parsing error. - return classDecl; + return new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + if (classDecl.getType() == null) { // missing type attribution, possibly parsing error. + return classDecl; + } + String fullQualifiedClassName = classDecl.getType().getFullyQualifiedName(); + return acc.isMigratable(fullQualifiedClassName) ? + SearchResult.found(classDecl) : + classDecl; } - String fullQualifiedClassName = classDecl.getType().getFullyQualifiedName(); - return accumulator.isMigratable(fullQualifiedClassName) ? - SearchResult.found(classDecl) : - classDecl; - } + }; } /** @@ -256,26 +250,22 @@ private boolean hasUnsupportedFeatures() { } private boolean hasJunit4Rules(J j) { - return matchRule(j, Junit4Utils.RULE) || matchRule(j, Junit4Utils.CLASS_RULE); - } - - private boolean matchRule(J j, String rule) { - return new Annotated.Matcher(rule) - .asVisitor((a, found) -> { - found.set(true); - return a.getTree(); - }).reduce(j, new AtomicBoolean(false)).get(); + return service(AnnotationService.class).matches(getCursor(), ANY_RULE_ANNOTATION_MATCHER); } } - /** Accumulator to store migratability information of classes. */ + /** + * Accumulator to store migratability information of classes. + */ public class MigratabilityAccumulator { private final Map classToParentMap = new HashMap<>(); private final Map declaredMigratable = emptySetIfNull(knownMigratableClasses).stream() .collect(toMap(Function.identity(), key -> Boolean.TRUE)); - /** Registers a class with its parent and whether it is migratable. */ + /** + * Registers a class with its parent and whether it is migratable. + */ public void registerClass(String className, @Nullable String parentClassName, boolean isMigratable) { classToParentMap.put(className, parentClassName); if (isMigratable) { @@ -293,7 +283,9 @@ public void registerClass(String className, @Nullable String parentClassName, bo } } - /** Returns true if the class and all of its ancestors are declared migratable. */ + /** + * Returns true if the class and all of its ancestors are declared migratable. + */ public boolean isMigratable(String className) { String currentClass = className; while (currentClass != null) { From 157a21af766097979dd732cbaeab9eb32eb4fad9 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 12:49:38 +0100 Subject: [PATCH 7/9] Inline method --- .../junit5/JUnit4ToJunit5Precondition.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index dc390ed64..baa497c42 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -151,7 +151,8 @@ public J.MethodDeclaration visitMethodDeclaration( J.MethodDeclaration methodDecl, ExecutionContext ctx) { // Flag @Parameters annotations with class-type source attributes flagParametersAnnotationWithClassTypeSourceAttribute(methodDecl, ctx); - if (hasJunit4Rules(methodDecl) && methodDecl.getMethodType() != null) { + if (service(AnnotationService.class).matches(getCursor(), ANY_RULE_ANNOTATION_MATCHER) && + methodDecl.getMethodType() != null) { flagUnsupportedRule(methodDecl.getMethodType().getReturnType()); } return methodDecl; @@ -160,10 +161,10 @@ public J.MethodDeclaration visitMethodDeclaration( @Override public J.VariableDeclarations visitVariableDeclarations( J.VariableDeclarations variableDeclarations, ExecutionContext ctx) { - if (!hasJunit4Rules(variableDeclarations)) { - return variableDeclarations; + if (service(AnnotationService.class).matches(getCursor(), ANY_RULE_ANNOTATION_MATCHER)) { + return super.visitVariableDeclarations(variableDeclarations, ctx); } - return super.visitVariableDeclarations(variableDeclarations, ctx); + return variableDeclarations; } @Override @@ -248,10 +249,6 @@ private boolean hasUnsupportedFeatures() { } return null; } - - private boolean hasJunit4Rules(J j) { - return service(AnnotationService.class).matches(getCursor(), ANY_RULE_ANNOTATION_MATCHER); - } } /** @@ -259,7 +256,7 @@ private boolean hasJunit4Rules(J j) { */ public class MigratabilityAccumulator { - private final Map classToParentMap = new HashMap<>(); + private final Map classToParentMap = new HashMap<>(); private final Map declaredMigratable = emptySetIfNull(knownMigratableClasses).stream() .collect(toMap(Function.identity(), key -> Boolean.TRUE)); From 0e9ed75669905c23f771584c3880b8837477818c Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 13:01:24 +0100 Subject: [PATCH 8/9] Remove unnecessary `SearchResult.found` --- .../junit5/JUnit4ToJunit5Precondition.java | 20 +++++++------------ .../JUnit4ToJunit5PreconditionTest.java | 4 +++- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index baa497c42..4de1e1d5f 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -16,6 +16,7 @@ package org.openrewrite.java.testing.junit5; import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.*; @@ -123,14 +124,11 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex * A visitor that implements the scanning logic for identifying JUnit 4 test classes that can be * migrated to JUnit 5. */ + @RequiredArgsConstructor private class JUnit4ToJunit5PreconditionScanner extends JavaIsoVisitor { private final MigratabilityAccumulator accumulator; - private JUnit4ToJunit5PreconditionScanner(MigratabilityAccumulator accumulator) { - this.accumulator = accumulator; - } - @Override public J.ClassDeclaration visitClassDeclaration( J.ClassDeclaration classDecl, ExecutionContext ctx) { @@ -212,22 +210,18 @@ private void flagUnsupportedRule(JavaType javaType) { } } - private void markSupportedRunner( - J.ClassDeclaration classDecl, ExecutionContext ctx) { + private void markSupportedRunner(J.ClassDeclaration classDecl, ExecutionContext ctx) { Cursor classCursor = getCursor(); new Annotated.Matcher("@org.junit.runner.RunWith").asVisitor(a -> (new JavaIsoVisitor() { @Override public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) { - JavaType.FullyQualified type = - TypeUtils.asFullyQualified(fieldAccess.getTarget().getType()); - if (type == null) { // missing type attribution, possibly parsing error - return fieldAccess; - } - if (!emptySetIfNull(supportedRunners).contains(type.getFullyQualifiedName())) { + JavaType.FullyQualified type = TypeUtils.asFullyQualified(fieldAccess.getTarget().getType()); + if (type != null && !emptySetIfNull(supportedRunners).contains(type.getFullyQualifiedName())) { classCursor.putMessage(HAS_UNSUPPORTED_RUNNER, true); } - return SearchResult.found(fieldAccess); + // missing type attribution, possibly parsing error + return fieldAccess; } }).visit(a.getTree(), ctx)) .visit(classDecl, ctx); diff --git a/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java b/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java index 33cff0ea4..4c2147ca1 100644 --- a/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java +++ b/src/test/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5PreconditionTest.java @@ -67,7 +67,9 @@ public class GrpcCleanupRule extends ExternalResource { public @interface Parameters { Class source(); } - """)); + """ + ) + ); } @DocumentExample From 776db7675ff238e5d03eacb7a372e41f48837c09 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 22 Dec 2025 13:11:36 +0100 Subject: [PATCH 9/9] Format --- .../testing/junit5/JUnit4ToJunit5Precondition.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java index 4de1e1d5f..ca3565be6 100644 --- a/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java +++ b/src/main/java/org/openrewrite/java/testing/junit5/JUnit4ToJunit5Precondition.java @@ -89,9 +89,9 @@ public String getDisplayName() { @Override public String getDescription() { - return "Marks JUnit 4 test classes that can be migrated to JUnit 5 with current recipe " + - "capabilities, including detection of unsupported rules, runners, and `@Parameters` annotations with " + - "class-type source attributes."; + return "Marks JUnit 4 test classes that can be migrated to JUnit 5 with current recipe capabilities, " + + "including detection of unsupported rules, runners, and `@Parameters` annotations with class-type " + + "source attributes."; } @Override @@ -178,8 +178,7 @@ public J.VariableDeclarations.NamedVariable visitVariable( private void flagParametersAnnotationWithClassTypeSourceAttribute( J.MethodDeclaration methodDecl, ExecutionContext ctx) { Cursor methodCursor = getCursor(); - new Annotated.Matcher("junitparams.Parameters") - .asVisitor(a -> + new Annotated.Matcher("junitparams.Parameters").asVisitor(a -> (new JavaIsoVisitor() { @Override public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) { @@ -235,8 +234,7 @@ private boolean hasUnsupportedFeatures() { private @Nullable String getParentClassName(J.ClassDeclaration classDecl) { if (classDecl.getExtends() != null) { - JavaType.FullyQualified parentClass = - TypeUtils.asFullyQualified(classDecl.getExtends().getType()); + JavaType.FullyQualified parentClass = TypeUtils.asFullyQualified(classDecl.getExtends().getType()); if (parentClass != null) { return parentClass.getFullyQualifiedName(); }