Repository: assertj/assertj

Problem statement:
Improve interoperability of `asInstanceOf` with `Set` instances
Right now, there is no nice way to call `asInstanceOf` when a `Set` instance is expected.

Current solutions:

```java
Object actual = Set.of("first", "second");

assertThat(actual)
  .asInstanceOf(InstanceOfAssertFactories.type(Set.class))
  .satisfies(set -> assertThat(set).containsExactly("first", "second")); // unchecked warning

assertThat(actual)
  .asInstanceOf(InstanceOfAssertFactories.COLLECTION) // no implicit `isInstanceOf(Set.class)`
  .containsExactly("first", "second"));

assertThat(map.get("value"))
  .asInstanceOf(new InstanceOfAssertFactory<>(Set.class, Assertions::<String> assertThat)) // verbose
  .containsExactly("first", "second");
```

By design, we didn't introduce any `InstanceOfAssertFactory` for types that don't have more specific assertions, i.e., there is no `AbstractSetAssert` today because it wouldn't have any added value compared to [`AbstractCollectionAssert`](https://www.javadoc.io/doc/org.assertj/assertj-core/latest/org/assertj/core/api/AbstractCollectionAssert.html).

We should investigate how to improve such use cases.

Triggered by: https://github.com/spring-projects/spring-framework/blob/2784f6008e97c7283b2e57e98d18a89fe8f530ae/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java#L144-L153


Hints:

Patch:
diff --git a/assertj-core/src/main/java/org/assertj/core/api/InstanceOfAssertFactories.java b/assertj-core/src/main/java/org/assertj/core/api/InstanceOfAssertFactories.java
--- a/assertj-core/src/main/java/org/assertj/core/api/InstanceOfAssertFactories.java
+++ b/assertj-core/src/main/java/org/assertj/core/api/InstanceOfAssertFactories.java
@@ -37,6 +37,7 @@
 import java.util.OptionalDouble;
 import java.util.OptionalInt;
 import java.util.OptionalLong;
+import java.util.Set;
 import java.util.Spliterator;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -908,6 +909,32 @@ static <ELEMENT> InstanceOfAssertFactory<List, ListAssert<ELEMENT>> list(Class<E
     return new InstanceOfAssertFactory<>(List.class, Assertions::<ELEMENT> assertThat);
   }
 
+  /**
+   * {@link InstanceOfAssertFactory} for a {@link Set}, assuming {@code Object} as element type.
+   *
+   * @see #set(Class)
+   * @since 3.26.0
+   */
+  @SuppressWarnings("rawtypes") // rawtypes: using Class instance
+  InstanceOfAssertFactory<Set, AbstractCollectionAssert<?, Collection<?>, Object, ObjectAssert<Object>>> SET = set(Object.class);
+
+  /**
+   * {@link InstanceOfAssertFactory} for a {@link Set}.
+   *
+   * @param <E>   the {@code Set} element type.
+   * @param elementType the element type instance.
+   * @return the factory instance.
+   *
+   * @see #SET
+   * @since 3.26.0
+   */
+  @SuppressWarnings({ "rawtypes", "unused", "unchecked", "RedundantSuppression" })
+  // rawtypes+unchecked: using Class instance, unused: parameter needed for type inference.
+  // IntelliJ can warn that this is redundant when it is not.
+  static <E> InstanceOfAssertFactory<Set, AbstractCollectionAssert<?, Collection<? extends E>, E, ObjectAssert<E>>> set(Class<E> elementType) {
+    return new InstanceOfAssertFactory<>(Set.class, Assertions::<E> assertThat);
+  }
+
   /**
    * {@link InstanceOfAssertFactory} for a {@link Stream}, assuming {@code Object} as element type.
    *


Test patch:
diff --git a/assertj-core/src/test/java/org/assertj/core/api/Assertions_sync_with_InstanceOfAssertFactories_Test.java b/assertj-core/src/test/java/org/assertj/core/api/Assertions_sync_with_InstanceOfAssertFactories_Test.java
--- a/assertj-core/src/test/java/org/assertj/core/api/Assertions_sync_with_InstanceOfAssertFactories_Test.java
+++ b/assertj-core/src/test/java/org/assertj/core/api/Assertions_sync_with_InstanceOfAssertFactories_Test.java
@@ -13,6 +13,7 @@
 package org.assertj.core.api;
 
 import static java.util.stream.Collectors.toMap;
+import static org.assertj.core.api.Assertions.as;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.from;
 import static org.assertj.core.api.BDDAssertions.then;
@@ -27,6 +28,7 @@
 import java.lang.reflect.TypeVariable;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
@@ -34,19 +36,24 @@
 
 class Assertions_sync_with_InstanceOfAssertFactories_Test extends BaseAssertionsTest {
 
-  private static final Class<?>[] FIELD_FACTORIES_IGNORED_TYPES = {
-      // There can be no Comparable field factory with a base type.
+  private static final Class<?>[] IGNORED_INPUT_TYPES = {
+      // There is no dedicated `assertThat`.
+      Set.class
+  };
+
+  private static final Class<?>[] IGNORED_ASSERT_TYPES_FOR_FIELD_FACTORIES = {
+      // There cannot be a `Comparable` field factory with a base type.
       AbstractComparableAssert.class,
-      // The comparison of the input GenericArrayTypes will always fail, since it verifies the inner TypeVariable which
-      // returns the defining Method as result of TypeVariable#getGenericDeclaration().
+      // The comparison of the input `GenericArrayTypes` will always fail, since it verifies the inner `TypeVariable`
+      // which returns the defining `Method` as a result of `TypeVariable#getGenericDeclaration()`.
       ObjectArrayAssert.class, Object2DArrayAssert.class,
       // A field factory for an object is pointless.
       ObjectAssert.class,
   };
 
-  private static final Class<?>[] METHOD_FACTORIES_IGNORED_TYPES = {
-      // The comparison of the input GenericArrayTypes will always fail, since it verifies the inner TypeVariable which
-      // returns the defining Method as result of TypeVariable#getGenericDeclaration().
+  private static final Class<?>[] IGNORED_ASSERT_TYPES_FOR_METHOD_FACTORIES = {
+      // The comparison of the input `GenericArrayTypes` will always fail, since it verifies the inner `TypeVariable`
+      // which returns the defining `Method` as a result of `TypeVariable#getGenericDeclaration()`.
       ObjectArrayAssert.class, Object2DArrayAssert.class,
   };
 
@@ -73,7 +80,8 @@ void each_standard_assertion_with_type_parameters_should_have_an_instance_of_ass
   }
 
   private Map<Type, Type> findAssertThatParameterAndReturnTypes() {
-    return Stream.of(findMethodsWithName(Assertions.class, "assertThat", ignoredReturnTypes(FIELD_FACTORIES_IGNORED_TYPES)))
+    return Stream.of(findMethodsWithName(Assertions.class, "assertThat",
+                                         ignoredReturnTypes(IGNORED_ASSERT_TYPES_FOR_FIELD_FACTORIES)))
                  .map(this::toParameterAndReturnTypeEntry)
                  .filter(not(this::isPrimitiveTypeKey))
                  .collect(toMap(Entry::getKey, Entry::getValue));
@@ -87,7 +95,8 @@ private <K, V> boolean isPrimitiveTypeKey(Entry<K, V> entry) {
   }
 
   private Map<Type, Type> findTypedAssertThatParameterAndReturnTypes() {
-    return Stream.of(findMethodsWithName(Assertions.class, "assertThat", ignoredReturnTypes(METHOD_FACTORIES_IGNORED_TYPES)))
+    return Stream.of(findMethodsWithName(Assertions.class, "assertThat",
+                                         ignoredReturnTypes(IGNORED_ASSERT_TYPES_FOR_METHOD_FACTORIES)))
                  .filter(this::hasTypeParameters)
                  .map(this::toParameterAndReturnTypeEntry)
                  .collect(toMap(Entry::getKey, Entry::getValue));
@@ -118,35 +127,40 @@ private Map<Type, Type> findFieldFactoryTypes() {
                  .filter(not(Field::isSynthetic)) // Exclude $jacocoData - see #590 and jacoco/jacoco#168
                  .map(Field::getGenericType)
                  .map(this::extractTypeParameters)
+                 .filter(not(this::isIgnoredInputType))
                  .filter(not(this::isIgnoredFieldFactory))
                  .collect(toMap(Entry::getKey, Entry::getValue));
   }
 
   private boolean isIgnoredFieldFactory(Entry<Type, Type> e) {
-    return isIgnoredFactory(e, FIELD_FACTORIES_IGNORED_TYPES);
+    return isIgnoredFactory(e, IGNORED_ASSERT_TYPES_FOR_FIELD_FACTORIES);
   }
 
   private Map<Type, Type> findMethodFactoryTypes() {
     return Stream.of(InstanceOfAssertFactories.class.getMethods())
                  .map(Method::getGenericReturnType)
                  .map(this::extractTypeParameters)
+                 .filter(not(this::isIgnoredInputType))
                  .filter(not(this::isIgnoredMethodFactory))
                  .collect(toMap(Entry::getKey, Entry::getValue));
   }
 
   private boolean isIgnoredMethodFactory(Entry<Type, Type> e) {
-    return isIgnoredFactory(e, METHOD_FACTORIES_IGNORED_TYPES);
+    return isIgnoredFactory(e, IGNORED_ASSERT_TYPES_FOR_METHOD_FACTORIES);
   }
 
-  private boolean isIgnoredFactory(Entry<Type, Type> e, Class<?>... ignoredTypes) {
+  private boolean isIgnoredFactory(Entry<Type, Type> e, Class<?>[] ignoredTypes) {
     return Stream.of(ignoredTypes).anyMatch(type -> e.getValue().equals(type));
   }
 
+  private boolean isIgnoredInputType(Entry<Type, Type> e) {
+    return Stream.of(IGNORED_INPUT_TYPES).anyMatch(type -> e.getKey().equals(type));
+  }
+
   private Entry<Type, Type> extractTypeParameters(Type type) {
     assertThat(type).asInstanceOf(type(ParameterizedType.class))
                     .returns(InstanceOfAssertFactory.class, from(ParameterizedType::getRawType))
-                    .extracting(ParameterizedType::getActualTypeArguments)
-                    .asInstanceOf(ARRAY)
+                    .extracting(ParameterizedType::getActualTypeArguments, as(ARRAY))
                     .hasSize(2);
     Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
     return entry(normalize(typeArguments[0]), normalize(typeArguments[1]));
diff --git a/assertj-core/src/test/java/org/assertj/core/api/InstanceOfAssertFactoriesTest.java b/assertj-core/src/test/java/org/assertj/core/api/InstanceOfAssertFactoriesTest.java
--- a/assertj-core/src/test/java/org/assertj/core/api/InstanceOfAssertFactoriesTest.java
+++ b/assertj-core/src/test/java/org/assertj/core/api/InstanceOfAssertFactoriesTest.java
@@ -87,6 +87,7 @@
 import static org.assertj.core.api.InstanceOfAssertFactories.PATH;
 import static org.assertj.core.api.InstanceOfAssertFactories.PERIOD;
 import static org.assertj.core.api.InstanceOfAssertFactories.PREDICATE;
+import static org.assertj.core.api.InstanceOfAssertFactories.SET;
 import static org.assertj.core.api.InstanceOfAssertFactories.SHORT;
 import static org.assertj.core.api.InstanceOfAssertFactories.SHORT_2D_ARRAY;
 import static org.assertj.core.api.InstanceOfAssertFactories.SHORT_ARRAY;
@@ -119,6 +120,7 @@
 import static org.assertj.core.api.InstanceOfAssertFactories.map;
 import static org.assertj.core.api.InstanceOfAssertFactories.optional;
 import static org.assertj.core.api.InstanceOfAssertFactories.predicate;
+import static org.assertj.core.api.InstanceOfAssertFactories.set;
 import static org.assertj.core.api.InstanceOfAssertFactories.stream;
 import static org.assertj.core.api.InstanceOfAssertFactories.throwable;
 import static org.assertj.core.api.InstanceOfAssertFactories.type;
@@ -176,6 +178,7 @@
 import java.util.stream.Stream;
 
 import org.assertj.core.util.Lists;
+import org.assertj.core.util.Sets;
 import org.assertj.core.util.Strings;
 import org.junit.jupiter.api.Test;
 
@@ -1135,6 +1138,26 @@ void collection_typed_factory_should_allow_collection_typed_assertions() {
     result.contains("Bart", "Lisa");
   }
 
+  @Test
+  void set_factory_should_allow_collection_assertions() {
+    // GIVEN
+    Object value = Sets.set("Homer", "Marge", "Bart", "Lisa", "Maggie");
+    // WHEN
+    AbstractCollectionAssert<?, Collection<?>, Object, ObjectAssert<Object>> result = assertThat(value).asInstanceOf(SET);
+    // THEN
+    result.contains("Bart", "Lisa");
+  }
+
+  @Test
+  void set_typed_factory_should_allow_collection_typed_assertions() {
+    // GIVEN
+    Object value = Sets.set("Homer", "Marge", "Bart", "Lisa", "Maggie");
+    // WHEN
+    AbstractCollectionAssert<?, Collection<? extends String>, String, ObjectAssert<String>> result = assertThat(value).asInstanceOf(set(String.class));
+    // THEN
+    result.contains("Bart", "Lisa");
+  }
+
   @Test
   void list_factory_should_allow_list_assertions() {
     // GIVEN
diff --git a/assertj-core/src/test/java/org/assertj/core/api/SoftAssertions_combined_with_asInstanceOf_Test.java b/assertj-core/src/test/java/org/assertj/core/api/SoftAssertions_combined_with_asInstanceOf_Test.java
--- a/assertj-core/src/test/java/org/assertj/core/api/SoftAssertions_combined_with_asInstanceOf_Test.java
+++ b/assertj-core/src/test/java/org/assertj/core/api/SoftAssertions_combined_with_asInstanceOf_Test.java
@@ -85,6 +85,7 @@
 import static org.assertj.core.api.InstanceOfAssertFactories.PATH;
 import static org.assertj.core.api.InstanceOfAssertFactories.PERIOD;
 import static org.assertj.core.api.InstanceOfAssertFactories.PREDICATE;
+import static org.assertj.core.api.InstanceOfAssertFactories.SET;
 import static org.assertj.core.api.InstanceOfAssertFactories.SHORT;
 import static org.assertj.core.api.InstanceOfAssertFactories.SHORT_2D_ARRAY;
 import static org.assertj.core.api.InstanceOfAssertFactories.SHORT_ARRAY;
@@ -99,6 +100,7 @@
 import static org.assertj.core.util.Lists.list;
 import static org.assertj.core.util.Maps.newHashMap;
 import static org.assertj.core.util.Sets.newHashSet;
+import static org.assertj.core.util.Sets.set;
 import static org.junit.jupiter.params.provider.Arguments.arguments;
 
 import java.io.ByteArrayInputStream;
@@ -261,6 +263,7 @@ public static Stream<Arguments> should_work_with_any_InstanceOfFactory_source()
                      arguments(OptionalLong.empty(), OPTIONAL_LONG),
                      arguments(Paths.get("."), PATH),
                      arguments((Predicate<String>) String::isEmpty, PREDICATE),
+                     arguments(set("foo"), SET),
                      arguments(Short.MIN_VALUE, SHORT),
                      arguments(new short[0], SHORT_ARRAY),
                      arguments(new short[0][0], SHORT_2D_ARRAY),