From 9839182af1af1cfb1d1b7c4953fe087ec6378327 Mon Sep 17 00:00:00 2001 From: Jan Ouwens Date: Thu, 21 Mar 2024 11:08:59 +0100 Subject: [PATCH] Supports @NonNullFields and @NullMarked --- CHANGELOG.md | 4 + .../NonnullAnnotationVerifier.java | 2 +- .../annotations/SupportedAnnotations.java | 16 +- .../AnnotationNonNullFieldsTest.java | 13 ++ .../AnnotationNullMarkedTest.java | 186 ++++++++++++++++++ .../nonnull/jspecify/NullMarkedOnPackage.java | 26 +++ .../nonnull/jspecify/package-info.java | 5 + .../NonNullFieldsOnPackage.java | 26 +++ .../nonnull/springframework/package-info.java | 5 + .../org/jspecify/annotations/NullMarked.java | 13 ++ .../org/jspecify/annotations/Nullable.java | 8 + .../springframework/lang/NonNullFields.java | 8 + 12 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNonNullFieldsTest.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNullMarkedTest.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/NullMarkedOnPackage.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/package-info.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/NonNullFieldsOnPackage.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/package-info.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/NullMarked.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/Nullable.java create mode 100644 equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/springframework/lang/NonNullFields.java diff --git a/CHANGELOG.md b/CHANGELOG.md index cb697a18f..a555ca206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support for [Spring Framework](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/lang/NonNullFields.html)'s `@NonNullFields` annotation and [JSpecify](https://jspecify.dev/)'s `@NullMarked` annotation. ([Issue 936](/~https://github.com/jqno/equalsverifier/issues/936)) + ### Changed - When `Warning.SURROGATE_OR_BUSINESS_KEY` is suppressed, it is now possible to use `#withOnlyTheseFields`, and the fields may include both `@Id` fields and regular fields. ([Issue 934](/~https://github.com/jqno/equalsverifier/issues/934)) diff --git a/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/NonnullAnnotationVerifier.java b/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/NonnullAnnotationVerifier.java index 85faf245e..f51469ffb 100644 --- a/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/NonnullAnnotationVerifier.java +++ b/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/NonnullAnnotationVerifier.java @@ -29,7 +29,7 @@ public static boolean fieldIsNonnull(Field field, AnnotationCache annotationCach return ( annotationCache.hasClassAnnotation(type, FINDBUGS1X_DEFAULT_ANNOTATION_NONNULL) || annotationCache.hasClassAnnotation(type, JSR305_DEFAULT_ANNOTATION_NONNULL) || - annotationCache.hasClassAnnotation(type, ECLIPSE_DEFAULT_ANNOTATION_NONNULL) + annotationCache.hasClassAnnotation(type, DEFAULT_ANNOTATION_NONNULL) ); } } diff --git a/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/SupportedAnnotations.java b/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/SupportedAnnotations.java index b405437d4..d9e368b72 100644 --- a/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/SupportedAnnotations.java +++ b/equalsverifier-core/src/main/java/nl/jqno/equalsverifier/internal/reflection/annotations/SupportedAnnotations.java @@ -118,17 +118,25 @@ public boolean validate( }, /** - * If a class or package is marked with @NonNullByDefault, EqualsVerifier will not complain - * about potential {@link NullPointerException}s being thrown if any of the fields in that class - * or package are null. + * If a class or package is marked with @NonNullByDefault or @NullMarked, EqualsVerifier will + * not complain about potential {@link NullPointerException}s being thrown if any of the fields + * in that class or package are null. */ - ECLIPSE_DEFAULT_ANNOTATION_NONNULL(false, "org.eclipse.jdt.annotation.NonNullByDefault") { + DEFAULT_ANNOTATION_NONNULL( + false, + "org.eclipse.jdt.annotation.NonNullByDefault", + "org.jspecify.annotations.NullMarked", + "org.springframework.lang.NonNullFields" + ) { @Override public boolean validate( AnnotationProperties properties, AnnotationCache annotationCache, Set ignoredAnnotations ) { + if (properties.getClassName().endsWith("NullMarked")) { + return true; + } Set values = properties.getArrayValues("value"); if (values == null) { return true; diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNonNullFieldsTest.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNonNullFieldsTest.java new file mode 100644 index 000000000..e2e4e224d --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNonNullFieldsTest.java @@ -0,0 +1,13 @@ +package nl.jqno.equalsverifier.integration.extra_features; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.integration.extra_features.nonnull.springframework.NonNullFieldsOnPackage; +import org.junit.jupiter.api.Test; + +public class AnnotationNonNullFieldsTest { + + @Test + public void succeed_whenEqualsDoesntCheckForNull_givenNonNullFieldsAnnotationOnPackage() { + EqualsVerifier.forClass(NonNullFieldsOnPackage.class).verify(); + } +} diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNullMarkedTest.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNullMarkedTest.java new file mode 100644 index 000000000..10706096b --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/AnnotationNullMarkedTest.java @@ -0,0 +1,186 @@ +package nl.jqno.equalsverifier.integration.extra_features; + +import java.util.Objects; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import nl.jqno.equalsverifier.integration.extra_features.nonnull.jspecify.NullMarkedOnPackage; +import nl.jqno.equalsverifier.internal.testhelpers.ExpectedException; +import nl.jqno.equalsverifier.testhelpers.annotations.org.jspecify.annotations.NullMarked; +import nl.jqno.equalsverifier.testhelpers.annotations.org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +public class AnnotationNullMarkedTest { + + @Test + public void succeed_whenEqualsDoesntCheckForNull_givenNullMarkedAnnotationOnClass() { + EqualsVerifier.forClass(NullMarkedOnClass.class).verify(); + } + + @Test + public void succeed_whenEqualsDoesntCheckForNull_givenNullMarkedAnnotationOnPackage() { + EqualsVerifier.forClass(NullMarkedOnPackage.class).verify(); + } + + @Test + public void succeed_whenEqualsDoesntCheckForNull_givenNullMarkedAnnotationOnOuterClass() { + EqualsVerifier.forClass(NullMarkedOuter.FInner.class).verify(); + } + + @Test + public void succeed_whenEqualsDoesntCheckForNull_givenNullMarkedAnnotationOnNestedOuterClass() { + EqualsVerifier.forClass(NullMarkedOuter.FMiddle.FInnerInner.class).verify(); + } + + @Test + public void fail_whenEqualsDoesntCheckForNull_givenNullMarkedAndNullableAnnotationOnClass() { + ExpectedException + .when(() -> EqualsVerifier.forClass(NullMarkedWithNullableOnClass.class).verify()) + .assertFailure() + .assertMessageContains( + "Non-nullity", + "equals throws NullPointerException", + "'this' object's field o" + ); + } + + @Test + public void succeed_whenEqualsDoesntCheckForNull_givenNullMarkedAndNullableAnnotationOnClassAndWarningSuppressed() { + EqualsVerifier + .forClass(NullMarkedWithNullableOnClass.class) + .suppress(Warning.NULL_FIELDS) + .verify(); + } + + @Test + public void succeed_whenEqualsChecksForNull_givenNullMarkedAndNullableAnnotationOnClass() { + EqualsVerifier.forClass(NullMarkedWithNullableOnClassAndNullCheckInEquals.class).verify(); + } + + @NullMarked + static final class NullMarkedOnClass { + + private final Object o; + + public NullMarkedOnClass(Object o) { + this.o = o; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NullMarkedOnClass)) { + return false; + } + NullMarkedOnClass other = (NullMarkedOnClass) obj; + return o.equals(other.o); + } + + @Override + public int hashCode() { + return Objects.hash(o); + } + } + + @NullMarked + static final class NullMarkedOuter { + + static final class FInner { + + private final Object o; + + public FInner(Object o) { + this.o = o; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FInner)) { + return false; + } + FInner other = (FInner) obj; + return o.equals(other.o); + } + + @Override + public int hashCode() { + return Objects.hash(o); + } + } + + static final class FMiddle { + + static final class FInnerInner { + + private final Object o; + + public FInnerInner(Object o) { + this.o = o; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FInnerInner)) { + return false; + } + FInnerInner other = (FInnerInner) obj; + return o.equals(other.o); + } + + @Override + public int hashCode() { + return Objects.hash(o); + } + } + } + } + + @NullMarked + static final class NullMarkedWithNullableOnClass { + + @Nullable + private final Object o; + + public NullMarkedWithNullableOnClass(Object o) { + this.o = o; + } + + @Override + public final boolean equals(Object obj) { + if (!(obj instanceof NullMarkedWithNullableOnClass)) { + return false; + } + NullMarkedWithNullableOnClass other = (NullMarkedWithNullableOnClass) obj; + return o.equals(other.o); + } + + @Override + public final int hashCode() { + return Objects.hash(o); + } + } + + @NullMarked + static final class NullMarkedWithNullableOnClassAndNullCheckInEquals { + + @Nullable + private final Object o; + + public NullMarkedWithNullableOnClassAndNullCheckInEquals(Object o) { + this.o = o; + } + + @Override + public final boolean equals(Object obj) { + if (!(obj instanceof NullMarkedWithNullableOnClassAndNullCheckInEquals)) { + return false; + } + NullMarkedWithNullableOnClassAndNullCheckInEquals other = + (NullMarkedWithNullableOnClassAndNullCheckInEquals) obj; + return o == null ? other.o == null : o.equals(other.o); + } + + @Override + public final int hashCode() { + return Objects.hash(o); + } + } +} diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/NullMarkedOnPackage.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/NullMarkedOnPackage.java new file mode 100644 index 000000000..5105ef43d --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/NullMarkedOnPackage.java @@ -0,0 +1,26 @@ +package nl.jqno.equalsverifier.integration.extra_features.nonnull.jspecify; + +import java.util.Objects; + +public final class NullMarkedOnPackage { + + private final Object o; + + public NullMarkedOnPackage(Object o) { + this.o = o; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NullMarkedOnPackage)) { + return false; + } + NullMarkedOnPackage other = (NullMarkedOnPackage) obj; + return o.equals(other.o); + } + + @Override + public int hashCode() { + return Objects.hash(o); + } +} diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/package-info.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/package-info.java new file mode 100644 index 000000000..4e338596b --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/jspecify/package-info.java @@ -0,0 +1,5 @@ +/** Applies JSpecify's NullMarked annotation to the package. */ +@NullMarked +package nl.jqno.equalsverifier.integration.extra_features.nonnull.jspecify; + +import nl.jqno.equalsverifier.testhelpers.annotations.org.jspecify.annotations.NullMarked; diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/NonNullFieldsOnPackage.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/NonNullFieldsOnPackage.java new file mode 100644 index 000000000..00dfb24fa --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/NonNullFieldsOnPackage.java @@ -0,0 +1,26 @@ +package nl.jqno.equalsverifier.integration.extra_features.nonnull.springframework; + +import java.util.Objects; + +public final class NonNullFieldsOnPackage { + + private final Object o; + + public NonNullFieldsOnPackage(Object o) { + this.o = o; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NonNullFieldsOnPackage)) { + return false; + } + NonNullFieldsOnPackage other = (NonNullFieldsOnPackage) obj; + return o.equals(other.o); + } + + @Override + public int hashCode() { + return Objects.hash(o); + } +} diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/package-info.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/package-info.java new file mode 100644 index 000000000..0d87b8e89 --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/integration/extra_features/nonnull/springframework/package-info.java @@ -0,0 +1,5 @@ +/** Applies Spring's NonNullFields annotation to the package. */ +@NonNullFields +package nl.jqno.equalsverifier.integration.extra_features.nonnull.springframework; + +import nl.jqno.equalsverifier.testhelpers.annotations.org.springframework.lang.NonNullFields; diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/NullMarked.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/NullMarked.java new file mode 100644 index 000000000..f4ddb5ae0 --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/NullMarked.java @@ -0,0 +1,13 @@ +package nl.jqno.equalsverifier.testhelpers.annotations.org.jspecify.annotations; + +import java.lang.annotation.*; + +/** + * This annotation serves as a placeholder for the real {@link org.jspecify.annotations.NullMarked} + * annotation. However, since that annotation is compiled for Java 9, and this code base must + * support Java 8, we use this copy instead. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.PACKAGE, ElementType.TYPE }) +public @interface NullMarked { +} diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/Nullable.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/Nullable.java new file mode 100644 index 000000000..1c27e15d2 --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/jspecify/annotations/Nullable.java @@ -0,0 +1,8 @@ +package nl.jqno.equalsverifier.testhelpers.annotations.org.jspecify.annotations; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface Nullable { +} diff --git a/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/springframework/lang/NonNullFields.java b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/springframework/lang/NonNullFields.java new file mode 100644 index 000000000..bdd65d987 --- /dev/null +++ b/equalsverifier-core/src/test/java/nl/jqno/equalsverifier/testhelpers/annotations/org/springframework/lang/NonNullFields.java @@ -0,0 +1,8 @@ +package nl.jqno.equalsverifier.testhelpers.annotations.org.springframework.lang; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PACKAGE) +public @interface NonNullFields { +}