raphw / byte-buddy

Runtime code generation for the Java virtual machine.
https://bytebuddy.net
Apache License 2.0
6.21k stars 798 forks source link

IllegalArgumentException for Kotlin bytecode with erased type argument #1680

Open milgner opened 1 month ago

milgner commented 1 month ago

In a project with mockk, I encountered a curious issue when trying to mock some code that uses Arrows NonEmptySet, which is a JVM-inlined class with a type parameter which apparently gets erased during compilation.

This triggers the IllegalArgumentException in TypeDefinition.

The field in question, MockkMePlease.foo, has the following definition: class MockkMePlease(val foo: NonEmptySet<Int>) {}

Which results in a FieldRepository whose genericType has a rawType of java.util.Set and a list of actualTypeArguments with one null element.

Here is a screenshot of the debugger which might help to grasp that information: screenshot

The resulting mockk issue is https://github.com/mockk/mockk/issues/1130

raphw commented 1 month ago

I am afraid that that is a JVM bug. If you update the JVM, the error will likely go away. Could you validate that? I submitted a long row of patched to OpenJDK myself, and this looks familiar. Could you give it a try?

milgner commented 1 month ago

Will give it a try. This happened on the last OpenJDK 21. Is there any specific version I should try? Otherwise I'll just try 22 GA first.

raphw commented 1 month ago

Should be any latest version. The exception occurs in code of the JDK. So either the generic type information is faulty, or there is a bug in the JDK. Even with a bug, there should not be a nullpointer, though.

milgner commented 1 month ago

I have tried this with an updated OpenJDK 21 as well as the latest OpenJDK 22 but they produce the same exception with the same null element in actualTypeArguments.

milgner commented 1 month ago

I looked into whether it would be possible to work around the issue by changing the mockk code. Unfortunately this is a critical path where the redefine method is being used to create the mock itself. And in order to instantiate the instrumented type, it has to transform the declared methods into the token list which is where it fails down the line.

So I delved into the bytebuddy code a bit and found the following place which seems to cause the issue:

in TypeDescription.Generic.Visitor.Substitutor.onWildcard, an instance of OfWildcardType.Latent is constructed. For this, it queries the upper bounds of the wildcard which in fact is a list with only one null element in it.

I am quite clueless about these JVM internals but it seems to contradict the API documentation which says

If no upper bound is explicitly declared, the upper bound is Object

So I guess this is probably an issue with Kotlin that this library cannot do anything about. Consequently I have opened an issue in the official Kotlin tracker: https://youtrack.jetbrains.com/issue/KT-70235/WildcardTypeupperBounds-for-Kotlin-based-code-returns-null-element

Let me know if anyone has further insights. It might be a good idea to keep this open as a reference.

raphw commented 1 month ago

Could you create me a quick reproducer? I am wondering if this might be a bug in the Kotlin compiler which triggers a bug in the JDK that is not yet resolved. Naturally, the JVM is tested with javac compiled classes, so wrongful metadata is not always handled correctly. It might be that the class in question is compiled with an outdated Kotlin compiler and that the problem would be solved with a compiler update.

milgner commented 1 month ago

There is a reproducer at https://github.com/milgner/Repro-Kotlin-WildcardType-UpperBounds-Null - does that help? There are two tests with mockk which both trigger the same issue.

raphw commented 1 month ago

This seems to me like a bug in the Kotlin compiler which generates an invalid generic signature which again is not handled properly in the Java reflection utility. The MockkMePlease class looks like this in Kotlin:

class MockkMePlease(val foo: NonEmptySet<Int>) { }

which generates a class file similar to the following:

public final class com.marcusilgner.mockk_repro_1130.MockkMePlease {
  private final java.util.Set<A> foo;
  private com.marcusilgner.mockk_repro_1130.MockkMePlease(java.util.Set<? extends A>);
  public final java.util.Set<A> getFoo-5sCjGKo();
  public com.marcusilgner.mockk_repro_1130.MockkMePlease(java.util.Set, kotlin.jvm.internal.DefaultConstructorMarker);
}

The type variable A is nowhere defined and as such it cannot be resolved. This results in the variable being represented as null, which should not be the case.

I can see if I can handle this more gracefully in Byte Buddy, and I will report the bug to OpenJDK. Would you like to take it up with the Kotlin team? To really solve this error, Kotlin needs to fix its compiler and then the library has to be recompiled with an updated version of that compiler.

raphw commented 1 month ago

OpenJDK issue: https://bugs.openjdk.org/browse/JDK-8337302