Skip to content

Commit cf00eef

Browse files
committed
#460: Add support for @EqualsAndHashCode(cacheStrategy = LAZY)
1 parent a7efe17 commit cf00eef

File tree

4 files changed

+190
-8
lines changed

4 files changed

+190
-8
lines changed

docs/_errormessages/significant-fields-equals-relies-on-foo-but-hashcode-does-not.md

+17
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,21 @@ EqualsVerifier.forClass(CachedHashCode.class)
5151
.withCachedHashCode("cachedHashCode", "calculateHashCode", new CachedHashCode());
5252
{% endhighlight %}
5353

54+
Another scenario in which you might experience this error message is when using Lombok's `@EqualsAndHashCode` with `cacheStrategy=LAZY`:
55+
56+
{% highlight java %}
57+
@RequiredArgsConstructor
58+
@EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY)
59+
public class CachedHashCode {
60+
private final String foo;
61+
}
62+
{% endhighlight %}
63+
64+
Using `.withLombokCachedHashCode` allows to test those classes as well:
65+
66+
{% highlight java %}
67+
EqualsVerifier.forClass(LazyPojo.class)
68+
.withLombokCachedHashCode(new CachedHashCode("bar"));
69+
{% endhighlight %}
70+
5471
For more help on how to use `withCachedHashCode`, read the [manual page about it](/equalsverifier/manual/caching-hashcodes).

src/main/java/nl/jqno/equalsverifier/api/SingleTypeEqualsVerifierApi.java

+14
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,20 @@ public SingleTypeEqualsVerifierApi<T> withCachedHashCode(
278278
return this;
279279
}
280280

281+
/**
282+
* Signals that T uses Lombok to cache its hashCode, instead of re-calculating it each time the
283+
* {@code hashCode()} method is called.
284+
*
285+
* @param example An instance of the class under test, to verify that the hashCode has been
286+
* initialized properly.
287+
* @return {@code this}, for easy method chaining.
288+
* @see #withCachedHashCode(String, String, T)
289+
*/
290+
public SingleTypeEqualsVerifierApi<T> withLombokCachedHashCode(T example) {
291+
cachedHashCodeInitializer = CachedHashCodeInitializer.lombokCachedHashcode(example);
292+
return this;
293+
}
294+
281295
/**
282296
* Performs the verification of the contracts for {@code equals} and {@code hashCode} and throws
283297
* an {@link AssertionError} if there is a problem.

src/main/java/nl/jqno/equalsverifier/internal/util/CachedHashCodeInitializer.java

+32-8
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,19 @@ public class CachedHashCodeInitializer<T> {
3030
private final T example;
3131

3232
private CachedHashCodeInitializer() {
33-
this.passthrough = true;
34-
this.cachedHashCodeField = null;
35-
this.calculateMethod = null;
36-
this.example = null;
33+
this(true, null, null, null);
34+
}
35+
36+
private CachedHashCodeInitializer(
37+
boolean passthrough,
38+
Field cachedHashCodeField,
39+
Method calculateMethod,
40+
T example
41+
) {
42+
this.passthrough = passthrough;
43+
this.cachedHashCodeField = cachedHashCodeField;
44+
this.calculateMethod = calculateMethod;
45+
this.example = example;
3746
}
3847

3948
public CachedHashCodeInitializer(
@@ -44,14 +53,25 @@ public CachedHashCodeInitializer(
4453
) {
4554
this.passthrough = false;
4655
this.cachedHashCodeField = findCachedHashCodeField(type, cachedHashCodeField);
47-
this.calculateMethod = findCalculateHashCodeMethod(type, calculateHashCodeMethod);
56+
this.calculateMethod = findCalculateHashCodeMethod(type, calculateHashCodeMethod, false);
4857
this.example = example;
4958
}
5059

5160
public static <T> CachedHashCodeInitializer<T> passthrough() {
5261
return new CachedHashCodeInitializer<>();
5362
}
5463

64+
public static <T> CachedHashCodeInitializer<T> lombokCachedHashcode(T example) {
65+
@SuppressWarnings("unchecked")
66+
final Class<T> type = (Class<T>) example.getClass();
67+
return new CachedHashCodeInitializer<>(
68+
false,
69+
findCachedHashCodeField(type, "$hashCodeCache"),
70+
findCalculateHashCodeMethod(type, "hashCode", true),
71+
example
72+
);
73+
}
74+
5575
public boolean isPassthrough() {
5676
return passthrough;
5777
}
@@ -85,7 +105,7 @@ private void recomputeCachedHashCode(Object object) {
85105
);
86106
}
87107

88-
private Field findCachedHashCodeField(Class<?> type, String cachedHashCodeFieldName) {
108+
private static Field findCachedHashCodeField(Class<?> type, String cachedHashCodeFieldName) {
89109
for (Field candidateField : FieldIterable.of(type)) {
90110
if (candidateField.getName().equals(cachedHashCodeFieldName)) {
91111
if (
@@ -104,12 +124,16 @@ private Field findCachedHashCodeField(Class<?> type, String cachedHashCodeFieldN
104124
);
105125
}
106126

107-
private Method findCalculateHashCodeMethod(Class<?> type, String calculateHashCodeMethodName) {
127+
private static Method findCalculateHashCodeMethod(
128+
Class<?> type,
129+
String calculateHashCodeMethodName,
130+
boolean acceptPublicMethod
131+
) {
108132
for (Class<?> currentClass : SuperclassIterable.ofIncludeSelf(type)) {
109133
try {
110134
Method method = currentClass.getDeclaredMethod(calculateHashCodeMethodName);
111135
if (
112-
!Modifier.isPublic(method.getModifiers()) &&
136+
(acceptPublicMethod || !Modifier.isPublic(method.getModifiers())) &&
113137
method.getReturnType().equals(int.class)
114138
) {
115139
method.setAccessible(true);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package nl.jqno.equalsverifier.integration.extra_features;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.core.StringContains.containsString;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import nl.jqno.equalsverifier.EqualsVerifier;
8+
import nl.jqno.equalsverifier.Warning;
9+
import org.junit.jupiter.api.Test;
10+
11+
// CHECKSTYLE OFF: LocalFinalVariableName
12+
// CHECKSTYLE OFF: MemberName
13+
// CHECKSTYLE OFF: NeedBraces
14+
15+
public class LombokLazyEqualsAndHashcodeTest {
16+
17+
@Test
18+
void testWithLombokCachedHashCode() {
19+
EqualsVerifier
20+
.forClass(LazyPojo.class)
21+
.withLombokCachedHashCode(new LazyPojo("a", new Object()))
22+
.suppress(Warning.STRICT_INHERITANCE)
23+
.verify();
24+
}
25+
26+
@Test
27+
void testDefaultEqualsVerifierFailsForCachedLombokEqualsAndHashcode() {
28+
final AssertionError error = assertThrows(
29+
AssertionError.class,
30+
() ->
31+
EqualsVerifier
32+
.forClass(LazyPojo.class)
33+
.suppress(Warning.STRICT_INHERITANCE)
34+
.verify()
35+
);
36+
assertThat(
37+
error.getMessage(),
38+
containsString("hashCode relies on $hashCodeCache, but equals does not.")
39+
);
40+
}
41+
42+
@Test
43+
void testDefaultEqualsVerifierFailsForCachedLombokEqualsAndHashcodeWhenUsingWithCachedHashCode() {
44+
final IllegalArgumentException error = assertThrows(
45+
IllegalArgumentException.class,
46+
() ->
47+
EqualsVerifier
48+
.forClass(LazyPojo.class)
49+
.suppress(Warning.STRICT_INHERITANCE)
50+
.withCachedHashCode(
51+
"$hashCodeCache",
52+
"hashCode",
53+
new LazyPojo("bar", new Object())
54+
)
55+
.verify()
56+
);
57+
assertThat(
58+
error.getMessage(),
59+
containsString(
60+
"Cached hashCode: Could not find calculateHashCodeMethod: must be 'private int hashCode()'"
61+
)
62+
);
63+
}
64+
65+
/**
66+
* This class has been generated with Lombok (1.18.20). It is equivalent to:
67+
* <pre>
68+
* &#64;RequiredArgsConstructor
69+
* &#64;EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY)
70+
* public class LazyPojo {
71+
*
72+
* private final String foo;
73+
*
74+
* private final Object bar;
75+
* }
76+
* </pre>
77+
*/
78+
@SuppressWarnings({ "RedundantIfStatement", "EqualsReplaceableByObjectsCall" })
79+
private static class LazyPojo {
80+
81+
private transient int $hashCodeCache;
82+
83+
private final String foo;
84+
private final Object bar;
85+
86+
public LazyPojo(String foo, Object bar) {
87+
this.foo = foo;
88+
this.bar = bar;
89+
}
90+
91+
@Override
92+
public boolean equals(final Object o) {
93+
if (o == this) return true;
94+
if (!(o instanceof LazyPojo)) return false;
95+
final LazyPojo other = (LazyPojo) o;
96+
if (!other.canEqual(this)) return false;
97+
final Object this$foo = this.foo;
98+
final Object other$foo = other.foo;
99+
if (this$foo == null ? other$foo != null : !this$foo.equals(other$foo)) return false;
100+
final Object this$bar = this.bar;
101+
final Object other$bar = other.bar;
102+
if (this$bar == null ? other$bar != null : !this$bar.equals(other$bar)) return false;
103+
return true;
104+
}
105+
106+
protected boolean canEqual(Object other) {
107+
return other instanceof LazyPojo;
108+
}
109+
110+
@Override
111+
public int hashCode() {
112+
if (this.$hashCodeCache != 0) {
113+
return this.$hashCodeCache;
114+
} else {
115+
final int PRIME = 59;
116+
int result = 1;
117+
final Object $foo = this.foo;
118+
result = result * PRIME + ($foo == null ? 43 : $foo.hashCode());
119+
final Object $bar = this.bar;
120+
result = result * PRIME + ($bar == null ? 43 : $bar.hashCode());
121+
122+
this.$hashCodeCache = result;
123+
return result;
124+
}
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)