Skip to content

Commit

Permalink
#460: Add support for @EqualsAndHashCode(cacheStrategy = LAZY)
Browse files Browse the repository at this point in the history
  • Loading branch information
janeisklar committed Jul 6, 2021
1 parent a7efe17 commit cf00eef
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,21 @@ EqualsVerifier.forClass(CachedHashCode.class)
.withCachedHashCode("cachedHashCode", "calculateHashCode", new CachedHashCode());
{% endhighlight %}

Another scenario in which you might experience this error message is when using Lombok's `@EqualsAndHashCode` with `cacheStrategy=LAZY`:

{% highlight java %}
@RequiredArgsConstructor
@EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY)
public class CachedHashCode {
private final String foo;
}
{% endhighlight %}

Using `.withLombokCachedHashCode` allows to test those classes as well:

{% highlight java %}
EqualsVerifier.forClass(LazyPojo.class)
.withLombokCachedHashCode(new CachedHashCode("bar"));
{% endhighlight %}

For more help on how to use `withCachedHashCode`, read the [manual page about it](/equalsverifier/manual/caching-hashcodes).
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,20 @@ public SingleTypeEqualsVerifierApi<T> withCachedHashCode(
return this;
}

/**
* Signals that T uses Lombok to cache its hashCode, instead of re-calculating it each time the
* {@code hashCode()} method is called.
*
* @param example An instance of the class under test, to verify that the hashCode has been
* initialized properly.
* @return {@code this}, for easy method chaining.
* @see #withCachedHashCode(String, String, T)
*/
public SingleTypeEqualsVerifierApi<T> withLombokCachedHashCode(T example) {
cachedHashCodeInitializer = CachedHashCodeInitializer.lombokCachedHashcode(example);
return this;
}

/**
* Performs the verification of the contracts for {@code equals} and {@code hashCode} and throws
* an {@link AssertionError} if there is a problem.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,19 @@ public class CachedHashCodeInitializer<T> {
private final T example;

private CachedHashCodeInitializer() {
this.passthrough = true;
this.cachedHashCodeField = null;
this.calculateMethod = null;
this.example = null;
this(true, null, null, null);
}

private CachedHashCodeInitializer(
boolean passthrough,
Field cachedHashCodeField,
Method calculateMethod,
T example
) {
this.passthrough = passthrough;
this.cachedHashCodeField = cachedHashCodeField;
this.calculateMethod = calculateMethod;
this.example = example;
}

public CachedHashCodeInitializer(
Expand All @@ -44,14 +53,25 @@ public CachedHashCodeInitializer(
) {
this.passthrough = false;
this.cachedHashCodeField = findCachedHashCodeField(type, cachedHashCodeField);
this.calculateMethod = findCalculateHashCodeMethod(type, calculateHashCodeMethod);
this.calculateMethod = findCalculateHashCodeMethod(type, calculateHashCodeMethod, false);
this.example = example;
}

public static <T> CachedHashCodeInitializer<T> passthrough() {
return new CachedHashCodeInitializer<>();
}

public static <T> CachedHashCodeInitializer<T> lombokCachedHashcode(T example) {
@SuppressWarnings("unchecked")
final Class<T> type = (Class<T>) example.getClass();
return new CachedHashCodeInitializer<>(
false,
findCachedHashCodeField(type, "$hashCodeCache"),
findCalculateHashCodeMethod(type, "hashCode", true),
example
);
}

public boolean isPassthrough() {
return passthrough;
}
Expand Down Expand Up @@ -85,7 +105,7 @@ private void recomputeCachedHashCode(Object object) {
);
}

private Field findCachedHashCodeField(Class<?> type, String cachedHashCodeFieldName) {
private static Field findCachedHashCodeField(Class<?> type, String cachedHashCodeFieldName) {
for (Field candidateField : FieldIterable.of(type)) {
if (candidateField.getName().equals(cachedHashCodeFieldName)) {
if (
Expand All @@ -104,12 +124,16 @@ private Field findCachedHashCodeField(Class<?> type, String cachedHashCodeFieldN
);
}

private Method findCalculateHashCodeMethod(Class<?> type, String calculateHashCodeMethodName) {
private static Method findCalculateHashCodeMethod(
Class<?> type,
String calculateHashCodeMethodName,
boolean acceptPublicMethod
) {
for (Class<?> currentClass : SuperclassIterable.ofIncludeSelf(type)) {
try {
Method method = currentClass.getDeclaredMethod(calculateHashCodeMethodName);
if (
!Modifier.isPublic(method.getModifiers()) &&
(acceptPublicMethod || !Modifier.isPublic(method.getModifiers())) &&
method.getReturnType().equals(int.class)
) {
method.setAccessible(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package nl.jqno.equalsverifier.integration.extra_features;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.assertThrows;

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

// CHECKSTYLE OFF: LocalFinalVariableName
// CHECKSTYLE OFF: MemberName
// CHECKSTYLE OFF: NeedBraces

public class LombokLazyEqualsAndHashcodeTest {

@Test
void testWithLombokCachedHashCode() {
EqualsVerifier
.forClass(LazyPojo.class)
.withLombokCachedHashCode(new LazyPojo("a", new Object()))
.suppress(Warning.STRICT_INHERITANCE)
.verify();
}

@Test
void testDefaultEqualsVerifierFailsForCachedLombokEqualsAndHashcode() {
final AssertionError error = assertThrows(
AssertionError.class,
() ->
EqualsVerifier
.forClass(LazyPojo.class)
.suppress(Warning.STRICT_INHERITANCE)
.verify()
);
assertThat(
error.getMessage(),
containsString("hashCode relies on $hashCodeCache, but equals does not.")
);
}

@Test
void testDefaultEqualsVerifierFailsForCachedLombokEqualsAndHashcodeWhenUsingWithCachedHashCode() {
final IllegalArgumentException error = assertThrows(
IllegalArgumentException.class,
() ->
EqualsVerifier
.forClass(LazyPojo.class)
.suppress(Warning.STRICT_INHERITANCE)
.withCachedHashCode(
"$hashCodeCache",
"hashCode",
new LazyPojo("bar", new Object())
)
.verify()
);
assertThat(
error.getMessage(),
containsString(
"Cached hashCode: Could not find calculateHashCodeMethod: must be 'private int hashCode()'"
)
);
}

/**
* This class has been generated with Lombok (1.18.20). It is equivalent to:
* <pre>
* &#64;RequiredArgsConstructor
* &#64;EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY)
* public class LazyPojo {
*
* private final String foo;
*
* private final Object bar;
* }
* </pre>
*/
@SuppressWarnings({ "RedundantIfStatement", "EqualsReplaceableByObjectsCall" })
private static class LazyPojo {

private transient int $hashCodeCache;

private final String foo;
private final Object bar;

public LazyPojo(String foo, Object bar) {
this.foo = foo;
this.bar = bar;
}

@Override
public boolean equals(final Object o) {
if (o == this) return true;
if (!(o instanceof LazyPojo)) return false;
final LazyPojo other = (LazyPojo) o;
if (!other.canEqual(this)) return false;
final Object this$foo = this.foo;
final Object other$foo = other.foo;
if (this$foo == null ? other$foo != null : !this$foo.equals(other$foo)) return false;
final Object this$bar = this.bar;
final Object other$bar = other.bar;
if (this$bar == null ? other$bar != null : !this$bar.equals(other$bar)) return false;
return true;
}

protected boolean canEqual(Object other) {
return other instanceof LazyPojo;
}

@Override
public int hashCode() {
if (this.$hashCodeCache != 0) {
return this.$hashCodeCache;
} else {
final int PRIME = 59;
int result = 1;
final Object $foo = this.foo;
result = result * PRIME + ($foo == null ? 43 : $foo.hashCode());
final Object $bar = this.bar;
result = result * PRIME + ($bar == null ? 43 : $bar.hashCode());

this.$hashCodeCache = result;
return result;
}
}
}
}

0 comments on commit cf00eef

Please sign in to comment.