Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sealed interface may be cause of StackOverflowError because of recursion #920

Closed
JarKz opened this issue Feb 16, 2024 · 4 comments
Closed
Labels

Comments

@JarKz
Copy link

JarKz commented Feb 16, 2024

Describe the bug
When type A have a field of interface type, which is sealed. And this type permits only type that have a field of type A. If i don't prefab type A with non-null field of interface type, verifier will seek sealed types and return to base Type, and again seek sealed types...

For better clarification, I put the example below. In the code I have three types: type A, sealed interface I and it's permitted type E. Type A have a field of sealed interface I and the permitted type E have a field of type A. It's the simple way to call recursion.

To Reproduce
Put the code below into any test container and run.

Code that triggers the behavior

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

class AppTest {

  @Test
  public void testEV() {
    EqualsVerifier.forClass(A.class).verify();
  }

  class A {
    public I sealedClassField;

    @Override
    public int hashCode() {
      return Objects.hash(sealedClassField);
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (!(obj instanceof A)) return false;
      A other = (A) obj;
      return Objects.equals(sealedClassField, other.sealedClassField);
    }
  }

  public sealed interface I permits E {}

  public final class E implements I {

    public A referenceToA;

    @Override
    public int hashCode() {
      return Objects.hash(referenceToA);
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (!(obj instanceof E)) return false;
      E other = (E) obj;
      return Objects.equals(referenceToA, other.referenceToA);
    }
  }
}

Error message

java.lang.AssertionError: EqualsVerifier found a problem in class ev.AppTest$A.
-> <no message>

For more information, go to: https://www.jqno.nl/equalsverifier/errormessages
(EqualsVerifier 3.15.6, JDK 17.0.9 on Mac OS X)
	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:351)
	at ev.AppTest.testEV(AppTest.java:14)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: java.lang.StackOverflowError
	at java.base/java.util.stream.StreamSupport.stream(StreamSupport.java:70)
	at java.base/java.util.Arrays.stream(Arrays.java:5447)
	at java.base/java.util.Arrays.stream(Arrays.java:5428)
	at java.base/java.lang.Class.getPermittedSubclasses(Class.java:4515)
	at java.base/java.lang.Class.isSealed(Class.java:4566)
	at nl.jqno.equalsverifier.internal.reflection.SealedTypesHelper.isSealed(SealedTypesHelper.java:12)
	at nl.jqno.equalsverifier.internal.reflection.Instantiator.of(Instantiator.java:50)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.buildObjectAccessor(ClassAccessor.java:278)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedAccessor(ClassAccessor.java:196)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedObject(ClassAccessor.java:185)
	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.factories.FallbackFactory.traverseFields(FallbackFactory.java:88)
	at nl.jqno.equalsverifier.internal.prefabvalues.factories.FallbackFactory.createValues(FallbackFactory.java:39)
	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$1(Rethrow.java:51)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:34)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:49)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:59)
	...infinity repeated rows are omitted.

Expected behavior
Throw an error that describes the recursion of this case.

Or disable this checker for sealed intrefaces by adding suppression or calling a method (i.e. verifySealedTypes(false)) before the method verify().

Version
I use latest version at this moment: 3.15.6.

Additional context
I understand, if you will say me that I must not use the field of type A in permitted type E. But it's not my desire. I have a lot of autogenerated types for API and I don't want to allow another user of library to implement an interface I. And I already sure that these premitted types are valid by EqualsVerifier package, so I want only turn off this uncomfortable function.

@jqno
Copy link
Owner

jqno commented Feb 16, 2024

Thanks for the code example, it was very easy to reproduce the issue that way.
Finding a solution is less easy, I'll let you know when I have something. In the mean time, I recommend that you use the workaround you've already found, of adding prefab values for A.

@jqno jqno added the accepted label Feb 16, 2024
@jqno
Copy link
Owner

jqno commented Feb 23, 2024

Version 3.15.7 is now syncing with Maven Central. It will give you a clean recursion error, instead of a StackOverflowError.

There's still some edge cases I should probably work out, but I think this should solve your issue.

@jqno jqno closed this as completed Feb 23, 2024
@JarKz
Copy link
Author

JarKz commented Feb 23, 2024

Thank you very much! It solved my problem!

I'm very glad to continue my coding without friction.

@jqno
Copy link
Owner

jqno commented Feb 23, 2024

Glad to hear it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants