Skip to content

Commit 9ba897e

Browse files
authored
Merge pull request #1087 from jqno/kotlin-delegate
Support Kotlin delegates
2 parents ee5a592 + dff313e commit 9ba897e

File tree

6 files changed

+100
-13
lines changed

6 files changed

+100
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
## [Unreleased]
1616

17+
### Added
18+
19+
- Support for Kotlin delegates. ([Issue 1083](https://github.com/jqno/equalsverifier/issues/1083))
20+
1721
## [4.0.1] - 2025-06-10
1822

1923
### Fixed

equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/FieldIterable.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,22 @@ private List<FieldProbe> addFieldsFor(Class<?> c) {
131131
var statics = new ArrayList<FieldProbe>();
132132

133133
for (Field field : c.getDeclaredFields()) {
134-
if (!field.isSynthetic()
135-
&& !"__cobertura_counters".equals(field.getName())
136-
&& !field.getName().startsWith("bitmap$init$") // Generated by Scala 2.x's -Xcheckinit flag
137-
) {
138-
FieldProbe probe = FieldProbe.of(field);
139-
boolean isStatic = probe.isStatic();
140-
if (isStatic && includeStatic) {
141-
statics.add(probe);
142-
}
143-
if (!isStatic) {
144-
fields.add(probe);
145-
}
134+
FieldProbe probe = FieldProbe.of(field);
135+
136+
if (field.isSynthetic() && !probe.isKotlinDelegate()) {
137+
continue;
138+
}
139+
if (probe.getName().startsWith("bitmap$init$") // Generated by Scala 2.x's -Xcheckinit flag
140+
|| probe.getName().equals("__cobertura_counters")) {
141+
continue;
142+
}
143+
144+
boolean isStatic = probe.isStatic();
145+
if (isStatic && includeStatic) {
146+
statics.add(probe);
147+
}
148+
if (!isStatic) {
149+
fields.add(probe);
146150
}
147151
}
148152

equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/FieldProbe.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,21 @@ public boolean isAnnotatedNonnull(AnnotationCache annotationCache) {
170170
* @return Whether or not the field can be modified reflectively.
171171
*/
172172
public boolean canBeModifiedReflectively() {
173-
if (field.isSynthetic()) {
173+
if (field.isSynthetic() && !isKotlinDelegate()) {
174174
return false;
175175
}
176176
if (isFinal() && isStatic()) {
177177
return false;
178178
}
179179
return true;
180180
}
181+
182+
/**
183+
* Determines whether the field is a synthetic Kotlin delegate field.
184+
*
185+
* @return Whether or not the field is a synthetic Kotlin delegate field.
186+
*/
187+
public boolean isKotlinDelegate() {
188+
return field.isSynthetic() && field.getName().startsWith("$$delegate_");
189+
}
181190
}

equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/util/Configuration.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import nl.jqno.equalsverifier.Mode;
99
import nl.jqno.equalsverifier.Warning;
10+
import nl.jqno.equalsverifier.internal.reflection.FieldIterable;
11+
import nl.jqno.equalsverifier.internal.reflection.FieldProbe;
1012
import nl.jqno.equalsverifier.internal.reflection.TypeTag;
1113
import nl.jqno.equalsverifier.internal.reflection.annotations.*;
1214

@@ -97,6 +99,14 @@ public static <T> Configuration<T> build(
9799
fieldnameToGetter != null ? fieldnameToGetter : Configuration::defaulFieldNameToGetterConverter;
98100
boolean isKotlin = annotationCache.hasClassAnnotation(type, SupportedAnnotations.KOTLIN);
99101

102+
if (isKotlin) {
103+
for (FieldProbe f : FieldIterable.ofKotlin(type)) {
104+
if (f.isKotlinDelegate()) {
105+
nonnullFields.add(f.getName());
106+
}
107+
}
108+
}
109+
100110
return new Configuration<>(type,
101111
typeTag,
102112
ignoredFields,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package nl.jqno.equalsverifier.internal.reflection
2+
3+
import nl.jqno.equalsverifier.internal.reflection.FieldProbe
4+
import org.junit.jupiter.api.Test
5+
6+
import org.assertj.core.api.Assertions.assertThat
7+
8+
class KotlinFieldProbeTest {
9+
10+
@Test
11+
fun isKotlinDelegate() {
12+
var f = FooContainer::class.java.getDeclaredField("\$\$delegate_0")
13+
var probe = FieldProbe.of(f)
14+
assertThat(probe.isKotlinDelegate()).isTrue()
15+
}
16+
17+
@Test
18+
fun isNotKotlinDelegate() {
19+
var f = FooContainer::class.java.getDeclaredField("bar")
20+
var probe = FieldProbe.of(f)
21+
assertThat(probe.isKotlinDelegate()).isFalse()
22+
}
23+
24+
interface Foo {
25+
val foo: Int
26+
}
27+
28+
data class FooImpl(override val foo: Int): Foo
29+
30+
class FooContainer(fooValue: Int, val bar: Int): Foo by FooImpl(fooValue)
31+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package nl.jqno.equalsverifier.kotlin
2+
3+
import nl.jqno.equalsverifier.EqualsVerifier
4+
import org.junit.jupiter.api.Test
5+
6+
class KotlinDelegationTest {
7+
8+
@Test
9+
fun `succeed when class uses interface delegation`() {
10+
EqualsVerifier.forClass(FooContainer::class.java).verify()
11+
}
12+
13+
interface Foo {
14+
val foo: Int
15+
}
16+
17+
data class FooImpl(override val foo: Int): Foo
18+
19+
class FooContainer(fooValue: Int): Foo by FooImpl(fooValue) {
20+
21+
override fun equals(other: Any?): Boolean {
22+
if (this === other) return true
23+
if (other !is FooContainer) return false
24+
return foo == other.foo
25+
}
26+
27+
override fun hashCode(): Int = foo
28+
}
29+
}

0 commit comments

Comments
 (0)