Skip to content

Commit

Permalink
Supports @NonNullFields and @NullMarked
Browse files Browse the repository at this point in the history
  • Loading branch information
jqno committed Mar 21, 2024
1 parent d8488c0 commit 9839182
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> ignoredAnnotations
) {
if (properties.getClassName().endsWith("NullMarked")) {
return true;
}
Set<String> values = properties.getArrayValues("value");
if (values == null) {
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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 {
}

0 comments on commit 9839182

Please sign in to comment.