equalsverifier icon indicating copy to clipboard operation
equalsverifier copied to clipboard

`IncompatibleClassChangeError` on sealed type fields

Open Stephan202 opened this issue 3 years ago • 4 comments

(This issue relates to #564 and possibly #638.)

If one applies EqualsVerifier to a class with a field of some sealed type, then an exception is thrown:

	...
Caused by: java.lang.IllegalStateException: java.lang.IllegalStateException: Failed to invoke proxy for public abstract java.lang.Class net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles$Lookup.defineClass(java.lang.Object,byte[]) throws java.lang.IllegalAccessException
	at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1640)
	at net.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.inject(ClassInjector.java:118)
	at net.bytebuddy.dynamic.loading.ClassLoadingStrategy$UsingLookup.load(ClassLoadingStrategy.java:519)
	at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:101)
	at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6166)
	at nl.jqno.equalsverifier.internal.reflection.Instantiator.giveDynamicSubclass(Instantiator.java:98)
	at nl.jqno.equalsverifier.internal.reflection.Instantiator.of(Instantiator.java:47)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.buildObjectAccessor(ClassAccessor.java:261)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedAccessor(ClassAccessor.java:179)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedObject(ClassAccessor.java:168)
	at nl.jqno.equalsverifier.internal.prefabvalues.factories.FallbackFactory.giveInstances(FallbackFactory.java:95)
	at nl.jqno.equalsverifier.internal.prefabvalues.factories.FallbackFactory.createValues(FallbackFactory.java:40)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.createTuple(PrefabValues.java:157)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.realizeCacheFor(PrefabValues.java:140)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveTuple(PrefabValues.java:81)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveOther(PrefabValues.java:104)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.lambda$changeField$2(FieldModifier.java:97)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.wrappedChange(FieldModifier.java:118)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.lambda$change$3(FieldModifier.java:113)
	at nl.jqno.equalsverifier.internal.util.Rethrow.lambda$rethrow$0(Rethrow.java:47)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:30)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:45)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:55)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.change(FieldModifier.java:113)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.changeField(FieldModifier.java:100)
	at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.scramble(InPlaceObjectAccessor.java:52)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedAccessor(ClassAccessor.java:179)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedObject(ClassAccessor.java:168)
	at nl.jqno.equalsverifier.internal.util.Configuration.ensureUnequalExamples(Configuration.java:199)
	at nl.jqno.equalsverifier.internal.util.Configuration.build(Configuration.java:99)
	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.buildConfig(SingleTypeEqualsVerifierApi.java:381)
	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.performVerification(SingleTypeEqualsVerifierApi.java:367)
	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:312)
	... 66 more
Caused by: java.lang.IllegalStateException: Failed to invoke proxy for public abstract java.lang.Class net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles$Lookup.defineClass(java.lang.Object,byte[]) throws java.lang.IllegalAccessException
	at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1173)
	at jdk.proxy2/jdk.proxy2.$Proxy60.defineClass(Unknown Source)
	at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1638)
	... 98 more
Caused by: java.lang.IncompatibleClassChangeError: class com.picnic.testing.Super$$DynamicSubclass$768795248 cannot implement sealed interface com.picnic.testing.TestTest$Super
	at java.base/java.lang.ClassLoader.defineClass0(Native Method)
	at java.base/java.lang.System$2.defineClass(System.java:2307)
	at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2439)
	at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2416)
	at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1843)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at net.bytebuddy.utility.Invoker$Dispatcher.invoke(Unknown Source)
	at net.bytebuddy.utility.dispatcher.JavaDispatcher$Dispatcher$ForNonStaticMethod.invoke(JavaDispatcher.java:1028)
	at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1158)
	... 100 more

Code to reproduce:

import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;

public final class SealedFieldTypeTest {
  private sealed interface Super {}

  private non-sealed interface Sub extends Super {}

  static final class Container<T extends Super> {
    private final T element;

    Container(T element) {
      this.element = element;
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof Container && ((Container<?>) o).element.equals(element);
    }

    @Override
    public int hashCode() {
      return element.hashCode();
    }
  }

  @Test
  void equality() {
    EqualsVerifier.forClass(Container.class).verify();
  }
}

Tested against EqualsVerifier 3.10. The issue can be worked around by adding .withPrefabValues(Super.class, mock(Sub.class), mock(Sub.class)).

Stephan202 avatar May 08 '22 13:05 Stephan202

Thanks for reporting this, I can reproduce it. It's indeed related to #564. Not sure about #638, since Kotlin (probably) doesn't compile its sealed classes down to Java's sealed classes (yet?), although the solutions to both problems might be similar.

I want to make some smart logic to find final or non-sealed implementations of the sealed class/interface to use as prefab values, but until then, adding prefab values yourself is indeed the way to go. Maybe I could make the error message a bit more helpful in the mean time though.

jqno avatar May 09 '22 09:05 jqno

I think you can leverage the new methods isSealed() and getPermittedSubclasses() added to Class<?> in JDK 17:

assertTrue(Super.class.isSealed());

assertFalse(Sub.class.isSealed());

assertTrue(Sub.class.getSuperclass().isSealed());

assertArrayEquals(Super.class.getPermittedSubclasses(), new Class<?>[]{Sub.class});

dsubelman avatar Jun 10 '22 22:06 dsubelman

Indeed, it shouldn't be too hard...just haven't found time to look into it yet :sweat_smile:

jqno avatar Jun 13 '22 07:06 jqno

Thanks for reporting this, I can reproduce it. It's indeed related to #564. Not sure about #638, since Kotlin (probably) doesn't compile its sealed classes down to Java's sealed classes (yet?), although the solutions to both problems might be similar.

Kotlin 1.7 combined with Java 17 does compile sealed classes down to Java's sealed classes.

cliffred avatar Jul 14 '22 22:07 cliffred

It finally happened! EqualsVerifier 3.14 is now syncing with Maven Central. It fixes this issue and also some other related to sealed types.

jqno avatar Feb 27 '23 07:02 jqno