From 9302cb2f85fbde03f85da841d694edb10064fc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Kami=C5=84ski?= Date: Wed, 13 Dec 2017 16:16:05 +0100 Subject: [PATCH] Support Kotlin suspending functions in MethodParameter Before this commit, the return type for Kotlin suspending functions (as returned by MethodParameter#getParameterType and MethodParameter#getGenericReturnType methods) was incorrect. This change leverages Kotlin reflection instead of Java one to return the correct type. Closes gh-21058 --- .../springframework/core/MethodParameter.java | 36 ++++++++- .../core/KotlinGenericTypeResolverTests.kt | 62 ++++++++++++++++ .../core/KotlinMethodParameterTests.kt | 74 ++++++++++++++++++- 3 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 spring-core/src/test/kotlin/org/springframework/core/KotlinGenericTypeResolverTests.kt diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 1cc1056ea87..ff5af1ece5e 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -402,7 +402,9 @@ public class MethodParameter { if (paramType == null) { if (this.parameterIndex < 0) { Method method = getMethod(); - paramType = (method != null ? method.getReturnType() : void.class); + paramType = (method != null ? + (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass()) ? + KotlinDelegate.getReturnType(method) : method.getReturnType()) : void.class); } else { paramType = this.executable.getParameterTypes()[this.parameterIndex]; @@ -422,7 +424,9 @@ public class MethodParameter { if (paramType == null) { if (this.parameterIndex < 0) { Method method = getMethod(); - paramType = (method != null ? method.getGenericReturnType() : void.class); + paramType = (method != null ? + (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass()) ? + KotlinDelegate.getGenericReturnType(method) : method.getGenericReturnType()) : void.class); } else { Type[] genericParameterTypes = this.executable.getGenericParameterTypes(); @@ -799,6 +803,32 @@ public class MethodParameter { } return false; } - } + /** + * Return the generic return type of the method, with support of suspending + * functions via Kotlin reflection. + */ + static private Type getGenericReturnType(Method method) { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + if (function != null && function.isSuspend()) { + return ReflectJvmMapping.getJavaType(function.getReturnType()); + } + return method.getGenericReturnType(); + } + + /** + * Return the return type of the method, with support of suspending + * functions via Kotlin reflection. + */ + static private Class getReturnType(Method method) { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + if (function != null && function.isSuspend()) { + Type paramType = ReflectJvmMapping.getJavaType(function.getReturnType()); + Class paramClass = ResolvableType.forType(paramType).resolve(); + Assert.notNull(paramClass, "Type " + paramType + "can't be resolved to a class"); + return paramClass; + } + return method.getReturnType(); + } + } } diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinGenericTypeResolverTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinGenericTypeResolverTests.kt new file mode 100644 index 00000000000..c9c6396abee --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinGenericTypeResolverTests.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2019 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 + * + * http://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.core + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.springframework.core.GenericTypeResolver.resolveReturnTypeArgument +import java.lang.reflect.Method + +/** + * Tests for Kotlin support in [GenericTypeResolver]. + * + * @author Konrad Kaminski + * @author Sebastien Deleuze + */ +class KotlinGenericTypeResolverTests { + + @Test + fun methodReturnTypes() { + assertEquals(Integer::class.java, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "integer")!!, + MyInterfaceType::class.java)) + assertEquals(String::class.java, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "string")!!, + MyInterfaceType::class.java)) + assertEquals(null, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "raw")!!, + MyInterfaceType::class.java)) + assertEquals(null, resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "object")!!, + MyInterfaceType::class.java)) + } + + private fun findMethod(clazz: Class<*>, name: String): Method? = + clazz.methods.firstOrNull { it.name == name } + + open class MyTypeWithMethods { + suspend fun integer(): MyInterfaceType? = null + + suspend fun string(): MySimpleInterfaceType? = null + + suspend fun `object`(): Any? = null + + suspend fun raw(): MyInterfaceType<*>? = null + } + + interface MyInterfaceType + + interface MySimpleInterfaceType: MyInterfaceType + + open class MySimpleTypeWithMethods: MyTypeWithMethods() +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt index 7580f254136..9d2aaf74701 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt @@ -15,9 +15,14 @@ */ package org.springframework.core +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test -import org.junit.Assert.* import java.lang.reflect.Method +import java.lang.reflect.TypeVariable +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.jvm.javaMethod /** * Tests for Kotlin support in [MethodParameter]. @@ -25,6 +30,7 @@ import java.lang.reflect.Method * @author Raman Gupta * @author Sebastien Deleuze * @author Juergen Hoeller + * @author Konrad Kaminski */ class KotlinMethodParameterTests { @@ -67,6 +73,43 @@ class KotlinMethodParameterTests { assertTrue(MethodParameter(regularClassConstructor, 1).isOptional) } + @Test + fun `Suspending function return type`() { + assertEquals(Number::class.java, returnParameterType("suspendFun")) + assertEquals(Number::class.java, returnGenericParameterType("suspendFun")) + + assertEquals(Producer::class.java, returnParameterType("suspendFun2")) + assertEquals("org.springframework.core.Producer", returnGenericParameterTypeName("suspendFun2")) + + assertEquals(Wrapper::class.java, returnParameterType("suspendFun3")) + assertEquals("org.springframework.core.Wrapper", returnGenericParameterTypeName("suspendFun3")) + + assertEquals(Consumer::class.java, returnParameterType("suspendFun4")) + assertEquals("org.springframework.core.Consumer", returnGenericParameterTypeName("suspendFun4")) + + assertEquals(Producer::class.java, returnParameterType("suspendFun5")) + assertTrue(returnGenericParameterType("suspendFun5") is TypeVariable<*>) + assertEquals("org.springframework.core.Producer", returnGenericParameterTypeBoundName("suspendFun5")) + + assertEquals(Wrapper::class.java, returnParameterType("suspendFun6")) + assertTrue(returnGenericParameterType("suspendFun6") is TypeVariable<*>) + assertEquals("org.springframework.core.Wrapper", returnGenericParameterTypeBoundName("suspendFun6")) + + assertEquals(Consumer::class.java, returnParameterType("suspendFun7")) + assertTrue(returnGenericParameterType("suspendFun7") is TypeVariable<*>) + assertEquals("org.springframework.core.Consumer", returnGenericParameterTypeBoundName("suspendFun7")) + + assertEquals(Object::class.java, returnParameterType("suspendFun8")) + assertEquals(Object::class.java, returnGenericParameterType("suspendFun8")) + } + + private fun returnParameterType(funName: String) = returnMethodParameter(funName).parameterType + private fun returnGenericParameterType(funName: String) = returnMethodParameter(funName).genericParameterType + private fun returnGenericParameterTypeName(funName: String) = returnGenericParameterType(funName).typeName + private fun returnGenericParameterTypeBoundName(funName: String) = (returnGenericParameterType(funName) as TypeVariable<*>).bounds[0].typeName + + private fun returnMethodParameter(funName: String) = + MethodParameter(this::class.declaredFunctions.first { it.name == funName }.javaMethod!!, -1) @Suppress("unused_parameter") fun nullable(nullable: String?): Int? = 42 @@ -82,4 +125,33 @@ class KotlinMethodParameterTests { @Suppress("unused_parameter") class RegularClass(nonNullable: String, nullable: String?) + @Suppress("unused", "unused_parameter") + suspend fun suspendFun(p1: String): Number = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun2(p1: String): Producer = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun3(p1: String): Wrapper = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun4(p1: String): Consumer = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun > suspendFun5(p1: String): T = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun > suspendFun6(p1: String): T = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun > suspendFun7(p1: String): T = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun8(p1: String): Any? = TODO() } + +interface Producer + +interface Wrapper + +interface Consumer