Skip to content

Introduce Nullability API #3115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>3.4.0-SNAPSHOT</version>
<version>3.4.0-GH-3100-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
@@ -41,40 +43,48 @@
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
@@ -92,19 +102,28 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jaxb}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation-api}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>0.3.0</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
Original file line number Diff line number Diff line change
@@ -25,6 +25,9 @@ They provide a tooling-friendly approach and opt-in `null` checks during runtime

Spring annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=305[JSR 305] annotations (a dormant but widely used JSR).
JSR 305 meta-annotations let tooling vendors (such as https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html[IDEA], https://help.eclipse.org/latest/index.jsp?topic=/org.eclipse.jdt.doc.user/tasks/task-using_external_null_annotations.htm[Eclipse], and link:https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[Kotlin]) provide null-safety support in a generic way, without having to hard-code support for Spring annotations.

NOTE: Spring Data can evaluate link:https://jspecify.dev/[JSpecify] annotations to determine nullability rules.0 JSpecify is still in development and might experience slight changes.

To enable runtime checking of nullability constraints for query methods, you need to activate non-nullability on the package level by using Spring’s `@NonNullApi` in `package-info.java`, as shown in the following example:

.Declaring Non-nullability in `package-info.java`
Original file line number Diff line number Diff line change
@@ -22,18 +22,18 @@

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.NullableUtils;
import org.springframework.data.util.Nullability;
import org.springframework.data.util.Nullability.MethodNullability;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
import org.springframework.util.ObjectUtils;

/**
@@ -45,12 +45,12 @@
* @since 2.0
* @see org.springframework.lang.NonNull
* @see ReflectionUtils#isNullable(MethodParameter)
* @see NullableUtils
* @see org.springframework.data.util.Nullability
*/
public class MethodInvocationValidator implements MethodInterceptor {

private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
private final Map<Method, Nullability> nullabilityCache = new ConcurrentHashMap<>(16);
private final Map<Method, CachedNullability> nullabilityCache = new ConcurrentHashMap<>(16);

/**
* Returns {@literal true} if the {@code repositoryInterface} is supported by this interceptor.
@@ -60,21 +60,26 @@ public class MethodInvocationValidator implements MethodInterceptor {
*/
public static boolean supports(Class<?> repositoryInterface) {

return KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(repositoryInterface)
|| NullableUtils.isNonNull(repositoryInterface, ElementType.METHOD)
|| NullableUtils.isNonNull(repositoryInterface, ElementType.PARAMETER);
if (KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(repositoryInterface)) {
return true;
}

org.springframework.data.util.Nullability.Introspector introspector = org.springframework.data.util.Nullability
.introspect(repositoryInterface);

return introspector.isDeclared(ElementType.METHOD) || introspector.isDeclared(ElementType.PARAMETER);
}

@Nullable
@Override
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {

Method method = invocation.getMethod();
Nullability nullability = nullabilityCache.get(method);
CachedNullability nullability = nullabilityCache.get(method);

if (nullability == null) {

nullability = Nullability.of(method, discoverer);
nullability = CachedNullability.of(method, discoverer);
nullabilityCache.put(method, nullability);
}

@@ -102,33 +107,36 @@ public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) thro
return result;
}

static final class Nullability {
static final class CachedNullability {

private final boolean nullableReturn;
private final boolean[] nullableParameters;
private final MethodParameter[] methodParameters;

private Nullability(boolean nullableReturn, boolean[] nullableParameters, MethodParameter[] methodParameters) {
private CachedNullability(boolean nullableReturn, boolean[] nullableParameters,
MethodParameter[] methodParameters) {
this.nullableReturn = nullableReturn;
this.nullableParameters = nullableParameters;
this.methodParameters = methodParameters;
}

static Nullability of(Method method, ParameterNameDiscoverer discoverer) {
static CachedNullability of(Method method, ParameterNameDiscoverer discoverer) {

MethodNullability methodNullability = Nullability.forMethod(method);

boolean nullableReturn = isNullableParameter(new MethodParameter(method, -1));
boolean nullableReturn = isNullableParameter(methodNullability, new MethodParameter(method, -1));
boolean[] nullableParameters = new boolean[method.getParameterCount()];
MethodParameter[] methodParameters = new MethodParameter[method.getParameterCount()];

for (int i = 0; i < method.getParameterCount(); i++) {

MethodParameter parameter = new MethodParameter(method, i);
parameter.initParameterNameDiscovery(discoverer);
nullableParameters[i] = isNullableParameter(parameter);
nullableParameters[i] = isNullableParameter(methodNullability, parameter);
methodParameters[i] = parameter;
}

return new Nullability(nullableReturn, nullableParameters, methodParameters);
return new CachedNullability(nullableReturn, nullableParameters, methodParameters);
}

String getMethodParameterName(int index) {
@@ -151,9 +159,9 @@ boolean isNullableParameter(int index) {
return nullableParameters[index];
}

private static boolean isNullableParameter(MethodParameter parameter) {
private static boolean isNullableParameter(MethodNullability methodNullability, MethodParameter parameter) {

return requiresNoValue(parameter) || NullableUtils.isExplicitNullable(parameter)
return requiresNoValue(parameter) || methodNullability.forParameter(parameter).isNullable()
|| (KotlinReflectionUtils.isSupportedKotlinClass(parameter.getDeclaringClass())
&& ReflectionUtils.isNullable(parameter));
}
@@ -177,7 +185,7 @@ public boolean equals(@Nullable Object o) {
return true;
}

if (!(o instanceof Nullability that)) {
if (!(o instanceof CachedNullability that)) {
return false;
}

302 changes: 302 additions & 0 deletions src/main/java/org/springframework/data/util/Nullability.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.util;

import java.lang.annotation.ElementType;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

import org.springframework.core.MethodParameter;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.Nullable;

/**
* Provides access to nullability declarations of methods and parameters, usually obtained from a source such as a
* {@link Class} or {@link Method}.
* <p>
* An application expresses nullability rules ideally expressed on the top-most element such as the package to let all
* inner elements participate in the defaults. Individual elements such as methods or parameters can be annotated with
* non-null annotations to express deviation from the default rule.
* <p>
* Nullability can be defined on various levels: Methods, (inner) classes, packages. We consider these as declaration
* anchors. Introspection of nullability traverses declaration anchor trees in their logical order (i.e. a class
* contains methods, an enclosing class contains inner classes, a package contains classes) to inherit nullability rules
* if the particular method or parameter does not declare nullability rules.
* <p>
* By default (no annotation use), a package and its types are considered allowing {@literal null} values in return
* values and method parameters. Nullability rules are expressed by annotating a package with annotations such as
* Spring's {@link NonNullApi}. All types of the package inherit the package rule. Subpackages do not inherit
* nullability rules and must be annotated themselves.
*
* <pre class="code">
* &#64;org.springframework.lang.NonNullApi
* package com.example;
* </pre>
*
* {@link Nullable} selectively permits {@literal null} values for method return values or method parameters by
* annotating the method respectively the parameters:
*
* <pre class="code">
* public class ExampleClass {
*
* String shouldNotReturnNull(@Nullable String acceptsNull, String doesNotAcceptNull) {
* // …
* }
*
* &#64;Nullable
* String nullableReturn(String parameter) {
* // …
* }
* }
* </pre>
*
* Note that nullability is also expressed through using specific types. Primitives ({@code int}, {@code char} etc.) are
* non-nullable by definition and cannot be {@code null}. {@code void}/{@code Void} types are {@code null}-only types.
* <p>
* {@code javax.annotation.Nonnull} is suitable for composition of meta-annotations and expresses via
* {@code javax.annotation.Nonnull#when()} in which cases non-nullability is applicable. Nullability introspection
* considers the following mechanisms:
* <ul>
* <li>Spring's {@link NonNullApi}, {@link Nullable}, and {@link org.springframework.lang.NonNull}.</li>
* <li>JSR-305 {@code javax.annotation.Nonnull} and meta-annotations.</li>
* <li><a href="https://https://jspecify.dev/">JSpecify</a>, a newly designed specification to opt-in for non-null by
* default through {@code org.jspecify.annotations.NullMarked} and {@code org.jspecify.annotations.Nullable}.</li>
* </ul>
* <p>
* A component might be interested on whether nullability is declared and if so, whether the particular element is
* nullable or non-null.
* <p>
* Here are some typical examples:
*
* <pre class="code">
* // is an nullability declared for a Method return type
* Nullability nullability = Nullability.forMethodReturnType(method);
* nullability.isDeclared();
* nullability.isNullable();
* nullability.isNonNull();
*
* // introspect multiple elements for their nullability in the scope of a class/package.
* Nullability.Introspector introspector = Nullability.introspect(NonNullOnPackage.class);
* Nullability nullability = introspector.forReturnType(method);
* </pre>
* <p>
* <b>NOTE: The Nullability API is primarily intended for framework components that want to introspect nullability
* declarations, for example to validate input or output.</b>
*
* @author Mark Paluch
* @see NonNullApi
* @see Nullable
*/
public interface Nullability {

/**
* Determine if nullability declaration is present on the source.
*
* @return {@code true} if the source (or any of its declaration anchors) defines nullability rules.
*/
boolean isDeclared();

/**
* Determine if the source is nullable.
*
* @return {@code true} if the source (or any of its declaration anchors) is nullable.
*/
boolean isNullable();

/**
* Determine if the source is non-nullable.
*
* @return {@code true} if the source (or any of its declaration anchors) is non-nullable.
*/
boolean isNonNull();

/**
* Creates a new {@link MethodNullability} instance by introspecting the {@link Method} return type.
*
* @param method the source method.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
static MethodNullability forMethod(Method method) {
return new NullabilityIntrospector(method.getDeclaringClass(), true).forMethod(method);
}

/**
* Creates a new {@link Nullability} instance by introspecting the {@link MethodParameter}.
*
* @param parameter the source method parameter.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
static Nullability forParameter(MethodParameter parameter) {
return new NullabilityIntrospector(parameter.getContainingClass(), false).forParameter(parameter);
}

/**
* Creates a new {@link Nullability} instance by introspecting the {@link Method} return type.
*
* @param method the source method.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
static Nullability forMethodReturnType(Method method) {
return new NullabilityIntrospector(method.getDeclaringClass(), false).forReturnType(method);
}

/**
* Creates a new {@link Nullability} instance by introspecting the {@link Parameter method parameter}.
*
* @param parameter the source method parameter.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
static Nullability forParameter(Parameter parameter) {
return new NullabilityIntrospector(parameter.getDeclaringExecutable().getDeclaringClass(), false)
.forParameter(parameter);
}

/**
* Creates introspector using the given {@link Class} as declaration anchor.
*
* @param cls the source class.
* @return a {@code Introspector} instance considering nullability declarations from the {@link Class} and package.
*/
static Introspector introspect(Class<?> cls) {
return new NullabilityIntrospector(cls, true);
}

/**
* Creates introspector using the given {@link Package} as declaration anchor.
*
* @param pkg the source package.
* @return a {@code Introspector} instance considering nullability declarations from package.
*/
static Introspector introspect(Package pkg) {
return new NullabilityIntrospector(pkg, true);
}

/**
* Nullability introspector to introspect multiple elements within the context of their source container.
*/
interface Introspector {

/**
* Returns whether nullability rules are defined for the given {@link ElementType}.
*
* @param elementType the element type to check.
* @return {@code true} if nullability is declared for the given element type; {@code false} otherwise.
*/
boolean isDeclared(ElementType elementType);

/**
* Creates a new {@link MethodNullability} instance by introspecting the {@link Method}.
* <p>
* If the method parameter does not declare any nullability rules, then introspection falls back to the source
* container that was used to create the introspector.
*
* @param method the source method.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
MethodNullability forMethod(Method method);

/**
* Creates a new {@link Nullability} instance by introspecting the {@link MethodParameter}.
* <p>
* If the method parameter does not declare any nullability rules, then introspection falls back to the source
* container that was used to create the introspector.
*
* @param parameter the source method parameter.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
default Nullability forParameter(MethodParameter parameter) {
return parameter.getParameterIndex() == -1 ? forReturnType(parameter.getMethod())
: forParameter(parameter.getParameter());
}

/**
* Creates a new {@link Nullability} instance by introspecting the {@link Method} return type.
* <p>
* If the method parameter does not declare any nullability rules, then introspection falls back to the source
* container that was used to create the introspector.
*
* @param method the source method.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
Nullability forReturnType(Method method);

/**
* Creates a new {@link Nullability} instance by introspecting the {@link Parameter}.
* <p>
* If the method parameter does not declare any nullability rules, then introspection falls back to the source
* container that was used to create the introspector.
*
* @param parameter the source method parameter.
* @return a {@code Nullability} instance containing the element's nullability declaration.
*/
Nullability forParameter(Parameter parameter);

}

/**
* Nullability introspector to introspect multiple elements within the context of their source container. Inherited
* nullability methods nullability of the method return type.
*/
interface MethodNullability extends Nullability {

/**
* Returns a {@link Nullability} instance for the method return type.
*
* @return a {@link Nullability} instance for the method return type.
*/
default Nullability forReturnType() {
return this;
}

/**
* Returns a {@link Nullability} instance for a method parameter.
*
* @param parameter the method parameter.
* @return a {@link Nullability} instance for a method parameter.
* @throws IllegalArgumentException if the method parameter is not defined by the underlying method.
*/
Nullability forParameter(Parameter parameter);

/**
* Returns a {@link Nullability} instance for a method parameter by index.
*
* @param index the method parameter index.
* @return a {@link Nullability} instance for a method parameter.
* @throws IndexOutOfBoundsException if the method parameter index is out of bounds.
*/
Nullability forParameter(int index);

/**
* Returns a {@link Nullability} instance for a method parameter.
*
* @param parameter the method parameter.
* @return a {@link Nullability} instance for a method parameter.
* @throws IllegalArgumentException if the method parameter is not defined by the underlying method.
*/
default Nullability forParameter(MethodParameter parameter) {
return parameter.getParameterIndex() == -1 ? forReturnType() : forParameter(parameter.getParameter());
}

/**
* Returns the method parameter count.
*
* @return the method parameter count.
*/
int getParameterCount();

}

}

Large diffs are not rendered by default.

49 changes: 9 additions & 40 deletions src/main/java/org/springframework/data/util/NullableUtils.java
Original file line number Diff line number Diff line change
@@ -22,21 +22,15 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;

/**
* Utility methods to introspect nullability rules declared in packages, classes and methods.
@@ -76,7 +70,10 @@
* @since 2.0
* @see NonNullApi
* @see Nullable
* @deprecated since xxx, use {@link Nullability} instead that supports a wider range of introspections with addition
* support for Jakarta Annotations and JSpecify.
*/
@Deprecated
public abstract class NullableUtils {

private static final String NON_NULL_CLASS_NAME = "javax.annotation.Nonnull";
@@ -157,12 +154,11 @@ private static boolean isNonNull(Annotation annotation, ElementType elementType)
return true;
}

if (!MergedAnnotations.from(annotation.annotationType()).isPresent(annotationClass)
|| !isNonNull(annotation)) {
if (!MergedAnnotations.from(annotation.annotationType()).isPresent(annotationClass) || !isNonNull(annotation)) {
return false;
}

return test(annotation, TYPE_QUALIFIER_CLASS_NAME, "value",
return NullabilityIntrospector.test(annotation, TYPE_QUALIFIER_CLASS_NAME, "value",
(ElementType[] o) -> Arrays.binarySearch(o, elementType) >= 0);
}

@@ -207,7 +203,8 @@ private static boolean isExplicitNullable(Annotation[] annotations) {
* @return {@literal true} if the annotation expresses non-nullability.
*/
private static boolean isNonNull(Annotation annotation) {
return test(annotation, NON_NULL_CLASS_NAME, "when", o -> WHEN_NON_NULLABLE.contains(o.toString()));
return NullabilityIntrospector.test(annotation, NON_NULL_CLASS_NAME, "when",
o -> WHEN_NON_NULLABLE.contains(o.toString()));
}

/**
@@ -218,36 +215,8 @@ private static boolean isNonNull(Annotation annotation) {
* @return {@literal true} if the annotation expresses nullability.
*/
private static boolean isNullable(Annotation annotation) {
return test(annotation, NON_NULL_CLASS_NAME, "when", o -> WHEN_NULLABLE.contains(o.toString()));
}

@SuppressWarnings("unchecked")
private static <T> boolean test(Annotation annotation, String metaAnnotationName, String attribute,
Predicate<T> filter) {

if (annotation.annotationType().getName().equals(metaAnnotationName)) {

Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);

return !attributes.isEmpty() && filter.test((T) attributes.get(attribute));
}

MultiValueMap<String, Object> attributes = AnnotatedElementUtils
.getAllAnnotationAttributes(annotation.annotationType(), metaAnnotationName);

if (attributes == null || attributes.isEmpty()) {
return false;
}

List<Object> elementTypes = attributes.get(attribute);

for (Object value : elementTypes) {

if (filter.test((T) value)) {
return true;
}
}
return false;
return NullabilityIntrospector.test(annotation, NON_NULL_CLASS_NAME, "when",
o -> WHEN_NULLABLE.contains(o.toString()));
}

private static Set<Class<?>> findClasses(String... classNames) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.util;

import static org.assertj.core.api.Assertions.*;

import java.lang.reflect.Method;

import org.junit.jupiter.api.Test;

/**
* Unit tests for {@link Nullability}.
*
* @author Mark Paluch
*/
class NullabilityUnitTests {

@Test
void shouldConsiderPrimitiveNullability() throws NoSuchMethodException {

Method method = getClass().getDeclaredMethod("someMethod", Integer.TYPE);
Nullability.MethodNullability methodNullability = Nullability.forMethod(method);

// method return type
assertThat(methodNullability.isDeclared()).isTrue();
assertThat(methodNullability.isNullable()).isTrue();

Nullability pn = methodNullability.forParameter(0);

// method return type
assertThat(pn.isDeclared()).isTrue();
assertThat(pn.isNullable()).isFalse();
}

void someMethod(int i) {

}
}
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
import java.lang.annotation.ElementType;

import org.junit.jupiter.api.Test;

import org.springframework.core.MethodParameter;
import org.springframework.data.util.nonnull.NullableAnnotatedType;
import org.springframework.data.util.nonnull.packagelevel.NonNullOnPackage;
@@ -38,11 +39,21 @@ class NullableUtilsUnitTests {
@Test // DATACMNS-1154
void packageAnnotatedShouldConsiderNonNullAnnotation() {

var method = ReflectionUtils.findMethod(NonNullOnPackage.class, "nonNullReturnValue");
var method = ReflectionUtils.findMethod(NonNullOnPackage.class, "nonNullArgs", String.class);

assertThat(NullableUtils.isNonNull(method, ElementType.METHOD)).isTrue();
assertThat(NullableUtils.isNonNull(method, ElementType.PARAMETER)).isTrue();
assertThat(NullableUtils.isNonNull(method, ElementType.PACKAGE)).isFalse();

Nullability.MethodNullability mrt = Nullability.forMethod(method);

assertThat(mrt.isNullable()).isFalse();
assertThat(mrt.isNonNull()).isTrue();

Nullability pn = mrt.forParameter(MethodParameter.forExecutable(method, 0));

assertThat(pn.isNullable()).isFalse();
assertThat(pn.isNonNull()).isTrue();
}

@Test // DATACMNS-1154
@@ -59,25 +70,87 @@ void packageAnnotatedShouldConsiderNonNullAnnotationForMethod() {
assertThat(NullableUtils.isNonNull(NonNullOnPackage.class, ElementType.PACKAGE)).isFalse();
}

@Test // GH-3100
void shouldConsiderJSpecifyNonNullParameters() {

var method = ReflectionUtils.findMethod(org.springframework.data.util.nonnull.jspecify.NonNullOnPackage.class,
"someMethod", String.class, String.class);
Nullability.Introspector introspector = Nullability.introspect(method.getDeclaringClass());
Nullability mrt = introspector.forReturnType(method);

assertThat(mrt.isDeclared()).isTrue();
assertThat(mrt.isNonNull()).isTrue();
assertThat(mrt.isNullable()).isFalse();

Nullability pn0 = introspector.forParameter(MethodParameter.forExecutable(method, 0));
assertThat(pn0.isDeclared()).isTrue();
assertThat(pn0.isNullable()).isFalse();
assertThat(pn0.isNonNull()).isTrue();

Nullability pn1 = introspector.forParameter(MethodParameter.forExecutable(method, 1));
assertThat(pn1.isDeclared()).isTrue();
assertThat(pn1.isNullable()).isTrue();
assertThat(pn1.isNonNull()).isFalse();
}

@Test // DATACMNS-1154
void shouldConsiderJsr305NonNullParameters() {

assertThat(NullableUtils.isNonNull(NonNullableParameters.class, ElementType.PARAMETER)).isTrue();
assertThat(NullableUtils.isNonNull(NonNullableParameters.class, ElementType.FIELD)).isFalse();

var method = ReflectionUtils.findMethod(NonNullableParameters.class, "someMethod", String.class);
Nullability.Introspector introspector = Nullability.introspect(method.getDeclaringClass());
Nullability mrt = introspector.forReturnType(method);

assertThat(mrt.isDeclared()).isFalse();
assertThat(mrt.isNonNull()).isFalse();
assertThat(mrt.isNullable()).isTrue();

Nullability pn = introspector.forParameter(MethodParameter.forExecutable(method, 0));
assertThat(pn.isDeclared()).isTrue();
assertThat(pn.isNullable()).isFalse();
assertThat(pn.isNonNull()).isTrue();
}

@Test // DATACMNS-1154
void shouldConsiderJsr305NonNullAnnotation() {

assertThat(NullableUtils.isNonNull(Jsr305NonnullAnnotatedType.class, ElementType.PARAMETER)).isTrue();
assertThat(NullableUtils.isNonNull(Jsr305NonnullAnnotatedType.class, ElementType.FIELD)).isTrue();

var method = ReflectionUtils.findMethod(Jsr305NonnullAnnotatedType.class, "someMethod", String.class);

Nullability mrt = Nullability.forMethodReturnType(method);
Nullability pn = Nullability.forParameter(method.getParameters()[0]);

assertThat(mrt.isDeclared()).isTrue();
assertThat(mrt.isNullable()).isFalse();
assertThat(mrt.isNonNull()).isTrue();

assertThat(pn.isDeclared()).isTrue();
assertThat(pn.isNullable()).isFalse();
assertThat(pn.isNonNull()).isTrue();
}

@Test // DATACMNS-1154
void shouldConsiderNonAnnotatedTypeNullable() {

assertThat(NullableUtils.isNonNull(NonAnnotatedType.class, ElementType.PARAMETER)).isFalse();
assertThat(NullableUtils.isNonNull(NonAnnotatedType.class, ElementType.FIELD)).isFalse();

var method = ReflectionUtils.findMethod(NonAnnotatedType.class, "someMethod", String.class);

Nullability mrt = Nullability.forMethodReturnType(method);
Nullability pn = Nullability.forParameter(method.getParameters()[0]);

assertThat(mrt.isDeclared()).isFalse();
assertThat(mrt.isNullable()).isTrue();
assertThat(mrt.isNonNull()).isFalse();

assertThat(pn.isDeclared()).isFalse();
assertThat(pn.isNullable()).isTrue();
assertThat(pn.isNonNull()).isFalse();
}

@Test // DATACMNS-1154
@@ -98,21 +171,40 @@ void shouldConsiderParametersNullableAnnotation() {
var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "nullableReturn");

assertThat(NullableUtils.isExplicitNullable(new MethodParameter(method, -1))).isTrue();

Nullability mrt = Nullability.forMethodReturnType(method);

assertThat(mrt.isDeclared()).isTrue();
assertThat(mrt.isNullable()).isTrue();
assertThat(mrt.isNonNull()).isFalse();
}

@Test // DATACMNS-1154
void shouldConsiderParametersJsr305NullableMetaAnnotation() {
void shouldConsiderMethodReturnJsr305NullableMetaAnnotation() {

var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jsr305NullableReturn");

assertThat(NullableUtils.isExplicitNullable(new MethodParameter(method, -1))).isTrue();

Nullability mrt = Nullability.forMethodReturnType(method);

assertThat(mrt.isDeclared()).isTrue();
assertThat(mrt.isNullable()).isTrue();
assertThat(mrt.isNonNull()).isFalse();
}

@Test // DATACMNS-1154
void shouldConsiderParametersJsr305NonnullAnnotation() {
void shouldConsiderMethodReturnJsr305NonnullAnnotation() {

var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jsr305NullableReturnWhen");

assertThat(NullableUtils.isExplicitNullable(new MethodParameter(method, -1))).isTrue();

Nullability mrt = Nullability.forMethodReturnType(method);

assertThat(mrt.isDeclared()).isTrue();
assertThat(mrt.isNullable()).isTrue();
assertThat(mrt.isNonNull()).isFalse();
}

}
Original file line number Diff line number Diff line change
@@ -34,4 +34,5 @@ public interface NullableAnnotatedType {

@javax.annotation.Nonnull(when = When.MAYBE)
String jsr305NullableReturnWhen();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2017-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.util.nonnull.jspecify;

import org.jspecify.annotations.Nullable;

/**
* @author Mark Paluch
*/
public interface NonNullOnPackage {

String someMethod(String arg, @Nullable String nullableArg);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @author Mark Paluch
*/
@NullMarked
package org.springframework.data.util.nonnull.jspecify;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -21,4 +21,6 @@
public interface NonNullOnPackage {

String nonNullReturnValue();

String nonNullArgs(String arg);
}
Original file line number Diff line number Diff line change
@@ -21,4 +21,8 @@
* @author Mark Paluch
*/
@Nonnull
public interface Jsr305NonnullAnnotatedType {}
public interface Jsr305NonnullAnnotatedType {

String someMethod(String arg);

}
Original file line number Diff line number Diff line change
@@ -18,4 +18,10 @@
/**
* @author Mark Paluch
*/
public class NonAnnotatedType {}
public class NonAnnotatedType {

String someMethod(String arg) {
return "";
}

}
Original file line number Diff line number Diff line change
@@ -21,4 +21,7 @@
* @author Mark Paluch
*/
@ParametersAreNonnullByDefault
public interface NonNullableParameters {}
public interface NonNullableParameters {

String someMethod(String arg);
}