Incorrect type check when using 'shouldBe()' on class with overloaded toString() method

The signature of io.kotlintest.shouldBe is infix fun <T, U : T> T.shouldBe(any: U?). Therefore, the following code should, in my opinion, not compile and produce a “Type argument is not within its bounds” error:

class Test {
    override fun toString(): String = "Hello"
}

Test() shouldBe "Hello"

However, not only does it compile, it produces the output Exception in thread "main" org.opentest4j.AssertionFailedError: expected: "Hello" but was: Hello, which is very confusing. I stumbled upon this by chance, having accidentally omitted the “toString()” call after Test() when writing some code. It seems that shouldBe fails, correctly, because the LHS is not the same as the RHS, and then toString() gets called on the output, leading to the error message. What I don’t understand is how this even compiles in the first place.

If I explicitly declare the types (Test().shouldBe<Test, String>("Hello")), then it fails during compilation as expected.

EDIT: I just noticed the function has a @Suppress("UNCHECKED_CAST") annotation, could that be the reason?

The method compiles with T as Any and U as String. Generic methods will use the most specific types that work, including supertypes, not just the exact type used.

Thank you for the explanation! Just a quick follow-up, could you point me to a Kotlin resource where this is stated? I’m not doubting what you wrote, it’s just that I couldn’t find it in the reference. Or is this considered standard generics knowledge?

I’ve simply seen a similar question before. The docs don’t seem to explicitly detail how the types to use for generic type replacement are chosen.

You can see what’s going on here yourself by writing your own inline reified function with the same signature and constraints and printing out the reified types.

2 Likes

Heh, that’s actually a pretty cool suggestion, didn’t think of that. Thanks!

For anyone dealing with something similar, I stumbled on the @OnlyInputTypes annotation (see https://youtrack.jetbrains.com/issue/KT-13198). It tackles exactly what I was talking about, i.e. it prevents type inference from raising the type to Any. If the shouldBe method I was talking about was declared as

infix fun <@OnlyInputTypes T, U : T> T.shouldBe(any: U?)

compilation would fail as expected. Unfortunately, the annotation is currently still private, but there is a workaround.