All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.freya02.botcommands.internal.utils.ReflectionMetadata.kt Maven / Gradle / Ivy

package com.freya02.botcommands.internal.utils

import com.freya02.botcommands.api.commands.annotations.Optional
import com.freya02.botcommands.api.core.config.BConfig
import com.freya02.botcommands.internal.BContextImpl
import com.freya02.botcommands.internal.commands.CommandsPresenceChecker
import com.freya02.botcommands.internal.core.HandlersPresenceChecker
import com.freya02.botcommands.internal.javaMethodOrConstructor
import com.freya02.botcommands.internal.parameters.resolvers.ResolverSupertypeChecker
import com.freya02.botcommands.internal.throwInternal
import com.freya02.botcommands.internal.throwUser
import com.freya02.botcommands.internal.utils.ReflectionUtils.function
import io.github.classgraph.*
import java.lang.reflect.Executable
import java.util.*
import kotlin.coroutines.Continuation
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.jvm.internal.impl.load.kotlin.header.KotlinClassHeader
import kotlin.reflect.jvm.jvmName

private typealias IsNullableAnnotated = Boolean

internal object ReflectionMetadata {
    private class ClassMetadata(val sourceFile: String)
    private class MethodMetadata(val line: Int, val nullabilities: List)

    private var scannedParams: Boolean = false

    private val classMetadataMap_: MutableMap, ClassMetadata> = hashMapOf()
    private val classMetadataMap: Map, ClassMetadata> by lazy {
        if (!scannedParams)
            throwInternal("Tried to access class metadata but they haven't been scanned yet")

        Collections.unmodifiableMap(classMetadataMap_)
    }

    private val methodMetadataMap_: MutableMap = hashMapOf()
    private val methodMetadataMap: Map by lazy {
        if (!scannedParams)
            throwInternal("Tried to access method metadata but they haven't been scanned yet")

        Collections.unmodifiableMap(methodMetadataMap_)
    }

    internal fun runScan(context: BContextImpl): List> {
        val config = context.config
        val packages = config.packages
        //This is a requirement for ClassGraph to work correctly
        if (packages.isEmpty()) {
            throwUser("You must specify at least 1 package to scan classes from")
        }

        val scanned: List> = buildList {
            ClassGraph()
                .acceptPackages("com.freya02.botcommands.api", "com.freya02.botcommands.internal")
                .enableMethodInfo()
                .enableAnnotationInfo()
                .disableModuleScanning()
                .disableNestedJarScanning()
                .scan()
                .also { scanResult -> // Don't keep test classes
                    add(scanResult to scanResult.allClasses.filter {
                        return@filter it.isServiceOrHasFactories(config)
                                || it.outerClasses.any { outer -> outer.isServiceOrHasFactories(config) }
                    })
                }

            ClassGraph()
                .acceptPackages(*config.packages.toTypedArray())
                .acceptClasses(*config.classes.map { it.name }.toTypedArray())
                .enableMethodInfo()
                .enableAnnotationInfo()
                .disableModuleScanning()
                .disableNestedJarScanning()
                .scan()
                .also { scanResult -> //No filtering is done as to allow checkers to log warnings/throw in case a service annotation is missing
                    add(scanResult to scanResult.allClasses)
                }
        }

        val lowercaseInnerClassRegex = Regex("\\$[a-z]")
        return scanned.flatMap { (scanResult, classes) ->
            classes
                .filter {
                    it.annotationInfo.directOnly()["kotlin.Metadata"]?.let { annotationInfo ->
                        //Only keep classes, not others such as file facades
                        if (KotlinClassHeader.Kind.getById(annotationInfo.parameterValues["k"].value as Int) != KotlinClassHeader.Kind.CLASS) {
                            return@filter false
                        }
                    }

                    if (lowercaseInnerClassRegex.containsMatchIn(it.name)) return@filter false
                    return@filter !it.isSynthetic && !it.isEnum && !it.isRecord && !it.isAnnotation
                }
                .processClasses(context)
                .map { classInfo ->
                    val kClass = classInfo.loadClass().kotlin
                    //Fill map with all the @Command, @Resolver, etc... declarations
                    if (classInfo.isService(config)) {
                        classInfo.annotationInfo.forEach { annotationInfo ->
                            if (config.serviceConfig.serviceAnnotations.any { it.jvmName == annotationInfo.name }) {
                                context.serviceAnnotationsMap.put(
                                    annotationReceiver = kClass,
                                    annotationType = annotationInfo.classInfo.loadClass(Annotation::class.java).kotlin,
                                    annotation = annotationInfo.loadClassAndInstantiate()
                                )
                            }
                        }
                    }

                    kClass
                }
                .also {
                    scanResult.close()
                }
        }
    }

    internal fun ClassInfo.isService(config: BConfig) =
        config.serviceConfig.serviceAnnotations.any { serviceAnnotation -> hasAnnotation(serviceAnnotation.jvmName) }

    internal fun MethodInfo.isService(config: BConfig) =
        config.serviceConfig.serviceAnnotations.any { serviceAnnotation -> hasAnnotation(serviceAnnotation.jvmName) }

    private fun ClassInfo.isServiceOrHasFactories(config: BConfig) =
        config.serviceConfig.serviceAnnotations.any { serviceAnnotation -> hasAnnotation(serviceAnnotation.jvmName) }
                //Keep classes which have service factories
                || config.serviceConfig.serviceAnnotations.any { serviceAnnotation -> methodAnnotations.any { it.name == serviceAnnotation.jvmName } }

    private fun List.processClasses(context: BContextImpl): List {
        val classGraphProcessors = context.config.classGraphProcessors +
                listOf(context.serviceProviders, CommandsPresenceChecker(), ResolverSupertypeChecker(), HandlersPresenceChecker())

        return onEach { classInfo ->
            try {
                val kClass = classInfo.loadClass().kotlin

                for (methodInfo in classInfo.declaredMethodAndConstructorInfo) {
                    //Don't inspect methods with generics
                    if (methodInfo.parameterInfo
                            .map { it.typeSignatureOrTypeDescriptor }
                            .any { it is TypeVariableSignature || (it is ArrayTypeSignature && it.elementTypeSignature is TypeVariableSignature) }
                    ) continue

                    val method = when {
                        methodInfo.isConstructor -> methodInfo.loadClassAndGetConstructor()
                        else -> methodInfo.loadClassAndGetMethod()
                    }
                    val nullabilities = getMethodParameterNullabilities(methodInfo, method)

                    methodMetadataMap_[method] = MethodMetadata(methodInfo.minLineNum, nullabilities)

                    classGraphProcessors.forEach { it.processMethod(context, methodInfo, method, classInfo, kClass) }
                }

                classMetadataMap_[classInfo.loadClass()] = ClassMetadata(classInfo.sourceFile)

                classGraphProcessors.forEach { it.processClass(context, classInfo, kClass) }
            } catch (e: Throwable) {
                throw RuntimeException("An exception occurred while scanning class: ${classInfo.name}", e)
            }
        }.also {
            classGraphProcessors.forEach { it.postProcess(context) }

            scannedParams = true
        }
    }

    private fun getMethodParameterNullabilities(methodInfo: MethodInfo, method: Executable): List {
        val nullabilities = methodInfo.parameterInfo.dropLast(if (method.isSuspend) 1 else 0).map { parameterInfo ->
            parameterInfo.annotationInfo.any { it.name.endsWith("Nullable") }
                    || parameterInfo.hasAnnotation(Optional::class.java)
        }

        return when {
            methodInfo.isStatic || methodInfo.isConstructor -> nullabilities
            //Pad with a non-null parameter to simulate the instance parameter
            else -> listOf(false) + nullabilities
        }
    }

    internal val Class<*>.sourceFile: String
        get() = (classMetadataMap[this]
            ?: throwUser("Tried to access a Method which hasn't been scanned: $this, the method must be accessible and in the search path")).sourceFile

    internal val KClass<*>.sourceFile: String
        get() = this.java.sourceFile

    internal val KParameter.isNullable: Boolean
        get() {
            val metadata = methodMetadataMap[function.javaMethodOrConstructor]
                ?: throwUser("Tried to access a Method which hasn't been scanned: $this, the method must be accessible and in the search path")
            val isNullableAnnotated =
                metadata.nullabilities[index]
            val isNullableMarked = type.isMarkedNullable

            return isNullableAnnotated || isNullableMarked
        }

    internal val KFunction<*>.lineNumber: Int
        get() = (methodMetadataMap[this.javaMethodOrConstructor]
            ?: throwUser("Tried to access a Method which hasn't been scanned: $this, the method must be accessible and in the search path")).line

    private val Executable.isSuspend: Boolean
        get() = parameters.any { it.type == Continuation::class.java }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy