mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-02-16 19:52:03 +00:00
add ClassCasts generator
This commit is contained in:
parent
9196e4c367
commit
2b9bb4f141
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## 0.21.2
|
## 0.21.2
|
||||||
|
|
||||||
|
* `KSP`:
|
||||||
|
* `ClassCasts`:
|
||||||
|
* Module has been initialized
|
||||||
|
|
||||||
## 0.21.1
|
## 0.21.1
|
||||||
|
|
||||||
* `KSP`:
|
* `KSP`:
|
||||||
|
7
ksp/classcasts/build.gradle
Normal file
7
ksp/classcasts/build.gradle
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
21
ksp/classcasts/generator/build.gradle
Normal file
21
ksp/classcasts/generator/build.gradle
Normal file
@ -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.classcasts")
|
||||||
|
api libs.kotlin.poet
|
||||||
|
api libs.ksp
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
106
ksp/classcasts/generator/src/main/kotlin/ClassCastsFiller.kt
Normal file
106
ksp/classcasts/generator/src/main/kotlin/ClassCastsFiller.kt
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package dev.inmo.micro_utils.ksp.classcasts.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.symbol.ClassKind
|
||||||
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
import com.squareup.kotlinpoet.*
|
||||||
|
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||||
|
import com.squareup.kotlinpoet.ksp.toClassName
|
||||||
|
import com.squareup.kotlinpoet.ksp.toTypeName
|
||||||
|
|
||||||
|
|
||||||
|
private fun FileSpec.Builder.addTopLevelImport(className: ClassName) {
|
||||||
|
className.topLevelClassName().let {
|
||||||
|
addImport(it.packageName, it.simpleNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FileSpec.Builder.createTypeDefinition(ksClassDeclaration: KSClassDeclaration): TypeName {
|
||||||
|
val className = ksClassDeclaration.toClassName()
|
||||||
|
return if (ksClassDeclaration.typeParameters.isNotEmpty()) {
|
||||||
|
className.parameterizedBy(
|
||||||
|
ksClassDeclaration.typeParameters.map {
|
||||||
|
it.bounds.first().resolve().also {
|
||||||
|
val typeClassName = it.toClassName()
|
||||||
|
addTopLevelImport(typeClassName)
|
||||||
|
}.toTypeName()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
className
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun FileSpec.Builder.fill(
|
||||||
|
sourceKSClassDeclaration: KSClassDeclaration,
|
||||||
|
subtypesMap: Map<KSClassDeclaration, Set<KSClassDeclaration>>,
|
||||||
|
targetClassDeclaration: KSClassDeclaration = sourceKSClassDeclaration
|
||||||
|
) {
|
||||||
|
if (sourceKSClassDeclaration == targetClassDeclaration) {
|
||||||
|
subtypesMap[sourceKSClassDeclaration] ?.forEach {
|
||||||
|
fill(sourceKSClassDeclaration, subtypesMap, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val sourceClassName = sourceKSClassDeclaration.toClassName()
|
||||||
|
val targetClassClassName = targetClassDeclaration.toClassName()
|
||||||
|
val targetClassTypeDefinition = createTypeDefinition(targetClassDeclaration)
|
||||||
|
val simpleName = targetClassDeclaration.simpleName.asString()
|
||||||
|
val withFirstLowerCase = simpleName.replaceFirstChar { it.lowercase() }
|
||||||
|
val castedOrNullName = "${withFirstLowerCase}OrNull"
|
||||||
|
|
||||||
|
addTopLevelImport(targetClassClassName)
|
||||||
|
addFunction(
|
||||||
|
FunSpec.builder(castedOrNullName).apply {
|
||||||
|
receiver(sourceClassName)
|
||||||
|
addCode(
|
||||||
|
"return this as? %L",
|
||||||
|
targetClassTypeDefinition
|
||||||
|
)
|
||||||
|
returns(targetClassTypeDefinition.copy(nullable = true))
|
||||||
|
addModifiers(KModifier.INLINE)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
addFunction(
|
||||||
|
FunSpec.builder("${withFirstLowerCase}OrThrow").apply {
|
||||||
|
receiver(sourceClassName)
|
||||||
|
addCode(
|
||||||
|
"return this as %L",
|
||||||
|
targetClassTypeDefinition
|
||||||
|
)
|
||||||
|
returns(targetClassTypeDefinition)
|
||||||
|
addModifiers(KModifier.INLINE)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
addFunction(
|
||||||
|
FunSpec.builder("if$simpleName").apply {
|
||||||
|
val genericType = TypeVariableName("T", null)
|
||||||
|
addTypeVariable(genericType)
|
||||||
|
receiver(sourceClassName)
|
||||||
|
addParameter(
|
||||||
|
"block",
|
||||||
|
LambdaTypeName.get(
|
||||||
|
null,
|
||||||
|
targetClassTypeDefinition,
|
||||||
|
returnType = genericType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addCode(
|
||||||
|
"return ${castedOrNullName}() ?.let(block)",
|
||||||
|
targetClassTypeDefinition
|
||||||
|
)
|
||||||
|
returns(genericType.copy(nullable = true))
|
||||||
|
addModifiers(KModifier.INLINE)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
subtypesMap[targetClassDeclaration] ?.let {
|
||||||
|
if (it.count { it.classKind == ClassKind.CLASS } > 1) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it.filter { it.classKind != ClassKind.CLASS }
|
||||||
|
}
|
||||||
|
} ?.forEach {
|
||||||
|
fill(sourceKSClassDeclaration, subtypesMap, it)
|
||||||
|
fill(targetClassDeclaration, subtypesMap, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
ksp/classcasts/generator/src/main/kotlin/Processor.kt
Normal file
116
ksp/classcasts/generator/src/main/kotlin/Processor.kt
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package dev.inmo.micro_utils.ksp.classcasts.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.KspExperimental
|
||||||
|
import com.google.devtools.ksp.getAllSuperTypes
|
||||||
|
import com.google.devtools.ksp.getAnnotationsByType
|
||||||
|
import com.google.devtools.ksp.isAnnotationPresent
|
||||||
|
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.*
|
||||||
|
import dev.inmo.micro_ksp.generator.writeFile
|
||||||
|
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded
|
||||||
|
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded
|
||||||
|
|
||||||
|
class Processor(
|
||||||
|
private val codeGenerator: CodeGenerator
|
||||||
|
) : SymbolProcessor {
|
||||||
|
private val classCastsIncludedClassName = ClassCastsIncluded::class.asClassName()
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
private fun FileSpec.Builder.generateClassCasts(
|
||||||
|
ksClassDeclaration: KSClassDeclaration,
|
||||||
|
resolver: Resolver
|
||||||
|
) {
|
||||||
|
val classes = resolver.getSymbolsWithAnnotation(classCastsIncludedClassName.canonicalName).filterIsInstance<KSClassDeclaration>()
|
||||||
|
val classesRegexes: Map<KSClassDeclaration, Pair<Regex?, Regex?>> = classes.mapNotNull {
|
||||||
|
it to (it.getAnnotationsByType(ClassCastsIncluded::class).firstNotNullOfOrNull {
|
||||||
|
it.typesRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) to it.excludeRegex.takeIf { it.isNotEmpty() } ?.let(::Regex)
|
||||||
|
} ?: return@mapNotNull null)
|
||||||
|
}.toMap()
|
||||||
|
val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>()
|
||||||
|
|
||||||
|
resolver.getAllFiles().forEach {
|
||||||
|
it.declarations.forEach { potentialSubtype ->
|
||||||
|
if (
|
||||||
|
potentialSubtype is KSClassDeclaration
|
||||||
|
&& potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class).not()
|
||||||
|
) {
|
||||||
|
val allSupertypes = potentialSubtype.getAllSuperTypes().map { it.declaration }
|
||||||
|
|
||||||
|
for (currentClass in classes) {
|
||||||
|
val regexes = classesRegexes[currentClass]
|
||||||
|
val simpleName = potentialSubtype.simpleName.getShortName()
|
||||||
|
when {
|
||||||
|
currentClass !in allSupertypes
|
||||||
|
|| regexes ?.first ?.matches(simpleName) == false
|
||||||
|
|| regexes ?.second ?.matches(simpleName) == true -> continue
|
||||||
|
else -> {
|
||||||
|
classesSubtypes.getOrPut(currentClass) { mutableSetOf() }.add(potentialSubtype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun fillWithSealeds(source: KSClassDeclaration, current: KSClassDeclaration = source) {
|
||||||
|
val regexes = classesRegexes[source]
|
||||||
|
current.getSealedSubclasses().forEach {
|
||||||
|
val simpleName = it.simpleName.getShortName()
|
||||||
|
if (
|
||||||
|
regexes ?.first ?.matches(simpleName) == false
|
||||||
|
|| regexes ?.second ?.matches(simpleName) == true
|
||||||
|
|| it.isAnnotationPresent(ClassCastsExcluded::class)
|
||||||
|
) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
classesSubtypes.getOrPut(source) { mutableSetOf() }.add(it)
|
||||||
|
fillWithSealeds(source, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classes.forEach { fillWithSealeds(it) }
|
||||||
|
|
||||||
|
addAnnotation(
|
||||||
|
AnnotationSpec.builder(Suppress::class).apply {
|
||||||
|
addMember("\"unused\"")
|
||||||
|
addMember("\"RemoveRedundantQualifierName\"")
|
||||||
|
addMember("\"RedundantVisibilityModifier\"")
|
||||||
|
addMember("\"NOTHING_TO_INLINE\"")
|
||||||
|
addMember("\"UNCHECKED_CAST\"")
|
||||||
|
addMember("\"OPT_IN_USAGE\"")
|
||||||
|
useSiteTarget(AnnotationSpec.UseSiteTarget.FILE)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
classes.forEach {
|
||||||
|
fill(
|
||||||
|
it,
|
||||||
|
classesSubtypes.toMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
|
(resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
|
||||||
|
val prefix = it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix
|
||||||
|
it.writeFile(prefix = prefix, suffix = "ClassCasts") {
|
||||||
|
FileSpec.builder(
|
||||||
|
it.packageName.asString(),
|
||||||
|
"${it.simpleName.getShortName()}ClassCasts"
|
||||||
|
).apply {
|
||||||
|
addFileComment(
|
||||||
|
"""
|
||||||
|
THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
|
||||||
|
TO REGENERATE IT JUST DELETE FILE
|
||||||
|
ORIGINAL FILE: ${it.containingFile ?.fileName}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
generateClassCasts(it, resolver)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
11
ksp/classcasts/generator/src/main/kotlin/Provider.kt
Normal file
11
ksp/classcasts/generator/src/main/kotlin/Provider.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_utils.ksp.classcasts.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
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
dev.inmo.micro_utils.ksp.classcasts.generator.Provider
|
27
ksp/classcasts/generator/test/build.gradle
Normal file
27
ksp/classcasts/generator/test/build.gradle
Normal file
@ -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.classcasts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
add("kspCommonMainMetadata", project(":micro_utils.ksp.classcasts.generator"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
}
|
12
ksp/classcasts/generator/test/src/commonMain/kotlin/Test.kt
Normal file
12
ksp/classcasts/generator/test/src/commonMain/kotlin/Test.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package dev.inmo.micro_utils.ksp.classcasts.generator.test
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded
|
||||||
|
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded
|
||||||
|
|
||||||
|
@ClassCastsIncluded
|
||||||
|
sealed interface Test {
|
||||||
|
object A : Test
|
||||||
|
@ClassCastsExcluded
|
||||||
|
object B : Test
|
||||||
|
object C : Test
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
|
||||||
|
// TO REGENERATE IT JUST DELETE FILE
|
||||||
|
// ORIGINAL FILE: Test.kt
|
||||||
|
@file:Suppress(
|
||||||
|
"unused",
|
||||||
|
"RemoveRedundantQualifierName",
|
||||||
|
"RedundantVisibilityModifier",
|
||||||
|
"NOTHING_TO_INLINE",
|
||||||
|
"UNCHECKED_CAST",
|
||||||
|
"OPT_IN_USAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.ksp.classcasts.generator.test
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.ksp.classcasts.generator.test.Test
|
||||||
|
import kotlin.Suppress
|
||||||
|
|
||||||
|
public inline fun Test.aOrNull(): Test.A? = this as?
|
||||||
|
dev.inmo.micro_utils.ksp.classcasts.generator.test.Test.A
|
||||||
|
|
||||||
|
public inline fun Test.aOrThrow(): Test.A = this as
|
||||||
|
dev.inmo.micro_utils.ksp.classcasts.generator.test.Test.A
|
||||||
|
|
||||||
|
public inline fun <T> Test.ifA(block: (Test.A) -> T): T? = aOrNull() ?.let(block)
|
||||||
|
|
||||||
|
public inline fun Test.cOrNull(): Test.C? = this as?
|
||||||
|
dev.inmo.micro_utils.ksp.classcasts.generator.test.Test.C
|
||||||
|
|
||||||
|
public inline fun Test.cOrThrow(): Test.C = this as
|
||||||
|
dev.inmo.micro_utils.ksp.classcasts.generator.test.Test.C
|
||||||
|
|
||||||
|
public inline fun <T> Test.ifC(block: (Test.C) -> T): T? = cOrNull() ?.let(block)
|
@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.ksp.classcasts
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class ClassCastsExcluded
|
@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.ksp.classcasts
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class ClassCastsIncluded(val typesRegex: String = "", val excludeRegex: String = "", val outputFilePrefix: String = "")
|
@ -55,6 +55,10 @@ String[] includes = [
|
|||||||
":ksp:sealed:generator",
|
":ksp:sealed:generator",
|
||||||
":ksp:sealed:generator:test",
|
":ksp:sealed:generator:test",
|
||||||
|
|
||||||
|
":ksp:classcasts",
|
||||||
|
":ksp:classcasts:generator",
|
||||||
|
":ksp:classcasts:generator:test",
|
||||||
|
|
||||||
":dokka"
|
":dokka"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user