Repository: assertj/assertj (Java)

Issue:
isExactlyInstanceOf gives confusing error message when instance is an anonymous inner class
#### Summary

When using isExactlyInstanceOf and the actual value is an anonymous inner class, the message is confusing as it claims that the actual value is an "instance of null".

#### Example

```java
    assertThat(x.red).isNotNull().isExactlyInstanceOf(A.class);
```
Will not fail `isNotNull` but fails on `isExactlyInstanceOf` with:
```
java.lang.AssertionError: 
Expecting:
 <hs.ddif.core.inject.store.BindingProvider$ProviderBinding$1@4f1bfe23>
to be exactly an instance of:
 <hs.ddif.core.InjectorProviderTest.A>
but was an instance of:
 <null>
```
This led me to believe the value in `x.red` is `null`, but it clearly isn't.

If it is not possible to detect the anonymous inner class type here  (it in this case is an anonymous implementation of the interface `Provider`) perhaps change the message to `but was an instance of an anonymous class`.


Hints:
Thanks for reporting it, @hjohn. We'll look into it.

A small reproducible example would be:

```java
Object actual = new Object() {};
assertThat(actual).isNotNull().isExactlyInstanceOf(Object.class);
```

which yields:

```
java.lang.AssertionError: 
Expecting actual:
  org.example.MyTest$1@37eeec90
to be exactly an instance of:
  java.lang.Object
but was an instance of:
  null
```

Patch:
diff --git a/src/main/java/org/assertj/core/presentation/StandardRepresentation.java b/src/main/java/org/assertj/core/presentation/StandardRepresentation.java
--- a/src/main/java/org/assertj/core/presentation/StandardRepresentation.java
+++ b/src/main/java/org/assertj/core/presentation/StandardRepresentation.java
@@ -262,7 +262,7 @@ private static boolean hasOverriddenToString(Class<?> clazz) {
       return !Object.class.equals(classDeclaringToString);
     } catch (NoSuchMethodException | SecurityException e) {
       // NoSuchMethodException should not occur as toString is always defined.
-      // if SecurityException occurs, returning false to use our own representation 
+      // if SecurityException occurs, returning false to use our own representation
       return false;
     }
   }
@@ -273,7 +273,7 @@ private static boolean hasOverriddenToString(Class<?> clazz) {
   private static boolean hasOverriddenToStringInSubclassOf(Class<?> objectClass, Class<?> clazz) {
     try {
       Class<?> classDeclaringToString = objectClass.getMethod("toString").getDeclaringClass();
-      // check if any classes between objectClass and clazz (excluded) have overridden toString 
+      // check if any classes between objectClass and clazz (excluded) have overridden toString
       Class<?> classToCheck = objectClass;
       while (!classToCheck.equals(clazz)) {
         if (classDeclaringToString.equals(classToCheck)) return true;
@@ -281,7 +281,7 @@ private static boolean hasOverriddenToStringInSubclassOf(Class<?> objectClass, C
       }
     } catch (NoSuchMethodException | SecurityException e) {
       // NoSuchMethodException should not occur as toString is always defined.
-      // if SecurityException occurs, returning false to use our own representation 
+      // if SecurityException occurs, returning false to use our own representation
     }
     return false;
   }
@@ -376,7 +376,11 @@ protected String toStringOf(Calendar calendar) {
   }
 
   protected String toStringOf(Class<?> c) {
-    return c.getCanonicalName();
+    String canonicalName = c.getCanonicalName();
+    if (canonicalName != null) return canonicalName;
+    if (c.getSimpleName().equals("")) return "anonymous class";
+    if (c.getSimpleName().equals("[]")) return "anonymous class array";
+    return String.format("local class %s", c.getSimpleName());
   }
 
   protected String toStringOf(String s) {


Test patch:
diff --git a/src/test/java/org/assertj/core/presentation/StandardRepresentation_toStringOf_Test.java b/src/test/java/org/assertj/core/presentation/StandardRepresentation_toStringOf_Test.java
--- a/src/test/java/org/assertj/core/presentation/StandardRepresentation_toStringOf_Test.java
+++ b/src/test/java/org/assertj/core/presentation/StandardRepresentation_toStringOf_Test.java
@@ -22,8 +22,10 @@
 import static org.assertj.core.util.Arrays.array;
 import static org.assertj.core.util.Lists.list;
 import static org.assertj.core.util.Maps.newHashMap;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
 
 import java.io.File;
+import java.lang.reflect.Array;
 import java.lang.reflect.Method;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -66,8 +68,6 @@
 import org.junit.jupiter.params.provider.MethodSource;
 
 /**
- * Tests for {@link org.assertj.core.presentation.StandardRepresentation#toStringOf(Object)}.
- *
  * @author Joel Costigliola
  */
 class StandardRepresentation_toStringOf_Test extends AbstractBaseRepresentationTest {
@@ -113,10 +113,35 @@ void should_return_toString_of_Class_with_its_name() {
     assertThat(STANDARD_REPRESENTATION.toStringOf(Object.class)).isEqualTo("java.lang.Object");
   }
 
+  @Test
+  void should_return_toString_of_anonymous_Class_with_generic_description() {
+    Object anonymous = new Object() {};
+    assertThat(STANDARD_REPRESENTATION.toStringOf(anonymous.getClass())).isEqualTo("anonymous class");
+  }
+
+  @Test
+  void should_return_toString_of_anonymous_Class_array_with_generic_description() {
+    Object anonymous = new Object() {};
+    Object anonymousArray = Array.newInstance(anonymous.getClass(), 1);
+    assertThat(STANDARD_REPRESENTATION.toStringOf(anonymousArray.getClass())).isEqualTo("anonymous class array");
+  }
+
+  @Test
+  void should_return_toString_of_local_Class_with_its_simple_name() {
+    class LocalClass {}
+    assertThat(STANDARD_REPRESENTATION.toStringOf(LocalClass.class)).isEqualTo("local class LocalClass");
+  }
+
+  @Test
+  void should_return_toString_of_local_Class_array_with_its_simple_name() {
+    class LocalClass {}
+    assertThat(STANDARD_REPRESENTATION.toStringOf(LocalClass[].class)).isEqualTo("local class LocalClass[]");
+  }
+
   @Test
   void should_return_toString_of_Collection_of_String() {
     Collection<String> collection = list("s1", "s2");
-    assertThat(STANDARD_REPRESENTATION.toStringOf(collection)).isEqualTo(format("[\"s1\", \"s2\"]"));
+    assertThat(STANDARD_REPRESENTATION.toStringOf(collection)).isEqualTo("[\"s1\", \"s2\"]");
   }
 
   @Test
@@ -531,9 +556,9 @@ public List<T> subList(int fromIndex, int toIndex) {
   }
 
   private static Stream<Arguments> durations() {
-    return Stream.of(Arguments.of(Duration.of(1L, MILLIS), "0.001S"),
-                     Arguments.of(Duration.of(1234L, MILLIS), "1.234S"),
-                     Arguments.of(Duration.of(3_661_001L, MILLIS), "1H1M1.001S"));
+    return Stream.of(arguments(Duration.of(1L, MILLIS), "0.001S"),
+                     arguments(Duration.of(1234L, MILLIS), "1.234S"),
+                     arguments(Duration.of(3_661_001L, MILLIS), "1H1M1.001S"));
   }
 
   private String toStringOf(Object o) {