Skip to content

Commit

Permalink
Merge pull request #927 from jqno/sealed-interface-recursion
Browse files Browse the repository at this point in the history
Sealed interface recursion
  • Loading branch information
jqno authored Feb 23, 2024
2 parents c582890 + f319cac commit 5a34b6a
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 42 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- StackOverflowError when a class has a field of a sealed type whose only permitted subtype has a reference to the original class. ([Issue 920](https://github.com/jqno/equalsverifier/issues/920))

## [3.15.6] - 2024-01-09

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
package nl.jqno.equalsverifier.internal.reflection;

import static nl.jqno.equalsverifier.internal.prefabvalues.factories.Factories.values;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import nl.jqno.equalsverifier.internal.exceptions.EqualsVerifierInternalBugException;
import nl.jqno.equalsverifier.internal.prefabvalues.FactoryCache;
import nl.jqno.equalsverifier.internal.prefabvalues.JavaApiPrefabValues;
import nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues;
import nl.jqno.equalsverifier.internal.prefabvalues.TypeTag;
import nl.jqno.equalsverifier.internal.prefabvalues.*;
import nl.jqno.equalsverifier.internal.testhelpers.ExpectedException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class RecordObjectAccessorScramblingTest {

private static final LinkedHashSet<TypeTag> EMPTY_TYPE_STACK = new LinkedHashSet<>();
private FactoryCache factoryCache;
private PrefabValues prefabValues;

Expand Down Expand Up @@ -137,7 +132,7 @@ private Object fieldValue(ObjectAccessor<?> accessor, String fieldName)
}

private ObjectAccessor<Object> doScramble(Object object) {
return create(object).scramble(prefabValues, TypeTag.NULL);
return create(object).scramble(prefabValues, TypeTag.NULL, EMPTY_TYPE_STACK);
}

record Point(int x, int y) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.LinkedHashSet;
import java.util.Objects;
import nl.jqno.equalsverifier.internal.exceptions.ReflectionException;
import nl.jqno.equalsverifier.internal.prefabvalues.JavaApiPrefabValues;
Expand All @@ -17,6 +18,7 @@

public class RecordObjectAccessorTest {

private static final LinkedHashSet<TypeTag> EMPTY_TYPE_STACK = new LinkedHashSet<>();
private Object recordInstance;

@BeforeEach
Expand Down Expand Up @@ -72,7 +74,7 @@ public void fail_whenConstructorThrowsOnSomethingElse() {

PrefabValues pv = new PrefabValues(JavaApiPrefabValues.build());
ExpectedException
.when(() -> accessorFor(instance).scramble(pv, TypeTag.NULL))
.when(() -> accessorFor(instance).scramble(pv, TypeTag.NULL, EMPTY_TYPE_STACK))
.assertThrows(ReflectionException.class)
.assertMessageContains("Record:", "failed to run constructor", "prefab values");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package nl.jqno.equalsverifier.integration.extended_contract;

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

class SealedTypesRecursionTest {

@Test
public void dontThrowStackOverflowError_whenOnlyPermittedSubclassInSealedInterfaceRefersBackToContainer() {
// A container with a field of a sealed interface.
// The sealed interface permits only 1 type, which refers back to the container.
ExpectedException
.when(() -> EqualsVerifier.forClass(SealedContainer.class).verify())
.assertFailure()
.assertMessageContains(
"Recursive datastructure",
"Add prefab values for one of the following types",
"SealedContainer",
"SealedInterface"
);
}

@Test
public void dontThrowStackOverflowError_whenOnlyPermittedRecordInSealedInterfaceRefersBackToContainer() {
// A container with a field of a sealed interface.
// The sealed interface permits only 1 type, which is a record that refers back to the container.
ExpectedException
.when(() -> EqualsVerifier.forClass(SealedRecordContainer.class).verify())
.assertFailure()
.assertMessageContains(
"Recursive datastructure",
"Add prefab values for one of the following types",
"SealedRecordContainer",
"SealedRecordInterface"
);
}

static final class SealedContainer {

public final SealedInterface sealed;

public SealedContainer(SealedInterface sealed) {
this.sealed = sealed;
}

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

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

sealed interface SealedInterface permits OnlyPermittedImplementation {}

static final class OnlyPermittedImplementation implements SealedInterface {

public final SealedContainer container;

public OnlyPermittedImplementation(SealedContainer container) {
this.container = container;
}

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

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

static final class SealedRecordContainer {

public final SealedRecordInterface sealed;

public SealedRecordContainer(SealedRecordInterface sealed) {
this.sealed = sealed;
}

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

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

sealed interface SealedRecordInterface permits OnlyPermittedRecordImplementation {}

static final record OnlyPermittedRecordImplementation(SealedRecordContainer container)
implements SealedRecordInterface {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,19 @@ public <T> T giveRedCopy(TypeTag tag) {
* @return A tuple of two different values of the given type.
*/
public <T> Tuple<T> giveTuple(TypeTag tag) {
realizeCacheFor(tag, emptyStack());
return giveTuple(tag, new LinkedHashSet<>());
}

/**
* Returns a tuple of two different prefabricated values of the specified type.
*
* @param <T> The returned tuple will have this generic type.
* @param tag A description of the desired type, including generic parameters.
* @param typeStack Keeps track of recursion in the type.
* @return A tuple of two different values of the given type.
*/
public <T> Tuple<T> giveTuple(TypeTag tag, LinkedHashSet<TypeTag> typeStack) {
realizeCacheFor(tag, typeStack);
return cache.getTuple(tag);
}

Expand All @@ -92,6 +104,20 @@ public <T> Tuple<T> giveTuple(TypeTag tag) {
* @return A value that is different from {@code value}.
*/
public <T> T giveOther(TypeTag tag, T value) {
return giveOther(tag, value, new LinkedHashSet<>());
}

/**
* Returns a prefabricated value of the specified type, that is different from the specified
* value.
*
* @param <T> The type of the value.
* @param tag A description of the desired type, including generic parameters.
* @param value A value that is different from the value that will be returned.
* @param typeStack Keeps track of recursion in the type.
* @return A value that is different from {@code value}.
*/
public <T> T giveOther(TypeTag tag, T value, LinkedHashSet<TypeTag> typeStack) {
Class<T> type = tag.getType();
if (
value != null &&
Expand All @@ -101,7 +127,7 @@ public <T> T giveOther(TypeTag tag, T value) {
throw new ReflectionException("TypeTag does not match value.");
}

Tuple<T> tuple = giveTuple(tag);
Tuple<T> tuple = giveTuple(tag, typeStack);
if (tuple.getRed() == null) {
return null;
}
Expand All @@ -123,10 +149,6 @@ private boolean arraysAreDeeplyEqual(Object x, Object y) {
return Arrays.deepEquals(new Object[] { x }, new Object[] { y });
}

private LinkedHashSet<TypeTag> emptyStack() {
return new LinkedHashSet<>();
}

/**
* Makes sure that values for the specified type are present in the cache, but doesn't return
* them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public Tuple<T> createValues(
}

traverseFields(tag, prefabValues, clone);
return giveInstances(tag, prefabValues);
return giveInstances(tag, prefabValues, clone);
}

private Tuple<T> giveEnumInstances(TypeTag tag) {
Expand Down Expand Up @@ -90,11 +90,15 @@ private void traverseFields(
}
}

private Tuple<T> giveInstances(TypeTag tag, PrefabValues prefabValues) {
private Tuple<T> giveInstances(
TypeTag tag,
PrefabValues prefabValues,
LinkedHashSet<TypeTag> typeStack
) {
ClassAccessor<T> accessor = ClassAccessor.of(tag.getType(), prefabValues);
T red = accessor.getRedObject(tag);
T blue = accessor.getBlueObject(tag);
T redCopy = accessor.getRedObject(tag);
T red = accessor.getRedObject(tag, typeStack);
T blue = accessor.getBlueObject(tag, typeStack);
T redCopy = accessor.getRedObject(tag, typeStack);
return new Tuple<>(red, blue, redCopy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Predicate;
import nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues;
Expand Down Expand Up @@ -185,6 +186,19 @@ public T getRedObject(TypeTag enclosingType) {
return getRedAccessor(enclosingType).get();
}

/**
* Returns an instance of T that is not equal to the instance of T returned by {@link
* #getBlueObject(TypeTag)}.
*
* @param enclosingType Describes the type that contains this object as a field, to determine
* any generic parameters it may contain.
* @param typeStack Keeps track of recursion in the type.
* @return An instance of T.
*/
public T getRedObject(TypeTag enclosingType, LinkedHashSet<TypeTag> typeStack) {
return getRedAccessor(enclosingType, typeStack).get();
}

/**
* Returns an {@link ObjectAccessor} for {@link #getRedObject(TypeTag)}.
*
Expand All @@ -193,7 +207,22 @@ public T getRedObject(TypeTag enclosingType) {
* @return An {@link ObjectAccessor} for {@link #getRedObject(TypeTag)}.
*/
public ObjectAccessor<T> getRedAccessor(TypeTag enclosingType) {
return buildObjectAccessor().scramble(prefabValues, enclosingType);
return getRedAccessor(enclosingType, new LinkedHashSet<>());
}

/**
* Returns an {@link ObjectAccessor} for {@link #getRedObject(TypeTag)}.
*
* @param enclosingType Describes the type that contains this object as a field, to determine
* any generic parameters it may contain.
* @param typeStack Keeps track of recursion in the type.
* @return An {@link ObjectAccessor} for {@link #getRedObject(TypeTag)}.
*/
public ObjectAccessor<T> getRedAccessor(
TypeTag enclosingType,
LinkedHashSet<TypeTag> typeStack
) {
return buildObjectAccessor().scramble(prefabValues, enclosingType, typeStack);
}

/**
Expand All @@ -208,6 +237,19 @@ public T getBlueObject(TypeTag enclosingType) {
return getBlueAccessor(enclosingType).get();
}

/**
* Returns an instance of T that is not equal to the instance of T returned by {@link
* #getRedObject(TypeTag)}.
*
* @param enclosingType Describes the type that contains this object as a field, to determine
* any generic parameters it may contain.
* @param typeStack Keeps track of recursion in the type.
* @return An instance of T.
*/
public T getBlueObject(TypeTag enclosingType, LinkedHashSet<TypeTag> typeStack) {
return getBlueAccessor(enclosingType, typeStack).get();
}

/**
* Returns an {@link ObjectAccessor} for {@link #getBlueObject(TypeTag)}.
*
Expand All @@ -216,9 +258,24 @@ public T getBlueObject(TypeTag enclosingType) {
* @return An {@link ObjectAccessor} for {@link #getBlueObject(TypeTag)}.
*/
public ObjectAccessor<T> getBlueAccessor(TypeTag enclosingType) {
return getBlueAccessor(enclosingType, new LinkedHashSet<>());
}

/**
* Returns an {@link ObjectAccessor} for {@link #getBlueObject(TypeTag)}.
*
* @param enclosingType Describes the type that contains this object as a field, to determine
* any generic parameters it may contain.
* @param typeStack Keeps track of recursion in the type.
* @return An {@link ObjectAccessor} for {@link #getBlueObject(TypeTag)}.
*/
public ObjectAccessor<T> getBlueAccessor(
TypeTag enclosingType,
LinkedHashSet<TypeTag> typeStack
) {
return buildObjectAccessor()
.scramble(prefabValues, enclosingType)
.scramble(prefabValues, enclosingType);
.scramble(prefabValues, enclosingType, typeStack)
.scramble(prefabValues, enclosingType, typeStack);
}

/**
Expand Down
Loading

0 comments on commit 5a34b6a

Please sign in to comment.