From 688e9df5e69cf0a85fbe2d0def4a080f324b859b Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:22:15 +0200 Subject: [PATCH 1/8] Add preprocessed lib classes list (first phase) --- .../internal/utils/ReflectionMetadata.kt | 187 +++++++++++------- build.gradle.kts | 4 + buildSrc/build.gradle.kts | 1 + buildSrc/settings.gradle.kts | 6 + ...lass-list-generator-conventions.gradle.kts | 32 +++ .../tasks/GenerateClassListTask.kt | 38 ++++ .../kotlin/publish-conventions.gradle.kts | 2 + class-list-generator/build.gradle.kts | 11 ++ class-list-generator/settings.gradle.kts | 9 + .../classlist/generator/ClassListGenerator.kt | 111 +++++++++++ 10 files changed, 333 insertions(+), 68 deletions(-) create mode 100644 buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts create mode 100644 buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt create mode 100644 class-list-generator/build.gradle.kts create mode 100644 class-list-generator/settings.gradle.kts create mode 100644 class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt index b1e88c77e..986f295fc 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt @@ -17,6 +17,7 @@ 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.ReflectionMetadataScanner.Companion.filterClasses import io.github.freya022.botcommands.internal.utils.ReflectionUtils.function import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.reflect.Executable @@ -32,6 +33,92 @@ private typealias IsNullableAnnotated = Boolean private val logger = KotlinLogging.logger { } +private interface LibClassesStrategy { + fun configureClassGraph(classGraph: ClassGraph) + + fun partitionClasses(scanResult: ScanResult): Pair, List> + + fun filterLibClasses(libClasses: Collection): Collection +} + +private class DefaultLibClassesStrategy(private val bootstrap: BotCommandsBootstrap) : LibClassesStrategy { + + private val libPackages = ReflectionMetadataScanner::class.java.classLoader + .resources("META-INF/bc.packages") + .asSequence() + .flatMap { it.readText().trim().lineSequence() } + .toList() + + override fun configureClassGraph(classGraph: ClassGraph) { + classGraph.acceptPackages(*libPackages.toTypedArray()) + } + + override fun partitionClasses(scanResult: ScanResult): Pair, List> { + return scanResult.allClasses.partition(::isFromLib) + } + + private fun isFromLib(classInfo: ClassInfo): Boolean { + val pkgName = classInfo.packageName + return libPackages.any { pkgName.startsWith(it) } + } + + override fun filterLibClasses(libClasses: Collection): Collection { + return libClasses.filterLibraryClasses().filterClasses(bootstrap) + } + + private fun Collection.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 class PreprocessedLibClassesStrategy : LibClassesStrategy { + + private val libClasses = ReflectionMetadataScanner::class.java.classLoader + .resources("META-INF/bc.classes") + .asSequence() + .flatMap { it.readText().lineSequence() } + .filter { it.isNotBlank() } + .toHashSet() + + override fun configureClassGraph(classGraph: ClassGraph) { + classGraph.acceptClasses(*libClasses.toTypedArray()) + } + + override fun partitionClasses(scanResult: ScanResult): Pair, List> { + return scanResult.allClasses.partition { it.name in libClasses } + } + + override fun filterLibClasses(libClasses: Collection): Collection { + // Class list is already filtered + return libClasses + } +} + internal class ReflectionMetadata( private val classMetadataMap: Map, ClassMetadata>, private val methodMetadataMap: Map, @@ -111,14 +198,12 @@ private class ReflectionMetadataScanner private constructor( if (classes.isNotEmpty()) logger.debug { "Scanning classes: ${classes.joinToString { it.simpleNestedName }}" } - val libPackages = ReflectionMetadataScanner::class.java.classLoader - .resources("META-INF/bc.packages") - .asSequence() - .flatMap { it.readText().trim().lineSequence() } - .toList() + val classGraphStrategy: LibClassesStrategy = DefaultLibClassesStrategy(bootstrap) + // TODO add config to switch to preprocessed +// val classGraphStrategy: LibClassesStrategy = PreprocessedLibClassesStrategy() ClassGraph() - .acceptPackages(*libPackages.toTypedArray()) + .also(classGraphStrategy::configureClassGraph) .acceptPackages(*packages.toTypedArray()) .acceptClasses(*classes.mapToArray { it.name }) .enableClassInfo() @@ -127,10 +212,9 @@ private class ReflectionMetadataScanner private constructor( .disableModuleScanning() .scan() .use { scan -> - val (libClasses, userClasses) = scan.allClasses.partition { it.isFromLib(libPackages) } + val (libClasses, userClasses) = classGraphStrategy.partitionClasses(scan) libClasses - .filterLibraryClasses() - .filterClasses() + .let(classGraphStrategy::filterLibClasses) .processClasses() userClasses @@ -151,65 +235,6 @@ private class ReflectionMetadataScanner private constructor( } } - private fun ClassInfo.isFromLib(libPackages: List): Boolean { - val pkgName = packageName - return libPackages.any { pkgName.startsWith(it) } - } - - 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 Collection.processClasses(): Unit = forEach { classInfo -> try { val clazz = tryGetClass(classInfo) ?: return@forEach @@ -307,6 +332,32 @@ private class ReflectionMetadataScanner private constructor( get() = parameters.any { it.type == Continuation::class.java } companion object { + private val lowercaseInnerClassRegex = Regex("\\$[a-z]") + + fun List.filterClasses(bootstrap: BotCommandsBootstrap): 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(bootstrap) + 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 + } + + fun ClassInfo.checkFacadeFactories(bootstrap: BotCommandsBootstrap) { + this.declaredMethodInfo.forEach { methodInfo -> + check(!bootstrap.isServiceFactory(methodInfo)) { + "Top-level service factories are not supported: ${methodInfo.shortSignature}" + } + } + } + fun scan( config: BConfig, bootstrap: BotCommandsBootstrap, diff --git a/build.gradle.kts b/build.gradle.kts index 50d4e6d59..2dfde0f88 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,6 +58,10 @@ dependencies { dokka(projects.botCommandsRestarter) } +tasks.generateClassList { + enabled = false +} + tasks.withType { useJUnitPlatform() } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index ea5284ec6..76bde13a8 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(libs.dokka.plugin) implementation("dev.freya02:spring-configuration-metadata-generator") + implementation("dev.freya02:class-list-generator") } tasks.withType { diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 7f4141c36..c245769f5 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -13,3 +13,9 @@ includeBuild("../spring-configuration-metadata-generator") { substitute(module("dev.freya02:spring-configuration-metadata-generator")).using(project(":")) } } + +includeBuild("../class-list-generator") { + dependencySubstitution { + substitute(module("dev.freya02:class-list-generator")).using(project(":")) + } +} diff --git a/buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts b/buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts new file mode 100644 index 000000000..4efdb2330 --- /dev/null +++ b/buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts @@ -0,0 +1,32 @@ +import dev.freya02.botcommands.tasks.GenerateClassListTask +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} + +val compileJava by tasks.getting(JavaCompile::class) +val compileKotlin by tasks.getting(KotlinCompile::class) + +val generateClassList by tasks.registering(GenerateClassListTask::class) { + buildDirs = listOf( + layout.buildDirectory.dir("classes/java/main").get().asFile.path, + layout.buildDirectory.dir("classes/kotlin/main").get().asFile.path, + ) + // This is necessary as meta-annotations from dependencies cannot be resolved without them on the classpath + classpath = configurations.compileClasspath.get().files.map { it.path } + + // Only regenerate list if classes changes + classes.from(compileJava.outputs.files, compileKotlin.outputs.files) + + outputRoot = layout.buildDirectory.dir("generated/sources/lib-class-list/main/resources") +} + +// Register our generated sources +sourceSets { + main { + resources { + srcDir(generateClassList) + } + } +} diff --git a/buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt b/buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt new file mode 100644 index 000000000..6695c7c7f --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt @@ -0,0 +1,38 @@ +package dev.freya02.botcommands.tasks + +import dev.freya02.bc.classlist.generator.ClassListGenerator +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.* +import java.io.File + +abstract class GenerateClassListTask : DefaultTask() { + + // Only for Gradle caching purposes + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val classes: ConfigurableFileCollection + + @get:Input + abstract val buildDirs: ListProperty + + @get:Input + abstract val classpath: ListProperty + + @get:OutputDirectory + abstract val outputRoot: DirectoryProperty + + @TaskAction + fun generate() { + val classList = ClassListGenerator.generate(buildDirs.get().map(::File), classpath.get().map(::File)) + if (classList.isBlank()) { + return + } + + val outputFile = outputRoot.get().asFile.resolve("META-INF").resolve("bc.classes") + outputFile.parentFile.mkdirs() + outputFile.writeText(classList) + } +} diff --git a/buildSrc/src/main/kotlin/publish-conventions.gradle.kts b/buildSrc/src/main/kotlin/publish-conventions.gradle.kts index 419a21101..16ad31b9a 100644 --- a/buildSrc/src/main/kotlin/publish-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/publish-conventions.gradle.kts @@ -6,6 +6,8 @@ plugins { `java-library` signing id("com.vanniktech.maven.publish") + // Better run the generator and have it run unnecessarily than have bugs when it should had been run + id("class-list-generator-conventions") } val environment = project.extensions.create("publishedProjectEnvironment") diff --git a/class-list-generator/build.gradle.kts b/class-list-generator/build.gradle.kts new file mode 100644 index 000000000..ef982c868 --- /dev/null +++ b/class-list-generator/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.kotlin) +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(libs.classgraph) +} diff --git a/class-list-generator/settings.gradle.kts b/class-list-generator/settings.gradle.kts new file mode 100644 index 000000000..be70ff02d --- /dev/null +++ b/class-list-generator/settings.gradle.kts @@ -0,0 +1,9 @@ +rootProject.name = "class-list-generator" + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt new file mode 100644 index 000000000..2fc7bfbea --- /dev/null +++ b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt @@ -0,0 +1,111 @@ +package dev.freya02.bc.classlist.generator + +import io.github.classgraph.ClassGraph +import io.github.classgraph.ClassInfo +import io.github.classgraph.MethodInfo +import java.io.File + +object ClassListGenerator { + + private val lowercaseInnerClassRegex = Regex("\\$[a-z]") + + // TODO make tests that depend on the core module to check that those exists + private const val BSERVICE_ANNOTATION = "io.github.freya022.botcommands.api.core.service.annotations.BService" + private const val CONDITION_ANNOTATION = "io.github.freya022.botcommands.api.core.service.annotations.Condition" + private const val CONDITIONAL_SERVICE_CHECKER_ANNOTATION = "io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker" + private const val CUSTOM_CONDITION_CHECKER_ANNOTATION = "io.github.freya022.botcommands.api.core.service.CustomConditionChecker" + + private const val COMPONENT_ANNOTATION_NAME = "org.springframework.stereotype.Component" + private const val BEAN_ANNOTATION_NAME = "org.springframework.context.annotation.Bean" + + fun generate(buildDirs: List, classpath: List): String { + // We've set CG's classpath to the entire "compileClasspath" configuration, + // but we only need to make a filtered class list of the current project, + // so we can tell CG to only look at classes that we can find in the project's file tree + // while allowing CG to resolve necessary stuff from dependencies, like meta annotations + // which are crucial for this task to work. + val builtClassNames = buildDirs.asSequence() + .flatMap { buildDir -> + buildDir.walk() + .filter { it.extension == "class" } + .map { it.toRelativeString(buildDir) } + } + .map { it.replace('/', '.').removeSuffix(".class") } + .toList() + + ClassGraph() + // Classpath includes compiled project classes + dependencies (required for CG to discover all meta-annotations) + .overrideClasspath(buildDirs + classpath) + // Only compiled project classes + .acceptClasses(*builtClassNames.toTypedArray()) + .enableClassInfo() + .enableMethodInfo() + .enableAnnotationInfo() + .disableModuleScanning() + .scan().use { scanResult -> + return scanResult.allClasses + .filterClasses() + .filterLibraryClasses() + .joinToString("\n") { it.name } + } + } + + 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(::isServiceFactory) + .mapTo(hashSetOf()) { it.typeDescriptor.resultType.toString() } + + return filter { classInfo -> filterLibraryClass(classInfo, referencedTypes) } + } + + private fun filterLibraryClass(classInfo: ClassInfo, referencedTypes: Set): Boolean { + if (classInfo.isServiceOrHasFactories()) + return true + + val interfaces = classInfo.interfaces + val allClasses = classInfo.superclasses + interfaces + classInfo + // Get metadata from all classes that extend a referenced type + // As we can't know exactly what object a factory could return + if (allClasses.mapTo(hashSetOf()) { it.name }.containsAny(referencedTypes)) + return true + + if (classInfo.outerClasses.any { it.isServiceOrHasFactories() }) + return true + if (classInfo.isAnnotation && classInfo.hasAnnotation(CONDITION_ANNOTATION)) + return true + if (interfaces.any { it.name == CONDITIONAL_SERVICE_CHECKER_ANNOTATION || it.name == CUSTOM_CONDITION_CHECKER_ANNOTATION }) + return true + + return false + } + + private fun ClassInfo.isServiceOrHasFactories(): Boolean { + return isService(this) || methodInfo.any(::isServiceFactory) + } + + // TODO try to find a way to make this more maintainable + private fun List.filterClasses(): List = filter { + it.annotationInfo.directOnly()["kotlin.Metadata"]?.let { annotationInfo -> + //Only keep classes, not others such as file facades + val kind = annotationInfo.parameterValues["k"].value as Int + if (kind != 1) { // Class + return@filter false + } + } + + if (lowercaseInnerClassRegex.containsMatchIn(it.name)) return@filter false + return@filter !it.isSynthetic && !it.isEnum && !it.isRecord + } + + private fun Iterable.containsAny(elements: Iterable): Boolean = elements.any { it in this } + + private fun isService(classInfo: ClassInfo): Boolean { + return classInfo.hasAnnotation(BSERVICE_ANNOTATION) || classInfo.hasAnnotation(COMPONENT_ANNOTATION_NAME) + } + + private fun isServiceFactory(methodInfo: MethodInfo): Boolean { + return methodInfo.hasAnnotation(BSERVICE_ANNOTATION) || methodInfo.hasAnnotation(BEAN_ANNOTATION_NAME) + } +} From d3e33146fddcad71fba340f993f491419155f6a6 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:13:46 +0200 Subject: [PATCH 2/8] Move common reflection scanning code to a shared directory --- BotCommands-core/build.gradle.kts | 34 ++++++++ .../internal/utils/ReflectionMetadata.kt | 82 ++++++------------- .../ReflectionMetadataScannerHelperTests.kt | 19 +++++ class-list-generator/build.gradle.kts | 1 + class-list-generator/settings.gradle.kts | 6 ++ .../classlist/generator/ClassListGenerator.kt | 69 ++-------------- reflection-metadata-commons/build.gradle.kts | 26 ++++++ .../settings.gradle.kts | 9 ++ .../ReflectionMetadataScannerHelper.kt | 80 ++++++++++++++++++ 9 files changed, 205 insertions(+), 121 deletions(-) create mode 100644 BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt create mode 100644 reflection-metadata-commons/build.gradle.kts create mode 100644 reflection-metadata-commons/settings.gradle.kts create mode 100644 reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt diff --git a/BotCommands-core/build.gradle.kts b/BotCommands-core/build.gradle.kts index 95020f2ae..119fb1a28 100644 --- a/BotCommands-core/build.gradle.kts +++ b/BotCommands-core/build.gradle.kts @@ -20,6 +20,14 @@ registerSourceSet(name = "kotlinDocExamples") val byteBuddyAgent: Configuration by configurations.creating +val embedded: Configuration by configurations.creating { + isTransitive = false +} + +configurations.compileOnly { + extendsFrom(embedded) +} + dependencies { // -------------------- CORE DEPENDENCIES -------------------- @@ -31,6 +39,8 @@ dependencies { api(libs.slf4j.api) implementation(libs.kotlin.logging) + embedded("dev.freya02:reflection-metadata-commons") + // JDA compileOnly(libs.jda) implementation(projects.botCommandsJdaKtx) @@ -110,6 +120,30 @@ dependencies { testImplementation(projects.botCommandsLocalization) testImplementation(libs.kotlin.metadata) + + testCompileOnly("dev.freya02:reflection-metadata-commons") +} + +val embeddedDepsDir = layout.buildDirectory.dir("generated/bins/reflection-metadata-commons") + +val copyEmbeddedDependencies by tasks.registering(Copy::class) { + description = "Copies contents of embedded dependencies" + + for (file in embedded.files) { + from(zipTree(file)) { + duplicatesStrategy = DuplicatesStrategy.FAIL + + exclude("META-INF/MANIFEST.MF") + exclude("META-INF/*.kotlin_module") + } + } + into(embeddedDepsDir) +} + +sourceSets { + main { + output.dir(copyEmbeddedDependencies) + } } tasks.withType { diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt index 986f295fc..f8280e0dd 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt @@ -1,15 +1,16 @@ package io.github.freya022.botcommands.internal.utils +import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper import io.github.classgraph.* 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.reflect.annotations.ExperimentalReflectionApi -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.api.core.utils.javaMethodOrConstructor +import io.github.freya022.botcommands.api.core.utils.mapToArray +import io.github.freya022.botcommands.api.core.utils.simpleNestedName +import io.github.freya022.botcommands.api.core.utils.toImmutableMap import io.github.freya022.botcommands.internal.core.ClassPathProcessor import io.github.freya022.botcommands.internal.core.ClassPathProcessorProvider import io.github.freya022.botcommands.internal.core.HandlersPresenceChecker @@ -17,7 +18,6 @@ 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.ReflectionMetadataScanner.Companion.filterClasses import io.github.freya022.botcommands.internal.utils.ReflectionUtils.function import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.reflect.Executable @@ -26,7 +26,6 @@ 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.streams.asSequence private typealias IsNullableAnnotated = Boolean @@ -41,7 +40,10 @@ private interface LibClassesStrategy { fun filterLibClasses(libClasses: Collection): Collection } -private class DefaultLibClassesStrategy(private val bootstrap: BotCommandsBootstrap) : LibClassesStrategy { +private class DefaultLibClassesStrategy( + private val helper: ReflectionMetadataScannerHelper, + private val bootstrap: BotCommandsBootstrap, +) : LibClassesStrategy { private val libPackages = ReflectionMetadataScanner::class.java.classLoader .resources("META-INF/bc.packages") @@ -63,37 +65,11 @@ private class DefaultLibClassesStrategy(private val bootstrap: BotCommandsBootst } override fun filterLibClasses(libClasses: Collection): Collection { - return libClasses.filterLibraryClasses().filterClasses(bootstrap) + return ReflectionMetadataScannerHelper.filterClasses( + helper.filterLibraryClasses(libClasses), + onFileFacade = { ReflectionMetadataScanner.checkFacadeFactories(it, bootstrap) } + ) } - - private fun Collection.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 class PreprocessedLibClassesStrategy : LibClassesStrategy { @@ -198,7 +174,8 @@ private class ReflectionMetadataScanner private constructor( if (classes.isNotEmpty()) logger.debug { "Scanning classes: ${classes.joinToString { it.simpleNestedName }}" } - val classGraphStrategy: LibClassesStrategy = DefaultLibClassesStrategy(bootstrap) + val helper = ReflectionMetadataScannerHelper(bootstrap::isService, bootstrap::isServiceFactory) + val classGraphStrategy: LibClassesStrategy = DefaultLibClassesStrategy(helper, bootstrap) // TODO add config to switch to preprocessed // val classGraphStrategy: LibClassesStrategy = PreprocessedLibClassesStrategy() @@ -218,7 +195,12 @@ private class ReflectionMetadataScanner private constructor( .processClasses() userClasses - .filterClasses() + .let { + ReflectionMetadataScannerHelper.filterClasses( + it, + onFileFacade = { c -> checkFacadeFactories(c, bootstrap) }, + ) + } .also { if (userClasses.isEmpty()) { logger.warn { "Found no user classes to scan, check the packages set in ${BConfigBuilder::packages.reference}" } @@ -332,26 +314,8 @@ private class ReflectionMetadataScanner private constructor( get() = parameters.any { it.type == Continuation::class.java } companion object { - private val lowercaseInnerClassRegex = Regex("\\$[a-z]") - - fun List.filterClasses(bootstrap: BotCommandsBootstrap): 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(bootstrap) - 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 - } - - fun ClassInfo.checkFacadeFactories(bootstrap: BotCommandsBootstrap) { - this.declaredMethodInfo.forEach { methodInfo -> + fun checkFacadeFactories(classInfo: ClassInfo, bootstrap: BotCommandsBootstrap) { + classInfo.declaredMethodInfo.forEach { methodInfo -> check(!bootstrap.isServiceFactory(methodInfo)) { "Top-level service factories are not supported: ${methodInfo.shortSignature}" } diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt new file mode 100644 index 000000000..ac3061020 --- /dev/null +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt @@ -0,0 +1,19 @@ +package io.github.freya022.botcommands.reflection + +import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +object ReflectionMetadataScannerHelperTests { + @ParameterizedTest + @MethodSource("classNamesToCheck") + fun `Check class names exist`(className: String) { + assertDoesNotThrow { Class.forName(className, false, Thread.currentThread().contextClassLoader) } + } + + @JvmStatic + fun classNamesToCheck(): List { + return ReflectionMetadataScannerHelper.classNamesToCheck + } +} diff --git a/class-list-generator/build.gradle.kts b/class-list-generator/build.gradle.kts index ef982c868..b9500f711 100644 --- a/class-list-generator/build.gradle.kts +++ b/class-list-generator/build.gradle.kts @@ -8,4 +8,5 @@ repositories { dependencies { implementation(libs.classgraph) + implementation("dev.freya02:reflection-metadata-commons") } diff --git a/class-list-generator/settings.gradle.kts b/class-list-generator/settings.gradle.kts index be70ff02d..98dfd349c 100644 --- a/class-list-generator/settings.gradle.kts +++ b/class-list-generator/settings.gradle.kts @@ -7,3 +7,9 @@ dependencyResolutionManagement { } } } + +includeBuild("../reflection-metadata-commons") { + dependencySubstitution { + substitute(module("dev.freya02:reflection-metadata-commons")).using(project(":")) + } +} diff --git a/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt index 2fc7bfbea..e818f427e 100644 --- a/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt +++ b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt @@ -1,5 +1,7 @@ package dev.freya02.bc.classlist.generator +import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper +import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper.Companion.BSERVICE_ANNOTATION import io.github.classgraph.ClassGraph import io.github.classgraph.ClassInfo import io.github.classgraph.MethodInfo @@ -7,14 +9,6 @@ import java.io.File object ClassListGenerator { - private val lowercaseInnerClassRegex = Regex("\\$[a-z]") - - // TODO make tests that depend on the core module to check that those exists - private const val BSERVICE_ANNOTATION = "io.github.freya022.botcommands.api.core.service.annotations.BService" - private const val CONDITION_ANNOTATION = "io.github.freya022.botcommands.api.core.service.annotations.Condition" - private const val CONDITIONAL_SERVICE_CHECKER_ANNOTATION = "io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker" - private const val CUSTOM_CONDITION_CHECKER_ANNOTATION = "io.github.freya022.botcommands.api.core.service.CustomConditionChecker" - private const val COMPONENT_ANNOTATION_NAME = "org.springframework.stereotype.Component" private const val BEAN_ANNOTATION_NAME = "org.springframework.context.annotation.Bean" @@ -33,6 +27,8 @@ object ClassListGenerator { .map { it.replace('/', '.').removeSuffix(".class") } .toList() + val helper = ReflectionMetadataScannerHelper(::isService, ::isServiceFactory) + ClassGraph() // Classpath includes compiled project classes + dependencies (required for CG to discover all meta-annotations) .overrideClasspath(buildDirs + classpath) @@ -43,64 +39,13 @@ object ClassListGenerator { .enableAnnotationInfo() .disableModuleScanning() .scan().use { scanResult -> - return scanResult.allClasses - .filterClasses() - .filterLibraryClasses() + return ReflectionMetadataScannerHelper + .filterClasses(scanResult.allClasses, onFileFacade = { /* noop */ }) + .let(helper::filterLibraryClasses) .joinToString("\n") { it.name } } } - 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(::isServiceFactory) - .mapTo(hashSetOf()) { it.typeDescriptor.resultType.toString() } - - return filter { classInfo -> filterLibraryClass(classInfo, referencedTypes) } - } - - private fun filterLibraryClass(classInfo: ClassInfo, referencedTypes: Set): Boolean { - if (classInfo.isServiceOrHasFactories()) - return true - - val interfaces = classInfo.interfaces - val allClasses = classInfo.superclasses + interfaces + classInfo - // Get metadata from all classes that extend a referenced type - // As we can't know exactly what object a factory could return - if (allClasses.mapTo(hashSetOf()) { it.name }.containsAny(referencedTypes)) - return true - - if (classInfo.outerClasses.any { it.isServiceOrHasFactories() }) - return true - if (classInfo.isAnnotation && classInfo.hasAnnotation(CONDITION_ANNOTATION)) - return true - if (interfaces.any { it.name == CONDITIONAL_SERVICE_CHECKER_ANNOTATION || it.name == CUSTOM_CONDITION_CHECKER_ANNOTATION }) - return true - - return false - } - - private fun ClassInfo.isServiceOrHasFactories(): Boolean { - return isService(this) || methodInfo.any(::isServiceFactory) - } - - // TODO try to find a way to make this more maintainable - private fun List.filterClasses(): List = filter { - it.annotationInfo.directOnly()["kotlin.Metadata"]?.let { annotationInfo -> - //Only keep classes, not others such as file facades - val kind = annotationInfo.parameterValues["k"].value as Int - if (kind != 1) { // Class - return@filter false - } - } - - if (lowercaseInnerClassRegex.containsMatchIn(it.name)) return@filter false - return@filter !it.isSynthetic && !it.isEnum && !it.isRecord - } - - private fun Iterable.containsAny(elements: Iterable): Boolean = elements.any { it in this } - private fun isService(classInfo: ClassInfo): Boolean { return classInfo.hasAnnotation(BSERVICE_ANNOTATION) || classInfo.hasAnnotation(COMPONENT_ANNOTATION_NAME) } diff --git a/reflection-metadata-commons/build.gradle.kts b/reflection-metadata-commons/build.gradle.kts new file mode 100644 index 000000000..f2fc571ff --- /dev/null +++ b/reflection-metadata-commons/build.gradle.kts @@ -0,0 +1,26 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.kotlin) +} + +group = "dev.freya02" + +repositories { + mavenCentral() +} + +dependencies { + implementation(libs.classgraph) +} + +tasks.named("compileJava") { + options.release = 17 +} + +tasks.named("compileKotlin") { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } +} diff --git a/reflection-metadata-commons/settings.gradle.kts b/reflection-metadata-commons/settings.gradle.kts new file mode 100644 index 000000000..45f79e913 --- /dev/null +++ b/reflection-metadata-commons/settings.gradle.kts @@ -0,0 +1,9 @@ +rootProject.name = "reflection-metadata-commons" + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt b/reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt new file mode 100644 index 000000000..adfb1c009 --- /dev/null +++ b/reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt @@ -0,0 +1,80 @@ +package dev.freya02.bc.reflection.metadata + +import io.github.classgraph.ClassInfo +import io.github.classgraph.MethodInfo + +class ReflectionMetadataScannerHelper( + private val isService: (ClassInfo) -> Boolean, + private val isServiceFactory: (MethodInfo) -> Boolean, +) { + + fun filterLibraryClasses(classes: Collection): List { + // Get types referenced by factories so we get metadata from those as well + val referencedTypes = classes.asSequence() + .flatMap { it.methodInfo } + .filter { isServiceFactory(it) } + .mapTo(hashSetOf()) { it.typeDescriptor.resultType.toString() } + + return classes.filter { classInfo -> filterLibraryClass(classInfo, referencedTypes) } + } + + private fun filterLibraryClass(classInfo: ClassInfo, referencedTypes: Set): Boolean { + if (classInfo.isServiceOrHasFactories()) + return true + + val interfaces = classInfo.interfaces + // 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 + interfaces + classInfo).mapTo(hashSetOf()) { it.name } + if (superclasses.containsAny(referencedTypes)) + return true + + if (classInfo.outerClasses.any { it.isServiceOrHasFactories() }) + return true + if (classInfo.hasAnnotation(CONDITION_ANNOTATION)) + return true + + if (interfaces.any { it.name == CONDITIONAL_SERVICE_CHECKER_ANNOTATION || it.name == CUSTOM_CONDITION_CHECKER_ANNOTATION }) + return true + + return false + } + + private fun ClassInfo.isServiceOrHasFactories(): Boolean { + return isService(this) || methodInfo.any { isServiceFactory(it) } + } + + private fun Set.containsAny(elements: Set): Boolean = elements.any { it in this } + + companion object { + private val lowercaseInnerClassRegex = Regex("\\$[a-z]") + + const val BSERVICE_ANNOTATION = "io.github.freya022.botcommands.api.core.service.annotations.BService" + private const val CONDITION_ANNOTATION = "io.github.freya022.botcommands.api.core.service.annotations.Condition" + private const val CONDITIONAL_SERVICE_CHECKER_ANNOTATION = "io.github.freya022.botcommands.api.core.service.ConditionalServiceChecker" + private const val CUSTOM_CONDITION_CHECKER_ANNOTATION = "io.github.freya022.botcommands.api.core.service.CustomConditionChecker" + + val classNamesToCheck = listOf( + BSERVICE_ANNOTATION, + CONDITION_ANNOTATION, + CONDITIONAL_SERVICE_CHECKER_ANNOTATION, + CUSTOM_CONDITION_CHECKER_ANNOTATION, + ) + + fun filterClasses(classes: Collection, onFileFacade: (ClassInfo) -> Unit): List = classes.filter { classInfo -> + classInfo.annotationInfo.directOnly()["kotlin.Metadata"]?.let { annotationInfo -> + //Only keep classes, not others such as file facades + val kind = annotationInfo.parameterValues["k"].value as Int + if (kind == 2) { // File facade + onFileFacade(classInfo) + return@filter false + } else if (kind != 1) { // Class + return@filter false + } + } + + if (lowercaseInnerClassRegex.containsMatchIn(classInfo.name)) return@filter false + return@filter !classInfo.isSynthetic && !classInfo.isEnum && !classInfo.isRecord + } + } +} From ea85931b765d90d6b5ea6c67338741e4a3036329 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:57:26 +0200 Subject: [PATCH 3/8] Add switch for preprocessed lib classes --- .../botcommands/api/core/config/BConfig.kt | 10 ++++++++++ .../internal/utils/ReflectionMetadata.kt | 14 ++++++++------ .../core/config/BotCommandsConfigurations.kt | 4 ++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt index 97557143e..10b4dc536 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt @@ -4,6 +4,7 @@ import io.github.freya022.botcommands.api.ReceiverConsumer import io.github.freya022.botcommands.api.core.BContext import io.github.freya022.botcommands.api.core.BotOwners import io.github.freya022.botcommands.api.core.annotations.BEventListener +import io.github.freya022.botcommands.api.core.annotations.ExperimentalCoreApi import io.github.freya022.botcommands.api.core.requests.PriorityGlobalRestRateLimiter import io.github.freya022.botcommands.api.core.service.annotations.InjectedService import io.github.freya022.botcommands.api.core.utils.enumSetOf @@ -84,6 +85,11 @@ interface BConfigProps { ) val classes: Set> + /** + * Instructs the reflection metadata scanner to use a predefined list of library classes. + */ + val usePreprocessedLibClassList: Boolean + /** * Disables sending exceptions to the bot owners. * @@ -180,6 +186,9 @@ class BConfigBuilder : BConfigProps { override val packages: MutableSet = HashSet() override val classes: MutableSet> = HashSet() + @ExperimentalCoreApi + override var usePreprocessedLibClassList: Boolean = false + override val predefinedOwnerIds: MutableSet = HashSet() @set:JvmName("disableExceptionsInDMs") @@ -324,6 +333,7 @@ class BConfigBuilder : BConfigProps { override val predefinedOwnerIds = this@BConfigBuilder.predefinedOwnerIds.toImmutableSet() override val packages = this@BConfigBuilder.packages.toImmutableSet() override val classes = this@BConfigBuilder.classes.toImmutableSet() + override val usePreprocessedLibClassList = this@BConfigBuilder.usePreprocessedLibClassList override val disableExceptionsInDMs = this@BConfigBuilder.disableExceptionsInDMs override val enableOwnerBypass = this@BConfigBuilder.enableOwnerBypass override val ignoredIntents = this@BConfigBuilder.ignoredIntents.toImmutableSet() diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt index f8280e0dd..c3f0f9719 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt @@ -175,12 +175,14 @@ private class ReflectionMetadataScanner private constructor( logger.debug { "Scanning classes: ${classes.joinToString { it.simpleNestedName }}" } val helper = ReflectionMetadataScannerHelper(bootstrap::isService, bootstrap::isServiceFactory) - val classGraphStrategy: LibClassesStrategy = DefaultLibClassesStrategy(helper, bootstrap) - // TODO add config to switch to preprocessed -// val classGraphStrategy: LibClassesStrategy = PreprocessedLibClassesStrategy() + val libClassesStrategy: LibClassesStrategy = if (config.usePreprocessedLibClassList) { + PreprocessedLibClassesStrategy() + } else { + DefaultLibClassesStrategy(helper, bootstrap) + } ClassGraph() - .also(classGraphStrategy::configureClassGraph) + .also(libClassesStrategy::configureClassGraph) .acceptPackages(*packages.toTypedArray()) .acceptClasses(*classes.mapToArray { it.name }) .enableClassInfo() @@ -189,9 +191,9 @@ private class ReflectionMetadataScanner private constructor( .disableModuleScanning() .scan() .use { scan -> - val (libClasses, userClasses) = classGraphStrategy.partitionClasses(scan) + val (libClasses, userClasses) = libClassesStrategy.partitionClasses(scan) libClasses - .let(classGraphStrategy::filterLibClasses) + .let(libClassesStrategy::filterLibClasses) .processClasses() userClasses diff --git a/BotCommands-spring/src/main/kotlin/io/github/freya022/botcommands/internal/core/config/BotCommandsConfigurations.kt b/BotCommands-spring/src/main/kotlin/io/github/freya022/botcommands/internal/core/config/BotCommandsConfigurations.kt index 03823574f..588336810 100644 --- a/BotCommands-spring/src/main/kotlin/io/github/freya022/botcommands/internal/core/config/BotCommandsConfigurations.kt +++ b/BotCommands-spring/src/main/kotlin/io/github/freya022/botcommands/internal/core/config/BotCommandsConfigurations.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.internal.core.config +import io.github.freya022.botcommands.api.core.annotations.ExperimentalCoreApi import io.github.freya022.botcommands.api.core.config.* import net.dv8tion.jda.api.requests.GatewayIntent import org.springframework.boot.context.properties.ConfigurationProperties @@ -11,6 +12,7 @@ internal class BotCommandsCoreConfiguration( override val predefinedOwnerIds: Set = emptySet(), override val packages: Set = emptySet(), override val classes: Set> = emptySet(), + override val usePreprocessedLibClassList: Boolean = false, override val disableExceptionsInDMs: Boolean = false, override val enableOwnerBypass: Boolean = false, override val ignoredIntents: Set = emptySet(), @@ -23,6 +25,8 @@ internal fun BConfigBuilder.applyConfig(configuration: BotCommandsCoreConfigurat predefinedOwnerIds += configuration.predefinedOwnerIds packages += configuration.packages classes += configuration.classes + @OptIn(ExperimentalCoreApi::class) + usePreprocessedLibClassList = configuration.usePreprocessedLibClassList disableExceptionsInDMs = configuration.disableExceptionsInDMs enableOwnerBypass = configuration.enableOwnerBypass ignoredIntents += configuration.ignoredIntents From 54b7db9c7820812c11ebcbd0f7b60e8a2e69699d Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:01:10 +0200 Subject: [PATCH 4/8] test-bot: sometimes use preprocessed lib classes --- .../src/test/kotlin/dev/freya02/botcommands/bot/Main.kt | 6 ++++++ .../resources/META-INF/BotCommands-restarter.properties | 1 + 2 files changed, 7 insertions(+) diff --git a/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt index f6ac96635..3d62f6a88 100644 --- a/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt +++ b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt @@ -8,11 +8,13 @@ import dev.freya02.botcommands.restarter.api.BotCommandsRestarter import dev.freya02.botcommands.restarter.api.annotations.ExperimentalRestartApi import dev.freya02.botcommands.restarter.internal.utils.AppClasspath import io.github.freya022.botcommands.api.core.BotCommands +import io.github.freya022.botcommands.api.core.annotations.ExperimentalCoreApi import io.github.freya022.botcommands.api.core.config.* import io.github.freya022.botcommands.api.core.utils.joinAsList import io.github.oshai.kotlinlogging.KotlinLogging import net.dv8tion.jda.api.interactions.DiscordLocale import kotlin.io.path.absolutePathString +import kotlin.random.Random import kotlin.system.exitProcess import kotlin.time.Duration.Companion.milliseconds @@ -37,6 +39,10 @@ object Main { MethodAccessorsConfig.preferClassFileAccessors() BotCommands.create { + // This will produce a few (soft) errors as it will scan classes related to the Spring support, + // which requires some compile-only classes + @OptIn(ExperimentalCoreApi::class) + usePreprocessedLibClassList = Random.nextBoolean() disableExceptionsInDMs = true addSearchPath("dev.freya02.botcommands.bot") diff --git a/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties b/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties index db676b9f4..8ef090c1b 100644 --- a/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties +++ b/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties @@ -8,6 +8,7 @@ restart.exclude.restarter-prod-res=BotCommands-restarter/build/resources/main restart.exclude.core-prod=BotCommands-core/build/classes/(?:kotlin|java)/main restart.exclude.core-prod-res=BotCommands-core/build/resources/main +restart.exclude.core-prod-generated-bins=BotCommands-core/build/generated/bins/reflection-metadata-commons restart.exclude.rate-limit-prod=BotCommands-rate-limit/build/classes/(?:kotlin|java)/main restart.exclude.rate-limit-prod-res=BotCommands-rate-limit/build/resources/main From 8cc160779571cc6d66b69b04e42ec098f52a39a9 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:11:54 +0200 Subject: [PATCH 5/8] Rename common module --- BotCommands-core/build.gradle.kts | 6 +++--- .../botcommands/internal/utils/ReflectionMetadata.kt | 10 +++++----- .../reflection/ReflectionMetadataScannerHelperTests.kt | 4 ++-- class-list-generator/build.gradle.kts | 2 +- class-list-generator/settings.gradle.kts | 4 ++-- .../bc/classlist/generator/ClassListGenerator.kt | 8 ++++---- .../build.gradle.kts | 0 .../settings.gradle.kts | 2 +- .../reflection/classpath/ClasspathScannerHelper.kt | 4 ++-- .../META-INF/BotCommands-restarter.properties | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) rename {reflection-metadata-commons => classpath-scanner-commons}/build.gradle.kts (100%) rename {reflection-metadata-commons => classpath-scanner-commons}/settings.gradle.kts (75%) rename reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt => classpath-scanner-commons/src/main/kotlin/dev/freya02/bc/internal/reflection/classpath/ClasspathScannerHelper.kt (97%) diff --git a/BotCommands-core/build.gradle.kts b/BotCommands-core/build.gradle.kts index 119fb1a28..4584f3ad9 100644 --- a/BotCommands-core/build.gradle.kts +++ b/BotCommands-core/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { api(libs.slf4j.api) implementation(libs.kotlin.logging) - embedded("dev.freya02:reflection-metadata-commons") + embedded("dev.freya02:classpath-scanner-commons") // JDA compileOnly(libs.jda) @@ -121,10 +121,10 @@ dependencies { testImplementation(libs.kotlin.metadata) - testCompileOnly("dev.freya02:reflection-metadata-commons") + testCompileOnly("dev.freya02:classpath-scanner-commons") } -val embeddedDepsDir = layout.buildDirectory.dir("generated/bins/reflection-metadata-commons") +val embeddedDepsDir = layout.buildDirectory.dir("generated/bins/classpath-scanner-commons") val copyEmbeddedDependencies by tasks.registering(Copy::class) { description = "Copies contents of embedded dependencies" diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt index c3f0f9719..87a3eb4b4 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/utils/ReflectionMetadata.kt @@ -1,6 +1,6 @@ package io.github.freya022.botcommands.internal.utils -import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper +import dev.freya02.bc.internal.reflection.classpath.ClasspathScannerHelper import io.github.classgraph.* import io.github.freya022.botcommands.api.core.config.BConfig import io.github.freya022.botcommands.api.core.config.BConfigBuilder @@ -41,7 +41,7 @@ private interface LibClassesStrategy { } private class DefaultLibClassesStrategy( - private val helper: ReflectionMetadataScannerHelper, + private val helper: ClasspathScannerHelper, private val bootstrap: BotCommandsBootstrap, ) : LibClassesStrategy { @@ -65,7 +65,7 @@ private class DefaultLibClassesStrategy( } override fun filterLibClasses(libClasses: Collection): Collection { - return ReflectionMetadataScannerHelper.filterClasses( + return ClasspathScannerHelper.filterClasses( helper.filterLibraryClasses(libClasses), onFileFacade = { ReflectionMetadataScanner.checkFacadeFactories(it, bootstrap) } ) @@ -174,7 +174,7 @@ private class ReflectionMetadataScanner private constructor( if (classes.isNotEmpty()) logger.debug { "Scanning classes: ${classes.joinToString { it.simpleNestedName }}" } - val helper = ReflectionMetadataScannerHelper(bootstrap::isService, bootstrap::isServiceFactory) + val helper = ClasspathScannerHelper(bootstrap::isService, bootstrap::isServiceFactory) val libClassesStrategy: LibClassesStrategy = if (config.usePreprocessedLibClassList) { PreprocessedLibClassesStrategy() } else { @@ -198,7 +198,7 @@ private class ReflectionMetadataScanner private constructor( userClasses .let { - ReflectionMetadataScannerHelper.filterClasses( + ClasspathScannerHelper.filterClasses( it, onFileFacade = { c -> checkFacadeFactories(c, bootstrap) }, ) diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt index ac3061020..524fda865 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/reflection/ReflectionMetadataScannerHelperTests.kt @@ -1,6 +1,6 @@ package io.github.freya022.botcommands.reflection -import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper +import dev.freya02.bc.internal.reflection.classpath.ClasspathScannerHelper import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -14,6 +14,6 @@ object ReflectionMetadataScannerHelperTests { @JvmStatic fun classNamesToCheck(): List { - return ReflectionMetadataScannerHelper.classNamesToCheck + return ClasspathScannerHelper.classNamesToCheck } } diff --git a/class-list-generator/build.gradle.kts b/class-list-generator/build.gradle.kts index b9500f711..4662d6f31 100644 --- a/class-list-generator/build.gradle.kts +++ b/class-list-generator/build.gradle.kts @@ -8,5 +8,5 @@ repositories { dependencies { implementation(libs.classgraph) - implementation("dev.freya02:reflection-metadata-commons") + implementation("dev.freya02:classpath-scanner-commons") } diff --git a/class-list-generator/settings.gradle.kts b/class-list-generator/settings.gradle.kts index 98dfd349c..ce4559add 100644 --- a/class-list-generator/settings.gradle.kts +++ b/class-list-generator/settings.gradle.kts @@ -8,8 +8,8 @@ dependencyResolutionManagement { } } -includeBuild("../reflection-metadata-commons") { +includeBuild("../classpath-scanner-commons") { dependencySubstitution { - substitute(module("dev.freya02:reflection-metadata-commons")).using(project(":")) + substitute(module("dev.freya02:classpath-scanner-commons")).using(project(":")) } } diff --git a/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt index e818f427e..d4cc798e7 100644 --- a/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt +++ b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt @@ -1,7 +1,7 @@ package dev.freya02.bc.classlist.generator -import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper -import dev.freya02.bc.reflection.metadata.ReflectionMetadataScannerHelper.Companion.BSERVICE_ANNOTATION +import dev.freya02.bc.internal.reflection.classpath.ClasspathScannerHelper +import dev.freya02.bc.internal.reflection.classpath.ClasspathScannerHelper.Companion.BSERVICE_ANNOTATION import io.github.classgraph.ClassGraph import io.github.classgraph.ClassInfo import io.github.classgraph.MethodInfo @@ -27,7 +27,7 @@ object ClassListGenerator { .map { it.replace('/', '.').removeSuffix(".class") } .toList() - val helper = ReflectionMetadataScannerHelper(::isService, ::isServiceFactory) + val helper = ClasspathScannerHelper(::isService, ::isServiceFactory) ClassGraph() // Classpath includes compiled project classes + dependencies (required for CG to discover all meta-annotations) @@ -39,7 +39,7 @@ object ClassListGenerator { .enableAnnotationInfo() .disableModuleScanning() .scan().use { scanResult -> - return ReflectionMetadataScannerHelper + return ClasspathScannerHelper .filterClasses(scanResult.allClasses, onFileFacade = { /* noop */ }) .let(helper::filterLibraryClasses) .joinToString("\n") { it.name } diff --git a/reflection-metadata-commons/build.gradle.kts b/classpath-scanner-commons/build.gradle.kts similarity index 100% rename from reflection-metadata-commons/build.gradle.kts rename to classpath-scanner-commons/build.gradle.kts diff --git a/reflection-metadata-commons/settings.gradle.kts b/classpath-scanner-commons/settings.gradle.kts similarity index 75% rename from reflection-metadata-commons/settings.gradle.kts rename to classpath-scanner-commons/settings.gradle.kts index 45f79e913..96c76e729 100644 --- a/reflection-metadata-commons/settings.gradle.kts +++ b/classpath-scanner-commons/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "reflection-metadata-commons" +rootProject.name = "classpath-scanner-commons" dependencyResolutionManagement { versionCatalogs { diff --git a/reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt b/classpath-scanner-commons/src/main/kotlin/dev/freya02/bc/internal/reflection/classpath/ClasspathScannerHelper.kt similarity index 97% rename from reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt rename to classpath-scanner-commons/src/main/kotlin/dev/freya02/bc/internal/reflection/classpath/ClasspathScannerHelper.kt index adfb1c009..2d20b657e 100644 --- a/reflection-metadata-commons/src/main/kotlin/dev/freya02/bc/reflection/metadata/ReflectionMetadataScannerHelper.kt +++ b/classpath-scanner-commons/src/main/kotlin/dev/freya02/bc/internal/reflection/classpath/ClasspathScannerHelper.kt @@ -1,9 +1,9 @@ -package dev.freya02.bc.reflection.metadata +package dev.freya02.bc.internal.reflection.classpath import io.github.classgraph.ClassInfo import io.github.classgraph.MethodInfo -class ReflectionMetadataScannerHelper( +class ClasspathScannerHelper( private val isService: (ClassInfo) -> Boolean, private val isServiceFactory: (MethodInfo) -> Boolean, ) { diff --git a/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties b/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties index 8ef090c1b..5b0670f3e 100644 --- a/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties +++ b/test-bot/src/test/resources/META-INF/BotCommands-restarter.properties @@ -8,7 +8,7 @@ restart.exclude.restarter-prod-res=BotCommands-restarter/build/resources/main restart.exclude.core-prod=BotCommands-core/build/classes/(?:kotlin|java)/main restart.exclude.core-prod-res=BotCommands-core/build/resources/main -restart.exclude.core-prod-generated-bins=BotCommands-core/build/generated/bins/reflection-metadata-commons +restart.exclude.core-prod-generated-bins=BotCommands-core/build/generated/bins/classpath-scanner-commons restart.exclude.rate-limit-prod=BotCommands-rate-limit/build/classes/(?:kotlin|java)/main restart.exclude.rate-limit-prod-res=BotCommands-rate-limit/build/resources/main From 0b0fc14ba606d1bf7413bac3a05bd9d37dc53247 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:38:09 +0200 Subject: [PATCH 6/8] Improve task configuration --- .../class-list-generator-conventions.gradle.kts | 10 +++++----- .../botcommands/tasks/GenerateClassListTask.kt | 14 ++++++++------ .../bc/classlist/generator/ClassListGenerator.kt | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts b/buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts index 4efdb2330..dc0f3fe12 100644 --- a/buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/class-list-generator-conventions.gradle.kts @@ -9,15 +9,15 @@ val compileJava by tasks.getting(JavaCompile::class) val compileKotlin by tasks.getting(KotlinCompile::class) val generateClassList by tasks.registering(GenerateClassListTask::class) { - buildDirs = listOf( - layout.buildDirectory.dir("classes/java/main").get().asFile.path, - layout.buildDirectory.dir("classes/kotlin/main").get().asFile.path, + buildDirs.from( + layout.buildDirectory.dir("classes/java/main"), + layout.buildDirectory.dir("classes/kotlin/main"), ) // This is necessary as meta-annotations from dependencies cannot be resolved without them on the classpath - classpath = configurations.compileClasspath.get().files.map { it.path } + classpath.from(configurations.compileClasspath) // Only regenerate list if classes changes - classes.from(compileJava.outputs.files, compileKotlin.outputs.files) + classes.from(compileJava.outputs, compileKotlin.outputs) outputRoot = layout.buildDirectory.dir("generated/sources/lib-class-list/main/resources") } diff --git a/buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt b/buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt index 6695c7c7f..123affbfb 100644 --- a/buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt +++ b/buildSrc/src/main/kotlin/dev/freya02/botcommands/tasks/GenerateClassListTask.kt @@ -8,25 +8,27 @@ import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.* import java.io.File +@CacheableTask abstract class GenerateClassListTask : DefaultTask() { - // Only for Gradle caching purposes + // For Gradle caching purposes and implicit dependency on compile tasks @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val classes: ConfigurableFileCollection - @get:Input - abstract val buildDirs: ListProperty + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val buildDirs: ConfigurableFileCollection - @get:Input - abstract val classpath: ListProperty + @get:Classpath + abstract val classpath: ConfigurableFileCollection @get:OutputDirectory abstract val outputRoot: DirectoryProperty @TaskAction fun generate() { - val classList = ClassListGenerator.generate(buildDirs.get().map(::File), classpath.get().map(::File)) + val classList = ClassListGenerator.generate(buildDirs.files, classpath.files) if (classList.isBlank()) { return } diff --git a/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt index d4cc798e7..dbc18c69e 100644 --- a/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt +++ b/class-list-generator/src/main/kotlin/dev/freya02/bc/classlist/generator/ClassListGenerator.kt @@ -12,7 +12,7 @@ object ClassListGenerator { private const val COMPONENT_ANNOTATION_NAME = "org.springframework.stereotype.Component" private const val BEAN_ANNOTATION_NAME = "org.springframework.context.annotation.Bean" - fun generate(buildDirs: List, classpath: List): String { + fun generate(buildDirs: Collection, classpath: Collection): String { // We've set CG's classpath to the entire "compileClasspath" configuration, // but we only need to make a filtered class list of the current project, // so we can tell CG to only look at classes that we can find in the project's file tree From ecd7abc640636c33e8c40a2fe4958ba3fa7d86c9 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:45:02 +0200 Subject: [PATCH 7/8] Improve docs, add missing Spring configuration metadata --- .../freya022/botcommands/api/core/config/BConfig.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt index 10b4dc536..950de3392 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/core/config/BConfig.kt @@ -86,8 +86,19 @@ interface BConfigProps { val classes: Set> /** - * Instructs the reflection metadata scanner to use a predefined list of library classes. + * Instructs the classpath scanner to use a predefined list of library classes. + * This speeds up startup, but may log a few false positive exceptions, + * which do not affect the functionality of your application. + * + * Default: `false` + * + * Spring property: `botcommands.core.usePreprocessedLibClassList` */ + @get:ConfigurationValue( + path = "botcommands.core.usePreprocessedLibClassList", + description = "Instructs the classpath scanner to use a predefined list of library classes. This speeds up startup, but may log a few false positive exceptions, which do not affect the functionality of your application.", + defaultValue = "false", + ) val usePreprocessedLibClassList: Boolean /** From cb7219909507ef977bbcff3a2d04bacce8269b75 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:45:09 +0200 Subject: [PATCH 8/8] Clean up --- test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt index 3d62f6a88..e84d94c18 100644 --- a/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt +++ b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/Main.kt @@ -43,6 +43,7 @@ object Main { // which requires some compile-only classes @OptIn(ExperimentalCoreApi::class) usePreprocessedLibClassList = Random.nextBoolean() + disableExceptionsInDMs = true addSearchPath("dev.freya02.botcommands.bot")