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

io.github.freya022.botcommands.internal.utils.ReflectionMetadata.kt Maven / Gradle / Ivy

Go to download

A Kotlin-first (and Java) framework that makes creating Discord bots a piece of cake, using the JDA library.

There is a newer version: 3.0.0-alpha.18
Show newest version
package io.github.freya022.botcommands.internal.utils

import io.github.classgraph.*
import io.github.freya022.botcommands.api.commands.annotations.Optional
import io.github.freya022.botcommands.api.core.config.BConfig
import io.github.freya022.botcommands.api.core.service.ClassGraphProcessor
import io.github.freya022.botcommands.api.core.service.annotations.Condition
import io.github.freya022.botcommands.api.core.utils.javaMethodOrConstructor
import io.github.freya022.botcommands.api.core.utils.shortSignature
import io.github.freya022.botcommands.api.core.utils.simpleNestedName
import io.github.freya022.botcommands.internal.commands.CommandsPresenceChecker
import io.github.freya022.botcommands.internal.core.BContextImpl
import io.github.freya022.botcommands.internal.core.HandlersPresenceChecker
import io.github.freya022.botcommands.internal.core.service.ConditionalObjectChecker
import io.github.freya022.botcommands.internal.parameters.resolvers.ResolverSupertypeChecker
import io.github.freya022.botcommands.internal.utils.ReflectionUtils.function
import io.github.oshai.kotlinlogging.KotlinLogging
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 val logger = KotlinLogging.logger { }

    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) {
        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")
        }

        logger.debug { "Scanning packages: ${config.packages.joinToString()}" }
        if (config.classes.isNotEmpty()) {
            logger.debug { "Scanning additional classes: ${config.classes.joinToString { it.simpleNestedName }}" }
        }

        val scanned: List> = buildList {
            ClassGraph()
                .acceptPackages("io.github.freya022.botcommands.api", "io.github.freya022.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) }
                                || it.hasAnnotation(Condition::class.java)
                    })
                }

            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]")
        val classGraphProcessors = context.config.classGraphProcessors +
                ConditionalObjectChecker +
                listOf(context.serviceProviders, context.customConditionsContainer, context.stagingClassAnnotations.processor) +
                listOf(CommandsPresenceChecker(), ResolverSupertypeChecker(), HandlersPresenceChecker())
        return scanned.forEach { (_, classes) ->
            classes
                .filter {
                    it.annotationInfo.directOnly()["kotlin.Metadata"]?.let { annotationInfo ->
                        //Only keep classes, not others such as file facades
                        val kind = KotlinClassHeader.Kind.getById(annotationInfo.parameterValues["k"].value as Int)
                        if (kind == KotlinClassHeader.Kind.FILE_FACADE) {
                            it.checkFacadeFactories(config)
                            return@filter false
                        } else if (kind != KotlinClassHeader.Kind.CLASS) {
                            return@filter false
                        }
                    }

                    if (lowercaseInnerClassRegex.containsMatchIn(it.name)) return@filter false
                    return@filter !it.isSynthetic && !it.isEnum && !it.isRecord
                }
                .processClasses(context, classGraphProcessors)
        }.also {
            classGraphProcessors.forEach { it.postProcess(context) }
            scanned.forEach { (scanResult, _) -> scanResult.close() }

            scannedParams = true
        }
    }

    private fun ClassInfo.checkFacadeFactories(config: BConfig) {
        this.declaredMethodInfo.forEach { methodInfo ->
            check(!methodInfo.isService(config)) {
                "Top-level service factories are not supported: ${methodInfo.shortSignature}"
            }
        }
    }

    private fun ClassInfo.isService(config: BConfig): Boolean {
        val declaredAnnotations = annotations.directOnly()
        return config.serviceConfig.serviceAnnotations.any { serviceAnnotation -> declaredAnnotations.containsName(serviceAnnotation.jvmName) }
    }

    private fun MethodInfo.isService(config: BConfig): Boolean {
        val declaredAnnotations = annotationInfo.directOnly()
        return config.serviceConfig.serviceAnnotations.any { serviceAnnotation -> declaredAnnotations.containsName(serviceAnnotation.jvmName) }
    }

    private fun ClassInfo.isServiceOrHasFactories(config: BConfig) =
        isService(config) || methodInfo.any { it.isService(config) }

    private fun List.processClasses(context: BContextImpl, classGraphProcessors: List): List {
        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)

                    val isServiceFactory = methodInfo.isService(context.config)
                    classGraphProcessors.forEach { it.processMethod(context, methodInfo, method, classInfo, kClass, isServiceFactory) }
                }

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

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

    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]

            return isNullableAnnotated || type.isMarkedNullable
        }

    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 - 2024 Weber Informatics LLC | Privacy Policy