diff --git a/CHANGELOG.md b/CHANGELOG.md index 02034c24319..e0085933516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.21.2 +* `KSP`: + * `ClassCasts`: + * Module has been initialized + ## 0.21.1 * `KSP`: diff --git a/ksp/classcasts/build.gradle b/ksp/classcasts/build.gradle new file mode 100644 index 00000000000..d425197852e --- /dev/null +++ b/ksp/classcasts/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/classcasts/generator/build.gradle b/ksp/classcasts/generator/build.gradle new file mode 100644 index 00000000000..8fcda0f1491 --- /dev/null +++ b/ksp/classcasts/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.classcasts") + api libs.kotlin.poet + api libs.ksp +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} diff --git a/ksp/classcasts/generator/src/main/kotlin/ClassCastsFiller.kt b/ksp/classcasts/generator/src/main/kotlin/ClassCastsFiller.kt new file mode 100644 index 00000000000..d70734b16f3 --- /dev/null +++ b/ksp/classcasts/generator/src/main/kotlin/ClassCastsFiller.kt @@ -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>, + 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) + } + } +} diff --git a/ksp/classcasts/generator/src/main/kotlin/Processor.kt b/ksp/classcasts/generator/src/main/kotlin/Processor.kt new file mode 100644 index 00000000000..d996ba7bd43 --- /dev/null +++ b/ksp/classcasts/generator/src/main/kotlin/Processor.kt @@ -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() + val classesRegexes: Map> = 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>() + + 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 { + (resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance().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() + } +} diff --git a/ksp/classcasts/generator/src/main/kotlin/Provider.kt b/ksp/classcasts/generator/src/main/kotlin/Provider.kt new file mode 100644 index 00000000000..11b2c94a8f0 --- /dev/null +++ b/ksp/classcasts/generator/src/main/kotlin/Provider.kt @@ -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 + ) +} diff --git a/ksp/classcasts/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/ksp/classcasts/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..88b6e894479 --- /dev/null +++ b/ksp/classcasts/generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +dev.inmo.micro_utils.ksp.classcasts.generator.Provider diff --git a/ksp/classcasts/generator/test/build.gradle b/ksp/classcasts/generator/test/build.gradle new file mode 100644 index 00000000000..ee86dd185d9 --- /dev/null +++ b/ksp/classcasts/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.classcasts") + } + } + } +} + + +dependencies { + add("kspCommonMainMetadata", project(":micro_utils.ksp.classcasts.generator")) +} + +ksp { +} diff --git a/ksp/classcasts/generator/test/src/commonMain/kotlin/Test.kt b/ksp/classcasts/generator/test/src/commonMain/kotlin/Test.kt new file mode 100644 index 00000000000..ddcd317a427 --- /dev/null +++ b/ksp/classcasts/generator/test/src/commonMain/kotlin/Test.kt @@ -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 +} diff --git a/ksp/classcasts/generator/test/src/commonMain/kotlin/TestClassCasts.kt b/ksp/classcasts/generator/test/src/commonMain/kotlin/TestClassCasts.kt new file mode 100644 index 00000000000..f1a9e967c91 --- /dev/null +++ b/ksp/classcasts/generator/test/src/commonMain/kotlin/TestClassCasts.kt @@ -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 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 Test.ifC(block: (Test.C) -> T): T? = cOrNull() ?.let(block) diff --git a/ksp/classcasts/src/commonMain/kotlin/ClassCastsExcluded.kt b/ksp/classcasts/src/commonMain/kotlin/ClassCastsExcluded.kt new file mode 100644 index 00000000000..1d9ad4d17d8 --- /dev/null +++ b/ksp/classcasts/src/commonMain/kotlin/ClassCastsExcluded.kt @@ -0,0 +1,5 @@ +package dev.inmo.micro_utils.ksp.classcasts + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class ClassCastsExcluded diff --git a/ksp/classcasts/src/commonMain/kotlin/ClassCastsIncluded.kt b/ksp/classcasts/src/commonMain/kotlin/ClassCastsIncluded.kt new file mode 100644 index 00000000000..3099a916092 --- /dev/null +++ b/ksp/classcasts/src/commonMain/kotlin/ClassCastsIncluded.kt @@ -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 = "") diff --git a/settings.gradle b/settings.gradle index b4c5e9abf8f..b31bd466a2e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -55,6 +55,10 @@ String[] includes = [ ":ksp:sealed:generator", ":ksp:sealed:generator:test", + ":ksp:classcasts", + ":ksp:classcasts:generator", + ":ksp:classcasts:generator:test", + ":dokka" ]