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, ExecutionContext> 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);
- }
- }
- """
- )
- );
- }
}