diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bbed6b0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,33 @@ +[*.{kt,kts}] +ktlint_code_style=android +ktlint_experimental_type-argument-list-spacing=enabled +ktlint_experimental_function-signature=enabled +ktlint_experimental_block-comment-initial-star-alignment=enabled +ktlint_experimental_unnecessary-parentheses-before-trailing-lambda=enabled +ktlint_experimental_class-naming=enabled +ktlint_experimental_package-naming=enabled +ktlint_experimental_fun-keyword-spacing=enabled +ktlint_experimental_function-return-type-spacing=enabled +ktlint_experimental_function-start-of-body-spacing=enabled +ktlint_experimental_function-type-reference-spacing=enabled +ktlint_experimental_modifier-list-spacing=enabled +ktlint_experimental_nullable-type-spacing=enabled +ktlint_experimental_parameter-list-spacing=enabled +ktlint_experimental_spacing-between-function-name-and-opening-parenthesis=enabled +ktlint_experimental_type-argument-list-spacing=enabled +ktlint_experimental_type-parameter-list-spacing=enabled +ktlint_experimental_comment-wrapping=enabled +ktlint_experimental_context-receiver-wrapping=enabled +ktlint_experimental_kdoc-wrapping=enabled + + +ktlint_ignore_back_ticked_identifier=true +ij_kotlin_allow_trailing_comma=true +ij_kotlin_allow_trailing_comma_on_call_site=true +indent_size = 4 +indent_style = space +insert_final_newline = true +# ktlint_function_signature_body_expression_wrapping = default +# ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = -1 +# ktlint_ignore_back_ticked_identifier = false +# max_line_length = -1 diff --git a/README.md b/README.md index 5bb4dfe..b34ed14 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,41 @@ dependencies { # Usage +## Xcode configuration + +The Swift code generated from this plugin is not automatically included in the shared framework you might have. + +You have 2 options to use it in your iOS project: +- Xcode direct file integration +- CocoaPods integration + +### Xcode direct file integration + +You can directly import the generated file in your Xcode project like it's a file you have written on your own. + +To do so: +- open the Xcode project +- right click on "iosApp" +- choose "Add files to iOSApp" +- add the file from the generated folder (you might need to read the FAQ to know where the generated folder is) +- you are now good to go! + +### CocoaPods integration + +After you have added the moko-kswift plugin to your shared module and synced your project, a new Gradle task should appear with name `kSwiftXXXXXPodspec` where `XXXXX` is the name of your shared module (so your task might be named `kSwiftsharedPodspec`). + +- Run the task doing `./gradlew kSwiftsharedPodspec` from the root of your project. + This will generate a new podspec file, `XXXXXSwift.podspec`, where `XXXXX` is still the name of your shared module (so e.g. `sharedSwift.podspec`) + +- Now edit the `Podfile` inside the iOS project adding this line + `pod 'sharedSwift', :path => '../shared'` + just after the one already there for the already available shared module + `pod 'shared', :path => '../shared'` + +- Now run `pod install` from the `iosApp` folder so the new framework is linked to your project. + +- Whenever you need a Swift file generated from moko-kswift just import the generated module (e.g. `import sharedSwift`) and you are good to go! + ## Sealed classes/interfaces to Swift enum Enable feature in project `build.gradle`: diff --git a/build.gradle.kts b/build.gradle.kts index 7f35a77..93cc216 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ buildscript { mavenCentral() google() gradlePluginPortal() + maven("https://jitpack.io") } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44f9be8..568c3c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ mokoResourcesVersion = "0.16.2" kotlinxMetadataKLibVersion = "0.0.1" kotlinPoetVersion = "1.6.0" -swiftPoetVersion = "1.3.1" +swiftPoetVersion = "1.5.1.4" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } @@ -30,7 +30,7 @@ mokoMvvmState = { module = "dev.icerock.moko:mvvm-state" , version.ref = "mokoMv mokoResources = { module = "dev.icerock.moko:resources" , version.ref = "mokoResourcesVersion" } kotlinPoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinPoetVersion" } -swiftPoet = { module = "io.outfoxx:swiftpoet", version.ref = "swiftPoetVersion" } +swiftPoet = { module = "com.github.hbmartin:swiftpoet", version.ref = "swiftPoetVersion" } kotlinCompilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlinVersion" } kotlinxMetadataKLib = { module = "org.jetbrains.kotlinx:kotlinx-metadata-klib", version.ref = "kotlinxMetadataKLibVersion" } diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..8706261 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,4 @@ +jdk: + - openjdk11 +install: + - ./gradlew -p kswift-gradle-plugin -Pgroup=com.github.hbmartin -Pversion=$VERSION -xtest assemble publishPluginJar publishToMavenLocal diff --git a/kswift-gradle-plugin/build.gradle.kts b/kswift-gradle-plugin/build.gradle.kts index c8b48aa..fdb6870 100644 --- a/kswift-gradle-plugin/build.gradle.kts +++ b/kswift-gradle-plugin/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { api(libs.kotlinxMetadataKLib) testImplementation(libs.kotlinTestJUnit) + testImplementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5") } gradlePlugin { diff --git a/kswift-gradle-plugin/settings.gradle.kts b/kswift-gradle-plugin/settings.gradle.kts index ad522e0..e3bd01b 100644 --- a/kswift-gradle-plugin/settings.gradle.kts +++ b/kswift-gradle-plugin/settings.gradle.kts @@ -10,8 +10,8 @@ pluginManagement { repositories { mavenCentral() google() - gradlePluginPortal() + maven("https://jitpack.io") } resolutionStrategy { @@ -28,6 +28,7 @@ dependencyResolutionManagement { repositories { mavenCentral() google() + maven("https://jitpack.io") } versionCatalogs { diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmClassExt.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmClassExt.kt index e530b34..6308e0e 100644 --- a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmClassExt.kt +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmClassExt.kt @@ -14,7 +14,7 @@ import kotlinx.metadata.Flag import kotlinx.metadata.KmClass fun KmClass.buildTypeVariableNames( - kotlinFrameworkName: String + kotlinFrameworkName: String, ) = this.typeParameters.map { typeParam -> val bounds: List = typeParam.upperBounds .map { it.toTypeName(kotlinFrameworkName, isUsedInGenerics = true) } @@ -25,7 +25,7 @@ fun KmClass.buildTypeVariableNames( fun KmClass.getDeclaredTypeNameWithGenerics( kotlinFrameworkName: String, - classes: List + classes: List, ): TypeName { val typeVariables: List = buildTypeVariableNames(kotlinFrameworkName) val haveGenerics: Boolean = typeVariables.isNotEmpty() @@ -34,21 +34,24 @@ fun KmClass.getDeclaredTypeNameWithGenerics( @Suppress("SpreadOperator") return getDeclaredTypeName(kotlinFrameworkName, classes) .let { type -> - if (haveGenerics.not() || isInterface) type - else type.parameterizedBy(*typeVariables.toTypedArray()) + if (haveGenerics.not() || isInterface) { + type + } else { + type.parameterizedBy(typeVariables) + } } } fun KmClass.getDeclaredTypeName( kotlinFrameworkName: String, - classes: List + classes: List, ): DeclaredTypeName { return DeclaredTypeName( moduleName = kotlinFrameworkName, simpleName = getSimpleName( className = name, - classes = classes - ) + classes = classes, + ), ) } diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmTypeExt.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmTypeExt.kt index 9a5f569..d717b9b 100644 --- a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmTypeExt.kt +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/KmTypeExt.kt @@ -16,6 +16,7 @@ import io.outfoxx.swiftpoet.UINT64 import io.outfoxx.swiftpoet.VOID import io.outfoxx.swiftpoet.parameterizedBy import kotlinx.metadata.ClassName +import kotlinx.metadata.Flag import kotlinx.metadata.KmClassifier import kotlinx.metadata.KmType @@ -24,15 +25,22 @@ fun KmType.toTypeName( moduleName: String, isUsedInGenerics: Boolean = false, typeVariables: Map = emptyMap(), - removeTypeVariables: Boolean = false + removeTypeVariables: Boolean = false, ): TypeName { return when (val classifier = classifier) { is KmClassifier.TypeParameter -> { val typeVariable: TypeVariableName? = typeVariables[classifier.id] if (typeVariable != null) { - return if (!removeTypeVariables) typeVariable - else typeVariable.bounds.firstOrNull()?.type ?: ANY_OBJECT - } else throw IllegalArgumentException("can't read type parameter $this without type variables list") + return if (!removeTypeVariables) { + typeVariable + } else { + typeVariable.bounds.firstOrNull()?.type ?: ANY_OBJECT + } + } else { + throw IllegalArgumentException( + "can't read type parameter $this without type variables list", + ) + } } is KmClassifier.TypeAlias -> { classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics) @@ -45,7 +53,7 @@ fun KmType.toTypeName( moduleName, classifier.name, typeVariables, - removeTypeVariables + removeTypeVariables, ) } } @@ -75,16 +83,18 @@ fun String.kotlinTypeNameToSwift(moduleName: String, isUsedInGenerics: Boolean): val className: String = moduleAndClass[1] DeclaredTypeName.typeName( - listOf(module, className).joinToString(".") + listOf(module, className).joinToString("."), ).objcNameToSwift() } else if (this.startsWith("kotlin/Function")) { null } else if (this.startsWith("kotlin/") && this.count { it == '/' } == 1) { DeclaredTypeName( moduleName = moduleName, - simpleName = "Kotlin" + this.split("/").last() + simpleName = "Kotlin" + this.split("/").last(), ) - } else null + } else { + null + } } } } @@ -93,11 +103,11 @@ fun KmType.kotlinTypeToTypeName( moduleName: String, classifierName: ClassName, typeVariables: Map, - removeTypeVariables: Boolean + removeTypeVariables: Boolean, ): TypeName { val typeName = DeclaredTypeName( moduleName = moduleName, - simpleName = classifierName.split("/").last() + simpleName = classifierName.split("/").last(), ) if (this.arguments.isEmpty()) return typeName @@ -108,17 +118,17 @@ fun KmType.kotlinTypeToTypeName( moduleName = moduleName, isUsedInGenerics = false, typeVariables = typeVariables, - removeTypeVariables = removeTypeVariables + removeTypeVariables = removeTypeVariables, )!! val outputType: TypeName = arguments[1].type?.toTypeName( moduleName = moduleName, isUsedInGenerics = false, typeVariables = typeVariables, - removeTypeVariables = removeTypeVariables + removeTypeVariables = removeTypeVariables, )!! FunctionTypeName.get( parameters = listOf(ParameterSpec.unnamed(inputType)), - returnType = outputType + returnType = outputType, ) } else -> { @@ -127,11 +137,11 @@ fun KmType.kotlinTypeToTypeName( moduleName = moduleName, isUsedInGenerics = true, typeVariables = typeVariables, - removeTypeVariables = removeTypeVariables + removeTypeVariables = removeTypeVariables, ) } @Suppress("SpreadOperator") - typeName.parameterizedBy(*arguments.toTypedArray()) + typeName.parameterizedBy(arguments) } } } @@ -142,3 +152,9 @@ fun DeclaredTypeName.objcNameToSwift(): DeclaredTypeName { else -> this } } + +val KmType.isNullable: Boolean + get() = Flag.Type.IS_NULLABLE(flags) + +val KmType.hasGenerics: Boolean + get() = this.arguments.isNotEmpty() diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/SealedToSwiftAssociatedEnumFeature.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/SealedToSwiftAssociatedEnumFeature.kt new file mode 100644 index 0000000..ceec584 --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/SealedToSwiftAssociatedEnumFeature.kt @@ -0,0 +1,87 @@ +package dev.icerock.moko.kswift.plugin.feature + +import dev.icerock.moko.kswift.plugin.buildTypeVariableNames +import dev.icerock.moko.kswift.plugin.context.ClassContext +import dev.icerock.moko.kswift.plugin.context.kLibClasses +import dev.icerock.moko.kswift.plugin.feature.associatedenum.AssociatedEnumCase +import dev.icerock.moko.kswift.plugin.feature.associatedenum.buildEnumCases +import dev.icerock.moko.kswift.plugin.feature.associatedenum.buildTypeSpec +import dev.icerock.moko.kswift.plugin.getSimpleName +import io.outfoxx.swiftpoet.FileSpec +import io.outfoxx.swiftpoet.TypeSpec +import kotlinx.metadata.Flag +import kotlinx.metadata.KmClass +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import kotlin.reflect.KClass + +class SealedToSwiftAssociatedEnumFeature( + override val featureContext: KClass, + override val filter: Filter, +) : ProcessorFeature() { + + @Suppress("ReturnCount") + override fun doProcess(featureContext: ClassContext, processorContext: ProcessorContext) { + val kotlinFrameworkName: String = processorContext.framework.baseName + + doProcess( + featureContext = featureContext, + fileSpecBuilder = processorContext.fileSpecBuilder, + kotlinFrameworkName = kotlinFrameworkName, + ) + } + + fun doProcess( + featureContext: ClassContext, + fileSpecBuilder: FileSpec.Builder, + kotlinFrameworkName: String, + ) { + val kmClass: KmClass = featureContext.clazz + val originalClassName: String = getSimpleName(kmClass.name, featureContext.kLibClasses) + + if (!Flag.IS_PUBLIC(kmClass.flags) || featureContext.clazz.sealedSubclasses.isEmpty()) { + return + } + + val sealedCases: List = buildEnumCases( + kotlinFrameworkName = kotlinFrameworkName, + featureContext = featureContext, + ) + if (sealedCases.isEmpty()) { + logger.warn("No public subclasses found for sealed class $originalClassName") + return + } else { + logger.lifecycle( + "Generating enum for sealed class $originalClassName (${sealedCases.size} public subclasses)", + ) + } + + val enumType: TypeSpec = buildTypeSpec( + featureContext = featureContext, + typeVariables = kmClass.buildTypeVariableNames(kotlinFrameworkName), + sealedCases = sealedCases, + kotlinFrameworkName = kotlinFrameworkName, + originalClassName = originalClassName, + ) + + fileSpecBuilder.addType(enumType) + } + + class Config : BaseConfig { + override var filter: Filter = Filter.Exclude(emptySet()) + } + + companion object : Factory { + override fun create(block: Config.() -> Unit): SealedToSwiftAssociatedEnumFeature { + val config = Config().apply(block) + return SealedToSwiftAssociatedEnumFeature(featureContext, config.filter) + } + + override val featureContext: KClass = ClassContext::class + + @JvmStatic + override val factory = Companion + + val logger: Logger = Logging.getLogger("SealedToSwiftAssociatedEnumFeature") + } +} diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/SealedToSwiftEnumFeature.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/SealedToSwiftEnumFeature.kt index 7994873..9cfa71f 100644 --- a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/SealedToSwiftEnumFeature.kt +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/SealedToSwiftEnumFeature.kt @@ -26,7 +26,7 @@ import kotlin.reflect.KClass class SealedToSwiftEnumFeature( override val featureContext: KClass, - override val filter: Filter + override val filter: Filter, ) : ProcessorFeature() { @Suppress("ReturnCount") @@ -59,15 +59,15 @@ class SealedToSwiftEnumFeature( kotlinFrameworkName = kotlinFrameworkName, sealedCases = sealedCases, className = className, - originalClassName = originalClassName - ) + originalClassName = originalClassName, + ), ) .addProperty( buildSealedProperty( featureContext = featureContext, kotlinFrameworkName = kotlinFrameworkName, - sealedCases = sealedCases - ) + sealedCases = sealedCases, + ), ) .build() @@ -79,7 +79,7 @@ class SealedToSwiftEnumFeature( kotlinFrameworkName: String, sealedCases: List, className: String, - originalClassName: String + originalClassName: String, ): FunctionSpec { return FunctionSpec.builder("init") .addModifiers(Modifier.PUBLIC) @@ -88,8 +88,8 @@ class SealedToSwiftEnumFeature( name = "obj", type = featureContext.clazz.getDeclaredTypeNameWithGenerics( kotlinFrameworkName = kotlinFrameworkName, - classes = featureContext.kLibClasses - ) + classes = featureContext.kLibClasses, + ), ) .addCode( CodeBlock.builder() @@ -117,14 +117,14 @@ class SealedToSwiftEnumFeature( unindent() add("}\n") } - .build() + .build(), ) .build() } private fun buildEnumCases( kotlinFrameworkName: String, - featureContext: ClassContext + featureContext: ClassContext, ): List { val kmClass = featureContext.clazz return kmClass.sealedSubclasses.mapNotNull { sealedClassName -> @@ -141,18 +141,20 @@ class SealedToSwiftEnumFeature( kotlinFrameworkName: String, featureContext: ClassContext, subclassName: ClassName, - sealedCaseClass: KmClass + sealedCaseClass: KmClass, ): EnumCase { val kmClass = featureContext.clazz val name: String = if (subclassName.startsWith(kmClass.name)) { subclassName.removePrefix(kmClass.name).removePrefix(".") - } else subclassName.removePrefix(kmClass.name.substringBeforeLast("/")).removePrefix("/") + } else { + subclassName.removePrefix(kmClass.name.substringBeforeLast("/")).removePrefix("/") + } val decapitalizedName: String = name.decapitalize(Locale.ROOT) val isObject: Boolean = Flag.Class.IS_OBJECT(sealedCaseClass.flags) val caseArg = sealedCaseClass.getDeclaredTypeNameWithGenerics( kotlinFrameworkName = kotlinFrameworkName, - classes = featureContext.kLibClasses + classes = featureContext.kLibClasses, ) return EnumCase( @@ -165,18 +167,18 @@ class SealedToSwiftEnumFeature( }, initBlock = if (isObject) "" else "(obj)", caseArg = caseArg, - caseBlock = if (isObject) "" else "(let obj)" + caseBlock = if (isObject) "" else "(let obj)", ) } private fun buildSealedProperty( featureContext: ClassContext, kotlinFrameworkName: String, - sealedCases: List + sealedCases: List, ): PropertySpec { val returnType: TypeName = featureContext.clazz.getDeclaredTypeNameWithGenerics( kotlinFrameworkName = kotlinFrameworkName, - classes = featureContext.kLibClasses + classes = featureContext.kLibClasses, ) return PropertySpec.builder("sealed", type = returnType) .addModifiers(Modifier.PUBLIC) @@ -184,13 +186,13 @@ class SealedToSwiftEnumFeature( FunctionSpec .getterBuilder() .addCode(buildSealedPropertyBody(sealedCases, returnType)) - .build() + .build(), ).build() } private fun buildSealedPropertyBody( sealedCases: List, - returnType: TypeName + returnType: TypeName, ): CodeBlock = CodeBlock.builder().apply { add("switch self {\n") sealedCases.forEach { enumCase -> @@ -209,7 +211,7 @@ class SealedToSwiftEnumFeature( private fun CodeBlock.Builder.addSealedCaseReturnCode( enumCase: EnumCase, - returnType: TypeName + returnType: TypeName, ) { val paramType: TypeName? = enumCase.param val cast: String @@ -219,7 +221,7 @@ class SealedToSwiftEnumFeature( returnedName = "${enumCase.caseArg}()" cast = if (returnType is ParameterizedTypeName) { // The return type is generic and there is no parameter, so it can - // be assumed that the case is NOT generic. Thus the case needs to + // be assumed that the case is NOT generic. Thus, the case needs to // be force-cast. "as!" } else { @@ -233,7 +235,7 @@ class SealedToSwiftEnumFeature( cast = if (paramType is ParameterizedTypeName && returnType is ParameterizedTypeName) { if (paramType.typeArguments == returnType.typeArguments) { // The parameter and return type have the same generic pattern. This - // is true if both are NOT generic OR if both are generic. Thus a + // is true if both are NOT generic OR if both are generic. Thus, a // regular cast can be used. "as" } else { @@ -254,7 +256,7 @@ class SealedToSwiftEnumFeature( val initCheck: String, val initBlock: String, val caseArg: TypeName, - val caseBlock: String + val caseBlock: String, ) { val enumCaseSpec: EnumerationCaseSpec get() { diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/AssociatedEnumCase.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/AssociatedEnumCase.kt new file mode 100644 index 0000000..ab26c3e --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/AssociatedEnumCase.kt @@ -0,0 +1,176 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import io.outfoxx.swiftpoet.ARRAY +import io.outfoxx.swiftpoet.DICTIONARY +import io.outfoxx.swiftpoet.DeclaredTypeName +import io.outfoxx.swiftpoet.EnumerationCaseSpec +import io.outfoxx.swiftpoet.ParameterizedTypeName +import io.outfoxx.swiftpoet.SET +import io.outfoxx.swiftpoet.TupleTypeName +import io.outfoxx.swiftpoet.TypeName +import io.outfoxx.swiftpoet.parameterizedBy +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmValueParameter + +private const val PAIR = 2 +private const val TRIPLE = 3 + +data class AssociatedEnumCase( + val frameworkName: String, + val name: String, + val param: TypeName?, + val initCheck: String, + val caseArg: TypeName, + val isObject: Boolean, + val constructorParams: List, + val typeParameters: List, +) { + private val explodedParams: List> = constructorParams.map { + Pair( + it.name, + it.type?.kotlinTypeToSwiftTypeName(frameworkName, typeParameters) + ?: DeclaredTypeName.typeName("Swift.FailedToGetReturnType"), + ) + } + + internal val initBlock: String = if (isObject) { + "" + } else { + "(" + .plus( + explodedParams.joinToString(",\n") { + val tupleType = it.second as? TupleTypeName + val paramType = (it.second as? ParameterizedTypeName) + when { + tupleType != null -> tupleType.generateTuple(it.first) + it.second.isCharacter -> { + "${it.first}: Character(UnicodeScalar(obj.${it.first})!)" + } + paramType?.rawType == DICTIONARY -> { + paramType.toDictionaryCaster(it.first) + } + + paramType?.rawType == SET -> paramType.toSetCaster(it.first) + paramType?.rawType == ARRAY -> paramType.toArrayCaster(it.first) + paramType?.optional == true -> { + val unwrapped = paramType.unwrapOptional() + when ((unwrapped as? ParameterizedTypeName)?.rawType) { + DICTIONARY -> { + unwrapped.toDictionaryCaster(it.first, true) + } + SET -> paramType.toSetCaster(it.first, true) + ARRAY -> paramType.toArrayCaster(it.first, true) + else -> paramType.generateInitParameter(it.first) + } + } + + else -> it.second.generateInitParameter(it.first) + } + }, + ) + .plus(")") + } + + internal val caseBlock = if (isObject) { + "" + } else { + "(" + explodedParams.joinToString { "let ${it.first}" } + ")" + } + + internal val enumCaseSpec: EnumerationCaseSpec + get() { + return if (param == null) { + EnumerationCaseSpec.builder(name).build() + } else if (explodedParams.isNotEmpty()) { + val stripGenericsFromObjC = explodedParams.map { param -> + (param.second as? ParameterizedTypeName)?.let { + if (it.rawType.moduleName != "Swift") { + param.first to it.rawType.parameterizedBy( + it.typeArguments.stripInnerGenerics(), + ) + } else { + null + } + } ?: param + } + EnumerationCaseSpec.builder( + name = name, + type = TupleTypeName.of(stripGenericsFromObjC), + ).build() + } else { + EnumerationCaseSpec.builder(name, param).build() + } + } + + internal val swiftToKotlinConstructor: String = explodedParams + .joinToString { (paramName, paramType) -> + "$paramName: " + when { + paramType.isCharacter -> "$paramName.utf16.first!" + paramType is TupleTypeName -> { + when (paramType.types.size) { + PAIR -> { + val first = paramType.types[0] + val firstType = first.second + val second = paramType.types[1] + val secondType = second.second + + "KotlinPair<" + .plus(firstType.kotlinInteropTypeWithFallback.toNSString()) + .plus(", ") + .plus(secondType.kotlinInteropTypeWithFallback.toNSString()) + .plus(">(first: ") + .plus( + firstType.generateKotlinConstructorIfNecessary("$paramName.0"), + ) + .plus(", second: ") + .plus( + secondType.generateKotlinConstructorIfNecessary("$paramName.1"), + ) + .plus(")") + } + TRIPLE -> { + val first = paramType.types[0] + val firstType = first.second + val second = paramType.types[1] + val secondType = second.second + val third = paramType.types[2] + val thirdType = third.second + "KotlinTriple<" + .plus(firstType.kotlinInteropTypeWithFallback.toNSString()) + .plus(", ") + .plus(secondType.kotlinInteropTypeWithFallback.toNSString()) + .plus(", ") + .plus(thirdType.kotlinInteropTypeWithFallback.toNSString()) + .plus(">(first: ") + .plus( + firstType.generateKotlinConstructorIfNecessary("$paramName.0"), + ) + .plus(", second: ") + .plus( + secondType.generateKotlinConstructorIfNecessary("$paramName.1"), + ) + .plus(", third: ") + .plus( + thirdType.generateKotlinConstructorIfNecessary("$paramName.2"), + ) + .plus(")") + } + else -> { + "unknown tuple type" + } + } + } + + else -> paramType.generateKotlinConstructorIfNecessaryForParameter(paramName) + } + } +} + +private fun String.toNSString(): String = + if (this == "String" || this == "Swift.String") { + "NSString" + } else if (this == "String?" || this == "Swift.String?") { + "NSString?" + } else { + this + } diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/AssociatedEnumCaseBuilder.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/AssociatedEnumCaseBuilder.kt new file mode 100644 index 0000000..a8ccd28 --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/AssociatedEnumCaseBuilder.kt @@ -0,0 +1,64 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import dev.icerock.moko.kswift.plugin.context.ClassContext +import dev.icerock.moko.kswift.plugin.context.kLibClasses +import dev.icerock.moko.kswift.plugin.getDeclaredTypeNameWithGenerics +import kotlinx.metadata.ClassName +import kotlinx.metadata.Flag +import kotlinx.metadata.KmClass +import java.util.Locale + +fun buildEnumCases( + kotlinFrameworkName: String, + featureContext: ClassContext, +): List = featureContext.clazz.sealedSubclasses + .mapNotNull { sealedClassName -> + val sealedClass: KmClass = featureContext.parentContext + .fragment.classes.first { it.name == sealedClassName } + + if (Flag.IS_PUBLIC(sealedClass.flags).not()) return@mapNotNull null + + buildEnumCase( + kotlinFrameworkName = kotlinFrameworkName, + featureContext = featureContext, + subclassName = sealedClassName, + sealedCaseClass = sealedClass, + ) + } + +private fun buildEnumCase( + kotlinFrameworkName: String, + featureContext: ClassContext, + subclassName: ClassName, + sealedCaseClass: KmClass, +): AssociatedEnumCase { + val kmClass = featureContext.clazz + val name: String = if (subclassName.startsWith(kmClass.name)) { + subclassName.removePrefix(kmClass.name).removePrefix(".") + } else { + subclassName.removePrefix(kmClass.name.substringBeforeLast("/")).removePrefix("/") + } + val decapitalizedName: String = name.decapitalize(Locale.ROOT) + + val isObject: Boolean = Flag.Class.IS_OBJECT(sealedCaseClass.flags) || + sealedCaseClass.constructors.first().valueParameters.isEmpty() + val caseArg = sealedCaseClass.getDeclaredTypeNameWithGenerics( + kotlinFrameworkName = kotlinFrameworkName, + classes = featureContext.kLibClasses, + ) + + return AssociatedEnumCase( + frameworkName = kotlinFrameworkName, + name = decapitalizedName, + param = if (isObject) null else caseArg, + initCheck = if (isObject) { + "obj is $caseArg" + } else { + "let obj = obj as? $caseArg" + }, + caseArg = caseArg, + isObject = isObject, + constructorParams = sealedCaseClass.constructors.first().valueParameters, + typeParameters = sealedCaseClass.typeParameters, + ) +} diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/KmTypeProjectionExt.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/KmTypeProjectionExt.kt new file mode 100644 index 0000000..1e565d3 --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/KmTypeProjectionExt.kt @@ -0,0 +1,40 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import dev.icerock.moko.kswift.plugin.isNullable +import io.outfoxx.swiftpoet.ANY_OBJECT +import io.outfoxx.swiftpoet.TupleTypeName +import io.outfoxx.swiftpoet.TypeName +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmTypeProjection + +internal fun List.generateTupleType( + moduleName: String, + typeParameters: List, +): TupleTypeName = + TupleTypeName( + this.map { projection -> + (projection.type?.kotlinTypeNameToInner( + moduleName = moduleName, + namingMode = NamingMode.SWIFT, + isOuterSwift = true, + typeParameters = typeParameters, + ) ?: ANY_OBJECT) + .let { + if (projection.type?.isNullable == true && !it.optional) { + it.wrapOptional() + } else { + it + } + } + } + .map { "" to it }, + ) + +internal fun List.getTypes( + moduleName: String, + namingMode: NamingMode, + isOuterSwift: Boolean, + typeParameters: List, +): List = this.map { + it.type?.kotlinTypeNameToInner(moduleName, namingMode, isOuterSwift, typeParameters) ?: ANY_OBJECT +} diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/NamingMode.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/NamingMode.kt new file mode 100644 index 0000000..a3d1b8a --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/NamingMode.kt @@ -0,0 +1,3 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +internal enum class NamingMode { KOTLIN, KOTLIN_NO_STRING, SWIFT, OBJC } diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/OtherExt.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/OtherExt.kt new file mode 100644 index 0000000..2e16e5a --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/OtherExt.kt @@ -0,0 +1,220 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import dev.icerock.moko.kswift.plugin.isNullable +import dev.icerock.moko.kswift.plugin.objcNameToSwift +import io.outfoxx.swiftpoet.ANY_OBJECT +import io.outfoxx.swiftpoet.ARRAY +import io.outfoxx.swiftpoet.DICTIONARY +import io.outfoxx.swiftpoet.DeclaredTypeName +import io.outfoxx.swiftpoet.FunctionTypeName +import io.outfoxx.swiftpoet.ParameterSpec +import io.outfoxx.swiftpoet.SET +import io.outfoxx.swiftpoet.STRING +import io.outfoxx.swiftpoet.TypeName +import io.outfoxx.swiftpoet.VOID +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmTypeProjection + +private val NSSTRING = DeclaredTypeName(moduleName = "Foundation", simpleName = "NSString") + +internal fun KmType.kotlinTypeNameToInner( + moduleName: String, + namingMode: NamingMode, + isOuterSwift: Boolean, + typeParameters: List, +): TypeName? { + val typeName = this.nameAsString(typeParameters) + return when { + typeName == null -> null + typeName.startsWith("kotlin/") -> { + kotlinPrimitiveToTypeNameWithNamingMode( + namingMode = namingMode, + typeName = typeName, + moduleName = moduleName, + typeParameters = typeParameters, + ) + } + else -> getDeclaredTypeNameFromNonPrimitive(typeName, moduleName) + }?.addGenericsAndOptional( + kmType = this, + moduleName = moduleName, + namingMode = namingMode, + isOuterSwift = isOuterSwift, + typeParameters = typeParameters, + ) +} + +private fun KmType.kotlinPrimitiveToTypeNameWithNamingMode( + namingMode: NamingMode, + typeName: String, + moduleName: String, + typeParameters: List, +) = when (namingMode) { + NamingMode.KOTLIN -> typeName.kotlinPrimitiveTypeNameToKotlinInterop(moduleName) + NamingMode.SWIFT -> typeName.kotlinPrimitiveTypeNameToSwift( + moduleName = moduleName, + arguments = arguments, + typeParameters = typeParameters, + ) + NamingMode.OBJC -> typeName.kotlinPrimitiveTypeNameToObjectiveC(moduleName) + NamingMode.KOTLIN_NO_STRING -> + typeName + .kotlinPrimitiveTypeNameToKotlinInterop(moduleName) + .let { if (it == STRING) NSSTRING else it } +} + +private fun String.kotlinPrimitiveTypeNameToSwift( + moduleName: String, + arguments: List, + typeParameters: List, +): TypeName { + require(this.startsWith("kotlin/") || this.startsWith("kotlinx/collections/immutable/")) + return when (this) { + "kotlin/Char" -> DeclaredTypeName.typeName("Swift.Character") + "kotlin/Comparable" -> DeclaredTypeName.typeName("Swift.Comparable") + "kotlin/Pair" -> arguments.generateTupleType(moduleName, typeParameters) + "kotlin/Result" -> ANY_OBJECT + "kotlin/String" -> STRING + "kotlin/Triple" -> arguments.generateTupleType(moduleName, typeParameters) + "kotlin/Throwable" -> DeclaredTypeName( + moduleName = moduleName, + simpleName = "KotlinThrowable", + ) + + "kotlin/Unit" -> VOID + "kotlinx/collections/immutable/ImmutableList", + "kotlin/collections/List", + -> ARRAY + "kotlinx/collections/immutable/ImmutableMap", + "kotlin/collections/Map", + -> DICTIONARY + "kotlinx/collections/immutable/ImmutableSet", + "kotlin/collections/Set", + -> SET + else -> unknownKotlinPrimitiveTypeToSwift(arguments, moduleName, typeParameters) + } +} + +private fun String.unknownKotlinPrimitiveTypeToSwift( + arguments: List, + moduleName: String, + typeParameters: List, +) = if (this.startsWith("kotlin/Function")) { + val typedArgs = arguments.getTypes(moduleName, NamingMode.KOTLIN, false, typeParameters) + val types = typedArgs.map { ParameterSpec.unnamed(it) }.dropLast(1) + FunctionTypeName.get(types, typedArgs.last()) +} else { + kotlinToSwiftTypeMap[this] ?: this.kotlinInteropName(moduleName) +} + +fun KmType.kotlinTypeToSwiftTypeName( + moduleName: String, + typeParameters: List, +): TypeName? { + val typeName = this.nameAsString(typeParameters) + + return when { + typeName == null -> null + typeName.startsWith("kotlin/") || typeName.startsWith("kotlinx/collections/immutable/") -> + typeName.kotlinPrimitiveTypeNameToSwift(moduleName, this.arguments, typeParameters) + + else -> getDeclaredTypeNameFromNonPrimitive(typeName, moduleName) + }?.addGenericsAndOptional( + kmType = this, + moduleName = moduleName, + namingMode = null, + isOuterSwift = true, + typeParameters = typeParameters, + ) +} + +private fun KmType.nameAsString(typeParameters: List): String? = + when (val classifier = this.classifier) { + is KmClassifier.Class -> classifier.name + is KmClassifier.TypeParameter -> { + typeParameters.recursivelyResolveToName(classifier.id) + } + is KmClassifier.TypeAlias -> classifier.name + } + +private fun List.recursivelyResolveToName(id: Int): String? { + return when { + id >= this.size -> this.recursivelyResolveToName(id - this.size) + id >= 0 -> { + this[id].name + if (this[id].upperBounds.firstOrNull()?.isNullable != false) { + "?" + } else { + "" + } + } + else -> null + } +} + +private fun String.kotlinPrimitiveTypeNameToKotlinInterop(moduleName: String): TypeName { + require(this.startsWith("kotlin/")) + return when (this) { + "kotlin/String" -> STRING + "kotlin/collections/List" -> ARRAY + "kotlin/collections/Map" -> DICTIONARY + "kotlin/collections/Set" -> SET + else -> this.kotlinInteropName(moduleName) + } +} + +private fun String.kotlinInteropName(moduleName: String) = DeclaredTypeName( + moduleName = moduleName, + simpleName = "Kotlin" + this.split("/").last(), +) + +private fun String.kotlinPrimitiveTypeNameToObjectiveC(moduleName: String): DeclaredTypeName { + require(this.startsWith("kotlin/")) + return when (this) { + "kotlin/Any" -> ANY_OBJECT + "kotlin/Boolean" -> DeclaredTypeName(moduleName = moduleName, simpleName = "KotlinBoolean") + "kotlin/Pair" -> DeclaredTypeName(moduleName = moduleName, simpleName = "KotlinPair") + "kotlin/Result" -> ANY_OBJECT + "kotlin/String" -> NSSTRING + "kotlin/Short" -> DeclaredTypeName(moduleName = "Foundation", simpleName = "NSNumber") + "kotlin/Triple" -> DeclaredTypeName(moduleName = moduleName, simpleName = "KotlinTriple") + "kotlin/collections/Map" -> DeclaredTypeName( + moduleName = "Foundation", + simpleName = "NSDictionary", + ) + + "kotlin/collections/Set" -> DeclaredTypeName( + moduleName = "Foundation", + simpleName = "NSSet", + ) + + "kotlin/collections/List" -> DeclaredTypeName( + moduleName = "Foundation", + simpleName = "NSArray", + ) + + else -> this.kotlinInteropName(moduleName) + } +} + +private fun getDeclaredTypeNameFromNonPrimitive( + typeName: String, + moduleName: String, +) = if (typeName.startsWith("platform/")) { + val withoutCompanion: String = typeName.removeSuffix(".Companion") + val moduleAndClass: List = withoutCompanion.split("/").drop(1) + val module: String = moduleAndClass[0] + val className: String = moduleAndClass[1] + + DeclaredTypeName.typeName( + listOf(module, className).joinToString("."), + ).objcNameToSwift() +} else { + // take type after final slash and generate declared type assuming module name + val simpleName: String = typeName.split("/").last() + DeclaredTypeName( + moduleName = moduleName, + simpleName = simpleName, + ) +} diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/ParameterizedTypeNameExt.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/ParameterizedTypeNameExt.kt new file mode 100644 index 0000000..1b6dc67 --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/ParameterizedTypeNameExt.kt @@ -0,0 +1,53 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import io.outfoxx.swiftpoet.ParameterizedTypeName +import io.outfoxx.swiftpoet.TypeName + +internal fun ParameterizedTypeName.toArrayCaster( + paramName: String, + optional: Boolean = false, +): String = "$paramName: " + .plus( + if (optional) "obj.$paramName != nil ? " else "", + ) + .plus("obj.$paramName as! ") + .plus(this.unwrapOptional().kotlinInteropTypeWithFallback) + .plus(if (optional) " : nil" else "") + +internal fun ParameterizedTypeName.toSetCaster( + paramName: String, + optional: Boolean = false, +): String = "$paramName: " + .plus( + if (optional) "obj.$paramName != nil ? " else "", + ) + .plus("obj.$paramName as! Set<") + .plus( + (this.unwrapOptional() as ParameterizedTypeName).typeArguments[0].kotlinInteropTypeWithFallback + ) + .plus(">") + .plus(if (optional) " : nil" else "") + +internal fun ParameterizedTypeName.toDictionaryCaster( + paramName: String, + optional: Boolean = false, +): String = "$paramName: " + .plus( + if (optional) "obj.$paramName != nil ? " else "", + ) + .plus("obj.$paramName as! [") + .plus(this.typeArguments[0].kotlinInteropTypeWithFallback) + .plus(" : ") + .plus(this.typeArguments[1].kotlinInteropTypeWithFallback) + .plus("]") + .plus(if (optional) " : nil" else "") + +internal fun TypeName.generateInitParameter(paramName: String): String { + return "$paramName: " + .plus( + this.generateSwiftRetrieverForKotlinType( + paramName = "obj.$paramName", + isForTuple = false, + ), + ) +} diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/SwiftTypeMappings.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/SwiftTypeMappings.kt new file mode 100644 index 0000000..06d4613 --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/SwiftTypeMappings.kt @@ -0,0 +1,52 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import io.outfoxx.swiftpoet.ANY_OBJECT +import io.outfoxx.swiftpoet.BOOL +import io.outfoxx.swiftpoet.DeclaredTypeName +import io.outfoxx.swiftpoet.FLOAT32 +import io.outfoxx.swiftpoet.FLOAT64 +import io.outfoxx.swiftpoet.INT16 +import io.outfoxx.swiftpoet.INT32 +import io.outfoxx.swiftpoet.INT64 +import io.outfoxx.swiftpoet.INT8 +import io.outfoxx.swiftpoet.ParameterizedTypeName +import io.outfoxx.swiftpoet.UINT16 +import io.outfoxx.swiftpoet.UINT32 +import io.outfoxx.swiftpoet.UINT64 +import io.outfoxx.swiftpoet.UINT8 + +internal val kotlinToSwiftTypeMap: Map = mapOf( + "kotlin/Any" to ANY_OBJECT, + "kotlin/Boolean" to BOOL, + "kotlin/Byte" to INT8, + "kotlin/Double" to FLOAT64, + "kotlin/Float" to FLOAT32, + "kotlin/Int" to INT32, + "kotlin/Long" to INT64, + "kotlin/Short" to INT16, + "kotlin/UByte" to UINT8, + "kotlin/UInt" to UINT32, + "kotlin/ULong" to UINT64, + "kotlin/UShort" to UINT16, +) + +internal val swiftTypeToKotlinMap: Map = mapOf( + ANY_OBJECT to "kotlin/Any", + BOOL to "kotlin/Boolean", + INT8 to "kotlin/Byte", + FLOAT64 to "kotlin/Double", + FLOAT32 to "kotlin/Float", + INT32 to "kotlin/Int", + INT64 to "kotlin/Long", + INT16 to "kotlin/Short", + UINT8 to "kotlin/UByte", + UINT32 to "kotlin/UInt", + UINT64 to "kotlin/ULong", + UINT16 to "kotlin/UShort", +) + +internal val swiftOptionalTypeToKotlinMap: Map = + swiftTypeToKotlinMap.map { (swiftType, kotlinName) -> + swiftType.wrapOptional() to kotlinName + } + .toMap() diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TupleTypeNameExt.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TupleTypeNameExt.kt new file mode 100644 index 0000000..bd442e4 --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TupleTypeNameExt.kt @@ -0,0 +1,45 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import io.outfoxx.swiftpoet.TupleTypeName + +internal fun TupleTypeName.generateTuple(paramName: String): String = + if (this.types.size == 2) { + "$paramName: (" + .plus( + this.types[0].second + .generateSwiftRetrieverForKotlinType( + "obj.$paramName.first", + ), + ) + .plus(", ") + .plus( + this.types[1].second + .generateSwiftRetrieverForKotlinType( + "obj.$paramName.second", + ), + ) + .plus(")") + } else { + "$paramName: (" + .plus( + this.types[0].second + .generateSwiftRetrieverForKotlinType( + "obj.$paramName.first", + ), + ) + .plus(", ") + .plus( + this.types[1].second + .generateSwiftRetrieverForKotlinType( + "obj.$paramName.second", + ), + ) + .plus(", ") + .plus( + this.types[2].second + .generateSwiftRetrieverForKotlinType( + "obj.$paramName.third", + ), + ) + .plus(")") + } diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TypeNameExt.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TypeNameExt.kt new file mode 100644 index 0000000..fc3909f --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TypeNameExt.kt @@ -0,0 +1,146 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import dev.icerock.moko.kswift.plugin.hasGenerics +import dev.icerock.moko.kswift.plugin.isNullable +import io.outfoxx.swiftpoet.ARRAY +import io.outfoxx.swiftpoet.DICTIONARY +import io.outfoxx.swiftpoet.DeclaredTypeName +import io.outfoxx.swiftpoet.ParameterizedTypeName +import io.outfoxx.swiftpoet.SET +import io.outfoxx.swiftpoet.STRING +import io.outfoxx.swiftpoet.TypeName +import io.outfoxx.swiftpoet.parameterizedBy +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeParameter + +internal val TypeName.isCharacter: Boolean + get() = this.name == "Swift.Character" + +internal val TypeName.kotlinInteropTypeWithFallback: String + get() = ( + this.firstTypeArgument?.kotlinInteropFromSwiftType + ?: this.kotlinInteropFromSwiftType + ?: this.name + ) + +internal fun TypeName.generateKotlinConstructorIfNecessaryForParameter(paramName: String): String { + return when { + this.optional -> this.generateKotlinConstructorIfNecessary(paramName, false) + else -> paramName + } +} + +internal fun TypeName.generateKotlinConstructorIfNecessary( + paramName: String, + isForTuple: Boolean = true, +): String { + val unwrapped = this.firstTypeArgument + return when { + unwrapped != null -> unwrapped.generateKotlinConstructorForNullableType(paramName) + this.optional && !isForTuple -> this.generateKotlinConstructorForNullableType(paramName) + else -> generateKotlinConstructorForNonNullableType(paramName) + }.let { + if (!isForTuple) { + it + } else if (this == STRING) { + it.replace(paramName, "$paramName as NSString") + } else if (unwrapped == STRING) { + it.replace("? $paramName :", "? $paramName! as NSString :") + } else { + it + } + } +} + +internal fun List.stripInnerGenerics(): List = map { typeName -> + (typeName as? ParameterizedTypeName)?.let { + if (it.rawType.simpleName.contains("NS")) it.rawType else null + } ?: typeName +} + +internal fun TypeName.addGenericsAndOptional( + kmType: KmType, + moduleName: String, + namingMode: NamingMode?, + isOuterSwift: Boolean, + typeParameters: List, +): TypeName { + val isSwift = (this as? DeclaredTypeName)?.moduleName == "Swift" + + return if (this is DeclaredTypeName && kmType.hasGenerics) { + val genericTypes = kmType.arguments.getTypes( + moduleName = moduleName, + namingMode = when { + this.simpleName.startsWith("Kotlin") -> NamingMode.KOTLIN_NO_STRING + this == ARRAY || this == SET || this == DICTIONARY -> NamingMode.KOTLIN + namingMode != null -> namingMode + isSwift -> NamingMode.SWIFT + else -> NamingMode.OBJC + }, + isOuterSwift = isSwift, + typeParameters = typeParameters, + ) + this.parameterizedBy(genericTypes) + } else { + this + }.let { + if (kmType.isNullable && isOuterSwift) it.makeOptional() else it + } +} + +private val TypeName.firstTypeArgument: TypeName? + get() = (this as? ParameterizedTypeName)?.typeArguments?.first() + +private val TypeName.kotlinInteropFromSwiftType: String? + get() = swiftTypeToKotlinMap[this]?.replace("kotlin/", "Kotlin") + +private val TypeName.swiftRetriever: String + get() = (if (!this.optional) "!" else "?") + .plus(".") + .plus( + this.name.split(".").last().lowercase() + .replace("?", "") + .let { + when (it) { + "float32" -> "float" + "float64" -> "double" + else -> it + } + }, + ) + .plus("Value") + +internal fun TypeName.generateSwiftRetrieverForKotlinType( + paramName: String, + isForTuple: Boolean = true, +): String = + if (swiftTypeToKotlinMap.containsKey(this) || swiftOptionalTypeToKotlinMap.containsKey(this)) { + paramName + .plus( + if (isForTuple || this.optional) { + this.swiftRetriever + } else { + "" + }, + ) + } else if (this == STRING) { + "$paramName${if (isForTuple) "!" else ""} as String" + } else if (this == STRING.wrapOptional()) { + "$paramName != nil ? $paramName! as String : nil" + } else { + "$paramName${if (!this.optional && isForTuple) "!" else ""}" + } + +private fun TypeName.generateKotlinConstructorForNonNullableType(paramName: String): String { + return this.kotlinInteropFromSwiftType?.plus("(value: $paramName)") + ?: paramName +} + +private fun TypeName.generateKotlinConstructorForNullableType(paramName: String): String { + return "$paramName != nil ? " + .plus( + this.kotlinInteropFromSwiftType?.plus("(value: $paramName!)") + ?: paramName, + ) + .plus(" : nil") +} diff --git a/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TypeSpecBuilder.kt b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TypeSpecBuilder.kt new file mode 100644 index 0000000..ea600c8 --- /dev/null +++ b/kswift-gradle-plugin/src/main/kotlin/dev/icerock/moko/kswift/plugin/feature/associatedenum/TypeSpecBuilder.kt @@ -0,0 +1,140 @@ +package dev.icerock.moko.kswift.plugin.feature.associatedenum + +import dev.icerock.moko.kswift.plugin.context.ClassContext +import dev.icerock.moko.kswift.plugin.context.kLibClasses +import dev.icerock.moko.kswift.plugin.getDeclaredTypeNameWithGenerics +import io.outfoxx.swiftpoet.CodeBlock +import io.outfoxx.swiftpoet.FunctionSpec +import io.outfoxx.swiftpoet.Modifier +import io.outfoxx.swiftpoet.PropertySpec +import io.outfoxx.swiftpoet.TypeName +import io.outfoxx.swiftpoet.TypeSpec +import io.outfoxx.swiftpoet.TypeVariableName +import org.gradle.configurationcache.extensions.capitalized + +@Suppress("LongParameterList") +fun buildTypeSpec( + featureContext: ClassContext, + typeVariables: List, + sealedCases: List, + kotlinFrameworkName: String, + originalClassName: String, +): TypeSpec { + val className: String = originalClassName.replace(".", "").plus("Ks") + + val enumType: TypeSpec = TypeSpec.enumBuilder(className) + .addDoc("selector: ${featureContext.prefixedUniqueId}") + .apply { + typeVariables.forEach { addTypeVariable(it) } + sealedCases.forEach { addEnumCase(it.enumCaseSpec) } + } + .addModifiers(Modifier.PUBLIC) + .addFunction( + buildEnumConstructor( + featureContext = featureContext, + kotlinFrameworkName = kotlinFrameworkName, + sealedCases = sealedCases, + className = className, + originalClassName = originalClassName, + ), + ) + .addProperty( + buildSealedProperty( + featureContext = featureContext, + kotlinFrameworkName = kotlinFrameworkName, + sealedCases = sealedCases, + ), + ) + .build() + return enumType +} + +private fun buildEnumConstructor( + featureContext: ClassContext, + kotlinFrameworkName: String, + sealedCases: List, + className: String, + originalClassName: String, +): FunctionSpec { + return FunctionSpec.builder("init") + .addModifiers(Modifier.PUBLIC) + .addParameter( + label = "_", + name = "obj", + type = featureContext.clazz.getDeclaredTypeNameWithGenerics( + kotlinFrameworkName = kotlinFrameworkName, + classes = featureContext.kLibClasses, + ), + ) + .addCode( + CodeBlock.builder() + .apply { + sealedCases.forEachIndexed { index, enumCase -> + buildString { + if (index != 0) append("} else ") + append("if ") + append(enumCase.initCheck) + append(" {") + append('\n') + }.also { add(it) } + indent() + buildString { + append("self = .") + append(enumCase.name) + append(enumCase.initBlock) + append('\n') + }.also { add(it) } + unindent() + } + add("} else {\n") + indent() + add("fatalError(\"$className not synchronized with $originalClassName class\")\n") + unindent() + add("}\n") + } + .build(), + ) + .build() +} + +private fun buildSealedProperty( + featureContext: ClassContext, + kotlinFrameworkName: String, + sealedCases: List, +): PropertySpec { + val returnType: TypeName = featureContext.clazz.getDeclaredTypeNameWithGenerics( + kotlinFrameworkName = kotlinFrameworkName, + classes = featureContext.kLibClasses, + ) + return PropertySpec.builder("sealed", type = returnType) + .addModifiers(Modifier.PUBLIC) + .getter( + FunctionSpec + .getterBuilder() + .addCode(buildSealedPropertyBody(sealedCases)) + .build(), + ).build() +} + +private fun buildSealedPropertyBody(sealedCases: List): CodeBlock = CodeBlock + .builder().apply { + add("switch self {\n") + sealedCases.forEach { enumCase -> + buildString { + append("case .") + append(enumCase.name) + append(enumCase.caseBlock) + append(":\n") + }.also { add(it) } + indent() + addSealedCaseReturnCode(enumCase) + unindent() + } + add("}\n") + } + .build() + +private fun CodeBlock.Builder.addSealedCaseReturnCode(enumCase: AssociatedEnumCase) { + val parameters = enumCase.swiftToKotlinConstructor + add("return ${enumCase.caseArg}($parameters)\n") +} diff --git a/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/InMemoryAppendable.kt b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/InMemoryAppendable.kt new file mode 100644 index 0000000..1c184af --- /dev/null +++ b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/InMemoryAppendable.kt @@ -0,0 +1,21 @@ +package dev.icerock.moko.kswift.plugin + +class InMemoryAppendable : Appendable { + private val sb = StringBuilder() + override fun append(csq: CharSequence?): java.lang.Appendable { + sb.append(csq) + return this + } + + override fun append(csq: CharSequence?, start: Int, end: Int): java.lang.Appendable { + sb.insert(0, csq, start, end) + return this + } + + override fun append(c: Char): java.lang.Appendable { + sb.append(c) + return this + } + + override fun toString(): String = sb.toString() +} diff --git a/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/SealedToSwiftAssociatedEnumFeatureTest.kt b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/SealedToSwiftAssociatedEnumFeatureTest.kt new file mode 100644 index 0000000..7b90e4b --- /dev/null +++ b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/SealedToSwiftAssociatedEnumFeatureTest.kt @@ -0,0 +1,352 @@ +package dev.icerock.moko.kswift.plugin + +import dev.icerock.moko.kswift.plugin.context.ClassContext +import dev.icerock.moko.kswift.plugin.context.LibraryContext +import dev.icerock.moko.kswift.plugin.feature.Filter +import dev.icerock.moko.kswift.plugin.feature.SealedToSwiftAssociatedEnumFeature +import io.outfoxx.swiftpoet.FileSpec +import kotlinx.metadata.klib.KlibModuleMetadata +import org.jetbrains.kotlin.library.ToolingSingleFileKlibResolveStrategy +import org.jetbrains.kotlin.library.resolveSingleFileKlib +import kotlin.test.Test +import kotlin.test.assertEquals + +@Suppress("LongMethod", "MaxLineLength") +class SealedToSwiftAssociatedEnumFeatureTest { + @Test + fun `associated enum feature should produce type mapped output`() { + val klibPath = this::class.java.classLoader.getResource("associated-enum.klib") + val konanFile = org.jetbrains.kotlin.konan.file.File(klibPath.toURI().path) + // Need to use tooling strategy here since the klib was generated with 1.8 + // kotlinc-native TestingSealed.kt -p library -o associated-enum + // or from project ./gradlew iosArm64MainKlibrary + val library = resolveSingleFileKlib( + libraryFile = konanFile, + strategy = ToolingSingleFileKlibResolveStrategy, + ) + val metadata = KlibModuleMetadata.read(KotlinMetadataLibraryProvider(library)) + val libraryContext = LibraryContext(metadata) + val fileSpecBuilder = FileSpec.builder(moduleName = "module", fileName = "file") + + libraryContext.visit { featureContext -> + (featureContext as? ClassContext)?.let { + SealedToSwiftAssociatedEnumFeature( + featureContext = ClassContext::class, + filter = Filter.Exclude(emptySet()), + ).doProcess( + featureContext = it, + fileSpecBuilder = fileSpecBuilder, + kotlinFrameworkName = "shared", + ) + } + } + + val appendable = InMemoryAppendable() + fileSpecBuilder.build().writeTo(appendable) + val expected = """import Foundation +import shared + +/** + * selector: ClassContext/My_Application:shared/dev/icerock/moko/kswift/plugin/associatedenum/LoadingState */ +public enum LoadingStateKs { + + case errorOnLoad(error: String) + case loading + case other(otherPayload: P) + case success(payload: T?) + + public var sealed: LoadingState { + switch self { + case .errorOnLoad(let error): + return shared.LoadingStateErrorOnLoad(error: error) + case .loading: + return shared.LoadingStateLoading() + case .other(let otherPayload): + return shared.LoadingStateOther(otherPayload: otherPayload) + case .success(let payload): + return shared.LoadingStateSuccess(payload: payload) + } + } + + public init(_ obj: LoadingState) { + if let obj = obj as? shared.LoadingStateErrorOnLoad { + self = .errorOnLoad(error: obj.error as String) + } else if obj is shared.LoadingStateLoading { + self = .loading + } else if let obj = obj as? shared.LoadingStateOther { + self = .other(otherPayload: obj.otherPayload) + } else if let obj = obj as? shared.LoadingStateSuccess { + self = .success(payload: obj.payload) + } else { + fatalError("LoadingStateKs not synchronized with LoadingState class") + } + } + +} + +/** + * selector: ClassContext/My_Application:shared/dev/icerock/moko/kswift/plugin/associatedenum/TestingSealed */ +public enum TestingSealedKs { + + case hasChar(mychar: Character) + case hasEnum(myenum: OwnEnum) + case hasFunction(myfunc: ( + KotlinInt, + [KotlinBoolean], + String + ) -> String) + case hasImmutableList(innerImmutableList: [String]) + case hasImmutableListNullable(innerImmutableListNullable: [String]?) + case hasImmutableListNullableInner(innerImmutableListNullableInner: [String?]?) + case hasInnerList(innerList: [[KotlinBoolean]]) + case hasInnerNullable(innerList: [[KotlinBoolean?]]) + case hasListInt(hasGeneric: [KotlinInt]) + case hasListIntNullable(hasGeneric: [KotlinInt?]) + case hasListOwn(hasGeneric: [OwnClass]) + case hasListString(hasGeneric: [String]) + case hasListStringNullable(hasGeneric: [String?]) + case hasListStringOuterNullable(hasGeneric: [String]?) + case hasMap(map: [String : KotlinInt]) + case hasMapNullableOuter(map: [String : KotlinInt]?) + case hasMapNullableParams(map: [String : KotlinInt?]) + case hasMultipleOwnParams(p1: OwnClass, p2: OwnClass?) + case hasNestedGeneric(nested: [KotlinPair]) + case hasNullableInnerList(innerList: [[KotlinBoolean]?]) + case hasNullableOuterList(innerList: [[KotlinBoolean]]?) + case hasOtherNullables(mystring: String, optstring: String?, myfloat: Float32, optfloat: Float32?, mydouble: Float64, optdouble: Float64?) + case hasOwnClass(ownClass: OwnClass) + case hasOwnClassWithGeneric(ownClassWithGeneric: OwnHasGeneric) + case hasOwnClassWithGenericAny(ownClassWithGeneric: OwnHasGeneric) + case hasOwnClassWithGenericEnum(ownClassWithGeneric: OwnHasGeneric) + case hasOwnClassWithGenericInnerMap(ownClassWithGeneric: OwnHasGeneric) + case hasOwnClassWithGenericInnerPair(ownClassWithGeneric: OwnHasGeneric>) + case hasOwnClassWithGenericInnerSet(ownClassWithGeneric: OwnHasGeneric) + case hasOwnClassWithGenericNested(ownClassWithGeneric: OwnHasGeneric>) + case hasOwnClassWithGenericNullable(ownClassWithGeneric: OwnHasGeneric) + case hasOwnClassWithGenericThrowable(ownClassWithGeneric: OwnHasGeneric) + case hasOwnClassWithGenericWildcard(ownClassWithGeneric: OwnHasGeneric) + case hasPairBool(pair: (Bool, Bool?)) + case hasPairFloat(pair: (Float32, Float32?)) + case hasPairGeneric(pair: (UInt8, OwnClass?)) + case hasPairString(pair: (String, String?)) + case hasSet(myset: Set) + case hasSetNullableInt(myset: Set) + case hasSetNullableOuter(myset: Set?) + case hasSetString(myset: Set) + case hasSetStringNullable(myset: Set) + case hasSomeNullables(myint: Int32, myintopt: Int32?, uintnotoptional: UInt32, uintoptional: UInt32?, mybool: Bool, optbool: Bool?) + case hasThrowable(throwable: KotlinThrowable) + case hasTriple(triple: (Float32, Int32?, OwnClass)) + case justAnObj + + public var sealed: TestingSealed { + switch self { + case .hasChar(let mychar): + return shared.HasChar(mychar: mychar.utf16.first!) + case .hasEnum(let myenum): + return shared.HasEnum(myenum: myenum) + case .hasFunction(let myfunc): + return shared.HasFunction(myfunc: myfunc) + case .hasImmutableList(let innerImmutableList): + return shared.HasImmutableList(innerImmutableList: innerImmutableList) + case .hasImmutableListNullable(let innerImmutableListNullable): + return shared.HasImmutableListNullable(innerImmutableListNullable: innerImmutableListNullable != nil ? innerImmutableListNullable : nil) + case .hasImmutableListNullableInner(let innerImmutableListNullableInner): + return shared.HasImmutableListNullableInner(innerImmutableListNullableInner: innerImmutableListNullableInner != nil ? innerImmutableListNullableInner : nil) + case .hasInnerList(let innerList): + return shared.HasInnerList(innerList: innerList) + case .hasInnerNullable(let innerList): + return shared.HasInnerNullable(innerList: innerList) + case .hasListInt(let hasGeneric): + return shared.HasListInt(hasGeneric: hasGeneric) + case .hasListIntNullable(let hasGeneric): + return shared.HasListIntNullable(hasGeneric: hasGeneric) + case .hasListOwn(let hasGeneric): + return shared.HasListOwn(hasGeneric: hasGeneric) + case .hasListString(let hasGeneric): + return shared.HasListString(hasGeneric: hasGeneric) + case .hasListStringNullable(let hasGeneric): + return shared.HasListStringNullable(hasGeneric: hasGeneric) + case .hasListStringOuterNullable(let hasGeneric): + return shared.HasListStringOuterNullable(hasGeneric: hasGeneric != nil ? hasGeneric : nil) + case .hasMap(let map): + return shared.HasMap(map: map) + case .hasMapNullableOuter(let map): + return shared.HasMapNullableOuter(map: map != nil ? map : nil) + case .hasMapNullableParams(let map): + return shared.HasMapNullableParams(map: map) + case .hasMultipleOwnParams(let p1, let p2): + return shared.HasMultipleOwnParams(p1: p1, p2: p2 != nil ? p2 : nil) + case .hasNestedGeneric(let nested): + return shared.HasNestedGeneric(nested: nested) + case .hasNullableInnerList(let innerList): + return shared.HasNullableInnerList(innerList: innerList) + case .hasNullableOuterList(let innerList): + return shared.HasNullableOuterList(innerList: innerList != nil ? innerList : nil) + case .hasOtherNullables(let mystring, let optstring, let myfloat, let optfloat, let mydouble, let optdouble): + return shared.HasOtherNullables(mystring: mystring, optstring: optstring != nil ? optstring : nil, myfloat: myfloat, optfloat: optfloat != nil ? KotlinFloat(value: optfloat!) : nil, mydouble: mydouble, optdouble: optdouble != nil ? KotlinDouble(value: optdouble!) : nil) + case .hasOwnClass(let ownClass): + return shared.HasOwnClass(ownClass: ownClass) + case .hasOwnClassWithGeneric(let ownClassWithGeneric): + return shared.HasOwnClassWithGeneric(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericAny(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericAny(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericEnum(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericEnum(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericInnerMap(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericInnerMap(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericInnerPair(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericInnerPair(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericInnerSet(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericInnerSet(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericNested(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericNested(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericNullable(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericNullable(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericThrowable(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericThrowable(ownClassWithGeneric: ownClassWithGeneric) + case .hasOwnClassWithGenericWildcard(let ownClassWithGeneric): + return shared.HasOwnClassWithGenericWildcard(ownClassWithGeneric: ownClassWithGeneric) + case .hasPairBool(let pair): + return shared.HasPairBool(pair: KotlinPair(first: KotlinBoolean(value: pair.0), second: pair.1 != nil ? KotlinBoolean(value: pair.1!) : nil)) + case .hasPairFloat(let pair): + return shared.HasPairFloat(pair: KotlinPair(first: KotlinFloat(value: pair.0), second: pair.1 != nil ? KotlinFloat(value: pair.1!) : nil)) + case .hasPairGeneric(let pair): + return shared.HasPairGeneric(pair: KotlinPair(first: KotlinUByte(value: pair.0), second: pair.1 != nil ? pair.1 : nil)) + case .hasPairString(let pair): + return shared.HasPairString(pair: KotlinPair(first: pair.0 as NSString, second: pair.1 != nil ? pair.1! as NSString : nil)) + case .hasSet(let myset): + return shared.HasSet(myset: myset) + case .hasSetNullableInt(let myset): + return shared.HasSetNullableInt(myset: myset) + case .hasSetNullableOuter(let myset): + return shared.HasSetNullableOuter(myset: myset != nil ? myset : nil) + case .hasSetString(let myset): + return shared.HasSetString(myset: myset) + case .hasSetStringNullable(let myset): + return shared.HasSetStringNullable(myset: myset) + case .hasSomeNullables(let myint, let myintopt, let uintnotoptional, let uintoptional, let mybool, let optbool): + return shared.HasSomeNullables(myint: myint, myintopt: myintopt != nil ? KotlinInt(value: myintopt!) : nil, uintnotoptional: uintnotoptional, uintoptional: uintoptional != nil ? KotlinUInt(value: uintoptional!) : nil, mybool: mybool, optbool: optbool != nil ? KotlinBoolean(value: optbool!) : nil) + case .hasThrowable(let throwable): + return shared.HasThrowable(throwable: throwable) + case .hasTriple(let triple): + return shared.HasTriple(triple: KotlinTriple(first: KotlinFloat(value: triple.0), second: triple.1 != nil ? KotlinInt(value: triple.1!) : nil, third: triple.2)) + case .justAnObj: + return shared.JustAnObj() + } + } + + public init(_ obj: TestingSealed) { + if let obj = obj as? shared.HasChar { + self = .hasChar(mychar: Character(UnicodeScalar(obj.mychar)!)) + } else if let obj = obj as? shared.HasEnum { + self = .hasEnum(myenum: obj.myenum) + } else if let obj = obj as? shared.HasFunction { + self = .hasFunction(myfunc: obj.myfunc) + } else if let obj = obj as? shared.HasImmutableList { + self = .hasImmutableList(innerImmutableList: obj.innerImmutableList as! [Swift.String]) + } else if let obj = obj as? shared.HasImmutableListNullable { + self = .hasImmutableListNullable(innerImmutableListNullable: obj.innerImmutableListNullable != nil ? obj.innerImmutableListNullable as! [Swift.String] : nil) + } else if let obj = obj as? shared.HasImmutableListNullableInner { + self = .hasImmutableListNullableInner(innerImmutableListNullableInner: obj.innerImmutableListNullableInner != nil ? obj.innerImmutableListNullableInner as! [Swift.String?] : nil) + } else if let obj = obj as? shared.HasInnerList { + self = .hasInnerList(innerList: obj.innerList as! [[shared.KotlinBoolean]]) + } else if let obj = obj as? shared.HasInnerNullable { + self = .hasInnerNullable(innerList: obj.innerList as! [[shared.KotlinBoolean?]]) + } else if let obj = obj as? shared.HasListInt { + self = .hasListInt(hasGeneric: obj.hasGeneric as! [shared.KotlinInt]) + } else if let obj = obj as? shared.HasListIntNullable { + self = .hasListIntNullable(hasGeneric: obj.hasGeneric as! [shared.KotlinInt?]) + } else if let obj = obj as? shared.HasListOwn { + self = .hasListOwn(hasGeneric: obj.hasGeneric as! [shared.OwnClass]) + } else if let obj = obj as? shared.HasListString { + self = .hasListString(hasGeneric: obj.hasGeneric as! [Swift.String]) + } else if let obj = obj as? shared.HasListStringNullable { + self = .hasListStringNullable(hasGeneric: obj.hasGeneric as! [Swift.String?]) + } else if let obj = obj as? shared.HasListStringOuterNullable { + self = .hasListStringOuterNullable(hasGeneric: obj.hasGeneric != nil ? obj.hasGeneric as! [Swift.String] : nil) + } else if let obj = obj as? shared.HasMap { + self = .hasMap(map: obj.map as! [Swift.String : shared.KotlinInt]) + } else if let obj = obj as? shared.HasMapNullableOuter { + self = .hasMapNullableOuter(map: obj.map != nil ? obj.map as! [Swift.String : shared.KotlinInt] : nil) + } else if let obj = obj as? shared.HasMapNullableParams { + self = .hasMapNullableParams(map: obj.map as! [Swift.String : shared.KotlinInt?]) + } else if let obj = obj as? shared.HasMultipleOwnParams { + self = .hasMultipleOwnParams(p1: obj.p1, + p2: obj.p2) + } else if let obj = obj as? shared.HasNestedGeneric { + self = .hasNestedGeneric(nested: obj.nested as! [shared.KotlinPair]) + } else if let obj = obj as? shared.HasNullableInnerList { + self = .hasNullableInnerList(innerList: obj.innerList as! [[shared.KotlinBoolean]?]) + } else if let obj = obj as? shared.HasNullableOuterList { + self = .hasNullableOuterList(innerList: obj.innerList != nil ? obj.innerList as! [[shared.KotlinBoolean]] : nil) + } else if let obj = obj as? shared.HasOtherNullables { + self = .hasOtherNullables(mystring: obj.mystring as String, + optstring: obj.optstring != nil ? obj.optstring! as String : nil, + myfloat: obj.myfloat, + optfloat: obj.optfloat?.floatValue, + mydouble: obj.mydouble, + optdouble: obj.optdouble?.doubleValue) + } else if let obj = obj as? shared.HasOwnClass { + self = .hasOwnClass(ownClass: obj.ownClass) + } else if let obj = obj as? shared.HasOwnClassWithGeneric { + self = .hasOwnClassWithGeneric(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericAny { + self = .hasOwnClassWithGenericAny(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericEnum { + self = .hasOwnClassWithGenericEnum(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericInnerMap { + self = .hasOwnClassWithGenericInnerMap(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericInnerPair { + self = .hasOwnClassWithGenericInnerPair(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericInnerSet { + self = .hasOwnClassWithGenericInnerSet(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericNested { + self = .hasOwnClassWithGenericNested(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericNullable { + self = .hasOwnClassWithGenericNullable(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericThrowable { + self = .hasOwnClassWithGenericThrowable(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasOwnClassWithGenericWildcard { + self = .hasOwnClassWithGenericWildcard(ownClassWithGeneric: obj.ownClassWithGeneric) + } else if let obj = obj as? shared.HasPairBool { + self = .hasPairBool(pair: (obj.pair.first!.boolValue, obj.pair.second?.boolValue)) + } else if let obj = obj as? shared.HasPairFloat { + self = .hasPairFloat(pair: (obj.pair.first!.floatValue, obj.pair.second?.floatValue)) + } else if let obj = obj as? shared.HasPairGeneric { + self = .hasPairGeneric(pair: (obj.pair.first!.uint8Value, obj.pair.second)) + } else if let obj = obj as? shared.HasPairString { + self = .hasPairString(pair: (obj.pair.first! as String, obj.pair.second != nil ? obj.pair.second! as String : nil)) + } else if let obj = obj as? shared.HasSet { + self = .hasSet(myset: obj.myset as! Set) + } else if let obj = obj as? shared.HasSetNullableInt { + self = .hasSetNullableInt(myset: obj.myset as! Set) + } else if let obj = obj as? shared.HasSetNullableOuter { + self = .hasSetNullableOuter(myset: obj.myset != nil ? obj.myset as! Set : nil) + } else if let obj = obj as? shared.HasSetString { + self = .hasSetString(myset: obj.myset as! Set) + } else if let obj = obj as? shared.HasSetStringNullable { + self = .hasSetStringNullable(myset: obj.myset as! Set) + } else if let obj = obj as? shared.HasSomeNullables { + self = .hasSomeNullables(myint: obj.myint, + myintopt: obj.myintopt?.int32Value, + uintnotoptional: obj.uintnotoptional, + uintoptional: obj.uintoptional?.uint32Value, + mybool: obj.mybool, + optbool: obj.optbool?.boolValue) + } else if let obj = obj as? shared.HasThrowable { + self = .hasThrowable(throwable: obj.throwable) + } else if let obj = obj as? shared.HasTriple { + self = .hasTriple(triple: (obj.triple.first!.floatValue, obj.triple.second?.int32Value, obj.triple.third!)) + } else if obj is shared.JustAnObj { + self = .justAnObj + } else { + fatalError("TestingSealedKs not synchronized with TestingSealed class") + } + } + +} +""" + assertEquals(expected, appendable.toString()) + } +} diff --git a/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/associatedenum/TestInstances.kt b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/associatedenum/TestInstances.kt new file mode 100644 index 0000000..d994146 --- /dev/null +++ b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/associatedenum/TestInstances.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.kswift.plugin.associatedenum + +object TestInstances { + val hasChar = HasChar('a') + val hasEnum = HasEnum(OwnEnum.A) + val hasFunction = HasFunction { i, l, s -> "$i $l $s" } + val hasNullableListNull = HasNullableOuterList(null) + val hasNullableList = HasNullableOuterList(emptyList()) + val hasInnerList = HasInnerList(listOf(listOf(true, false), listOf(true))) + val hasInnerNullable = HasInnerNullable(listOf(listOf(true, null), listOf(null))) +} diff --git a/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/associatedenum/TestingSealed.kt b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/associatedenum/TestingSealed.kt new file mode 100644 index 0000000..c07028d --- /dev/null +++ b/kswift-gradle-plugin/src/test/kotlin/dev/icerock/moko/kswift/plugin/associatedenum/TestingSealed.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.kswift.plugin.associatedenum + +import kotlinx.collections.immutable.ImmutableList + +@Suppress("LongLine") +sealed interface TestingSealed +data class HasChar(val mychar: Char) : TestingSealed +data class HasEnum(val myenum: OwnEnum) : TestingSealed +data class HasFunction(val myfunc: (Int, List, String) -> String) : TestingSealed +data class HasNullableOuterList(val innerList: List>?) : TestingSealed +data class HasImmutableList(val innerList: ImmutableList) : TestingSealed +data class HasInnerList(val innerList: List>) : TestingSealed +data class HasNullableInnerList(val innerList: List?>) : TestingSealed +data class HasInnerNullable(val innerList: List>) : TestingSealed +data class HasListInt(val hasGeneric: List) : TestingSealed +data class HasListIntNullable(val hasGeneric: List) : TestingSealed +data class HasListString(val hasGeneric: List) : TestingSealed +data class HasListStringOuterNullable(val hasGeneric: List?) : TestingSealed +data class HasListStringNullable(val hasGeneric: List) : TestingSealed +data class HasListOwn(val hasGeneric: List) : TestingSealed +data class HasMap(val map: Map) : TestingSealed +data class HasMapNullableParams(val map: Map) : TestingSealed +data class HasMapNullableOuter(val map: Map?) : TestingSealed +data class HasMultipleOwnParams(val p1: OwnClass, val p2: OwnClass?) : TestingSealed +data class HasNestedGeneric(val nested: List>) : TestingSealed +data class HasSomeNullables( + val myint: Int, + val myintopt: Int?, + val uintnotoptional: UInt, + val uintoptional: UInt?, + val mybool: Boolean, + val optbool: Boolean?, +) : TestingSealed +data class HasOtherNullables( + val mystring: String, + val optstring: String?, + val myfloat: Float, + val optfloat: Float?, + val mydouble: Double, + val optdouble: Double?, +) : TestingSealed +data class HasOwnClass(val ownClass: OwnClass) : TestingSealed +data class HasOwnClassWithGeneric(val ownClassWithGeneric: OwnHasGeneric) : TestingSealed +data class HasOwnClassWithGenericAny(val ownClassWithGeneric: OwnHasGeneric) : TestingSealed +data class HasOwnClassWithGenericEnum(val ownClassWithGeneric: OwnHasGeneric) : TestingSealed +data class HasOwnClassWithGenericInnerMap(val ownClassWithGeneric: OwnHasGeneric>) : + TestingSealed +data class HasOwnClassWithGenericInnerPair( + val ownClassWithGeneric: OwnHasGeneric>, +) : TestingSealed +data class HasOwnClassWithGenericInnerSet(val ownClassWithGeneric: OwnHasGeneric>) : + TestingSealed +data class HasOwnClassWithGenericNested(val ownClassWithGeneric: OwnHasGeneric>) : + TestingSealed +data class HasOwnClassWithGenericNullable(val ownClassWithGeneric: OwnHasGeneric) : + TestingSealed +data class HasOwnClassWithGenericThrowable(val ownClassWithGeneric: OwnHasGeneric) : + TestingSealed +data class HasOwnClassWithGenericWildcard(val ownClassWithGeneric: OwnHasGeneric<*>) : TestingSealed +data class HasPairGeneric(val pair: Pair) : TestingSealed +data class HasPairBool(val pair: Pair) : TestingSealed +data class HasPairString(val pair: Pair) : TestingSealed +data class HasPairFloat(val pair: Pair) : TestingSealed +data class HasSet(val myset: Set) : TestingSealed +data class HasSetNullableOuter(val myset: Set?) : TestingSealed +data class HasSetNullableInt(val myset: Set) : TestingSealed +data class HasSetString(val myset: Set) : TestingSealed +data class HasSetStringNullable(val myset: Set) : TestingSealed +data class HasThrowable(val throwable: Throwable) : TestingSealed +data class HasTriple(val triple: Triple) : TestingSealed { + val anotherProp = "anotherProp" +} +object JustAnObj : TestingSealed + +data class OwnClass(val whatever: String) + +class OwnHasGeneric { + val value: T? = null +} + +enum class OwnEnum { + A, B, C +} + +sealed class LoadingState { + class Loading : LoadingState() + data class Success(val payload: T) : LoadingState() + data class Other(val otherPayload: P) : LoadingState() + data class ErrorOnLoad(val error: String) : LoadingState() +} diff --git a/kswift-gradle-plugin/src/test/resources/associated-enum.klib b/kswift-gradle-plugin/src/test/resources/associated-enum.klib new file mode 100644 index 0000000..18ed993 Binary files /dev/null and b/kswift-gradle-plugin/src/test/resources/associated-enum.klib differ diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 4a0f56a..e765553 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -11,7 +11,7 @@ plugins { kswift { install(dev.icerock.moko.kswift.plugin.feature.PlatformExtensionFunctionsFeature) - install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) + install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftAssociatedEnumFeature) excludeLibrary("kotlinx-coroutines-core")