From 6a1580c8c1470966a673de88029704507370be32 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 25 Jun 2024 08:49:25 +0200 Subject: [PATCH 1/9] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d55079dbe8..b47538cccb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 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. From d1741a15520eec72684a618bd62498db027ea924 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 25 Jun 2024 15:45:29 +0200 Subject: [PATCH 2/9] Introduce Nullability API. --- .../data/util/Nullability.java | 174 +++++++++ .../data/util/NullabilityIntrospector.java | 363 ++++++++++++++++++ .../data/util/NullableUtils.java | 2 +- .../data/util/NullableUtilsUnitTests.java | 71 +++- .../packagelevel/NonNullOnPackage.java | 2 + .../type/Jsr305NonnullAnnotatedType.java | 6 +- .../util/nonnull/type/NonAnnotatedType.java | 8 +- .../nonnull/type/NonNullableParameters.java | 5 +- 8 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/data/util/Nullability.java create mode 100644 src/main/java/org/springframework/data/util/NullabilityIntrospector.java 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..7da0914676 --- /dev/null +++ b/src/main/java/org/springframework/data/util/Nullability.java @@ -0,0 +1,174 @@ +/* + * 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.reflect.Method; +import java.lang.reflect.Parameter; + +import org.springframework.core.MethodParameter; + +/** + * 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. + *

+ * 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 + */ +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 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 from(MethodParameter parameter) { + return introspect(parameter.getContainingClass()).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 introspect(method.getDeclaringClass()).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 forMethodParameter(Parameter parameter) { + return introspect(parameter.getDeclaringExecutable().getDeclaringClass()).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); + } + + /** + * 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); + } + + /** + * Nullability introspector to introspect multiple elements within the context of their source container. + */ + interface Introspector { + + /** + * 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 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. + */ + Nullability forParameter(Parameter parameter); + + } + +} 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..83c634e692 --- /dev/null +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -0,0 +1,363 @@ +/* + * 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.Field; +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.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +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; + +/** + * 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); + + if (Jsr305Provider.isAvailable()) { + providers.add(new Jsr305Provider()); + } + + providers.add(new SpringProvider()); + } + + private final DeclarationAnchor anchor; + + NullabilityIntrospector(AnnotatedElement segment) { + this.anchor = createTree(segment); + } + + /** + * 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 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; + } + + /** + * 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); + } + + /** + * 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}. + */ + 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 NullableUtils.test(annotation, TYPE_QUALIFIER_CLASS_NAME, "value", + (ElementType[] o) -> Arrays.binarySearch(o, elementType) >= 0); + } + + /** + * Introspect {@link Annotation} for being either a meta-annotation composed from {@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 NullableUtils.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 + * @return {@literal true} if the annotation expresses nullability. + */ + static boolean isNullable(Annotation annotation) { + return NullableUtils.test(annotation, NON_NULL.getName(), "when", o -> WHEN_NULLABLE.contains(o.toString())); + } + } + + private static ElementType getElementType(AnnotatedElement element) { + return element instanceof Method ? ElementType.METHOD + : element instanceof Field ? ElementType.FIELD : ElementType.PARAMETER; + } + + @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; + } + } + + enum Spec { + UNSPECIFIED, NULLABLE, NON_NULL + } + + interface DeclarationAnchor { + + Spec evaluate(ElementType 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..a8392703e6 100644 --- a/src/main/java/org/springframework/data/util/NullableUtils.java +++ b/src/main/java/org/springframework/data/util/NullableUtils.java @@ -222,7 +222,7 @@ private static boolean isNullable(Annotation annotation) { } @SuppressWarnings("unchecked") - private static boolean test(Annotation annotation, String metaAnnotationName, String attribute, + static boolean test(Annotation annotation, String metaAnnotationName, String attribute, Predicate filter) { if (annotation.annotationType().getName().equals(metaAnnotationName)) { diff --git a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java index 8dae1deb92..93446e1fc3 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,22 @@ 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.Introspector introspector = Nullability.introspect(NonNullOnPackage.class); + Nullability mrt = introspector.forReturnType(method); + + assertThat(mrt.isNullable()).isFalse(); + assertThat(mrt.isNonNull()).isTrue(); + + Nullability pn = introspector.forParameter(MethodParameter.forExecutable(method, 0)); + + assertThat(pn.isNullable()).isFalse(); + assertThat(pn.isNonNull()).isTrue(); } @Test // DATACMNS-1154 @@ -64,6 +76,19 @@ 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 +96,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.forMethodParameter(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 +116,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.forMethodParameter(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,6 +149,12 @@ 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 @@ -106,6 +163,12 @@ void shouldConsiderParametersJsr305NullableMetaAnnotation() { 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 @@ -114,5 +177,11 @@ void shouldConsiderParametersJsr305NonnullAnnotation() { 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/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..169c9a6fef 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 { + + void 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..a00911c538 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 { + + void 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..45183a8e07 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 { + + void someMethod(String arg); +} From 4146ba8c50e45618b63ad3d42363def01472212b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 25 Jun 2024 15:59:51 +0200 Subject: [PATCH 3/9] Add support for Jakarta Nullability Annotations. --- pom.xml | 11 ++++ .../data/util/NullabilityIntrospector.java | 61 +++++++++++++++++-- .../data/util/NullableUtilsUnitTests.java | 50 ++++++++++++++- .../util/nonnull/NullableAnnotatedType.java | 6 ++ .../nonnull/jakarta/NonNullOnPackage.java | 26 ++++++++ .../util/nonnull/jakarta/package-info.java | 23 +++++++ 6 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 src/test/java/org/springframework/data/util/nonnull/jakarta/NonNullOnPackage.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/jakarta/package-info.java diff --git a/pom.xml b/pom.xml index b47538cccb..0c2d4c0c4e 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ 2.11.7 1.4.24 spring.data.commons + 2.1.1 1.8 @@ -41,40 +42,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 +101,14 @@ jakarta.servlet-api provided + jakarta.xml.bind jakarta.xml.bind-api ${jaxb} provided + jakarta.annotation jakarta.annotation-api diff --git a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java index 83c634e692..6a32bebfba 100644 --- a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -18,7 +18,6 @@ import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -53,6 +52,10 @@ class NullabilityIntrospector implements Nullability.Introspector { providers.add(new Jsr305Provider()); } + if (JakartaAnnotation.isAvailable()) { + providers.add(new JakartaAnnotation()); + } + providers.add(new SpringProvider()); } @@ -270,9 +273,59 @@ static boolean isNullable(Annotation annotation) { } } - private static ElementType getElementType(AnnotatedElement element) { - return element instanceof Method ? ElementType.METHOD - : element instanceof Field ? ElementType.FIELD : ElementType.PARAMETER; + /** + * 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}. + */ + static class JakartaAnnotation extends NullabilityProvider { + + private static final Class NON_NULL = findClass("jakarta.annotation.Nonnull"); + private static final Class NULLABLE = findClass("jakarta.annotation.Nullable"); + + 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 (test(NON_NULL, annotation)) { + return Spec.NON_NULL; + } + + if (test(NULLABLE, annotation)) { + return Spec.NULLABLE; + } + } + + return Spec.UNSPECIFIED; + } + + private static boolean test(Class annotationClass, Annotation annotation) { + + if (annotation.annotationType().equals(annotationClass)) { + return true; + } + + MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType()); + if (annotations.isPresent(annotationClass)) { + Annotation meta = annotations.get(annotationClass).synthesize(); + + return true; + } + + return false; + } + } @Nullable diff --git a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java index 93446e1fc3..2ac072d944 100644 --- a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java @@ -71,6 +71,28 @@ void packageAnnotatedShouldConsiderNonNullAnnotationForMethod() { assertThat(NullableUtils.isNonNull(NonNullOnPackage.class, ElementType.PACKAGE)).isFalse(); } + @Test // + void shouldConsiderJakartaNonNullParameters() { + + var method = ReflectionUtils.findMethod(org.springframework.data.util.nonnull.jakarta.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() { @@ -158,7 +180,7 @@ void shouldConsiderParametersNullableAnnotation() { } @Test // DATACMNS-1154 - void shouldConsiderParametersJsr305NullableMetaAnnotation() { + void shouldConsiderMethodReturnJsr305NullableMetaAnnotation() { var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jsr305NullableReturn"); @@ -172,7 +194,7 @@ void shouldConsiderParametersJsr305NullableMetaAnnotation() { } @Test // DATACMNS-1154 - void shouldConsiderParametersJsr305NonnullAnnotation() { + void shouldConsiderMethodReturnJsr305NonnullAnnotation() { var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jsr305NullableReturnWhen"); @@ -184,4 +206,28 @@ void shouldConsiderParametersJsr305NonnullAnnotation() { assertThat(mrt.isNullable()).isTrue(); assertThat(mrt.isNonNull()).isFalse(); } + + @Test // + void shouldConsiderMethodReturnJakartaNonnullAnnotation() { + + var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jakartaNonnullReturnWhen"); + + Nullability mrt = Nullability.forMethodReturnType(method); + + assertThat(mrt.isDeclared()).isTrue(); + assertThat(mrt.isNullable()).isFalse(); + assertThat(mrt.isNonNull()).isTrue(); + } + + @Test // + void shouldConsiderMethodReturnJakartaNullableAnnotation() { + + var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jakartaNullableReturnWhen"); + + 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..16200009f2 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,10 @@ public interface NullableAnnotatedType { @javax.annotation.Nonnull(when = When.MAYBE) String jsr305NullableReturnWhen(); + + @jakarta.annotation.Nonnull + String jakartaNonnullReturnWhen(); + + @jakarta.annotation.Nullable + String jakartaNullableReturnWhen(); } diff --git a/src/test/java/org/springframework/data/util/nonnull/jakarta/NonNullOnPackage.java b/src/test/java/org/springframework/data/util/nonnull/jakarta/NonNullOnPackage.java new file mode 100644 index 0000000000..79bcda9a74 --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/jakarta/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.jakarta; + +import jakarta.annotation.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/jakarta/package-info.java b/src/test/java/org/springframework/data/util/nonnull/jakarta/package-info.java new file mode 100644 index 0000000000..260655c353 --- /dev/null +++ b/src/test/java/org/springframework/data/util/nonnull/jakarta/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 + */ +@Nonnull +package org.springframework.data.util.nonnull.jakarta; + +import jakarta.annotation.Nonnull; From ba1485ada1752d60b165796ecad4dd45e81205d4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 25 Jun 2024 16:21:49 +0200 Subject: [PATCH 4/9] Add support for JSpecify. --- pom.xml | 11 +- .../pages/repositories/null-handling.adoc | 3 + .../data/util/NullabilityIntrospector.java | 112 +++++++++++++++--- .../data/util/NullableUtilsUnitTests.java | 23 ++++ .../nonnull/jspecify/NonNullOnPackage.java | 26 ++++ .../util/nonnull/jspecify/package-info.java | 23 ++++ 6 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 src/test/java/org/springframework/data/util/nonnull/jspecify/NonNullOnPackage.java create mode 100644 src/test/java/org/springframework/data/util/nonnull/jspecify/package-info.java diff --git a/pom.xml b/pom.xml index 0c2d4c0c4e..2dff192787 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -116,6 +118,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/util/NullabilityIntrospector.java b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java index 6a32bebfba..c9ae086859 100644 --- a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -18,6 +18,8 @@ import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -52,8 +54,12 @@ class NullabilityIntrospector implements Nullability.Introspector { providers.add(new Jsr305Provider()); } - if (JakartaAnnotation.isAvailable()) { - providers.add(new JakartaAnnotation()); + if (JakartaAnnotationProvider.isAvailable()) { + providers.add(new JakartaAnnotationProvider()); + } + + if (JSpecifyAnnotationProvider.isAvailable()) { + providers.add(new JSpecifyAnnotationProvider()); } providers.add(new SpringProvider()); @@ -251,7 +257,7 @@ private static boolean isInScope(Annotation annotation, ElementType elementType) } /** - * Introspect {@link Annotation} for being either a meta-annotation composed from {@code Nonnull} or {code Nonnull} + * Introspect {@link Annotation} for being either a meta-annotation composed of {@code Nonnull} or {code Nonnull} * itself expressing non-nullability. * * @param annotation @@ -274,56 +280,124 @@ static boolean isNullable(Annotation annotation) { } /** - * 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}. + * Simplified variant of {@link Jsr305Provider} without {@code when} and {@code @TypeQualifierDefault} support. */ - static class JakartaAnnotation extends NullabilityProvider { + static class JakartaAnnotationProvider extends SimpleAnnotationNullabilityProvider { private static final Class NON_NULL = findClass("jakarta.annotation.Nonnull"); private static final Class NULLABLE = findClass("jakarta.annotation.Nullable"); + JakartaAnnotationProvider() { + super(NON_NULL, NULLABLE); + } + public static boolean isAvailable() { return NON_NULL != null && NULLABLE != null; } + } + + /** + * Provider for JSpecify annotations. + */ + 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) { - if (element.isAnnotationPresent(NULLABLE) || MergedAnnotations.from(element).isPresent(NULLABLE)) { - return Spec.NULLABLE; + Annotation[] annotations = element.getAnnotations(); + AnnotatedType annotatedType = null; + + if (element instanceof Parameter p) { + + Spec result = evaluate(p.getAnnotatedType(), elementType); + + if (result != Spec.UNSPECIFIED) { + return result; + } } - Annotation[] annotations = element.getAnnotations(); + 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(NON_NULL, annotation)) { + if (SimpleAnnotationNullabilityProvider.test(NULL_UNMARKED, annotation)) { + return Spec.UNSPECIFIED; + } + + if (SimpleAnnotationNullabilityProvider.test(NULL_MARKED, annotation) + || SimpleAnnotationNullabilityProvider.test(NON_NULL, annotation)) { return Spec.NON_NULL; } - if (test(NULLABLE, annotation)) { + if (SimpleAnnotationNullabilityProvider.test(NULLABLE, annotation)) { return Spec.NULLABLE; } } return Spec.UNSPECIFIED; } + } + + /** + * Annotation-based {@link NullabilityProvider} leveraging simple or meta-annotations. + */ + static class SimpleAnnotationNullabilityProvider extends NullabilityProvider { - private static boolean test(Class annotationClass, Annotation annotation) { + private final Class nonNull; + private final Class nullable; - if (annotation.annotationType().equals(annotationClass)) { - return true; + SimpleAnnotationNullabilityProvider(Class nonNull, Class nullable) { + this.nonNull = nonNull; + this.nullable = nullable; + } + + @Override + Spec evaluate(AnnotatedElement element, ElementType elementType) { + + Annotation[] annotations = element.getAnnotations(); + + for (Annotation annotation : annotations) { + + if (test(nonNull, annotation)) { + return Spec.NON_NULL; + } + + if (test(nullable, annotation)) { + return Spec.NULLABLE; + } } - MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType()); - if (annotations.isPresent(annotationClass)) { - Annotation meta = annotations.get(annotationClass).synthesize(); + return Spec.UNSPECIFIED; + } + + static boolean test(Class annotationClass, Annotation annotation) { + if (annotation.annotationType().equals(annotationClass)) { return true; } - return false; + MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType()); + return annotations.isPresent(annotationClass); } } diff --git a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java index 2ac072d944..99949729dc 100644 --- a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java @@ -93,6 +93,29 @@ void shouldConsiderJakartaNonNullParameters() { assertThat(pn1.isNonNull()).isFalse(); } + @Test // + 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() { 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; From c63b236b3c837f4200d5ed671c8438a6af0b4d54 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Jun 2024 10:38:18 +0200 Subject: [PATCH 5/9] Introduce MethodNullability API. --- .../data/util/Nullability.java | 75 +++++++++++++++++-- .../data/util/NullabilityIntrospector.java | 72 +++++++++++++++++- .../data/util/NullableUtilsUnitTests.java | 9 +-- 3 files changed, 141 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/springframework/data/util/Nullability.java b/src/main/java/org/springframework/data/util/Nullability.java index 7da0914676..8bc603b582 100644 --- a/src/main/java/org/springframework/data/util/Nullability.java +++ b/src/main/java/org/springframework/data/util/Nullability.java @@ -78,14 +78,24 @@ public interface Nullability { */ 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 from(MethodParameter parameter) { - return introspect(parameter.getContainingClass()).forParameter(parameter); + static Nullability forParameter(MethodParameter parameter) { + return new NullabilityIntrospector(parameter.getContainingClass(), false).forParameter(parameter); } /** @@ -95,7 +105,7 @@ static Nullability from(MethodParameter parameter) { * @return a {@code Nullability} instance containing the element's nullability declaration. */ static Nullability forMethodReturnType(Method method) { - return introspect(method.getDeclaringClass()).forReturnType(method); + return new NullabilityIntrospector(method.getDeclaringClass(), false).forReturnType(method); } /** @@ -104,8 +114,9 @@ static Nullability forMethodReturnType(Method method) { * @param parameter the source method parameter. * @return a {@code Nullability} instance containing the element's nullability declaration. */ - static Nullability forMethodParameter(Parameter parameter) { - return introspect(parameter.getDeclaringExecutable().getDeclaringClass()).forParameter(parameter); + static Nullability forParameter(Parameter parameter) { + return new NullabilityIntrospector(parameter.getDeclaringExecutable().getDeclaringClass(), false) + .forParameter(parameter); } /** @@ -115,7 +126,7 @@ static Nullability forMethodParameter(Parameter parameter) { * @return a {@code Introspector} instance considering nullability declarations from the {@link Class} and package. */ static Introspector introspect(Class cls) { - return new NullabilityIntrospector(cls); + return new NullabilityIntrospector(cls, true); } /** @@ -125,7 +136,7 @@ static Introspector introspect(Class cls) { * @return a {@code Introspector} instance considering nullability declarations from package. */ static Introspector introspect(Package pkg) { - return new NullabilityIntrospector(pkg); + return new NullabilityIntrospector(pkg, true); } /** @@ -133,6 +144,17 @@ static Introspector introspect(Package pkg) { */ interface Introspector { + /** + * 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}. *

@@ -159,7 +181,7 @@ default Nullability forParameter(MethodParameter parameter) { Nullability forReturnType(Method method); /** - * Creates a new {@link Nullability} instance by introspecting the {@link MethodParameter}. + * 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. @@ -171,4 +193,41 @@ default Nullability forParameter(MethodParameter 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. + * + * @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()); + } + + } + } diff --git a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java index c9ae086859..71a0aab7db 100644 --- a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -25,8 +25,10 @@ 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; @@ -37,6 +39,7 @@ import org.springframework.lang.NonNullFields; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentLruCache; /** * Default {@link Nullability.Introspector} implementation backed by {@link NullabilityProvider nullability providers}. @@ -67,8 +70,9 @@ class NullabilityIntrospector implements Nullability.Introspector { private final DeclarationAnchor anchor; - NullabilityIntrospector(AnnotatedElement segment) { - this.anchor = createTree(segment); + NullabilityIntrospector(AnnotatedElement segment, boolean cache) { + DeclarationAnchor tree = createTree(segment); + this.anchor = cache ? new CachingDeclarationAnchor(tree) : tree; } /** @@ -107,6 +111,19 @@ static DeclarationAnchor createTree(AnnotatedElement element) { throw new IllegalArgumentException(String.format("Cannot create DeclarationAnchor for %s", element)); } + @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) { @@ -437,16 +454,67 @@ public boolean isNonNull() { } } + 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; + } + } + enum Spec { UNSPECIFIED, 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 + * @return + */ 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; diff --git a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java index 99949729dc..85ed4acc3f 100644 --- a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java @@ -45,13 +45,12 @@ void packageAnnotatedShouldConsiderNonNullAnnotation() { assertThat(NullableUtils.isNonNull(method, ElementType.PARAMETER)).isTrue(); assertThat(NullableUtils.isNonNull(method, ElementType.PACKAGE)).isFalse(); - Nullability.Introspector introspector = Nullability.introspect(NonNullOnPackage.class); - Nullability mrt = introspector.forReturnType(method); + Nullability.MethodNullability mrt = Nullability.forMethod(method); assertThat(mrt.isNullable()).isFalse(); assertThat(mrt.isNonNull()).isTrue(); - Nullability pn = introspector.forParameter(MethodParameter.forExecutable(method, 0)); + Nullability pn = mrt.forParameter(MethodParameter.forExecutable(method, 0)); assertThat(pn.isNullable()).isFalse(); assertThat(pn.isNonNull()).isTrue(); @@ -145,7 +144,7 @@ void shouldConsiderJsr305NonNullAnnotation() { var method = ReflectionUtils.findMethod(Jsr305NonnullAnnotatedType.class, "someMethod", String.class); Nullability mrt = Nullability.forMethodReturnType(method); - Nullability pn = Nullability.forMethodParameter(method.getParameters()[0]); + Nullability pn = Nullability.forParameter(method.getParameters()[0]); assertThat(mrt.isDeclared()).isTrue(); assertThat(mrt.isNullable()).isFalse(); @@ -165,7 +164,7 @@ void shouldConsiderNonAnnotatedTypeNullable() { var method = ReflectionUtils.findMethod(NonAnnotatedType.class, "someMethod", String.class); Nullability mrt = Nullability.forMethodReturnType(method); - Nullability pn = Nullability.forMethodParameter(method.getParameters()[0]); + Nullability pn = Nullability.forParameter(method.getParameters()[0]); assertThat(mrt.isDeclared()).isFalse(); assertThat(mrt.isNullable()).isTrue(); From ea6fbbb9643ed52f181e70ccd9feb6ecef0a58da Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Jun 2024 10:49:34 +0200 Subject: [PATCH 6/9] Improve documentation. --- .../data/util/Nullability.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/main/java/org/springframework/data/util/Nullability.java b/src/main/java/org/springframework/data/util/Nullability.java index 8bc603b582..21c705e266 100644 --- a/src/main/java/org/springframework/data/util/Nullability.java +++ b/src/main/java/org/springframework/data/util/Nullability.java @@ -19,6 +19,8 @@ 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 @@ -33,6 +35,46 @@ * 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) {
+ * 		// …
+ * 	}
+ * }
+ * 
+ *

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

+ *

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

@@ -54,6 +96,8 @@ * declarations, for example to validate input or output. * * @author Mark Paluch + * @see NonNullApi + * @see Nullable */ public interface Nullability { From 622c6f3512f7beeca8cff1361b84e487af938262 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Jun 2024 11:15:16 +0200 Subject: [PATCH 7/9] Deprecate NullableUtils. --- .../support/MethodInvocationValidator.java | 46 +++++++----- .../data/util/Nullability.java | 9 +++ .../data/util/NullabilityIntrospector.java | 75 ++++++++++++++++--- .../data/util/NullableUtils.java | 49 +++--------- 4 files changed, 110 insertions(+), 69 deletions(-) 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 index 21c705e266..e8aaf39b13 100644 --- a/src/main/java/org/springframework/data/util/Nullability.java +++ b/src/main/java/org/springframework/data/util/Nullability.java @@ -15,6 +15,7 @@ */ package org.springframework.data.util; +import java.lang.annotation.ElementType; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -188,6 +189,14 @@ static Introspector introspect(Package pkg) { */ 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}. *

diff --git a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java index 71a0aab7db..3805201af2 100644 --- a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -18,7 +18,6 @@ import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.AnnotatedType; import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -33,6 +32,8 @@ 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; @@ -40,6 +41,7 @@ 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}. @@ -111,6 +113,11 @@ static DeclarationAnchor createTree(AnnotatedElement 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) { @@ -138,7 +145,7 @@ public Nullability forParameter(Parameter parameter) { return new TheNullability(element.evaluate(ElementType.PARAMETER)); } - static Spec doWith(Function function) { + static Spec doWith(Function function) { for (NullabilityProvider provider : providers) { Spec result = function.apply(provider); @@ -151,6 +158,34 @@ static Spec doWith(Function function) { 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. */ @@ -208,6 +243,7 @@ Spec evaluate(AnnotatedElement element, ElementType elementType) { * {@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"); @@ -269,7 +305,7 @@ private static boolean test(Class annotationClass, Annotation annota } private static boolean isInScope(Annotation annotation, ElementType elementType) { - return NullableUtils.test(annotation, TYPE_QUALIFIER_CLASS_NAME, "value", + return NullabilityIntrospector.test(annotation, TYPE_QUALIFIER_CLASS_NAME, "value", (ElementType[] o) -> Arrays.binarySearch(o, elementType) >= 0); } @@ -281,24 +317,27 @@ private static boolean isInScope(Annotation annotation, ElementType elementType) * @return {@literal true} if the annotation expresses non-nullability. */ static boolean isNonNull(Annotation annotation) { - return NullableUtils.test(annotation, NON_NULL.getName(), "when", o -> WHEN_NON_NULLABLE.contains(o.toString())); + 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 + * @param annotation the annotation to introspect. * @return {@literal true} if the annotation expresses nullability. */ static boolean isNullable(Annotation annotation) { - return NullableUtils.test(annotation, NON_NULL.getName(), "when", o -> WHEN_NULLABLE.contains(o.toString())); + return NullabilityIntrospector.test(annotation, NON_NULL.getName(), "when", + o -> WHEN_NULLABLE.contains(o.toString())); } } /** * Simplified variant of {@link Jsr305Provider} without {@code when} and {@code @TypeQualifierDefault} support. */ + @SuppressWarnings("DataFlowIssue") static class JakartaAnnotationProvider extends SimpleAnnotationNullabilityProvider { private static final Class NON_NULL = findClass("jakarta.annotation.Nonnull"); @@ -316,6 +355,7 @@ public static boolean isAvailable() { /** * Provider for JSpecify annotations. */ + @SuppressWarnings("DataFlowIssue") static class JSpecifyAnnotationProvider extends NullabilityProvider { private static final Class NON_NULL = findClass("org.jspecify.annotations.NonNull"); @@ -331,7 +371,6 @@ public static boolean isAvailable() { Spec evaluate(AnnotatedElement element, ElementType elementType) { Annotation[] annotations = element.getAnnotations(); - AnnotatedType annotatedType = null; if (element instanceof Parameter p) { @@ -478,8 +517,24 @@ public Nullability forParameter(Parameter parameter) { } } + /** + * Declaration result. + */ enum Spec { - UNSPECIFIED, NULLABLE, NON_NULL + /** + * No nullabilty rule declared. + */ + UNSPECIFIED, + + /** + * Declaration yields nullable. + */ + NULLABLE, + + /** + * Declaration yields non-nullable. + */ + NON_NULL } /** @@ -490,8 +545,8 @@ interface DeclarationAnchor { /** * Evaluate nullability declarations for the given {@link ElementType}. * - * @param target - * @return + * @param target target element type to evaluate declarations for. + * @return specification result. */ Spec evaluate(ElementType target); diff --git a/src/main/java/org/springframework/data/util/NullableUtils.java b/src/main/java/org/springframework/data/util/NullableUtils.java index a8392703e6..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") - 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) { From 33dbc9a89587da8e478b41daded497e0dbf1b6f4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Jun 2024 11:30:03 +0200 Subject: [PATCH 8/9] Introduce PrimiviteProvider to determine nullability definitions. --- .../data/util/Nullability.java | 19 +++++++ .../data/util/NullabilityIntrospector.java | 52 ++++++++++++++++++- .../data/util/NullabilityUnitTests.java | 51 ++++++++++++++++++ .../type/Jsr305NonnullAnnotatedType.java | 2 +- .../util/nonnull/type/NonAnnotatedType.java | 4 +- .../nonnull/type/NonNullableParameters.java | 2 +- 6 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/springframework/data/util/NullabilityUnitTests.java diff --git a/src/main/java/org/springframework/data/util/Nullability.java b/src/main/java/org/springframework/data/util/Nullability.java index e8aaf39b13..938960f8d4 100644 --- a/src/main/java/org/springframework/data/util/Nullability.java +++ b/src/main/java/org/springframework/data/util/Nullability.java @@ -62,6 +62,9 @@ * } * } * + * + * 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 @@ -270,6 +273,15 @@ default Nullability forReturnType() { */ 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. * @@ -281,6 +293,13 @@ 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 index 3805201af2..944ae112c0 100644 --- a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -53,7 +53,9 @@ class NullabilityIntrospector implements Nullability.Introspector { private static final List providers; static { - providers = new ArrayList<>(4); + providers = new ArrayList<>(5); + + providers.add(new PrimitiveProvider()); if (Jsr305Provider.isAvailable()) { providers.add(new Jsr305Provider()); @@ -202,6 +204,39 @@ static abstract class NullabilityProvider { 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. @@ -515,6 +550,21 @@ public Nullability forParameter(Parameter parameter) { 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(); + } } /** 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/nonnull/type/Jsr305NonnullAnnotatedType.java b/src/test/java/org/springframework/data/util/nonnull/type/Jsr305NonnullAnnotatedType.java index 169c9a6fef..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 @@ -23,6 +23,6 @@ @Nonnull public interface Jsr305NonnullAnnotatedType { - void someMethod(String arg); + 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 a00911c538..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 @@ -20,8 +20,8 @@ */ public class NonAnnotatedType { - void someMethod(String arg){ - return; + 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 45183a8e07..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 @@ -23,5 +23,5 @@ @ParametersAreNonnullByDefault public interface NonNullableParameters { - void someMethod(String arg); + String someMethod(String arg); } From ec06257224d12cbfcc7c8a66e32c138dbf17177f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Jun 2024 12:12:35 +0200 Subject: [PATCH 9/9] Remove Jakarta annotation support. --- pom.xml | 1 - .../data/util/Nullability.java | 3 - .../data/util/NullabilityIntrospector.java | 65 ++----------------- .../data/util/NullableUtilsUnitTests.java | 47 +------------- .../util/nonnull/NullableAnnotatedType.java | 5 -- .../nonnull/jakarta/NonNullOnPackage.java | 26 -------- .../util/nonnull/jakarta/package-info.java | 23 ------- 7 files changed, 5 insertions(+), 165 deletions(-) delete mode 100644 src/test/java/org/springframework/data/util/nonnull/jakarta/NonNullOnPackage.java delete mode 100644 src/test/java/org/springframework/data/util/nonnull/jakarta/package-info.java diff --git a/pom.xml b/pom.xml index 2dff192787..bccac4ece9 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,6 @@ 2.11.7 1.4.24 spring.data.commons - 2.1.1 1.8 diff --git a/src/main/java/org/springframework/data/util/Nullability.java b/src/main/java/org/springframework/data/util/Nullability.java index 938960f8d4..f7ba0f6801 100644 --- a/src/main/java/org/springframework/data/util/Nullability.java +++ b/src/main/java/org/springframework/data/util/Nullability.java @@ -72,9 +72,6 @@ *

    *
  • Spring's {@link NonNullApi}, {@link Nullable}, and {@link org.springframework.lang.NonNull}.
  • *
  • JSR-305 {@code javax.annotation.Nonnull} and meta-annotations.
  • - *
  • Jakarta Annotations API via - * {@code jakarta.annotation.Nonnull} which is a simplified variant of JSR-305 {@code javax.annotation.Nonnull} without - * {@code when} and {@code @TypeQualifierDefault}.
  • *
  • 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}.
  • *
diff --git a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java index 944ae112c0..89fe6604bf 100644 --- a/src/main/java/org/springframework/data/util/NullabilityIntrospector.java +++ b/src/main/java/org/springframework/data/util/NullabilityIntrospector.java @@ -53,7 +53,7 @@ class NullabilityIntrospector implements Nullability.Introspector { private static final List providers; static { - providers = new ArrayList<>(5); + providers = new ArrayList<>(4); providers.add(new PrimitiveProvider()); @@ -61,10 +61,6 @@ class NullabilityIntrospector implements Nullability.Introspector { providers.add(new Jsr305Provider()); } - if (JakartaAnnotationProvider.isAvailable()) { - providers.add(new JakartaAnnotationProvider()); - } - if (JSpecifyAnnotationProvider.isAvailable()) { providers.add(new JSpecifyAnnotationProvider()); } @@ -369,24 +365,6 @@ static boolean isNullable(Annotation annotation) { } } - /** - * Simplified variant of {@link Jsr305Provider} without {@code when} and {@code @TypeQualifierDefault} support. - */ - @SuppressWarnings("DataFlowIssue") - static class JakartaAnnotationProvider extends SimpleAnnotationNullabilityProvider { - - private static final Class NON_NULL = findClass("jakarta.annotation.Nonnull"); - private static final Class NULLABLE = findClass("jakarta.annotation.Nullable"); - - JakartaAnnotationProvider() { - super(NON_NULL, NULLABLE); - } - - public static boolean isAvailable() { - return NON_NULL != null && NULLABLE != null; - } - } - /** * Provider for JSpecify annotations. */ @@ -431,49 +409,15 @@ Spec evaluate(AnnotatedElement element, ElementType elementType) { private static Spec evaluate(Annotation[] annotations) { for (Annotation annotation : annotations) { - if (SimpleAnnotationNullabilityProvider.test(NULL_UNMARKED, annotation)) { + if (test(NULL_UNMARKED, annotation)) { return Spec.UNSPECIFIED; } - if (SimpleAnnotationNullabilityProvider.test(NULL_MARKED, annotation) - || SimpleAnnotationNullabilityProvider.test(NON_NULL, annotation)) { + if (test(NULL_MARKED, annotation) || test(NON_NULL, annotation)) { return Spec.NON_NULL; } - if (SimpleAnnotationNullabilityProvider.test(NULLABLE, annotation)) { - return Spec.NULLABLE; - } - } - - return Spec.UNSPECIFIED; - } - } - - /** - * Annotation-based {@link NullabilityProvider} leveraging simple or meta-annotations. - */ - static class SimpleAnnotationNullabilityProvider extends NullabilityProvider { - - private final Class nonNull; - private final Class nullable; - - SimpleAnnotationNullabilityProvider(Class nonNull, Class nullable) { - this.nonNull = nonNull; - this.nullable = nullable; - } - - @Override - Spec evaluate(AnnotatedElement element, ElementType elementType) { - - Annotation[] annotations = element.getAnnotations(); - - for (Annotation annotation : annotations) { - - if (test(nonNull, annotation)) { - return Spec.NON_NULL; - } - - if (test(nullable, annotation)) { + if (test(NULLABLE, annotation)) { return Spec.NULLABLE; } } @@ -490,7 +434,6 @@ static boolean test(Class annotationClass, Annotation annotation) { MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType()); return annotations.isPresent(annotationClass); } - } @Nullable diff --git a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java index 85ed4acc3f..87f8be8d52 100644 --- a/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java @@ -70,29 +70,7 @@ void packageAnnotatedShouldConsiderNonNullAnnotationForMethod() { assertThat(NullableUtils.isNonNull(NonNullOnPackage.class, ElementType.PACKAGE)).isFalse(); } - @Test // - void shouldConsiderJakartaNonNullParameters() { - - var method = ReflectionUtils.findMethod(org.springframework.data.util.nonnull.jakarta.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 // + @Test // GH-3100 void shouldConsiderJSpecifyNonNullParameters() { var method = ReflectionUtils.findMethod(org.springframework.data.util.nonnull.jspecify.NonNullOnPackage.class, @@ -229,27 +207,4 @@ void shouldConsiderMethodReturnJsr305NonnullAnnotation() { assertThat(mrt.isNonNull()).isFalse(); } - @Test // - void shouldConsiderMethodReturnJakartaNonnullAnnotation() { - - var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jakartaNonnullReturnWhen"); - - Nullability mrt = Nullability.forMethodReturnType(method); - - assertThat(mrt.isDeclared()).isTrue(); - assertThat(mrt.isNullable()).isFalse(); - assertThat(mrt.isNonNull()).isTrue(); - } - - @Test // - void shouldConsiderMethodReturnJakartaNullableAnnotation() { - - var method = ReflectionUtils.findMethod(NullableAnnotatedType.class, "jakartaNullableReturnWhen"); - - 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 16200009f2..a56a6cc00b 100644 --- a/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java +++ b/src/test/java/org/springframework/data/util/nonnull/NullableAnnotatedType.java @@ -35,9 +35,4 @@ public interface NullableAnnotatedType { @javax.annotation.Nonnull(when = When.MAYBE) String jsr305NullableReturnWhen(); - @jakarta.annotation.Nonnull - String jakartaNonnullReturnWhen(); - - @jakarta.annotation.Nullable - String jakartaNullableReturnWhen(); } diff --git a/src/test/java/org/springframework/data/util/nonnull/jakarta/NonNullOnPackage.java b/src/test/java/org/springframework/data/util/nonnull/jakarta/NonNullOnPackage.java deleted file mode 100644 index 79bcda9a74..0000000000 --- a/src/test/java/org/springframework/data/util/nonnull/jakarta/NonNullOnPackage.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.jakarta; - -import jakarta.annotation.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/jakarta/package-info.java b/src/test/java/org/springframework/data/util/nonnull/jakarta/package-info.java deleted file mode 100644 index 260655c353..0000000000 --- a/src/test/java/org/springframework/data/util/nonnull/jakarta/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 - */ -@Nonnull -package org.springframework.data.util.nonnull.jakarta; - -import jakarta.annotation.Nonnull;