From d218b0899a9312cd1f6a55ae917fddb04eedd246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 5 Sep 2025 17:37:44 +0200 Subject: [PATCH] Invalid Nullness information for Kotlin properties This commit adds support for Kotlin properties to Nullness forMethodReturnType and forParameter methods. Closes gh-35419 --- .../org/springframework/core/Nullness.java | 56 +++++++++++++++---- .../core/NullnessKotlinTests.kt | 32 +++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/Nullness.java b/spring-core/src/main/java/org/springframework/core/Nullness.java index e8d713fd7cb..29382aac665 100644 --- a/spring-core/src/main/java/org/springframework/core/Nullness.java +++ b/spring-core/src/main/java/org/springframework/core/Nullness.java @@ -27,9 +27,13 @@ import java.lang.reflect.Parameter; import java.util.Objects; import java.util.function.Predicate; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.KProperty; +import kotlin.reflect.KType; +import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.ReflectJvmMapping; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; @@ -181,8 +185,23 @@ public enum Nullness { public static Nullness forMethodReturnType(Method method) { KFunction function = ReflectJvmMapping.getKotlinFunction(method); - if (function != null && ReflectJvmMapping.getJavaType(function.getReturnType()) != void.class) { - return (function.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); + if (function == null) { + String methodName = method.getName(); + if (methodName.startsWith("get")) { + String propertyName = accessorToPropertyName(methodName); + KClass kClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass()); + for (KProperty property : KClasses.getMemberProperties(kClass)) { + if (property.getName().equals(propertyName)) { + return (property.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); + } + } + } + } + else { + KType type = function.getReturnType(); + if (ReflectJvmMapping.getJavaType(type) != void.class) { + return (type.isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); + } } return Nullness.UNSPECIFIED; } @@ -200,12 +219,23 @@ public enum Nullness { KParameter.Kind.INSTANCE.equals(p.getKind())); } if (function == null) { - return Nullness.UNSPECIFIED; + String methodName = executable.getName(); + if (methodName.startsWith("set")) { + String propertyName = accessorToPropertyName(methodName); + KClass kClass = JvmClassMappingKt.getKotlinClass(executable.getDeclaringClass()); + for (KProperty property : KClasses.getMemberProperties(kClass)) { + if (property.getName().equals(propertyName)) { + return (property.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); + } + } + } } - int i = 0; - for (KParameter kParameter : function.getParameters()) { - if (predicate.test(kParameter) && parameterIndex == i++) { - return (kParameter.getType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); + else { + int i = 0; + for (KParameter kParameter : function.getParameters()) { + if (predicate.test(kParameter) && parameterIndex == i++) { + return (kParameter.getType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); + } } } return Nullness.UNSPECIFIED; @@ -213,10 +243,16 @@ public enum Nullness { public static Nullness forField(Field field) { KProperty property = ReflectJvmMapping.getKotlinProperty(field); - if (property != null && property.getReturnType().isMarkedNullable()) { - return Nullness.NULLABLE; + if (property != null) { + return (property.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); } - return Nullness.NON_NULL; + return Nullness.UNSPECIFIED; + } + + private static String accessorToPropertyName(String method) { + char[] methodNameChars = method.toCharArray(); + methodNameChars[3] = Character.toLowerCase(methodNameChars[3]); + return new String(methodNameChars, 3, methodNameChars.length - 3); } } diff --git a/spring-core/src/test/kotlin/org/springframework/core/NullnessKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/NullnessKotlinTests.kt index 87e9c405392..11962aae544 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/NullnessKotlinTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/NullnessKotlinTests.kt @@ -79,6 +79,34 @@ class NullnessKotlinTests { Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL) } + @Test + fun nullableDataClassGetter() { + val method = NullableName::class.java.getDeclaredMethod("getName") + val nullness = Nullness.forMethodReturnType(method) + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE) + } + + @Test + fun nonNullableDataClassGetter() { + val method = NonNullableName::class.java.getDeclaredMethod("getName") + val nullness = Nullness.forMethodReturnType(method) + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL) + } + + @Test + fun nullableDataClassSetter() { + val method = NullableName::class.java.getDeclaredMethod("setName", String::class.java) + val nullness = Nullness.forParameter(method.parameters[0]) + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE) + } + + @Test + fun nonNullableDataClassSetter() { + val method = NonNullableName::class.java.getDeclaredMethod("setName", String::class.java) + val nullness = Nullness.forParameter(method.parameters[0]) + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL) + } + @Suppress("unused_parameter") fun nullable(nullable: String?): String? = "foo" @@ -88,4 +116,8 @@ class NullnessKotlinTests { fun unit() { } + data class NullableName(var name: String?) + + data class NonNullableName(var name: String) + } \ No newline at end of file