diff --git a/src/main/java/org/openrewrite/java/testing/assertj/MigrateAssertionsForClassTypes.java b/src/main/java/org/openrewrite/java/testing/assertj/MigrateAssertionsForClassTypes.java new file mode 100644 index 000000000..ba478f932 --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/assertj/MigrateAssertionsForClassTypes.java @@ -0,0 +1,111 @@ +/* + * Copyright 2025 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.assertj; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesType; +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 java.util.Arrays; +import java.util.List; + +public class MigrateAssertionsForClassTypes extends Recipe { + + private static final String ASSERTIONS_FOR_CLASS_TYPES = "org.assertj.core.api.AssertionsForClassTypes"; + + private static final MethodMatcher ASSERT_THAT = new MethodMatcher(ASSERTIONS_FOR_CLASS_TYPES + " assertThat(..)"); + + /** + * Argument types for which the unified {@code Assertions} entry point declares a more specific + * {@code assertThat} overload than the generic {@code ObjectAssert assertThat(T)} offered by + * {@code AssertionsForClassTypes}. For these the call must migrate to {@code assertThatObject(..)} to keep + * returning an {@code ObjectAssert}; otherwise it would re-bind to e.g. {@code IterableAssert} and stop + * compiling whenever an {@code ObjectAssert}-only assertion is chained. + */ + private static final List COLLISION_TYPES = Arrays.asList( + "java.lang.Iterable", + "java.util.Iterator", + "java.util.Map", + "java.nio.file.Path", + "java.util.stream.Stream", + "java.util.stream.IntStream", + "java.util.stream.LongStream", + "java.util.stream.DoubleStream", + "java.util.function.Predicate", + "java.util.function.IntPredicate", + "java.util.function.LongPredicate", + "java.util.function.DoublePredicate", + "java.lang.Comparable", + "org.assertj.core.api.AssertProvider", + "org.assertj.core.api.AssertDelegateTarget" + ); + + @Override + public String getDisplayName() { + return "Use `Assertions.assertThatObject` for ambiguous `AssertionsForClassTypes.assertThat` calls"; + } + + @Override + public String getDescription() { + return "The deprecated `AssertionsForClassTypes.assertThat(T)` always returns an `ObjectAssert`, while the " + + "unified `Assertions.assertThat` additionally offers more specific overloads (e.g. for `Iterable`, " + + "`Map`, `Predicate`). For arguments matching those overloads, rename `assertThat` to `assertThatObject` " + + "so that migrating to `Assertions` keeps returning an `ObjectAssert` and the code keeps compiling."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesType<>(ASSERTIONS_FOR_CLASS_TYPES, false), new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + if (!ASSERT_THAT.matches(mi) || mi.getMethodType() == null || mi.getArguments().size() != 1) { + return mi; + } + // Only the generic ` ObjectAssert assertThat(T)` overload returns ObjectAssert; the typed + // overloads (String, primitives, arrays, ...) return their own assertion types and need no change. + if (!TypeUtils.isOfClassType(mi.getMethodType().getReturnType(), "org.assertj.core.api.ObjectAssert")) { + return mi; + } + Expression argument = mi.getArguments().get(0); + if (!isCollisionType(argument.getType())) { + return mi; + } + JavaType.Method newType = mi.getMethodType().withName("assertThatObject"); + return mi + .withName(mi.getName().withSimpleName("assertThatObject").withType(newType)) + .withMethodType(newType); + } + + private boolean isCollisionType(@org.jspecify.annotations.Nullable JavaType type) { + for (String collisionType : COLLISION_TYPES) { + if (TypeUtils.isAssignableTo(collisionType, type)) { + return true; + } + } + return false; + } + }); + } +} diff --git a/src/main/resources/META-INF/rewrite/assertj.yml b/src/main/resources/META-INF/rewrite/assertj.yml index 42d1e1fef..e75af112d 100644 --- a/src/main/resources/META-INF/rewrite/assertj.yml +++ b/src/main/resources/META-INF/rewrite/assertj.yml @@ -85,6 +85,7 @@ recipeList: - org.openrewrite.java.testing.assertj.CollapseConsecutiveAssertThatStatements - org.openrewrite.java.testing.assertj.ReturnActual - org.openrewrite.java.testing.assertj.SimplifyRedundantAssertJChains + - org.openrewrite.java.testing.assertj.MigrateAssertionsForClassAndInterfaceTypes - org.openrewrite.java.testing.assertj.StaticImports --- @@ -98,19 +99,37 @@ tags: - testing - assertj recipeList: -# https://github.com/openrewrite/rewrite-testing-frameworks/issues/664 -# - org.openrewrite.java.ChangeMethodTargetToStatic: -# methodPattern: "org.assertj.core.api.AssertionsForClassTypes assertThat(..)" -# fullyQualifiedTargetTypeName: "org.assertj.core.api.Assertions" -# - org.openrewrite.java.ChangeMethodTargetToStatic: -# methodPattern: "org.assertj.core.api.AssertionsForInterfaceTypes assertThat(..)" -# fullyQualifiedTargetTypeName: "org.assertj.core.api.Assertions" + # AssertionsForClassTypes / AssertionsForInterfaceTypes are converged onto Assertions by + # MigrateAssertionsForClassAndInterfaceTypes; see https://github.com/openrewrite/rewrite-testing-frameworks/issues/664 - org.openrewrite.java.ChangeMethodTargetToStatic: methodPattern: "org.assertj.core.api.Fail fail(..)" fullyQualifiedTargetTypeName: "org.assertj.core.api.Assertions" - org.openrewrite.java.UseStaticImport: methodPattern: "org.assertj.core.api.Assertions *(..)" +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.testing.assertj.MigrateAssertionsForClassAndInterfaceTypes +displayName: Migrate `AssertionsForClassTypes` and `AssertionsForInterfaceTypes` to `Assertions` +description: >- + AssertJ deprecated `AssertionsForClassTypes` and `AssertionsForInterfaceTypes` in favor of the unified + `Assertions` entry point. This recipe retargets their static methods to `Assertions`, using `assertThatObject` + where a plain `assertThat` would otherwise re-bind to a more specific overload and stop compiling + (see https://github.com/openrewrite/rewrite-testing-frameworks/issues/664). +preconditions: + - org.openrewrite.Singleton +tags: + - testing + - assertj +recipeList: + - org.openrewrite.java.testing.assertj.MigrateAssertionsForClassTypes + - org.openrewrite.java.ChangeMethodTargetToStatic: + methodPattern: "org.assertj.core.api.AssertionsForClassTypes *(..)" + fullyQualifiedTargetTypeName: "org.assertj.core.api.Assertions" + - org.openrewrite.java.ChangeMethodTargetToStatic: + methodPattern: "org.assertj.core.api.AssertionsForInterfaceTypes *(..)" + fullyQualifiedTargetTypeName: "org.assertj.core.api.Assertions" + --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.testing.assertj.SimplifyChainedAssertJAssertions diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index d67de20c7..4ec584044 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -14,6 +14,8 @@ maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.tes maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.AssertJShortRulesRecipes$AbstractShortAssertIsNotZeroRecipe,Replace `isNotEqualTo(0)` with `isNotZero()`,Replace `isNotEqualTo(0)` with `isNotZero()`.,1,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.SimplifyChainedAssertJAssertion,Simplify AssertJ chained assertions,Many AssertJ chained assertions have dedicated assertions that function the same. It is best to use the dedicated assertions.,1,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""chainedAssertion"",""type"":""String"",""displayName"":""AssertJ chained assertion"",""description"":""The chained AssertJ assertion to move to dedicated assertion."",""example"":""equals""},{""name"":""assertToReplace"",""type"":""String"",""displayName"":""AssertJ replaced assertion"",""description"":""The AssertJ assert that should be replaced."",""example"":""isTrue""},{""name"":""dedicatedAssertion"",""type"":""String"",""displayName"":""AssertJ replacement assertion"",""description"":""The AssertJ method to migrate to."",""example"":""isEqualTo""},{""name"":""requiredType"",""type"":""String"",""displayName"":""Required type"",""description"":""The type of the actual assertion argument."",""example"":""java.lang.String""}]", maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.SimplifyRedundantAssertJChains,Simplify redundant AssertJ assertion chains,Removes redundant AssertJ assertions when chained methods already provide the same or stronger guarantees.,1,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.MigrateAssertionsForClassAndInterfaceTypes,Migrate `AssertionsForClassTypes` and `AssertionsForInterfaceTypes` to `Assertions`,"AssertJ deprecated `AssertionsForClassTypes` and `AssertionsForInterfaceTypes` in favor of the unified `Assertions` entry point. This recipe retargets their static methods to `Assertions`, using `assertThatObject` where a plain `assertThat` would otherwise re-bind to a more specific overload and stop compiling (see https://github.com/openrewrite/rewrite-testing-frameworks/issues/664).",4,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.MigrateAssertionsForClassTypes,Use `Assertions.assertThatObject` for ambiguous `AssertionsForClassTypes.assertThat` calls,"The deprecated `AssertionsForClassTypes.assertThat(T)` always returns an `ObjectAssert`, while the unified `Assertions.assertThat` additionally offers more specific overloads (e.g. for `Iterable`, `Map`, `Predicate`). For arguments matching those overloads, rename `assertThat` to `assertThatObject` so that migrating to `Assertions` keeps returning an `ObjectAssert` and the code keeps compiling.",1,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.AssertJIntegerRulesRecipes$AbstractIntegerAssertIsEqualToRecipe,Replace `isCloseTo` with `isEqualTo`,Replace `isCloseTo` with `isEqualTo` when `offset` or `percentage` is zero.,1,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.JUnitAssertNotNullToAssertThat,JUnit `assertNotNull` to AssertJ,Convert JUnit-style `assertNotNull()` to AssertJ's `assertThat().isNotNull()`.,1,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.assertj.AssertJFloatRulesRecipes$AbstractFloatAssertIsNotZeroRecipe,Replace `isNotEqualTo(0)` with `isNotZero()`,Replace `isNotEqualTo(0)` with `isNotZero()`.,1,AssertJ,Testing,Java,,,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/testing/assertj/MigrateAssertionsForClassAndInterfaceTypesTest.java b/src/test/java/org/openrewrite/java/testing/assertj/MigrateAssertionsForClassAndInterfaceTypesTest.java new file mode 100644 index 000000000..433a5721d --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/assertj/MigrateAssertionsForClassAndInterfaceTypesTest.java @@ -0,0 +1,264 @@ +/* + * Copyright 2025 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.assertj; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.Issue; +import org.openrewrite.config.Environment; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/664") +class MigrateAssertionsForClassAndInterfaceTypesTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion() + .classpathFromResources(new InMemoryExecutionContext(), "assertj-core-3")) + .recipe(Environment.builder() + .scanRuntimeClasspath("org.openrewrite.java.testing.assertj") + .build() + .activateRecipes("org.openrewrite.java.testing.assertj.MigrateAssertionsForClassAndInterfaceTypes")); + } + + @DocumentExample + @Test + void collisionTypesUseAssertThatObject() { + // An Iterable/Map argument relies on AssertionsForClassTypes returning an ObjectAssert; plain + // Assertions.assertThat would re-bind to IterableAssert/MapAssert and `hasNoNullFieldsOrProperties()` + // would no longer compile. Pin ObjectAssert via assertThatObject. + //language=java + rewriteRun( + java( + """ + import org.assertj.core.api.AssertionsForClassTypes; + + import java.util.Map; + import java.util.Set; + + class Test { + void method(Set set, Map map) { + AssertionsForClassTypes.assertThat(set).hasNoNullFieldsOrProperties(); + AssertionsForClassTypes.assertThat(map).isNotNull(); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + import java.util.Map; + import java.util.Set; + + class Test { + void method(Set set, Map map) { + Assertions.assertThatObject(set).hasNoNullFieldsOrProperties(); + Assertions.assertThatObject(map).isNotNull(); + } + } + """ + ) + ); + } + + @Test + void plainObjectKeepsAssertThat() { + //language=java + rewriteRun( + java( + """ + import org.assertj.core.api.AssertionsForClassTypes; + + class Test { + void method(Object o) { + AssertionsForClassTypes.assertThat(o).isNotNull(); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + class Test { + void method(Object o) { + Assertions.assertThat(o).isNotNull(); + } + } + """ + ) + ); + } + + @Test + void comparableArgumentUsesAssertThatObject() { + // Assertions has a more specific assertThat(Comparable) overload; preserve the ObjectAssert binding. + //language=java + rewriteRun( + java( + """ + import org.assertj.core.api.AssertionsForClassTypes; + + class Test { + enum Color { RED, GREEN } + void method(Color color) { + AssertionsForClassTypes.assertThat(color).isNotNull(); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + class Test { + enum Color { RED, GREEN } + void method(Color color) { + Assertions.assertThatObject(color).isNotNull(); + } + } + """ + ) + ); + } + + @Test + void typedOverloadsOnlyRetarget() { + //language=java + rewriteRun( + java( + """ + import org.assertj.core.api.AssertionsForClassTypes; + + class Test { + void method() { + AssertionsForClassTypes.assertThat(true).isTrue(); + AssertionsForClassTypes.assertThat("value").isNotEmpty(); + AssertionsForClassTypes.assertThat(1).isPositive(); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + class Test { + void method() { + Assertions.assertThat(true).isTrue(); + Assertions.assertThat("value").isNotEmpty(); + Assertions.assertThat(1).isPositive(); + } + } + """ + ) + ); + } + + @Test + void interfaceTypesAreAlwaysSafe() { + //language=java + rewriteRun( + java( + """ + import org.assertj.core.api.AssertionsForInterfaceTypes; + + import java.util.List; + + class Test { + void method(List list) { + AssertionsForInterfaceTypes.assertThat(list).hasSize(0); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + import java.util.List; + + class Test { + void method(List list) { + Assertions.assertThat(list).hasSize(0); + } + } + """ + ) + ); + } + + @Test + void nonAssertThatHelpersRetarget() { + //language=java + rewriteRun( + java( + """ + import org.assertj.core.api.AssertionsForClassTypes; + + class Test { + void method() { + Throwable thrown = AssertionsForClassTypes.catchThrowable(() -> { + throw new IllegalStateException("boom"); + }); + AssertionsForClassTypes.assertThat(thrown).isInstanceOf(IllegalStateException.class); + } + } + """, + """ + import org.assertj.core.api.Assertions; + + class Test { + void method() { + Throwable thrown = Assertions.catchThrowable(() -> { + throw new IllegalStateException("boom"); + }); + Assertions.assertThat(thrown).isInstanceOf(IllegalStateException.class); + } + } + """ + ) + ); + } + + @Test + void staticImportCollisionForm() { + //language=java + rewriteRun( + java( + """ + import java.util.Set; + + import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + class Test { + void method(Set set) { + assertThat(set).hasNoNullFieldsOrProperties(); + } + } + """, + """ + import java.util.Set; + + import static org.assertj.core.api.Assertions.assertThatObject; + + class Test { + void method(Set set) { + assertThatObject(set).hasNoNullFieldsOrProperties(); + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/assertj/StaticImportsTest.java b/src/test/java/org/openrewrite/java/testing/assertj/StaticImportsTest.java index 998df5f43..d5a9ed1f9 100644 --- a/src/test/java/org/openrewrite/java/testing/assertj/StaticImportsTest.java +++ b/src/test/java/org/openrewrite/java/testing/assertj/StaticImportsTest.java @@ -16,11 +16,9 @@ package org.openrewrite.java.testing.assertj; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.InMemoryExecutionContext; -import org.openrewrite.Issue; import org.openrewrite.config.Environment; import org.openrewrite.java.JavaParser; import org.openrewrite.test.RecipeSpec; @@ -79,47 +77,4 @@ void method() { ) ); } - - @Disabled("Requires changes in AssertJ to adopt `assertThatClass` and `assertThatInterface`") - @Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/664") - @Test - void assertionsForClassTypes() { - //language=java - rewriteRun( - java( - """ - import java.util.List; - import org.assertj.core.api.AssertionsForClassTypes; - import org.assertj.core.api.AssertionsForInterfaceTypes; - import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; - - public class Test { - List exampleList; - void method() { - AssertionsForInterfaceTypes.assertThat(exampleList).hasSize(0); - AssertionsForClassTypes.assertThat(true).isTrue(); - assertThat(true).isTrue(); - assertThat(exampleList).hasSize(0); - } - } - """, - """ - import java.util.List; - - import static org.assertj.core.api.Assertions.assertThat; - - public class Test { - List exampleList; - void method() { - assertThat(exampleList).hasSize(0); - assertThat(true).isTrue(); - assertThat(true).isTrue(); - assertThat(exampleList).hasSize(0); - } - } - """ - ) - ); - } }