From ac483aeb051d79c364e9c5130e2006c6a862339a Mon Sep 17 00:00:00 2001 From: GuilhE Date: Tue, 5 Nov 2024 15:10:42 +0000 Subject: [PATCH] Fixes kotlin to swift type convertion and adds generics --- .idea/caches/deviceStreaming.xml | 22 ++-- .../composeuiviewcontroller/ksp/Processor.kt | 4 +- .../kmp/composeuiviewcontroller/ksp/Utils.kt | 110 +++++++++++------- .../composeuiviewcontroller/ProcessorTest.kt | 37 +++++- 4 files changed, 109 insertions(+), 64 deletions(-) diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml index 821dc38..5a2f866 100644 --- a/.idea/caches/deviceStreaming.xml +++ b/.idea/caches/deviceStreaming.xml @@ -256,6 +256,17 @@ diff --git a/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Processor.kt b/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Processor.kt index 66a9b8d..1b12901 100644 --- a/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Processor.kt +++ b/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Processor.kt @@ -278,7 +278,7 @@ internal class Processor( val frameworks = frameworkBaseName.joinToString("\n") { "import ${it.name()}" } val makeParametersParsed = makeParameters.joinToString(", ") { "${it.name()}: ${it.name()}" } val letParameters = makeParameters.joinToString("\n") { - val type = kotlinTypeToSwift(it) + val type = it.resolveType(toSwift = true) val finalType = if (externalParameters.containsKey(type)) { externalParameters[type] } else type @@ -322,7 +322,7 @@ internal class Processor( val frameworks = frameworkBaseName.joinToString("\n") { "import ${it.name()}" } val makeParametersParsed = makeParameters.joinToString(", ") { "${it.name()}: ${it.name()}" } val letParameters = makeParameters.joinToString("\n") { - val type = kotlinTypeToSwift(it) + val type = it.resolveType(toSwift = true) val finalType = externalParameters[type] ?: type "let ${it.name()}: $finalType" } diff --git a/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Utils.kt b/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Utils.kt index 5be4430..19c9120 100644 --- a/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Utils.kt +++ b/kmp-composeuiviewcontroller-ksp/src/main/kotlin/com/github/guilhe/kmp/composeuiviewcontroller/ksp/Utils.kt @@ -4,38 +4,72 @@ import com.github.guilhe.kmp.composeuiviewcontroller.common.FILE_NAME_ARGS import com.github.guilhe.kmp.composeuiviewcontroller.common.ModuleMetadata import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration -import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSValueParameter /** - * Transforms Kotlin types into their Swift representation. - * [Apple framework generated framework headers](https://kotlinlang.org/docs/apple-framework.html#generated-framework-headers) - * - * @param type [KSTypeReference] to be converted to Swift type - * @return String with Swift type + * Resolves KSValueParameter type + * @param toSwift If true, transforms Kotlin types into their Swift representation. [Apple framework generated framework headers](https://kotlinlang.org/docs/apple-framework.html#generated-framework-headers) + * @return String with type resolved + * @throws TypeResolutionError when type cannot be resolved */ -internal fun kotlinTypeToSwift(type: KSValueParameter): String { - val regex = "\\b(Unit|List|MutableList|Map|MutableMap|Byte|UByte|Short|UShort|Int|UInt|Long|ULong|Float|Double|Boolean)\\b".toRegex() - return regex.replace(type.resolveType()) { matchResult -> - when (matchResult.value) { - "Unit" -> "Void" - "List" -> "Array" - "MutableList" -> "NSMutableArray" - "Map" -> "Dictionary" - "MutableMap" -> "NSMutableDictionary" - "Byte" -> "KotlinByte" - "UByte" -> "KotlinUByte" - "Short" -> "KotlinShort" - "UShort" -> "KotlinUShort" - "Int" -> "KotlinInt" - "UInt" -> "KotlinUInt" - "Long" -> "KotlinLong" - "ULong" -> "KotlinULong" - "Float" -> "KotlinFloat" - "Double" -> "KotlinDouble" - "Boolean" -> "KotlinBoolean" - else -> "KotlinNumber" +internal fun KSValueParameter.resolveType(toSwift: Boolean = false): String { + //println(">> KSValueParameter type: ${type}") + val resolvedType = type.resolve() + return if (resolvedType.isFunctionType) { + buildString { + append("(") + append(resolvedType.arguments.dropLast(1).joinToString(", ") { arg -> + val argType = arg.type?.resolve() + if (argType == null || argType.isError) { + throw TypeResolutionException(resolvedType) + } else { + convertGenericType(argType, toSwift) + } + }) + append(") -> ") + val returnType = resolvedType.arguments.last().type?.resolve() + val returnTypeName = if (returnType == null || returnType.isError) { + throw TypeResolutionException(resolvedType) + } else { + convertGenericType(returnType, toSwift) + } + append(returnTypeName) } + } else { + convertGenericType(resolvedType, toSwift) + } +} + +private fun convertGenericType(type: KSType, toSwift: Boolean): String { + val baseType = type.declaration.simpleName.asString() + val convertedBaseType = if (toSwift) convertToSwift(baseType) else baseType + if (type.arguments.isEmpty()) return convertedBaseType + val generics = type.arguments.joinToString(", ") { arg -> + arg.type?.resolve()?.let { convertGenericType(it, toSwift) } ?: "Unknown" + } + return "$convertedBaseType<$generics>" +} + +private fun convertToSwift(baseType: String): String { + return when (baseType) { + "Unit" -> "Void" + "List" -> "Array" + "MutableList" -> "NSMutableArray" + "Map" -> "Dictionary" + "MutableMap" -> "NSMutableDictionary" + "Byte" -> "KotlinByte" + "UByte" -> "KotlinUByte" + "Short" -> "KotlinShort" + "UShort" -> "KotlinUShort" + "Int" -> "KotlinInt" + "UInt" -> "KotlinUInt" + "Long" -> "KotlinLong" + "ULong" -> "KotlinULong" + "Float" -> "KotlinFloat" + "Double" -> "KotlinDouble" + "Boolean" -> "KotlinBoolean" + else -> baseType } } @@ -151,23 +185,6 @@ internal fun List.joinToStringDeclaration(separator: CharSeque "${it.name!!.getShortName()}: ${it.resolveType()}" } -internal fun KSValueParameter.resolveType(): String { - //println(">> KSValueParameter type: ${type}") - val resolvedType = type.resolve() - return if (resolvedType.isFunctionType) { - buildString { - append("(") - append(resolvedType.arguments.dropLast(1).joinToString(", ") { arg -> - arg.type?.resolve()?.declaration?.simpleName?.asString() ?: "Unknown" - }) - append(") -> ") - append(resolvedType.arguments.last().type?.resolve()?.declaration?.simpleName?.asString() ?: "Unit") - } - } else { - resolvedType.declaration.simpleName.asString() - } -} - internal fun KSFunctionDeclaration.name(): String = qualifiedName!!.getShortName() internal fun KSValueParameter.name(): String = name!!.getShortName() @@ -189,4 +206,7 @@ internal class TypeResolutionError(parameter: KSValueParameter) : IllegalArgumen "Cannot resolve type for parameter ${parameter.name()} from ${parameter.location}. Check your file imports" ) -internal class ModuleDecodeException(e: Exception) : IllegalArgumentException("Could not decode $FILE_NAME_ARGS file with exception: ${e.localizedMessage}") \ No newline at end of file +internal class ModuleDecodeException(e: Exception) : + IllegalArgumentException("Could not decode $FILE_NAME_ARGS file with exception: ${e.localizedMessage}") + +internal class TypeResolutionException(type: KSType) : IllegalArgumentException("Could not resolve function parameter: ${type}") \ No newline at end of file diff --git a/kmp-composeuiviewcontroller-ksp/src/test/kotlin/composeuiviewcontroller/ProcessorTest.kt b/kmp-composeuiviewcontroller-ksp/src/test/kotlin/composeuiviewcontroller/ProcessorTest.kt index 8d1b0d7..139b8e9 100644 --- a/kmp-composeuiviewcontroller-ksp/src/test/kotlin/composeuiviewcontroller/ProcessorTest.kt +++ b/kmp-composeuiviewcontroller-ksp/src/test/kotlin/composeuiviewcontroller/ProcessorTest.kt @@ -439,10 +439,10 @@ class ProcessorTest { fun Screen( @ComposeUIViewControllerState state: ViewState, callBackA: () -> Unit, - callBackB: (List) -> Unit, - callBackC: (MutableList) -> Unit, - callBackD: (Map) -> Unit, - callBackE: (MutableMap) -> Unit, + callBackB: (List) -> Unit, + callBackC: (MutableList) -> Unit, + callBackD: (Map) -> Unit, + callBackE: (MutableMap) -> Unit, callBackF: (Byte) -> Unit, callBackG: (UByte) -> Unit, callBackH: (Short) -> Unit, @@ -467,9 +467,9 @@ class ProcessorTest { val expectedSwiftTypes = listOf( "Void", - "Array", + "Array", "NSMutableArray", - "Dictionary", + "Dictionary", "NSMutableDictionary", "KotlinByte", "KotlinUByte", @@ -489,6 +489,31 @@ class ProcessorTest { } } + @Test + fun `Function parameter with List, MutableList, Map or MutableMap without type specification will throw TypeResolutionError`() { + val code = """ + package com.mycomposable.test + import $composeUIViewControllerAnnotationName + import $composeUIViewControllerStateAnnotationName + import com.mycomposable.data.ViewState + + private data class ViewState(val field: Int) + + @ComposeUIViewController("MyFramework") + @Composable + fun Screen( + @ComposeUIViewControllerState state: ViewState, + callBackB: (List) -> Unit, + callBackC: (MutableList) -> Unit, + callBackD: (Map) -> Unit, + callBackE: (MutableMap) -> Unit, + ) { } + """.trimIndent() + val compilation = prepareCompilation(kotlin("Screen.kt", code)) + val result = compilation.compile() + assertEquals(result.exitCode, KotlinCompilation.ExitCode.COMPILATION_ERROR) + } + @Test fun `Types imported from different KMP modules will not produce Swift files by default`() { val data = """