Skip to content

Commit

Permalink
Fixes AbstractMethodError in field's class's equals
Browse files Browse the repository at this point in the history
  • Loading branch information
jqno committed Mar 27, 2024
1 parent 28e42aa commit 7307f34
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 2 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

- AbstractMethodError when a the `equals` method in a field's class calls an abstract method. ([Issue 938](https://github.com/jqno/equalsverifier/issues/938))

## [3.16] - 2024-03-22

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public <T> T giveOther(TypeTag tag, T value) {
* @param typeStack Keeps track of recursion in the type.
* @return A value that is different from {@code value}.
*/
// CHECKSTYLE OFF: CyclomaticComplexity
public <T> T giveOther(TypeTag tag, T value, LinkedHashSet<TypeTag> typeStack) {
Class<T> type = tag.getType();
if (
Expand All @@ -134,12 +135,21 @@ public <T> T giveOther(TypeTag tag, T value, LinkedHashSet<TypeTag> typeStack) {
if (type.isArray() && arraysAreDeeplyEqual(tuple.getRed(), value)) {
return tuple.getBlue();
}
if (!type.isArray() && value != null && tuple.getRed().equals(value)) {
return tuple.getBlue();
if (!type.isArray() && value != null) {
try {
// red's equals can potentially call an abstract method
if (tuple.getRed().equals(value)) {
return tuple.getBlue();
}
} catch (AbstractMethodError e) {
return tuple.getRed();
}
}
return tuple.getRed();
}

// CHECKSTYLE ON: CyclomaticComplexity

private boolean wraps(Class<?> expectedClass, Class<?> actualClass) {
return PrimitiveMappers.PRIMITIVE_OBJECT_MAPPER.get(expectedClass) == actualClass;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,36 @@ public void originalMessageIsIncludedInErrorMessage_whenEqualsVerifierSignalsAnA
.assertMessageContains("This is AbstractMethodError's original message");
}

@Test
public void failGracefully_whenAFieldsEqualsMethodDoesntDoAnIdentityCheckButCallsAnAbstractField() {
ExpectedException
.when(() ->
EqualsVerifier
.forClass(EqualsInFieldWithoutIdentityCheckDelegatesToAbstractMethod.class)
.verify()
)
.assertFailure()
.assertCause(AbstractMethodError.class)
.assertMessageContains(
ABSTRACT_DELEGATION,
EQUALS_DELEGATES,
PREFAB,
AbstractEqualsWithoutIdentityCheckDelegator.class.getSimpleName()
);
}

@Test
public void succeed_whenAFieldsEqualsMethodDoesntDoAnIdentityCheckButCallsAnAbstractField_givenAConcretePrefabImplementationOfSaidField() {
EqualsVerifier
.forClass(EqualsInFieldWithoutIdentityCheckDelegatesToAbstractMethod.class)
.withPrefabValues(
AbstractEqualsWithoutIdentityCheckDelegator.class,
new AbstractEqualsWithoutIdentityCheckDelegatorImpl(1),
new AbstractEqualsWithoutIdentityCheckDelegatorImpl(2)
)
.verify();
}

private abstract static class AbstractClass {

private int i;
Expand Down Expand Up @@ -291,6 +321,48 @@ public boolean theAnswer() {
}
}

abstract static class AbstractEqualsWithoutIdentityCheckDelegator {

private final int i;

public AbstractEqualsWithoutIdentityCheckDelegator(int i) {
this.i = i;
}

abstract boolean theAnswer();

@Override
public boolean equals(Object obj) {
if (!(obj instanceof AbstractEqualsWithoutIdentityCheckDelegator)) {
return false;
}
if (theAnswer()) {
return true;
}
AbstractEqualsWithoutIdentityCheckDelegator other =
(AbstractEqualsWithoutIdentityCheckDelegator) obj;
return i == other.i;
}

@Override
public int hashCode() {
return defaultHashCode(this);
}
}

static final class AbstractEqualsWithoutIdentityCheckDelegatorImpl
extends AbstractEqualsWithoutIdentityCheckDelegator {

public AbstractEqualsWithoutIdentityCheckDelegatorImpl(int i) {
super(i);
}

@Override
public boolean theAnswer() {
return false;
}
}

abstract static class AbstractHashCodeDelegator {

private final int i;
Expand Down Expand Up @@ -612,4 +684,30 @@ public int hashCode() {
return defaultHashCode(this);
}
}

static class EqualsInFieldWithoutIdentityCheckDelegatesToAbstractMethod {

private final AbstractEqualsWithoutIdentityCheckDelegator id;

protected EqualsInFieldWithoutIdentityCheckDelegatesToAbstractMethod(
AbstractEqualsWithoutIdentityCheckDelegator id
) {
this.id = id;
}

@Override
public final boolean equals(Object other) {
if (!(other instanceof EqualsInFieldWithoutIdentityCheckDelegatesToAbstractMethod)) {
return false;
}
EqualsInFieldWithoutIdentityCheckDelegatesToAbstractMethod that =
(EqualsInFieldWithoutIdentityCheckDelegatesToAbstractMethod) other;
return Objects.equals(id, that.id);
}

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

0 comments on commit 7307f34

Please sign in to comment.