Skip to content

Commit 875fdda

Browse files
authored
Merge pull request #1112 from jqno/sealed-null
Sealed null
2 parents 333270d + 21aef54 commit 875fdda

File tree

10 files changed

+188
-18
lines changed

10 files changed

+188
-18
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+
### Fixed
18+
19+
- `NullPointerException` with abstract sealed types whose subtypes add state and need `Warning.NULL_FIELDS` suppressed. ([Issue 1111](https://github.com/jqno/equalsverifier/issues/1111))
20+
1721
## [4.0.7] - 2025-07-30
1822

1923
### Fixed

equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/instantiation/InstanceCreator.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,20 @@ public class InstanceCreator<T> {
2424
* @param objenesis To instantiate non-record classes.
2525
*/
2626
public InstanceCreator(ClassProbe<T> probe, Objenesis objenesis) {
27-
this.type = probe.getType();
2827
this.probe = probe;
29-
this.instantiator = Instantiator.of(type, objenesis);
28+
this.instantiator = Instantiator.of(probe.getType(), objenesis);
29+
this.type = instantiator.getType();
30+
}
31+
32+
/**
33+
* Returns the actual type as determined by the Instantiator. The instantiator might defer to a subtype of the given
34+
* type, for example if it's a sealed abstract type. The subtype might have additional fields, which need to receive
35+
* values too.
36+
*
37+
* @return The actual type.
38+
*/
39+
public Class<T> getActualType() {
40+
return type;
3041
}
3142

3243
/**

equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/instantiation/SubjectCreator.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class SubjectCreator<T> {
1818

1919
private final TypeTag typeTag;
2020
private final Class<T> type;
21+
private final Class<? extends T> actualType;
2122
private final Configuration<T> config;
2223
private final ValueProvider valueProvider;
2324
private final ClassProbe<T> classProbe;
@@ -39,6 +40,7 @@ public SubjectCreator(Configuration<T> config, ValueProvider valueProvider, Obje
3940
this.classProbe = ClassProbe.of(type);
4041
this.objenesis = objenesis;
4142
this.instanceCreator = new InstanceCreator<>(classProbe, objenesis);
43+
this.actualType = instanceCreator.getActualType();
4244
}
4345

4446
/**
@@ -236,10 +238,14 @@ private Map<Field, Object> with(Field f, Object v) {
236238
}
237239

238240
private FieldIterable fields() {
239-
return FieldIterable.ofIgnoringStatic(type);
241+
return FieldIterable.ofIgnoringStatic(actualType);
240242
}
241243

242244
private FieldIterable nonSuperFields() {
245+
// This should probably use `actualType` instead of `type`, but then we'd need to find
246+
// a way to include all fields from `type` and `actualType` together but without the fields
247+
// from `type`'s superclass. That's hard, and it doesn't seem to come up in practice. I'm
248+
// leaving this comment here as an explanation, in case it does come up at some point.
243249
return FieldIterable.ofIgnoringSuperAndStatic(type);
244250
}
245251

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ public static <T> Instantiator<T> of(Class<T> type, Objenesis objenesis) {
5656
return new Instantiator<>(type, objenesis);
5757
}
5858

59+
/**
60+
* The actual type that will be instantiated. Could be a subclass of the requested type.
61+
*
62+
* @return The actual type that will be instantiated.
63+
*/
64+
public Class<T> getType() {
65+
return type;
66+
}
67+
5968
/**
6069
* Instantiates an object of type T.
6170
*

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@ public final class SealedTypesFinder {
1010
private SealedTypesFinder() {}
1111

1212
public static <T, U extends T> Optional<Class<U>> findInstantiableSubclass(Class<T> type) {
13-
return findInstantiablePermittedClass(type, false);
13+
return findInstantiablePermittedClass(type);
1414
}
1515

16-
private static <T, U extends T> Optional<Class<U>> findInstantiablePermittedClass(
17-
Class<T> type,
18-
boolean checkCurrent) {
19-
if (checkCurrent && (!isAbstract(type) || !type.isSealed())) {
16+
private static <T, U extends T> Optional<Class<U>> findInstantiablePermittedClass(Class<T> type) {
17+
if (!isAbstract(type) || !type.isSealed()) {
2018
@SuppressWarnings("unchecked")
2119
var result = (Class<U>) type;
2220
return Optional.of(result);
@@ -29,7 +27,7 @@ private static <T, U extends T> Optional<Class<U>> findInstantiablePermittedClas
2927
@SuppressWarnings("unchecked")
3028
Class<U> subType = (Class<U>) permitted;
3129

32-
var c = findInstantiablePermittedClass(subType, true);
30+
var c = findInstantiablePermittedClass(subType);
3331
if (c.isPresent()) {
3432
return c;
3533
}

equalsverifier-core/src/test/java/nl/jqno/equalsverifier/internal/instantiation/InstanceCreatorTest.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,31 @@
1212

1313
class InstanceCreatorTest {
1414

15+
private final Objenesis objenesis = new ObjenesisStd();
16+
17+
@Test
18+
void getActualType() {
19+
var probe = ClassProbe.of(SomeClass.class);
20+
var sut = new InstanceCreator<>(probe, objenesis);
21+
22+
Class<SomeClass> actual = sut.getActualType();
23+
24+
assertThat(actual).isEqualTo(SomeClass.class);
25+
}
26+
27+
@Test
28+
void getActualType_sealedAbstract() {
29+
var probe = ClassProbe.of(SealedAbstract.class);
30+
var sut = new InstanceCreator<>(probe, objenesis);
31+
32+
Class<SealedAbstract> actual = sut.getActualType();
33+
34+
assertThat(actual).isEqualTo(SealedSub.class);
35+
}
36+
1537
@Test
1638
void instantiate() throws NoSuchFieldException {
1739
ClassProbe<SomeClass> probe = ClassProbe.of(SomeClass.class);
18-
Objenesis objenesis = new ObjenesisStd();
1940
var sut = new InstanceCreator<InstanceCreatorTest.SomeClass>(probe, objenesis);
2041

2142
Field x = SomeClass.class.getDeclaredField("x");
@@ -32,7 +53,6 @@ void instantiate() throws NoSuchFieldException {
3253
@Test
3354
void copy() throws NoSuchFieldException {
3455
ClassProbe<SomeSubClass> probe = ClassProbe.of(SomeSubClass.class);
35-
Objenesis objenesis = new ObjenesisStd();
3656
var sut = new InstanceCreator<InstanceCreatorTest.SomeSubClass>(probe, objenesis);
3757

3858
SomeClass original = new SomeClass(42, 1337, "yeah");
@@ -66,4 +86,8 @@ public SomeSubClass(int x, int y, String z, int a) {
6686
this.a = a;
6787
}
6888
}
89+
90+
sealed static abstract class SealedAbstract permits SealedSub {}
91+
92+
static final class SealedSub extends SealedAbstract {}
6993
}

equalsverifier-core/src/test/java/nl/jqno/equalsverifier/internal/instantiation/SubjectCreatorTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ void plain() {
5252
assertThat(actual).isEqualTo(expected);
5353
}
5454

55+
@Test
56+
void plain_sealedAbstract() {
57+
var anotherConfig = ConfigurationHelper.emptyConfiguration(SealedAbstract.class);
58+
var anotherSut = new SubjectCreator<>(anotherConfig, valueProvider, objenesis);
59+
var anotherActual = anotherSut.plain();
60+
61+
assertThat(anotherActual.s).isEqualTo(S_RED);
62+
63+
// SubjectCreator returns a subclass with new fields!
64+
assertThat(((SealedSub) anotherActual).i).isEqualTo(I_RED);
65+
}
66+
5567
@Test
5668
void withFieldDefaulted_super() {
5769
expected = new SomeClass(0, I_RED, S_RED);
@@ -330,4 +342,21 @@ public String toString() {
330342
return "SomeSub: " + x + "," + i + "," + s + "," + q;
331343
}
332344
}
345+
346+
sealed static abstract class SealedAbstract permits SealedSub {
347+
private final String s;
348+
349+
public SealedAbstract(String s) {
350+
this.s = s;
351+
}
352+
}
353+
354+
static final class SealedSub extends SealedAbstract {
355+
private final int i;
356+
357+
public SealedSub(String s, int i) {
358+
super(s);
359+
this.i = i;
360+
}
361+
}
333362
}

equalsverifier-core/src/test/java/nl/jqno/equalsverifier/internal/reflection/InstantiatorTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ class InstantiatorTest {
2323

2424
private final Objenesis objenesis = new ObjenesisStd();
2525

26+
@Test
27+
void getType() {
28+
Instantiator<Point> instantiator = Instantiator.of(Point.class, objenesis);
29+
Class<Point> type = instantiator.getType();
30+
assertThat(type).isEqualTo(Point.class);
31+
}
32+
33+
@Test
34+
void getType_sealedAbstract() {
35+
Instantiator<SealedAbstract> instantiator = Instantiator.of(SealedAbstract.class, objenesis);
36+
Class<SealedAbstract> type = instantiator.getType();
37+
assertThat(type).isEqualTo(SealedSub.class);
38+
}
39+
40+
sealed static abstract class SealedAbstract permits SealedSub {}
41+
42+
static final class SealedSub extends SealedAbstract {}
43+
2644
@Test
2745
void instantiateClass() {
2846
Instantiator<Point> instantiator = Instantiator.of(Point.class, objenesis);
Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44

5-
import java.util.Optional;
6-
75
import org.junit.jupiter.api.Test;
86

9-
class SealedTypesHelperTest {
7+
class SealedTypesFinderTest {
108

119
@Test
1210
void twoLevels() {
@@ -35,7 +33,7 @@ static final class FourLevelChild implements FourLevelMiddle2 {}
3533
@Test
3634
void allConcrete() {
3735
var actual = SealedTypesFinder.findInstantiableSubclass(AllConcreteParent.class);
38-
assertThat(actual.get()).isEqualTo(AllConcreteMiddle.class);
36+
assertThat(actual.get()).isEqualTo(AllConcreteParent.class);
3937
}
4038

4139
sealed static class AllConcreteParent {}
@@ -44,6 +42,18 @@ sealed static class AllConcreteMiddle extends AllConcreteParent {}
4442

4543
static final class AllConcreteChild extends AllConcreteMiddle {}
4644

45+
@Test
46+
void abstractTopThreeLevels() {
47+
var actual = SealedTypesFinder.findInstantiableSubclass(AbstractParent.class);
48+
assertThat(actual.get()).isEqualTo(AbstractMiddle.class);
49+
}
50+
51+
sealed abstract static class AbstractParent {}
52+
53+
sealed static class AbstractMiddle extends AbstractParent {}
54+
55+
static final class AbstractChild extends AbstractMiddle {}
56+
4757
@Test
4858
void nonSealedAtTheBottom() {
4959
var actual = SealedTypesFinder.findInstantiableSubclass(NonSealedAtTheBottomParent.class);
@@ -57,6 +67,6 @@ non-sealed interface NonSealedAtTheBottomChild extends NonSealedAtTheBottomParen
5767
@Test
5868
void notSealed() {
5969
var actual = SealedTypesFinder.findInstantiableSubclass(Object.class);
60-
assertThat(actual).isEqualTo(Optional.empty());
70+
assertThat(actual.get()).isEqualTo(Object.class);
6171
}
6272
}

equalsverifier-test/src/test/java/nl/jqno/equalsverifier/integration/extended_contract/SealedTypesTest.java

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Objects;
44

55
import nl.jqno.equalsverifier.EqualsVerifier;
6+
import nl.jqno.equalsverifier.Warning;
67
import nl.jqno.equalsverifier_testhelpers.ExpectedException;
78
import org.junit.jupiter.api.Test;
89

@@ -64,6 +65,11 @@ void fail_whenSealeadParentHasAnIncorrectImplementationOfEquals() {
6465
.assertFailure();
6566
}
6667

68+
@Test
69+
void succeed_whenSealedParentHasTwoChildren_parent() {
70+
EqualsVerifier.forClass(SealedParentWithTwoChildren.class).verify();
71+
}
72+
6773
@Test
6874
void succeed_whenSealedParentHasTwoChildren_a() {
6975
EqualsVerifier.forClass(SealedChildA.class).verify();
@@ -74,6 +80,22 @@ void succeed_whenSealedParentHasTwoChildren_b() {
7480
EqualsVerifier.forClass(SealedChildB.class).verify();
7581
}
7682

83+
@Test
84+
void succeed_whenSealedParentThrowsNull() {
85+
EqualsVerifier
86+
.forClass(SealedParentThrowsNull.class)
87+
.suppress(Warning.STRICT_INHERITANCE, Warning.NULL_FIELDS)
88+
.verify();
89+
}
90+
91+
@Test
92+
void succeed_whenSealedChildThrowsNull() {
93+
EqualsVerifier
94+
.forClass(SealedChildThrowsNull.class)
95+
.suppress(Warning.STRICT_INHERITANCE, Warning.NULL_FIELDS)
96+
.verify();
97+
}
98+
7799
public abstract static sealed class SealedParentWithFinalChild permits FinalSealedChild {
78100

79101
private final int i;
@@ -212,14 +234,14 @@ protected SealedParentWithTwoChildren(String value) {
212234
}
213235

214236
@Override
215-
public boolean equals(Object other) {
237+
public final boolean equals(Object other) {
216238
return other != null
217239
&& (this.getClass() == other.getClass())
218240
&& Objects.equals(this.value, ((SealedParentWithTwoChildren) other).value);
219241
}
220242

221243
@Override
222-
public int hashCode() {
244+
public final int hashCode() {
223245
return Objects.hashCode(this.value);
224246
}
225247

@@ -242,4 +264,43 @@ public static final class SealedChildB extends SealedParentWithTwoChildren {
242264
super(value);
243265
}
244266
}
267+
268+
public static abstract sealed class SealedParentThrowsNull permits SealedChildThrowsNull {
269+
private final String string;
270+
271+
public SealedParentThrowsNull(String string) {
272+
this.string = string;
273+
}
274+
275+
@Override
276+
public boolean equals(Object obj) {
277+
return obj instanceof SealedParentThrowsNull other && Objects.equals(string, other.string);
278+
}
279+
280+
@Override
281+
public int hashCode() {
282+
return string.hashCode();
283+
}
284+
}
285+
286+
public static final class SealedChildThrowsNull extends SealedParentThrowsNull {
287+
private final Integer i;
288+
289+
public SealedChildThrowsNull(String string, Integer i) {
290+
super(string);
291+
this.i = i;
292+
}
293+
294+
@Override
295+
public boolean equals(Object obj) {
296+
return obj instanceof SealedChildThrowsNull other && super.equals(obj) && Objects.equals(i, other.i);
297+
}
298+
299+
@Override
300+
public int hashCode() {
301+
int hashCode = super.hashCode();
302+
hashCode = 31 * hashCode + i.hashCode();
303+
return hashCode;
304+
}
305+
}
245306
}

0 commit comments

Comments
 (0)