Skip to content

Commit d218b08

Browse files
committed
Invalid Nullness information for Kotlin properties
This commit adds support for Kotlin properties to Nullness forMethodReturnType and forParameter methods. Closes gh-35419
1 parent d2bdf11 commit d218b08

File tree

2 files changed

+78
-10
lines changed

2 files changed

+78
-10
lines changed

spring-core/src/main/java/org/springframework/core/Nullness.java

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@
2727
import java.util.Objects;
2828
import java.util.function.Predicate;
2929

30+
import kotlin.jvm.JvmClassMappingKt;
31+
import kotlin.reflect.KClass;
3032
import kotlin.reflect.KFunction;
3133
import kotlin.reflect.KParameter;
3234
import kotlin.reflect.KProperty;
35+
import kotlin.reflect.KType;
36+
import kotlin.reflect.full.KClasses;
3337
import kotlin.reflect.jvm.ReflectJvmMapping;
3438
import org.jspecify.annotations.NonNull;
3539
import org.jspecify.annotations.NullMarked;
@@ -181,8 +185,23 @@ private static class KotlinDelegate {
181185

182186
public static Nullness forMethodReturnType(Method method) {
183187
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
184-
if (function != null && ReflectJvmMapping.getJavaType(function.getReturnType()) != void.class) {
185-
return (function.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
188+
if (function == null) {
189+
String methodName = method.getName();
190+
if (methodName.startsWith("get")) {
191+
String propertyName = accessorToPropertyName(methodName);
192+
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass());
193+
for (KProperty<?> property : KClasses.getMemberProperties(kClass)) {
194+
if (property.getName().equals(propertyName)) {
195+
return (property.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
196+
}
197+
}
198+
}
199+
}
200+
else {
201+
KType type = function.getReturnType();
202+
if (ReflectJvmMapping.getJavaType(type) != void.class) {
203+
return (type.isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
204+
}
186205
}
187206
return Nullness.UNSPECIFIED;
188207
}
@@ -200,23 +219,40 @@ public static Nullness forParameter(Executable executable, int parameterIndex) {
200219
KParameter.Kind.INSTANCE.equals(p.getKind()));
201220
}
202221
if (function == null) {
203-
return Nullness.UNSPECIFIED;
222+
String methodName = executable.getName();
223+
if (methodName.startsWith("set")) {
224+
String propertyName = accessorToPropertyName(methodName);
225+
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(executable.getDeclaringClass());
226+
for (KProperty<?> property : KClasses.getMemberProperties(kClass)) {
227+
if (property.getName().equals(propertyName)) {
228+
return (property.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
229+
}
230+
}
231+
}
204232
}
205-
int i = 0;
206-
for (KParameter kParameter : function.getParameters()) {
207-
if (predicate.test(kParameter) && parameterIndex == i++) {
208-
return (kParameter.getType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
233+
else {
234+
int i = 0;
235+
for (KParameter kParameter : function.getParameters()) {
236+
if (predicate.test(kParameter) && parameterIndex == i++) {
237+
return (kParameter.getType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
238+
}
209239
}
210240
}
211241
return Nullness.UNSPECIFIED;
212242
}
213243

214244
public static Nullness forField(Field field) {
215245
KProperty<?> property = ReflectJvmMapping.getKotlinProperty(field);
216-
if (property != null && property.getReturnType().isMarkedNullable()) {
217-
return Nullness.NULLABLE;
246+
if (property != null) {
247+
return (property.getReturnType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
218248
}
219-
return Nullness.NON_NULL;
249+
return Nullness.UNSPECIFIED;
250+
}
251+
252+
private static String accessorToPropertyName(String method) {
253+
char[] methodNameChars = method.toCharArray();
254+
methodNameChars[3] = Character.toLowerCase(methodNameChars[3]);
255+
return new String(methodNameChars, 3, methodNameChars.length - 3);
220256
}
221257

222258
}

spring-core/src/test/kotlin/org/springframework/core/NullnessKotlinTests.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,34 @@ class NullnessKotlinTests {
7979
Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL)
8080
}
8181

82+
@Test
83+
fun nullableDataClassGetter() {
84+
val method = NullableName::class.java.getDeclaredMethod("getName")
85+
val nullness = Nullness.forMethodReturnType(method)
86+
Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE)
87+
}
88+
89+
@Test
90+
fun nonNullableDataClassGetter() {
91+
val method = NonNullableName::class.java.getDeclaredMethod("getName")
92+
val nullness = Nullness.forMethodReturnType(method)
93+
Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL)
94+
}
95+
96+
@Test
97+
fun nullableDataClassSetter() {
98+
val method = NullableName::class.java.getDeclaredMethod("setName", String::class.java)
99+
val nullness = Nullness.forParameter(method.parameters[0])
100+
Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE)
101+
}
102+
103+
@Test
104+
fun nonNullableDataClassSetter() {
105+
val method = NonNullableName::class.java.getDeclaredMethod("setName", String::class.java)
106+
val nullness = Nullness.forParameter(method.parameters[0])
107+
Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL)
108+
}
109+
82110
@Suppress("unused_parameter")
83111
fun nullable(nullable: String?): String? = "foo"
84112

@@ -88,4 +116,8 @@ class NullnessKotlinTests {
88116
fun unit() {
89117
}
90118

119+
data class NullableName(var name: String?)
120+
121+
data class NonNullableName(var name: String)
122+
91123
}

0 commit comments

Comments
 (0)