improve class casts including filtering

This commit is contained in:
InsanusMokrassar 2023-06-16 13:50:34 +06:00
parent 6f60ecbf2e
commit f153b31464
15 changed files with 3230 additions and 858 deletions

View File

@ -1,6 +1,6 @@
[versions] [versions]
kotlin = "1.8.21" kotlin = "1.8.22"
kotlin-serialization = "1.5.1" kotlin-serialization = "1.5.1"
kotlin-coroutines = "1.6.4" kotlin-coroutines = "1.6.4"
@ -10,7 +10,7 @@ korlibs = "4.0.3"
uuid = "0.7.1" uuid = "0.7.1"
ktor = "2.3.1" ktor = "2.3.1"
ksp = "1.8.21-1.0.11" ksp = "1.8.22-1.0.11"
kotlin-poet = "1.14.2" kotlin-poet = "1.14.2"
microutils = "0.19.2" microutils = "0.19.2"

View File

@ -8,7 +8,7 @@ import dev.inmo.tgbotapi.types.chat.User
* *
* @see FromUser * @see FromUser
*/ */
@ClassCastsIncluded @ClassCastsIncluded(excludeRegex = ".*Impl")
interface WithUser { interface WithUser {
val user: User val user: User
} }

View File

@ -7,7 +7,7 @@ import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable(InlineQueryResultSerializer::class) @Serializable(InlineQueryResultSerializer::class)
@ClassCastsIncluded @ClassCastsIncluded(excludeRegex = ".*Impl")
interface InlineQueryResult { interface InlineQueryResult {
val type: String val type: String
val id: InlineQueryIdentifier val id: InlineQueryIdentifier

View File

@ -49,7 +49,7 @@ sealed interface AbleToAddInAttachmentMenuChat : Chat {
} }
@Serializable(PreviewChatSerializer::class) @Serializable(PreviewChatSerializer::class)
@ClassCastsIncluded(excludeRegex = ".*Impl.kt") @ClassCastsIncluded(excludeRegex = ".*Impl")
sealed interface Chat { sealed interface Chat {
val id: IdChatIdentifier val id: IdChatIdentifier
} }

View File

@ -4,12 +4,14 @@ import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.abstracts.Message
import dev.inmo.tgbotapi.types.message.abstracts.TelegramBotAPIMessageDeserializeOnlySerializer import dev.inmo.tgbotapi.types.message.abstracts.TelegramBotAPIMessageDeserializeOnlySerializer
import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.RiskFeature
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use ExtendedChannelChat due") @RiskFeature("This class is a subject of changes. It is better to use ExtendedChannelChat due")
@ClassCastsExcluded
data class ExtendedChannelChatImpl( data class ExtendedChannelChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: ChatId, override val id: ChatId,
@ -36,6 +38,7 @@ data class ExtendedChannelChatImpl(
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use ExtendedGroupChat due") @RiskFeature("This class is a subject of changes. It is better to use ExtendedGroupChat due")
@ClassCastsExcluded
data class ExtendedGroupChatImpl( data class ExtendedGroupChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: ChatId, override val id: ChatId,
@ -58,6 +61,7 @@ data class ExtendedGroupChatImpl(
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use ExtendedPrivateChat due") @RiskFeature("This class is a subject of changes. It is better to use ExtendedPrivateChat due")
@ClassCastsExcluded
data class ExtendedPrivateChatImpl( data class ExtendedPrivateChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: UserId, override val id: UserId,
@ -85,6 +89,7 @@ typealias ExtendedUser = ExtendedPrivateChatImpl
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use ExtendedSupergroupChat due") @RiskFeature("This class is a subject of changes. It is better to use ExtendedSupergroupChat due")
@ClassCastsExcluded
data class ExtendedSupergroupChatImpl( data class ExtendedSupergroupChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: ChatId, override val id: ChatId,
@ -127,6 +132,7 @@ data class ExtendedSupergroupChatImpl(
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use ExtendedForumChat due") @RiskFeature("This class is a subject of changes. It is better to use ExtendedForumChat due")
@ClassCastsExcluded
data class ExtendedForumChatImpl( data class ExtendedForumChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: IdChatIdentifier, override val id: IdChatIdentifier,

View File

@ -5,11 +5,13 @@ import dev.inmo.micro_utils.language_codes.IetfLanguageCodeSerializer
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.abstracts.WithOptionalLanguageCode import dev.inmo.tgbotapi.types.abstracts.WithOptionalLanguageCode
import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.RiskFeature
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use GroupChat due") @RiskFeature("This class is a subject of changes. It is better to use GroupChat due")
@ClassCastsExcluded
data class GroupChatImpl( data class GroupChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: ChatId, override val id: ChatId,
@ -19,6 +21,7 @@ data class GroupChatImpl(
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use PrivateChat due") @RiskFeature("This class is a subject of changes. It is better to use PrivateChat due")
@ClassCastsExcluded
data class PrivateChatImpl( data class PrivateChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: UserId, override val id: UserId,
@ -32,6 +35,7 @@ data class PrivateChatImpl(
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use SupergroupChat due") @RiskFeature("This class is a subject of changes. It is better to use SupergroupChat due")
@ClassCastsExcluded
data class SupergroupChatImpl( data class SupergroupChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: ChatId, override val id: ChatId,
@ -43,6 +47,7 @@ data class SupergroupChatImpl(
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use ForumChat due") @RiskFeature("This class is a subject of changes. It is better to use ForumChat due")
@ClassCastsExcluded
data class ForumChatImpl( data class ForumChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: IdChatIdentifier, override val id: IdChatIdentifier,
@ -54,6 +59,7 @@ data class ForumChatImpl(
@Serializable @Serializable
@RiskFeature("This class is a subject of changes. It is better to use ChannelChat due") @RiskFeature("This class is a subject of changes. It is better to use ChannelChat due")
@ClassCastsExcluded
data class ChannelChatImpl( data class ChannelChatImpl(
@SerialName(idField) @SerialName(idField)
override val id: ChatId, override val id: ChatId,

View File

@ -2,9 +2,11 @@ package dev.inmo.tgbotapi.types.chat.member
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.User import dev.inmo.tgbotapi.types.chat.User
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
import kotlinx.serialization.* import kotlinx.serialization.*
@Serializable @Serializable
@ClassCastsExcluded
data class AdministratorChatMemberImpl( data class AdministratorChatMemberImpl(
@SerialName(userField) @SerialName(userField)
override val user: User, override val user: User,

View File

@ -1,10 +1,12 @@
package dev.inmo.tgbotapi.types.chat.member package dev.inmo.tgbotapi.types.chat.member
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ClassCastsExcluded
data class ChatAdministratorRightsImpl( data class ChatAdministratorRightsImpl(
@SerialName(canChangeInfoField) @SerialName(canChangeInfoField)
override val canChangeInfo: Boolean = false, override val canChangeInfo: Boolean = false,

View File

@ -2,9 +2,11 @@ package dev.inmo.tgbotapi.types.chat.member
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.User import dev.inmo.tgbotapi.types.chat.User
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
import kotlinx.serialization.* import kotlinx.serialization.*
@Serializable @Serializable
@ClassCastsExcluded
data class LeftChatMemberImpl(@SerialName(userField) override val user: User) : LeftChatMember { data class LeftChatMemberImpl(@SerialName(userField) override val user: User) : LeftChatMember {
@SerialName(statusField) @SerialName(statusField)
@Required @Required

View File

@ -2,9 +2,11 @@ package dev.inmo.tgbotapi.types.chat.member
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.User import dev.inmo.tgbotapi.types.chat.User
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
import kotlinx.serialization.* import kotlinx.serialization.*
@Serializable @Serializable
@ClassCastsExcluded
data class MemberChatMemberImpl(@SerialName(userField) override val user: User) : MemberChatMember { data class MemberChatMemberImpl(@SerialName(userField) override val user: User) : MemberChatMember {
@SerialName(statusField) @SerialName(statusField)
@Required @Required

View File

@ -11,7 +11,7 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
@ClassCastsIncluded @ClassCastsIncluded(excludeRegex = ".*Impl")
interface Message : WithChat { interface Message : WithChat {
val messageId: MessageId val messageId: MessageId
val date: DateTime val date: DateTime

View File

@ -1,5 +1,13 @@
package dev.inmo.tgbotapi.utils.internal package dev.inmo.tgbotapi.utils.internal
import dev.inmo.tgbotapi.utils.RiskFeature
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
internal annotation class ClassCastsIncluded(val typesRegex: String = ".*", val excludeRegex: String = "") @RiskFeature("It is internal API in tgbotapi.core and should not be used outside")
annotation class ClassCastsIncluded(val typesRegex: String = "", val excludeRegex: String = "")
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
@RiskFeature("It is internal API in tgbotapi.core and should not be used outside")
annotation class ClassCastsExcluded()

View File

@ -9,4 +9,5 @@ repositories {
dependencies { dependencies {
implementation libs.kotlin.poet implementation libs.kotlin.poet
implementation libs.ksp implementation libs.ksp
implementation project(":tgbotapi.core")
} }

View File

@ -1,15 +1,21 @@
package dev.inmo.tgbotapi.ksp.processor package dev.inmo.tgbotapi.ksp.processor
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAllSuperTypes import com.google.devtools.ksp.getAllSuperTypes
import com.google.devtools.ksp.getAnnotationsByType import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.* import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.tgbotapi.utils.RiskFeature
import dev.inmo.tgbotapi.utils.internal.ClassCastsExcluded
import dev.inmo.tgbotapi.utils.internal.ClassCastsIncluded
import java.io.File import java.io.File
class TelegramBotAPISymbolProcessor( class TelegramBotAPISymbolProcessor(
@ -18,35 +24,33 @@ class TelegramBotAPISymbolProcessor(
private val outputFile: String = "Output", private val outputFile: String = "Output",
private val outputFolder: String? = null private val outputFolder: String? = null
) : SymbolProcessor { ) : SymbolProcessor {
private val classCastsIncludedClassName = ClassName("dev.inmo.tgbotapi.utils.internal", "ClassCastsIncluded") private val classCastsIncludedClassName = ClassCastsIncluded::class.asClassName()
@OptIn(KspExperimental::class, RiskFeature::class)
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
val classes = resolver.getSymbolsWithAnnotation(classCastsIncludedClassName.canonicalName).filterIsInstance<KSClassDeclaration>() val classes = resolver.getSymbolsWithAnnotation(classCastsIncludedClassName.canonicalName).filterIsInstance<KSClassDeclaration>()
val classesRegexes: Map<KSClassDeclaration, Pair<Regex, Regex?>> = classes.mapNotNull { val classesRegexes: Map<KSClassDeclaration, Pair<Regex?, Regex?>> = classes.mapNotNull {
it to (it.annotations.firstNotNullOfOrNull { it to (it.getAnnotationsByType(ClassCastsIncluded::class).firstNotNullOfOrNull {
runCatching { it.typesRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) to it.excludeRegex.takeIf { it.isNotEmpty() } ?.let(::Regex)
if (it.annotationType.resolve().toClassName() == classCastsIncludedClassName) {
val regex = it.arguments.first().value as? String ?: return@runCatching null
val negativeRegex = (it.arguments.first().value as? String) ?.takeIf { it.isNotEmpty() }
Regex(regex) to (negativeRegex ?.let(::Regex))
} else {
null
}
}.getOrNull()
} ?: return@mapNotNull null) } ?: return@mapNotNull null)
}.toMap() }.toMap()
val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>() val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>()
resolver.getAllFiles().forEach { resolver.getAllFiles().forEach {
it.declarations.forEach { potentialSubtype -> it.declarations.forEach { potentialSubtype ->
if (potentialSubtype is KSClassDeclaration) { if (
potentialSubtype is KSClassDeclaration
&& potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class).not()
) {
val allSupertypes = potentialSubtype.getAllSuperTypes().map { it.declaration } val allSupertypes = potentialSubtype.getAllSuperTypes().map { it.declaration }
for (currentClass in classes) { for (currentClass in classes) {
val regexes = classesRegexes[currentClass] val regexes = classesRegexes[currentClass]
val simpleName = potentialSubtype.simpleName.getShortName()
when { when {
currentClass in allSupertypes currentClass !in allSupertypes
&& regexes ?.first ?.matches(potentialSubtype.simpleName.toString()) != false || regexes ?.first ?.matches(simpleName) == false
&& regexes ?.second ?.matches(potentialSubtype.simpleName.toString()) != true-> { || regexes ?.second ?.matches(simpleName) == true -> continue
else -> {
classesSubtypes.getOrPut(currentClass) { mutableSetOf() }.add(potentialSubtype) classesSubtypes.getOrPut(currentClass) { mutableSetOf() }.add(potentialSubtype)
} }
} }
@ -55,7 +59,16 @@ class TelegramBotAPISymbolProcessor(
} }
} }
fun fillWithSealeds(source: KSClassDeclaration, current: KSClassDeclaration = source) { fun fillWithSealeds(source: KSClassDeclaration, current: KSClassDeclaration = source) {
val regexes = classesRegexes[source]
current.getSealedSubclasses().forEach { 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) classesSubtypes.getOrPut(source) { mutableSetOf() }.add(it)
fillWithSealeds(source, it) fillWithSealeds(source, it)
} }