io.github.freya022.botcommands.internal.utils.ReflectionMetadata.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of BotCommands Show documentation
Show all versions of BotCommands Show documentation
A Kotlin-first (and Java) framework that makes creating Discord bots a piece of cake, using the JDA library.
The 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.config.BConfigBuilder
import io.github.freya022.botcommands.api.core.debugNull
import io.github.freya022.botcommands.api.core.service.ClassGraphProcessor
import io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker
import io.github.freya022.botcommands.api.core.service.CustomConditionChecker
import io.github.freya022.botcommands.api.core.service.annotations.Condition
import io.github.freya022.botcommands.api.core.traceNull
import io.github.freya022.botcommands.api.core.utils.*
import io.github.freya022.botcommands.internal.commands.CommandsPresenceChecker
import io.github.freya022.botcommands.internal.core.HandlersPresenceChecker
import io.github.freya022.botcommands.internal.core.service.BotCommandsBootstrap
import io.github.freya022.botcommands.internal.parameters.resolvers.ResolverSupertypeChecker
import io.github.freya022.botcommands.internal.utils.ReflectionMetadata.ClassMetadata
import io.github.freya022.botcommands.internal.utils.ReflectionMetadata.MethodMetadata
import io.github.freya022.botcommands.internal.utils.ReflectionUtils.function
import io.github.oshai.kotlinlogging.KotlinLogging
import java.lang.reflect.Executable
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
private typealias IsNullableAnnotated = Boolean
private val logger = KotlinLogging.logger { }
internal class ReflectionMetadata(
private val classMetadataMap: Map, ClassMetadata>,
private val methodMetadataMap: Map,
) {
internal class ClassMetadata(val sourceFile: String)
internal class MethodMetadata(val line: Int, val nullabilities: List)
internal fun getClassMetadata(clazz: Class<*>): ClassMetadata {
return classMetadataMap[clazz]
?: throwArgument("Tried to access a Class which hasn't been scanned: $this, the class must be accessible and in the search path")
}
internal fun getClassMetadataOrNull(clazz: Class<*>): ClassMetadata? {
return classMetadataMap[clazz]
}
internal fun getMethodMetadata(executable: Executable): MethodMetadata {
return methodMetadataMap[executable]
?: throwArgument("Tried to access a Method which hasn't been scanned: $this, the method must be accessible and in the search path")
}
internal fun getMethodMetadataOrNull(executable: Executable): MethodMetadata? {
return methodMetadataMap[executable]
}
internal companion object {
private var _instance: ReflectionMetadata? = null
internal val instance: ReflectionMetadata
get() = _instance ?: throwInternal("Tried to access reflection metadata but they haven't been scanned yet")
internal fun runScan(config: BConfig, bootstrap: BotCommandsBootstrap) {
_instance = ReflectionMetadataScanner.scan(config, bootstrap)
}
}
}
private fun ReflectionMetadata.getMethodMetadata(function: KFunction<*>): MethodMetadata {
return getMethodMetadata(function.javaMethodOrConstructor)
}
private fun ReflectionMetadata.getMethodMetadataOrNull(function: KFunction<*>): MethodMetadata? {
return getMethodMetadataOrNull(function.javaMethodOrConstructor)
}
private class ReflectionMetadataScanner private constructor(
private val config: BConfig,
private val bootstrap: BotCommandsBootstrap
) {
private val classGraphProcessors: List =
config.classGraphProcessors +
bootstrap.classGraphProcessors +
listOf(CommandsPresenceChecker(), ResolverSupertypeChecker(), HandlersPresenceChecker())
private val classMetadataMap: MutableMap, ClassMetadata> = hashMapOf()
private val methodMetadataMap: MutableMap = hashMapOf()
private fun scan() {
val packages = config.packages
val classes = config.classes
require(packages.isNotEmpty() || classes.isNotEmpty()) {
"You must specify at least 1 package or class to scan from"
}
if (packages.isNotEmpty())
logger.debug { "Scanning packages: ${packages.joinToString()}" }
if (classes.isNotEmpty())
logger.debug { "Scanning classes: ${classes.joinToString { it.simpleNestedName }}" }
ClassGraph()
.acceptPackages(
"io.github.freya022.botcommands.api",
"io.github.freya022.botcommands.internal",
*packages.toTypedArray()
)
.acceptClasses(*classes.map { it.name }.toTypedArray())
.enableClassInfo()
.enableMethodInfo()
.enableAnnotationInfo()
.disableModuleScanning()
.scan()
.use { scan ->
val (libClasses, userClasses) = scan.allClasses.partition { it.isFromLib() }
libClasses
.filterLibraryClasses()
.filterClasses()
.processClasses()
userClasses
.filterClasses()
.also {
if (userClasses.isEmpty()) {
logger.warn { "Found no user classes to scan, check the packages set in ${BConfigBuilder::packages.reference}" }
} else if (logger.isTraceEnabled()) {
logger.trace { "Found ${userClasses.size} user classes: ${userClasses.joinToString { it.simpleNestedName }}" }
} else {
logger.debug { "Found ${userClasses.size} user classes" }
}
}
.processClasses()
classGraphProcessors.forEach(ClassGraphProcessor::postProcess)
}
}
private fun ClassInfo.isFromLib() =
packageName.startsWith("io.github.freya022.botcommands.api") || packageName.startsWith("io.github.freya022.botcommands.internal")
private fun List.filterLibraryClasses(): List {
// Get types referenced by factories so we get metadata from those as well
val referencedTypes = asSequence()
.flatMap { it.methodInfo }
.filter { bootstrap.isServiceFactory(it) }
.mapTo(hashSetOf()) { it.typeDescriptor.resultType.toString() }
fun ClassInfo.isServiceOrHasFactories(): Boolean {
return bootstrap.isService(this) || methodInfo.any { bootstrap.isServiceFactory(it) }
}
return filter { classInfo ->
if (classInfo.isServiceOrHasFactories()) return@filter true
// Get metadata from all classes that extend a referenced type
// As we can't know exactly what object a factory could return
val superclasses = (classInfo.superclasses + classInfo.interfaces + classInfo).mapTo(hashSetOf()) { it.name }
if (superclasses.containsAny(referencedTypes)) return@filter true
if (classInfo.outerClasses.any { it.isServiceOrHasFactories() }) return@filter true
if (classInfo.hasAnnotation(Condition::class.java)) return@filter true
if (classInfo.interfaces.containsAny(CustomConditionChecker::class.java, ConditionalServiceChecker::class.java)) return@filter true
return@filter false
}
}
private fun ClassInfoList.containsAny(vararg classes: Class<*>): Boolean = classes.any { containsName(it.name) }
private val lowercaseInnerClassRegex = Regex("\\$[a-z]")
private fun List.filterClasses(): List = 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()
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
}
private fun ClassInfo.checkFacadeFactories() {
this.declaredMethodInfo.forEach { methodInfo ->
check(!bootstrap.isServiceFactory(methodInfo)) {
"Top-level service factories are not supported: ${methodInfo.shortSignature}"
}
}
}
private fun List.processClasses(): List {
return onEach { classInfo ->
try {
val kClass = tryGetClass(classInfo) ?: return@onEach
processMethods(classInfo, kClass)
classMetadataMap[kClass.java] = ClassMetadata(classInfo.sourceFile)
val isService = bootstrap.isService(classInfo)
classGraphProcessors.forEach { it.processClass(classInfo, kClass, isService) }
} catch (e: Throwable) {
e.rethrow("An exception occurred while scanning class: ${classInfo.name}")
}
}
}
private fun tryGetClass(classInfo: ClassInfo): KClass<*>? {
// Ignore unknown classes
return try {
classInfo.loadClass().kotlin
} catch(e: IllegalArgumentException) {
// ClassGraph wraps Class#forName exceptions in an IAE
val cause = e.cause
if (cause is ClassNotFoundException || cause is NoClassDefFoundError) {
return if (logger.isTraceEnabled()) {
logger.traceNull(e) { "Ignoring ${classInfo.name} due to unsatisfied dependency" }
} else {
logger.debugNull { "Ignoring ${classInfo.name} due to unsatisfied dependency: ${cause.message}" }
}
} else {
throw e
}
}
}
private fun processMethods(
classInfo: ClassInfo,
kClass: KClass,
) {
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: Executable = tryGetExecutable(methodInfo) ?: continue
val nullabilities = getMethodParameterNullabilities(methodInfo, method)
methodMetadataMap[method] = MethodMetadata(methodInfo.minLineNum, nullabilities)
val isServiceFactory = bootstrap.isServiceFactory(methodInfo)
classGraphProcessors.forEach { it.processMethod(methodInfo, method, classInfo, kClass, isServiceFactory) }
}
}
private fun tryGetExecutable(methodInfo: MethodInfo): Executable? {
// Ignore methods with missing dependencies (such as parameters from unknown dependencies)
try {
return when {
methodInfo.isConstructor -> methodInfo.loadClassAndGetConstructor()
else -> methodInfo.loadClassAndGetMethod()
}
} catch(e: IllegalArgumentException) {
// ClassGraph wraps exceptions in an IAE
val cause = e.cause
if (cause is ClassNotFoundException || cause is NoClassDefFoundError) {
return if (logger.isTraceEnabled()) {
logger.traceNull(e) { "Ignoring method due to unsatisfied dependencies in ${methodInfo.shortSignature}" }
} else {
logger.debugNull { "Ignoring method due to unsatisfied dependency ${e.message} in ${methodInfo.shortSignature}" }
}
} else {
throw 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
}
}
private val Executable.isSuspend: Boolean
get() = parameters.any { it.type == Continuation::class.java }
companion object {
fun scan(
config: BConfig,
bootstrap: BotCommandsBootstrap,
): ReflectionMetadata {
val scanner = ReflectionMetadataScanner(config, bootstrap)
scanner.scan()
return ReflectionMetadata(
scanner.classMetadataMap.toImmutableMap(),
scanner.methodMetadataMap.toImmutableMap(),
)
}
}
}
internal val Class<*>.sourceFile: String
get() = ReflectionMetadata.instance.getClassMetadata(this).sourceFile
internal val Class<*>.sourceFileOrNull: String?
get() = ReflectionMetadata.instance.getClassMetadataOrNull(this)?.sourceFile
internal val KClass<*>.sourceFile: String
get() = this.java.sourceFile
internal val KClass<*>.sourceFileOrNull: String?
get() = this.java.sourceFileOrNull
internal val KParameter.isNullable: Boolean
get() {
val isNullableAnnotated = ReflectionMetadata.instance.getMethodMetadata(function).nullabilities[index]
return isNullableAnnotated || type.isMarkedNullable
}
internal val KFunction<*>.lineNumber: Int
get() = ReflectionMetadata.instance.getMethodMetadata(this).line
internal val KFunction<*>.lineNumberOrNull: Int?
get() = ReflectionMetadata.instance.getMethodMetadataOrNull(this)?.line
© 2015 - 2025 Weber Informatics LLC | Privacy Policy