diff --git a/CHANGELOG.md b/CHANGELOG.md index 1485e631d43..d24e17eb4df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## 0.21.1 +* `KSP`: + * Module has been initialized + * `Generator`: + * Module has been initialized + * `Sealed`: + * Module has been initialized + ## 0.21.0 **THIS UPDATE CONTAINS BREAKING CHANGES IN `safely*`-ORIENTED FUNCTIONS** diff --git a/ksp/generator/build.gradle b/ksp/generator/build.gradle new file mode 100644 index 00000000000..2f2ffb16628 --- /dev/null +++ b/ksp/generator/build.gradle @@ -0,0 +1,20 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +apply from: "$publishJvmOnlyPath" + +repositories { + mavenCentral() +} + +dependencies { + api project(":micro_utils.common") + api libs.kotlin.poet + api libs.ksp +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} diff --git a/ksp/generator/src/main/kotlin/FilesWorkaround.kt b/ksp/generator/src/main/kotlin/FilesWorkaround.kt new file mode 100644 index 00000000000..17bbabc22b4 --- /dev/null +++ b/ksp/generator/src/main/kotlin/FilesWorkaround.kt @@ -0,0 +1,49 @@ +package dev.inmo.micro_ksp.generator + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.squareup.kotlinpoet.FileSpec +import java.io.File + +fun KSClassDeclaration.writeFile( + prefix: String = "", + suffix: String = "", + relatedPath: String = "", + force: Boolean = false, + fileSpecBuilder: () -> FileSpec +) { + val containingFile = containingFile!! + File( + File( + File(containingFile.filePath).parent, + relatedPath + ), + "$prefix${simpleName.asString()}$suffix.kt" + ).takeIf { force || !it.exists() } ?.apply { + parentFile.mkdirs() + writer().use { writer -> + fileSpecBuilder().writeTo(writer) + } + } +} + +fun KSFile.writeFile( + prefix: String = "", + suffix: String = "", + relatedPath: String = "", + force: Boolean = false, + fileSpecBuilder: () -> FileSpec +) { + File( + File( + File(filePath).parent, + relatedPath + ), + "$prefix${fileName.dropLastWhile { it != '.' }.removeSuffix(".")}$suffix.kt" + ).takeIf { force || !it.exists() } ?.apply { + parentFile.mkdirs() + writer().use { writer -> + fileSpecBuilder().writeTo(writer) + } + } +} \ No newline at end of file diff --git a/ksp/sealed/build.gradle b/ksp/sealed/build.gradle new file mode 100644 index 00000000000..d425197852e --- /dev/null +++ b/ksp/sealed/build.gradle @@ -0,0 +1,7 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" diff --git a/ksp/sealed/generator/build.gradle b/ksp/sealed/generator/build.gradle new file mode 100644 index 00000000000..026e94b30ca --- /dev/null +++ b/ksp/sealed/generator/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +apply from: "$publishJvmOnlyPath" + +repositories { + mavenCentral() +} + +dependencies { + api project(":micro_utils.ksp.generator") + api project(":micro_utils.ksp.sealed") + api libs.kotlin.poet + api libs.ksp +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} diff --git a/ksp/sealed/generator/src/main/kotlin/Processor.kt b/ksp/sealed/generator/src/main/kotlin/Processor.kt new file mode 100644 index 00000000000..e8816e7bb14 --- /dev/null +++ b/ksp/sealed/generator/src/main/kotlin/Processor.kt @@ -0,0 +1,110 @@ +package dev.inmo.micro_utils.ksp.sealed.generator + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.* +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.ksp.toClassName +import dev.inmo.micro_ksp.generator.writeFile +import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround + +class Processor( + private val codeGenerator: CodeGenerator +) : SymbolProcessor { + private fun KSClassDeclaration.resolveSubclasses(): List { + return (getSealedSubclasses().flatMap { + it.resolveSubclasses() + }.ifEmpty { + sequenceOf(this) + }).toList() + } + + @OptIn(KspExperimental::class) + private fun FileSpec.Builder.generateSealedWorkaround( + ksClassDeclaration: KSClassDeclaration, + resolver: Resolver + ) { + val subClasses = ksClassDeclaration.resolveSubclasses().distinct() + val subClassesNames = subClasses.filter { + when (it.classKind) { + ClassKind.ENUM_ENTRY, + ClassKind.OBJECT -> true + ClassKind.INTERFACE, + ClassKind.CLASS, + ClassKind.ENUM_CLASS, + ClassKind.ANNOTATION_CLASS -> false + } + }.filter { + it.getAnnotationsByType(GenerateSealedWorkaround.Exclude::class).count() == 0 + }.sortedBy { + (it.getAnnotationsByType(GenerateSealedWorkaround.Order::class).firstOrNull()) ?.order ?: 0 + }.map { + it.toClassName() + } + val className = ksClassDeclaration.toClassName() + val setType = Set::class.asTypeName().parameterizedBy( + ksClassDeclaration.toClassName() + ) + addProperty( + PropertySpec.builder( + "values", + setType + ).apply { + modifiers.add( + KModifier.PRIVATE + ) + initializer( + CodeBlock.of( + """setOf(${subClassesNames.joinToString(",\n") {it.simpleNames.joinToString(".")}})""" + ) + ) + }.build() + ) + addFunction( + FunSpec.builder("values").apply { + receiver(ClassName(className.packageName, *className.simpleNames.toTypedArray(), "Companion")) + returns(setType) + addCode( + CodeBlock.of( + """return values""" + ) + ) + }.build() + ) + } + + @OptIn(KspExperimental::class) + override fun process(resolver: Resolver): List { + (resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance().forEach { + val prefix = it.getAnnotationsByType(GenerateSealedWorkaround::class).first().prefix + it.writeFile(prefix = prefix, suffix = "SealedWorkaround") { + FileSpec.builder( + it.packageName.asString(), + "${it.simpleName.getShortName()}SealedWorkaround" + ).apply { + addFileComment( + """ + THIS CODE HAVE BEEN GENERATED AUTOMATICALLY + TO REGENERATE IT JUST DELETE FILE + ORIGINAL FILE: ${it.containingFile ?.fileName} + """.trimIndent() + ) + generateSealedWorkaround(it, resolver) + }.build() + } + } + + return emptyList() + } +} diff --git a/ksp/sealed/generator/src/main/kotlin/Provider.kt b/ksp/sealed/generator/src/main/kotlin/Provider.kt new file mode 100644 index 00000000000..aba4854597c --- /dev/null +++ b/ksp/sealed/generator/src/main/kotlin/Provider.kt @@ -0,0 +1,11 @@ +package dev.inmo.micro_utils.ksp.sealed.generator + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = Processor( + environment.codeGenerator + ) +} diff --git a/ksp/sealed/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/ksp/sealed/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..8281f5b4646 --- /dev/null +++ b/ksp/sealed/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +dev.inmo.micro_utils.ksp.sealed.generator.Provider diff --git a/ksp/sealed/generator/test/build.gradle b/ksp/sealed/generator/test/build.gradle new file mode 100644 index 00000000000..4f49ec90f3c --- /dev/null +++ b/ksp/sealed/generator/test/build.gradle @@ -0,0 +1,27 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + id "com.google.devtools.ksp" +} + +apply from: "$mppProjectWithSerializationPresetPath" + + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":micro_utils.ksp.sealed") + } + } + } +} + + +dependencies { + add("kspCommonMainMetadata", project(":micro_utils.ksp.sealed.generator")) +} + +ksp { +} diff --git a/ksp/sealed/generator/test/src/commonMain/kotlin/Test.kt b/ksp/sealed/generator/test/src/commonMain/kotlin/Test.kt new file mode 100644 index 00000000000..b88dc45e563 --- /dev/null +++ b/ksp/sealed/generator/test/src/commonMain/kotlin/Test.kt @@ -0,0 +1,16 @@ +package dev.inmo.micro_utils.ksp.sealed.generator.test + +import dev.inmo.microutils.kps.sealed.GenerateSealedWorkaround + +@GenerateSealedWorkaround +sealed interface Test { + @GenerateSealedWorkaround.Order(2) + object A : Test + @GenerateSealedWorkaround.Exclude + object B : Test + @GenerateSealedWorkaround.Order(0) + object C : Test + + // Required for successful sealed workaround generation + companion object +} diff --git a/ksp/sealed/generator/test/src/commonMain/kotlin/TestSealedWorkaround.kt b/ksp/sealed/generator/test/src/commonMain/kotlin/TestSealedWorkaround.kt new file mode 100644 index 00000000000..dc1b467958d --- /dev/null +++ b/ksp/sealed/generator/test/src/commonMain/kotlin/TestSealedWorkaround.kt @@ -0,0 +1,11 @@ +// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY +// TO REGENERATE IT JUST DELETE FILE +// ORIGINAL FILE: Test.kt +package dev.inmo.micro_utils.ksp.`sealed`.generator.test + +import kotlin.collections.Set + +private val values: Set = setOf(Test.C, + Test.A) + +public fun Test.Companion.values(): Set = values diff --git a/ksp/sealed/src/commonMain/kotlin/GenerateSealedWorkaround.kt b/ksp/sealed/src/commonMain/kotlin/GenerateSealedWorkaround.kt new file mode 100644 index 00000000000..0dcf29fb50c --- /dev/null +++ b/ksp/sealed/src/commonMain/kotlin/GenerateSealedWorkaround.kt @@ -0,0 +1,14 @@ +package dev.inmo.microutils.kps.sealed + +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS) +annotation class GenerateSealedWorkaround( + val prefix: String = "" +) { + @Retention(AnnotationRetention.BINARY) + @Target(AnnotationTarget.CLASS) + annotation class Order(val order: Int) + @Retention(AnnotationRetention.BINARY) + @Target(AnnotationTarget.CLASS) + annotation class Exclude +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 79702a0bc1e..b4c5e9abf8f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -49,6 +49,12 @@ String[] includes = [ ":fsm:common", ":fsm:repos:common", + ":ksp:generator", + + ":ksp:sealed", + ":ksp:sealed:generator", + ":ksp:sealed:generator:test", + ":dokka" ]