diff --git a/pom.xml b/pom.xml index d55079dbe8..bccac4ece9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,13 @@ - + 4.0.0 org.springframework.data spring-data-commons - 3.4.0-SNAPSHOT + 3.4.0-GH-3100-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. @@ -41,40 +43,48 @@ org.springframework spring-core + org.springframework spring-beans + org.springframework spring-context true + org.springframework spring-expression true + org.springframework spring-tx true + org.springframework spring-oxm true + com.fasterxml.jackson.core jackson-databind true + org.springframework spring-web true + org.springframework spring-webflux @@ -92,12 +102,14 @@ jakarta.servlet-api provided + jakarta.xml.bind jakarta.xml.bind-api ${jaxb} provided + jakarta.annotation jakarta.annotation-api @@ -105,6 +117,13 @@ true + + org.jspecify + jspecify + 0.3.0 + true + + com.google.code.findbugs jsr305 diff --git a/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc b/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc index b8ccd5c83d..87a7cfc4f5 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc @@ -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` diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java b/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java index 64fbae3c0c..a008abf6fb 100644 --- a/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java +++ b/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java @@ -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 nullabilityCache = new ConcurrentHashMap<>(16); + private final Map nullabilityCache = new ConcurrentHashMap<>(16); /** * Returns {@literal true} if the {@code repositoryInterface} is supported by this interceptor. @@ -60,9 +60,14 @@ 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 @@ -70,11 +75,11 @@ public static boolean supports(Class repositoryInterface) { 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,21 +107,24 @@ 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()]; @@ -124,11 +132,11 @@ static Nullability of(Method method, ParameterNameDiscoverer discoverer) { 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; } diff --git a/src/main/java/org/springframework/data/util/Nullability.java b/src/main/java/org/springframework/data/util/Nullability.java new file mode 100644 index 0000000000..f7ba0f6801 --- /dev/null +++ b/src/main/java/org/springframework/data/util/Nullability.java @@ -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}. + *

+ * 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. + *

+ * 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. + *

+ * 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. + * + *

+ * @org.springframework.lang.NonNullApi
+ * package com.example;
+ * 
+ * + * {@link Nullable} selectively permits {@literal null} values for method return values or method parameters by + * annotating the method respectively the parameters: + * + *
+ * public class ExampleClass {
+ *
+ * 	String shouldNotReturnNull(@Nullable String acceptsNull, String doesNotAcceptNull) {
+ * 		// …
+ * 	}
+ *
+ * 	@Nullable
+ * 	String nullableReturn(String parameter) {
+ * 		// …
+ * 	}
+ * }
+ * 
+ * + * 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. + *

+ * {@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: + *

    + *
  • Spring's {@link NonNullApi}, {@link Nullable}, and {@link org.springframework.lang.NonNull}.
  • + *
  • JSR-305 {@code javax.annotation.Nonnull} and meta-annotations.
  • + *
  • JSpecify, a newly designed specification to opt-in for non-null by + * default through {@code org.jspecify.annotations.NullMarked} and {@code org.jspecify.annotations.Nullable}.
  • + *
+ *

+ * A component might be interested on whether nullability is declared and if so, whether the particular element is + * nullable or non-null. + *

+ * Here are some typical examples: + * + *

+ * // 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);
+ * 
+ *

+ * NOTE: The Nullability API is primarily intended for framework components that want to introspect nullability + * declarations, for example to validate input or output. + * + * @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}. + *

+ * 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}. + *

+ * 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. + *

+ * 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}. + *

+ * 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(); + + } + +} diff --git a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java new file mode 100644 index 0000000000..89fe6604bf --- /dev/null +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -0,0 +1,606 @@ +/* + * 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.Annotation; +import java.lang.annotation.ElementType; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.lang.NonNull; +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentLruCache; +import org.springframework.util.MultiValueMap; + +/** + * Default {@link Nullability.Introspector} implementation backed by {@link NullabilityProvider nullability providers}. + * + * @author Mark Paluch + */ +class NullabilityIntrospector implements Nullability.Introspector { + + private static final List providers; + + static { + providers = new ArrayList<>(4); + + providers.add(new PrimitiveProvider()); + + if (Jsr305Provider.isAvailable()) { + providers.add(new Jsr305Provider()); + } + + if (JSpecifyAnnotationProvider.isAvailable()) { + providers.add(new JSpecifyAnnotationProvider()); + } + + providers.add(new SpringProvider()); + } + + private final DeclarationAnchor anchor; + + NullabilityIntrospector(AnnotatedElement segment, boolean cache) { + DeclarationAnchor tree = createTree(segment); + this.anchor = cache ? new CachingDeclarationAnchor(tree) : tree; + } + + /** + * Create a tree of declaration anchors. + * + * @param element the element to create the tree for. + * @return DeclarationAnchor encapsulating the source {@code element}. + */ + static DeclarationAnchor createTree(AnnotatedElement element) { + + if (element instanceof Package || element instanceof Module) { + return new AnnotatedElementAnchor(element); + } + + if (element instanceof Class cls) { + + Class enclosingClass = cls.getEnclosingClass(); + + if (enclosingClass == null) { + + if (cls.getPackage() == null) { + return new HierarchicalAnnotatedElementAnchor(createTree(cls.getModule()), element); + } + + return new HierarchicalAnnotatedElementAnchor( + new HierarchicalAnnotatedElementAnchor(createTree(cls.getModule()), cls.getPackage()), element); + } + + return new HierarchicalAnnotatedElementAnchor(createTree(enclosingClass), element); + } + + if (element instanceof Method m) { + return new HierarchicalAnnotatedElementAnchor(createTree(m.getDeclaringClass()), element); + } + + throw new IllegalArgumentException(String.format("Cannot create DeclarationAnchor for %s", element)); + } + + @Override + public boolean isDeclared(ElementType elementType) { + return anchor.evaluate(elementType) != Spec.UNSPECIFIED; + } + + @Override + public Nullability.MethodNullability forMethod(Method method) { + + HierarchicalAnnotatedElementAnchor element = new HierarchicalAnnotatedElementAnchor(anchor, method); + Map parameters = new HashMap<>(); + + for (Parameter parameter : method.getParameters()) { + parameters.put(parameter, Nullability.forParameter(parameter)); + } + + return new DefaultMethodNullability(element.evaluate(ElementType.METHOD), parameters, method); + } + + @Override + public Nullability forReturnType(Method method) { + + HierarchicalAnnotatedElementAnchor element = new HierarchicalAnnotatedElementAnchor(anchor, method); + return new TheNullability(element.evaluate(ElementType.METHOD)); + } + + @Override + public Nullability forParameter(Parameter parameter) { + + HierarchicalAnnotatedElementAnchor element = new HierarchicalAnnotatedElementAnchor(anchor, parameter); + return new TheNullability(element.evaluate(ElementType.PARAMETER)); + } + + static Spec doWith(Function function) { + + for (NullabilityProvider provider : providers) { + Spec result = function.apply(provider); + + if (result != Spec.UNSPECIFIED) { + return result; + } + } + + return Spec.UNSPECIFIED; + } + + @SuppressWarnings("unchecked") + static boolean test(Annotation annotation, String metaAnnotationName, String attribute, Predicate filter) { + + if (annotation.annotationType().getName().equals(metaAnnotationName)) { + + Map attributes = AnnotationUtils.getAnnotationAttributes(annotation); + + return !attributes.isEmpty() && filter.test((T) attributes.get(attribute)); + } + + MultiValueMap attributes = AnnotatedElementUtils + .getAllAnnotationAttributes(annotation.annotationType(), metaAnnotationName); + + if (attributes == null || attributes.isEmpty()) { + return false; + } + + List elementTypes = attributes.get(attribute); + + for (Object value : elementTypes) { + + if (filter.test((T) value)) { + return true; + } + } + return false; + } + + /** + * Provider for nullability rules. + */ + static abstract class NullabilityProvider { + + /** + * Evaluate nullability rules for a given {@link ElementType} in the scope of an {@link AnnotatedElement element} + * (i.e. enclosing class or package). + * + * @param element the contextual element. + * @param elementType element type to inspect. + * @return Specification result. Can be {@link Spec#UNSPECIFIED}. + */ + abstract Spec evaluate(AnnotatedElement element, ElementType elementType); + } + + /** + * Provider considering primitive types. + */ + static class PrimitiveProvider extends NullabilityProvider { + + @Override + Spec evaluate(AnnotatedElement element, ElementType elementType) { + + Class type = null; + + if (element instanceof Method m) { + type = m.getReturnType(); + } + + if (element instanceof Parameter p) { + type = p.getType(); + } + + if (type != null) { + + if (ReflectionUtils.isVoid(type)) { + return Spec.NULLABLE; + } + + if (type.isPrimitive()) { + return Spec.NON_NULL; + } + } + + return Spec.UNSPECIFIED; + } + } + + /** + * Spring provider leveraging {@link NonNullApi @NonNullApi}, {@link NonNullFields @NonNullFields}, + * {@link NonNull @NonNull}, and {@link Nullable @Nullable} annotations. + */ + static class SpringProvider extends NullabilityProvider { + + @Override + Spec evaluate(AnnotatedElement element, ElementType elementType) { + + if (element instanceof Package) { + + if (elementType == ElementType.METHOD || elementType == ElementType.PARAMETER) { + return element.isAnnotationPresent(NonNullApi.class) ? Spec.NON_NULL : Spec.UNSPECIFIED; + } + + if (elementType == ElementType.FIELD) { + return element.isAnnotationPresent(NonNullFields.class) ? Spec.NON_NULL : Spec.UNSPECIFIED; + } + } + + if (elementType == ElementType.METHOD || elementType == ElementType.PARAMETER + || elementType == ElementType.FIELD) { + + if (element.isAnnotationPresent(NonNull.class)) { + return Spec.NON_NULL; + } + + if (element.isAnnotationPresent(Nullable.class)) { + return Spec.NULLABLE; + } + } + + return Spec.UNSPECIFIED; + } + } + + /** + * Provider based on the JSR-305 (dormant) spec. Elements can be either annotated with + * {@code @Nonnull}/{@code @Nullable} directly or through meta-annotations that are composed of + * {@code @Nonnull}/{@code @Nullable} and {@code @TypeQualifierDefault}. + */ + @SuppressWarnings("DataFlowIssue") + static class Jsr305Provider extends NullabilityProvider { + + private static final Class NON_NULL = findClass("javax.annotation.Nonnull"); + private static final Class NULLABLE = findClass("javax.annotation.Nullable"); + private static final String TYPE_QUALIFIER_CLASS_NAME = "javax.annotation.meta.TypeQualifierDefault"; + private static final Set WHEN_NULLABLE = new HashSet<>(Arrays.asList("UNKNOWN", "MAYBE", "NEVER")); + private static final Set WHEN_NON_NULLABLE = new HashSet<>(Collections.singletonList("ALWAYS")); + + public static boolean isAvailable() { + return NON_NULL != null && NULLABLE != null; + } + + @Override + Spec evaluate(AnnotatedElement element, ElementType elementType) { + + if (element.isAnnotationPresent(NULLABLE) || MergedAnnotations.from(element).isPresent(NULLABLE)) { + return Spec.NULLABLE; + } + + Annotation[] annotations = element.getAnnotations(); + + for (Annotation annotation : annotations) { + + if (isNonNull(NON_NULL, annotation, elementType)) { + return Spec.NON_NULL; + } + + if (isNullable(NON_NULL, annotation, elementType)) { + return Spec.NULLABLE; + } + } + + return Spec.UNSPECIFIED; + } + + static boolean isNonNull(Class annotationClass, Annotation annotation, ElementType elementType) { + return test(annotationClass, annotation, elementType, Jsr305Provider::isNonNull); + } + + static boolean isNullable(Class annotationClass, Annotation annotation, ElementType elementType) { + return test(annotationClass, annotation, elementType, Jsr305Provider::isNullable); + } + + private static boolean test(Class annotationClass, Annotation annotation, ElementType elementType, + Predicate predicate) { + + if (annotation.annotationType().equals(annotationClass)) { + return predicate.test(annotation); + } + + MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType()); + if (annotations.isPresent(annotationClass) && isInScope(annotation, elementType)) { + Annotation meta = annotations.get(annotationClass).synthesize(); + + return predicate.test(meta); + } + + return false; + } + + private static boolean isInScope(Annotation annotation, ElementType elementType) { + return NullabilityIntrospector.test(annotation, TYPE_QUALIFIER_CLASS_NAME, "value", + (ElementType[] o) -> Arrays.binarySearch(o, elementType) >= 0); + } + + /** + * Introspect {@link Annotation} for being either a meta-annotation composed of {@code Nonnull} or {code Nonnull} + * itself expressing non-nullability. + * + * @param annotation + * @return {@literal true} if the annotation expresses non-nullability. + */ + static boolean isNonNull(Annotation annotation) { + return NullabilityIntrospector.test(annotation, NON_NULL.getName(), "when", + o -> WHEN_NON_NULLABLE.contains(o.toString())); + } + + /** + * Introspect {@link Annotation} for being either a meta-annotation composed of {@code Nonnull} or {@code Nonnull} + * itself expressing nullability. + * + * @param annotation the annotation to introspect. + * @return {@literal true} if the annotation expresses nullability. + */ + static boolean isNullable(Annotation annotation) { + return NullabilityIntrospector.test(annotation, NON_NULL.getName(), "when", + o -> WHEN_NULLABLE.contains(o.toString())); + } + } + + /** + * Provider for JSpecify annotations. + */ + @SuppressWarnings("DataFlowIssue") + static class JSpecifyAnnotationProvider extends NullabilityProvider { + + private static final Class NON_NULL = findClass("org.jspecify.annotations.NonNull"); + private static final Class NULLABLE = findClass("org.jspecify.annotations.Nullable"); + private static final Class NULL_MARKED = findClass("org.jspecify.annotations.NullMarked"); + private static final Class NULL_UNMARKED = findClass("org.jspecify.annotations.NullUnmarked"); + + public static boolean isAvailable() { + return NON_NULL != null && NULLABLE != null && NULL_MARKED != null && NULL_UNMARKED != null; + } + + @Override + Spec evaluate(AnnotatedElement element, ElementType elementType) { + + Annotation[] annotations = element.getAnnotations(); + + if (element instanceof Parameter p) { + + Spec result = evaluate(p.getAnnotatedType(), elementType); + + if (result != Spec.UNSPECIFIED) { + return result; + } + } + + if (element instanceof Executable e) { + + Spec result = evaluate(e.getAnnotatedReturnType(), elementType); + + if (result != Spec.UNSPECIFIED) { + return result; + } + } + + return evaluate(annotations); + } + + private static Spec evaluate(Annotation[] annotations) { + for (Annotation annotation : annotations) { + + if (test(NULL_UNMARKED, annotation)) { + return Spec.UNSPECIFIED; + } + + if (test(NULL_MARKED, annotation) || test(NON_NULL, annotation)) { + return Spec.NON_NULL; + } + + if (test(NULLABLE, annotation)) { + return Spec.NULLABLE; + } + } + + return Spec.UNSPECIFIED; + } + + static boolean test(Class annotationClass, Annotation annotation) { + + if (annotation.annotationType().equals(annotationClass)) { + return true; + } + + MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType()); + return annotations.isPresent(annotationClass); + } + } + + @Nullable + @SuppressWarnings("unchecked") + static Class findClass(String className) { + + try { + return (Class) ClassUtils.forName(className, NullabilityIntrospector.class.getClassLoader()); + } catch (ClassNotFoundException e) { + return null; + } + } + + static class TheNullability implements Nullability { + + private final Spec spec; + + TheNullability(Spec spec) { + this.spec = spec; + } + + @Override + public boolean isDeclared() { + return spec != Spec.UNSPECIFIED; + } + + @Override + public boolean isNullable() { + return spec == Spec.NULLABLE || spec == Spec.UNSPECIFIED; + } + + @Override + public boolean isNonNull() { + return spec == Spec.NON_NULL; + } + } + + static class DefaultMethodNullability extends TheNullability implements Nullability.MethodNullability { + + private final Map parameters; + private final Method method; + + public DefaultMethodNullability(Spec spec, Map parameters, Method method) { + super(spec); + this.parameters = parameters; + this.method = method; + } + + @Override + public Nullability forParameter(Parameter parameter) { + + Nullability nullability = parameters.get(parameter); + + if (nullability == null) { + throw new IllegalArgumentException(String.format("Parameter %s is not defined by %s", parameter, method)); + } + + return nullability; + } + + @Override + public Nullability forParameter(int index) { + + if (index >= method.getParameterCount() || index < 0) { + throw new IndexOutOfBoundsException(); + } + + return forParameter(method.getParameters()[index]); + } + + @Override + public int getParameterCount() { + return method.getParameterCount(); + } + } + + /** + * Declaration result. + */ + enum Spec { + /** + * No nullabilty rule declared. + */ + UNSPECIFIED, + + /** + * Declaration yields nullable. + */ + NULLABLE, + + /** + * Declaration yields non-nullable. + */ + NON_NULL + } + + /** + * Declaration anchors represent elements that hold nullability declarations such as classes, packages, modules. + */ + interface DeclarationAnchor { + + /** + * Evaluate nullability declarations for the given {@link ElementType}. + * + * @param target target element type to evaluate declarations for. + * @return specification result. + */ + Spec evaluate(ElementType target); + + } + + /** + * Caching variant of {@link DeclarationAnchor}. + */ + static class CachingDeclarationAnchor implements DeclarationAnchor { + + private final ConcurrentLruCache cache; + + public CachingDeclarationAnchor(DeclarationAnchor delegate) { + this.cache = new ConcurrentLruCache<>(ElementType.values().length, delegate::evaluate); + } + + @Override + public Spec evaluate(ElementType target) { + return this.cache.get(target); + } + + } + + static class AnnotatedElementAnchor implements DeclarationAnchor { + + private final AnnotatedElement element; + + AnnotatedElementAnchor(AnnotatedElement element) { + this.element = element; + } + + @Override + public Spec evaluate(ElementType target) { + return doWith(np -> np.evaluate(element, target)); + } + + @Override + public String toString() { + return "DeclarationAnchor[" + element + "]"; + } + } + + static class HierarchicalAnnotatedElementAnchor extends AnnotatedElementAnchor { + + private final DeclarationAnchor parent; + + public HierarchicalAnnotatedElementAnchor(DeclarationAnchor parent, AnnotatedElement element) { + super(element); + this.parent = parent; + } + + @Override + public Spec evaluate(ElementType target) { + + Spec result = super.evaluate(target); + if (result != Spec.UNSPECIFIED) { + return result; + } + + return parent.evaluate(target); + } + } + +} diff --git a/src/main/java/org/springframework/data/util/NullableUtils.java b/src/main/java/org/springframework/data/util/NullableUtils.java index 6ea9ce87b5..53903a24f2 100644 --- a/src/main/java/org/springframework/data/util/NullableUtils.java +++ b/src/main/java/org/springframework/data/util/NullableUtils.java @@ -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 boolean test(Annotation annotation, String metaAnnotationName, String attribute, - Predicate filter) { - - if (annotation.annotationType().getName().equals(metaAnnotationName)) { - - Map attributes = AnnotationUtils.getAnnotationAttributes(annotation); - - return !attributes.isEmpty() && filter.test((T) attributes.get(attribute)); - } - - MultiValueMap attributes = AnnotatedElementUtils - .getAllAnnotationAttributes(annotation.annotationType(), metaAnnotationName); - - if (attributes == null || attributes.isEmpty()) { - return false; - } - - List 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> findClasses(String... classNames) { diff --git a/src/test/java/org/springframework/data/util/NullabilityUnitTests.java b/src/test/java/org/springframework/data/util/NullabilityUnitTests.java new file mode 100644 index 0000000000..89e3aaa33c --- /dev/null +++ b/src/test/java/org/springframework/data/util/NullabilityUnitTests.java @@ -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) { + + } +} diff --git a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java index 8dae1deb92..87f8be8d52 100644 --- a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java @@ -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,11 +70,47 @@ 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 @@ -71,6 +118,19 @@ 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 @@ -78,6 +138,19 @@ 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(); } + } diff --git a/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java index 684c6017da..a56a6cc00b 100644 --- a/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java +++ b/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java @@ -34,4 +34,5 @@ public interface NullableAnnotatedType { @javax.annotation.Nonnull(when = When.MAYBE) String jsr305NullableReturnWhen(); + } diff --git a/src/test/java/org/springframework/data/util/nonnull/jspecify/NonNullOnPackage.java b/src/test/java/org/springframework/data/util/nonnull/jspecify/NonNullOnPackage.java new file mode 100644 index 0000000000..473b10b50c --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/jspecify/NonNullOnPackage.java @@ -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); +} diff --git a/src/test/java/org/springframework/data/util/nonnull/jspecify/package-info.java b/src/test/java/org/springframework/data/util/nonnull/jspecify/package-info.java new file mode 100644 index 0000000000..956c75299e --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/jspecify/package-info.java @@ -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; diff --git a/src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java b/src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java index dc2524608e..243e5664ab 100644 --- a/src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java +++ b/src/test/java/org/springframework/data/util/nonnull/packagelevel/NonNullOnPackage.java @@ -21,4 +21,6 @@ public interface NonNullOnPackage { String nonNullReturnValue(); + + String nonNullArgs(String arg); } diff --git a/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java index 109292d32d..76e5f069c3 100644 --- a/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java +++ b/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java @@ -21,4 +21,8 @@ * @author Mark Paluch */ @Nonnull -public interface Jsr305NonnullAnnotatedType {} +public interface Jsr305NonnullAnnotatedType { + + String someMethod(String arg); + +} diff --git a/src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java index 094d9a5a3c..259cc3c571 100644 --- a/src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java +++ b/src/test/java/org/springframework/data/util/nonnull/type/NonAnnotatedType.java @@ -18,4 +18,10 @@ /** * @author Mark Paluch */ -public class NonAnnotatedType {} +public class NonAnnotatedType { + + String someMethod(String arg) { + return ""; + } + +} diff --git a/src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java b/src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java index 723016dde5..6e02dc1b3a 100644 --- a/src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java +++ b/src/test/java/org/springframework/data/util/nonnull/type/NonNullableParameters.java @@ -21,4 +21,7 @@ * @author Mark Paluch */ @ParametersAreNonnullByDefault -public interface NonNullableParameters {} +public interface NonNullableParameters { + + String someMethod(String arg); +}