Skip to content

Commit

Permalink
Merge pull request #78 from ema987/feat/dataClassCopyGenerationFeature
Browse files Browse the repository at this point in the history
Add feature to generate improved copy method
  • Loading branch information
Alex009 authored Jan 21, 2024
2 parents d962c84 + f8d8c72 commit 114be2f
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 49 deletions.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mokoResourcesVersion = "0.16.2"
kotlinxMetadataKLibVersion = "0.0.1"

kotlinPoetVersion = "1.6.0"
swiftPoetVersion = "1.3.1"
swiftPoetVersion = "1.5.0"

[libraries]
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import io.outfoxx.swiftpoet.parameterizedBy
import kotlinx.metadata.ClassName
import kotlinx.metadata.Flag
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmConstructor

fun KmClass.isDataClass(): Boolean {
return Flag.Class.IS_DATA(flags)
}

fun KmClass.getPrimaryConstructor(): KmConstructor = constructors.getPrimaryConstructor()

fun KmClass.buildTypeVariableNames(
kotlinFrameworkName: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.kswift.plugin

import kotlinx.metadata.Flag
import kotlinx.metadata.KmConstructor

fun KmConstructor.isPrimary(): Boolean = Flag.Constructor.IS_SECONDARY(flags).not()

fun List<KmConstructor>.getPrimaryConstructor(): KmConstructor = single { constructor -> constructor.isPrimary() }
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
package dev.icerock.moko.kswift.plugin

import io.outfoxx.swiftpoet.ANY_OBJECT
import io.outfoxx.swiftpoet.ARRAY
import io.outfoxx.swiftpoet.BOOL
import io.outfoxx.swiftpoet.DICTIONARY
import io.outfoxx.swiftpoet.DeclaredTypeName
import io.outfoxx.swiftpoet.FunctionTypeName
import io.outfoxx.swiftpoet.INT32
import io.outfoxx.swiftpoet.ParameterSpec
import io.outfoxx.swiftpoet.SET
import io.outfoxx.swiftpoet.STRING
import io.outfoxx.swiftpoet.TypeName
import io.outfoxx.swiftpoet.TypeVariableName
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
import kotlinx.metadata.KmTypeProjection

@Suppress("ReturnCount")
fun KmType.toTypeName(
Expand All @@ -35,12 +41,12 @@ fun KmType.toTypeName(
} else throw IllegalArgumentException("can't read type parameter $this without type variables list")
}
is KmClassifier.TypeAlias -> {
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics)
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics, arguments)
?: throw IllegalArgumentException("can't read type alias $this")
}
is KmClassifier.Class -> {
val name: TypeName? =
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics)
classifier.name.kotlinTypeNameToSwift(moduleName, isUsedInGenerics, arguments)
return name ?: kotlinTypeToTypeName(
moduleName,
classifier.name,
Expand All @@ -51,14 +57,23 @@ fun KmType.toTypeName(
}
}

fun String.kotlinTypeNameToSwift(moduleName: String, isUsedInGenerics: Boolean): TypeName? {
@Suppress("LongMethod", "ComplexMethod")
fun String.kotlinTypeNameToSwift(
moduleName: String,
isUsedInGenerics: Boolean,
arguments: MutableList<KmTypeProjection>
): TypeName? {
return when (this) {
"kotlin/String" -> if (isUsedInGenerics) {
DeclaredTypeName(moduleName = "Foundation", simpleName = "NSString")
} else {
STRING
}
"kotlin/Int" -> DeclaredTypeName(moduleName = "Foundation", simpleName = "NSNumber")
"kotlin/Int" -> if (isUsedInGenerics) {
DeclaredTypeName(moduleName = moduleName, simpleName = "KotlinInt")
} else {
INT32
}
"kotlin/Boolean" -> if (isUsedInGenerics) {
DeclaredTypeName(moduleName = moduleName, simpleName = "KotlinBoolean")
} else {
Expand All @@ -67,6 +82,44 @@ fun String.kotlinTypeNameToSwift(moduleName: String, isUsedInGenerics: Boolean):
"kotlin/ULong" -> UINT64
"kotlin/Unit" -> VOID
"kotlin/Any" -> ANY_OBJECT
"kotlin/collections/List" -> {
arguments.first().type?.run {
DeclaredTypeName.typeName(ARRAY.name).parameterizedBy(
this.toTypeName(
moduleName,
isUsedInGenerics = this.shouldUseKotlinTypeWhenHandlingCollections()
)
)
}
}
"kotlin/collections/Set" -> {
arguments.first().type?.run {
DeclaredTypeName.typeName(SET.name).parameterizedBy(
this.toTypeName(
moduleName,
isUsedInGenerics = this.shouldUseKotlinTypeWhenHandlingCollections()
)
)
}
}
"kotlin/collections/Map" -> {
val firstArgumentType = arguments.first().type
val secondArgumentType = arguments[1].type
if (firstArgumentType != null && secondArgumentType != null) {
DeclaredTypeName.typeName(DICTIONARY.name).parameterizedBy(
firstArgumentType.toTypeName(
moduleName,
isUsedInGenerics = firstArgumentType.shouldUseKotlinTypeWhenHandlingCollections()
),
secondArgumentType.toTypeName(
moduleName,
isUsedInGenerics = secondArgumentType.shouldUseKotlinTypeWhenHandlingCollections()
)
)
} else {
null
}
}
else -> {
if (this.startsWith("platform/")) {
val withoutCompanion: String = this.removeSuffix(".Companion")
Expand Down Expand Up @@ -142,3 +195,13 @@ fun DeclaredTypeName.objcNameToSwift(): DeclaredTypeName {
else -> this
}
}

fun KmType.shouldUseKotlinTypeWhenHandlingOptionalType(): Boolean =
if (classifier.toString().contains("kotlin/String")) {
false
} else {
Flag.Type.IS_NULLABLE(flags)
}

fun KmType.shouldUseKotlinTypeWhenHandlingCollections(): Boolean =
!classifier.toString().contains("kotlin/String")
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

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.getDeclaredTypeNameWithGenerics
import dev.icerock.moko.kswift.plugin.getPrimaryConstructor
import dev.icerock.moko.kswift.plugin.isDataClass
import dev.icerock.moko.kswift.plugin.shouldUseKotlinTypeWhenHandlingOptionalType
import dev.icerock.moko.kswift.plugin.toTypeName
import io.outfoxx.swiftpoet.CodeBlock
import io.outfoxx.swiftpoet.ExtensionSpec
import io.outfoxx.swiftpoet.FunctionSpec
import io.outfoxx.swiftpoet.FunctionTypeName
import io.outfoxx.swiftpoet.ParameterSpec
import io.outfoxx.swiftpoet.TypeSpec
import io.outfoxx.swiftpoet.TypeVariableName
import kotlinx.metadata.Flag
import kotlinx.metadata.KmClass
import kotlin.reflect.KClass

/**
* Creates an improved copy method for data classes
*/

class DataClassCopyFeature(
override val featureContext: KClass<ClassContext>,
override val filter: Filter<ClassContext>
) : ProcessorFeature<ClassContext>() {

@Suppress("ReturnCount")
override fun doProcess(featureContext: ClassContext, processorContext: ProcessorContext) {

val kotlinFrameworkName: String = processorContext.framework.baseName
val kmClass: KmClass = featureContext.clazz

if (Flag.IS_PUBLIC(kmClass.flags).not()) return

if (kmClass.isDataClass().not()) return

val typeVariables: List<TypeVariableName> =
kmClass.buildTypeVariableNames(kotlinFrameworkName)

// doesn't support generic data classes right now
if (typeVariables.isNotEmpty()) return

val functionReturnType = kmClass.getDeclaredTypeNameWithGenerics(
kotlinFrameworkName,
featureContext.kLibClasses
)

val constructorParameters = kmClass.getPrimaryConstructor().valueParameters

val functionParameters = constructorParameters.mapNotNull { parameter ->

val parameterType = parameter.type ?: return@mapNotNull null

ParameterSpec.builder(
parameterName = parameter.name,
type = FunctionTypeName.get(
parameters = emptyList(),
returnType = parameterType.toTypeName(
moduleName = kotlinFrameworkName,
isUsedInGenerics = parameterType.shouldUseKotlinTypeWhenHandlingOptionalType()
).run {
if (Flag.Type.IS_NULLABLE(parameterType.flags)) {
makeOptional()
} else {
this
}
}
).makeOptional(),
).defaultValue(
codeBlock = CodeBlock.builder().add("nil").build()
).build()
}
val functionBody = constructorParameters.joinToString(separator = ",") { property ->
property.name.run {
"$this: ($this != nil) ? $this!() : self.$this"
}
}

val copyFunction = FunctionSpec.builder("copy")
.addParameters(functionParameters)
.returns(functionReturnType)
.addCode(
CodeBlock.builder()
.addStatement("return %T($functionBody)", functionReturnType)
.build()
)
.build()

val extensionSpec =
ExtensionSpec.builder(TypeSpec.classBuilder(functionReturnType.name).build())
.addFunction(copyFunction)
.addDoc("selector: ${featureContext.prefixedUniqueId}")
.build()

processorContext.fileSpecBuilder.addExtension(extensionSpec)
}

class Config : BaseConfig<ClassContext> {
override var filter: Filter<ClassContext> = Filter.Exclude(emptySet())
}

companion object : Factory<ClassContext, DataClassCopyFeature, Config> {
override fun create(block: Config.() -> Unit): DataClassCopyFeature {
val config = Config().apply(block)
return DataClassCopyFeature(featureContext, config.filter)
}

override val featureContext: KClass<ClassContext> = ClassContext::class

@JvmStatic
override val factory = Companion
}
}
Loading

0 comments on commit 114be2f

Please sign in to comment.