From 20a8b8a6a8f37861ef07f24191f6378b04daa795 Mon Sep 17 00:00:00 2001 From: hundun Date: Sun, 3 Jul 2022 20:11:15 +0800 Subject: [PATCH] =?UTF-8?q?interface=20SubCommandProvider=E6=96=B9?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../compatibility-validation/jvm/api/jvm.api | 42 +++-- .../src/command/AbstractCommand.kt | 8 - ...mmand.kt => AbstractSubCommandProvider.kt} | 54 +++--- .../src/command/CompositeCommand.kt | 17 +- .../src/command/SubCommandProvider.kt | 3 +- .../src/command/descriptor/Exceptions.kt | 7 + .../src/internal/command/CommandReflector.kt | 167 ++++++++++-------- .../test/command/InstanceTestCommand.kt | 52 +++--- 8 files changed, 193 insertions(+), 157 deletions(-) rename mirai-console/backend/mirai-console/src/command/{GroupedCommand.kt => AbstractSubCommandProvider.kt} (80%) diff --git a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api index d5f99de0973..89e0ce34138 100644 --- a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api +++ b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api @@ -59,9 +59,6 @@ public abstract class net/mamoe/mirai/console/command/AbstractCommand : net/mamo public fun getUsage ()Ljava/lang/String; } -protected abstract interface annotation class net/mamoe/mirai/console/command/AbstractCommand$AsSubCommandProvider : java/lang/annotation/Annotation { -} - public abstract class net/mamoe/mirai/console/command/AbstractCommandSender : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/console/command/CommandSender { public abstract fun getBot ()Lnet/mamoe/mirai/Bot; public abstract fun getSubject ()Lnet/mamoe/mirai/contact/Contact; @@ -91,6 +88,24 @@ public abstract class net/mamoe/mirai/console/command/AbstractPluginCustomComman protected abstract fun sendMessageImpl (Ljava/lang/String;)V } +public abstract class net/mamoe/mirai/console/command/AbstractSubCommandProvider : net/mamoe/mirai/console/command/SubCommandProvider, net/mamoe/mirai/console/command/descriptor/CommandArgumentContextAware { + public fun (Lnet/mamoe/mirai/console/command/CommandOwner;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;)V + public synthetic fun (Lnet/mamoe/mirai/console/command/CommandOwner;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getContext ()Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext; + public final fun getProvideOverloads ()Ljava/util/List; +} + +protected abstract interface annotation class net/mamoe/mirai/console/command/AbstractSubCommandProvider$AnotherCombinedCommand : java/lang/annotation/Annotation { +} + +protected abstract interface annotation class net/mamoe/mirai/console/command/AbstractSubCommandProvider$AnotherDescription : java/lang/annotation/Annotation { + public abstract fun value ()Ljava/lang/String; +} + +protected abstract interface annotation class net/mamoe/mirai/console/command/AbstractSubCommandProvider$AnotherSubCommand : java/lang/annotation/Annotation { + public abstract fun value ()[Ljava/lang/String; +} + public abstract class net/mamoe/mirai/console/command/AbstractUserCommandSender : net/mamoe/mirai/console/command/AbstractCommandSender, net/mamoe/mirai/console/command/UserCommandSender { public fun getBot ()Lnet/mamoe/mirai/Bot; public final fun getName ()Ljava/lang/String; @@ -384,15 +399,17 @@ public abstract interface class net/mamoe/mirai/console/command/CommandSenderOnM public abstract fun getFromEvent ()Lnet/mamoe/mirai/event/events/MessageEvent; } -public abstract class net/mamoe/mirai/console/command/CompositeCommand : net/mamoe/mirai/console/command/AbstractCommand, net/mamoe/mirai/console/command/Command, net/mamoe/mirai/console/command/SubCommandProvider, net/mamoe/mirai/console/command/descriptor/CommandArgumentContextAware { +public abstract class net/mamoe/mirai/console/command/CompositeCommand : net/mamoe/mirai/console/command/AbstractCommand, net/mamoe/mirai/console/command/Command, net/mamoe/mirai/console/command/descriptor/CommandArgumentContextAware { public fun (Lnet/mamoe/mirai/console/command/CommandOwner;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/console/permission/Permission;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;)V public synthetic fun (Lnet/mamoe/mirai/console/command/CommandOwner;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/console/permission/Permission;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun getContext ()Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext; public final fun getOverloads ()Ljava/util/List; - public final fun getProvideOverloads ()Ljava/util/List; public fun getUsage ()Ljava/lang/String; } +protected abstract interface annotation class net/mamoe/mirai/console/command/CompositeCommand$CombinedCommand : java/lang/annotation/Annotation { +} + protected abstract interface annotation class net/mamoe/mirai/console/command/CompositeCommand$Description : java/lang/annotation/Annotation { public abstract fun value ()Ljava/lang/String; } @@ -512,15 +529,6 @@ public final class net/mamoe/mirai/console/command/GroupTempCommandSenderOnMessa public synthetic fun getFromEvent ()Lnet/mamoe/mirai/event/events/MessageEvent; } -public abstract class net/mamoe/mirai/console/command/GroupedCommand : net/mamoe/mirai/console/command/AbstractCommand, net/mamoe/mirai/console/command/Command, net/mamoe/mirai/console/command/SubCommandProvider, net/mamoe/mirai/console/command/descriptor/CommandArgumentContextAware { - public fun (Lnet/mamoe/mirai/console/command/CommandOwner;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/console/permission/Permission;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;)V - public synthetic fun (Lnet/mamoe/mirai/console/command/CommandOwner;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Lnet/mamoe/mirai/console/permission/Permission;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun getContext ()Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext; - public final fun getOverloads ()Ljava/util/List; - public final fun getProvideOverloads ()Ljava/util/List; - public fun getUsage ()Ljava/lang/String; -} - public class net/mamoe/mirai/console/command/IllegalCommandArgumentException : java/lang/IllegalArgumentException { public fun (Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/Throwable;)V @@ -1112,6 +1120,12 @@ public final class net/mamoe/mirai/console/command/descriptor/StringValueArgumen public fun parse (Ljava/lang/String;Lnet/mamoe/mirai/console/command/CommandSender;)Ljava/lang/String; } +public class net/mamoe/mirai/console/command/descriptor/SubcommandDeclarationClashException : net/mamoe/mirai/console/command/descriptor/CommandDeclarationException { + public fun (Ljava/lang/Object;Ljava/util/List;)V + public final fun getOwner ()Ljava/lang/Object; + public final fun getSignatures ()Ljava/util/List; +} + public abstract interface class net/mamoe/mirai/console/command/descriptor/TypeVariant { public static final field Companion Lnet/mamoe/mirai/console/command/descriptor/TypeVariant$Companion; public abstract fun getOutType ()Lkotlin/reflect/KType; diff --git a/mirai-console/backend/mirai-console/src/command/AbstractCommand.kt b/mirai-console/backend/mirai-console/src/command/AbstractCommand.kt index 9803f8909f2..8f200d1d53e 100644 --- a/mirai-console/backend/mirai-console/src/command/AbstractCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/AbstractCommand.kt @@ -40,14 +40,6 @@ public abstract class AbstractCommand secondaryNames.forEach(Command.Companion::checkCommandName) } - /** - * 标记一个属性为子指令集合 - */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.PROPERTY) - protected annotation class AsSubCommandProvider( - ) - public override val usage: String get() = description public override val permission: Permission by lazy { findOrCreateCommandPermission(parentPermission) } } \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/command/GroupedCommand.kt b/mirai-console/backend/mirai-console/src/command/AbstractSubCommandProvider.kt similarity index 80% rename from mirai-console/backend/mirai-console/src/command/GroupedCommand.kt rename to mirai-console/backend/mirai-console/src/command/AbstractSubCommandProvider.kt index 9a0bf64bf88..ada73371822 100644 --- a/mirai-console/backend/mirai-console/src/command/GroupedCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/AbstractSubCommandProvider.kt @@ -12,18 +12,18 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.descriptor.* import net.mamoe.mirai.console.command.java.JCompositeCommand import net.mamoe.mirai.console.compiler.common.ResolveContext -import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER +import net.mamoe.mirai.console.internal.command.GroupedCommandSubCommandAnnotationResolver +import net.mamoe.mirai.console.internal.command.SubCommandReflector +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.internal.command.CommandReflector import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver -import net.mamoe.mirai.console.internal.command.GroupedCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.util.ConsoleExperimentalApi import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION import kotlin.annotation.AnnotationTarget.PROPERTY - /** * 复合指令. 指令注册时候会通过反射构造指令解析器. * @@ -86,38 +86,48 @@ import kotlin.annotation.AnnotationTarget.PROPERTY * * @see buildCommandArgumentContext */ -public abstract class GroupedCommand( +public abstract class AbstractSubCommandProvider( @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) owner: CommandOwner, - @ResolveContext(COMMAND_NAME) primaryName: String, - @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, - description: String = "no description available", - parentPermission: Permission = owner.parentPermission, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, -) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission), - CommandArgumentContextAware, SubCommandProvider { +) : CommandArgumentContextAware, SubCommandProvider { - private val reflector by lazy { CommandReflector(this, GroupedCommandSubCommandAnnotationResolver) } + private val reflector by lazy { SubCommandReflector(this, GroupedCommandSubCommandAnnotationResolver) } @ExperimentalCommandDescriptors - public final override val overloads: List<@JvmWildcard CommandSignatureFromKFunction> by lazy { + public final override val provideOverloads: List by lazy { reflector.findSubCommands().also { reflector.validate(it) } } - @ExperimentalCommandDescriptors - public final override val provideOverloads: List by lazy { - // TODO 再加上额外的filter/validate? - overloads - } + /** + * 标记一个属性为子指令集合 + */ + @Retention(RUNTIME) + @Target(PROPERTY) + protected annotation class AnotherCombinedCommand( + ) /** - * 自动根据带有 [SubCommand] 注解的函数签名生成 [usage]. 也可以被覆盖. + * 标记一个函数为子指令, 当 [value] 为空时使用函数名. + * @param value 子指令名 */ - public override val usage: String by lazy { - @OptIn(ExperimentalCommandDescriptors::class) - reflector.generateUsage(overloads) - } + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class AnotherSubCommand( + @ResolveContext(COMMAND_NAME) vararg val value: String = [], + ) + + /** 指令描述 */ + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class AnotherDescription(val value: String) + + /** 参数名, 将参与构成 [usage] */ + @ConsoleExperimentalApi("Classname might change") + @Retention(RUNTIME) + @Target(AnnotationTarget.VALUE_PARAMETER) + protected annotation class AnotherName(val value: String) /** * 智能参数解析环境 diff --git a/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt b/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt index bfd238c90d0..5743d44e5e4 100644 --- a/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt @@ -22,7 +22,6 @@ import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION import kotlin.annotation.AnnotationTarget.PROPERTY - /** * 复合指令. 指令注册时候会通过反射构造指令解析器. * @@ -93,7 +92,7 @@ public abstract class CompositeCommand( parentPermission: Permission = owner.parentPermission, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, ) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission), - CommandArgumentContextAware, SubCommandProvider { + CommandArgumentContextAware { private val reflector by lazy { CommandReflector(this, CompositeCommandSubCommandAnnotationResolver) } @@ -104,12 +103,6 @@ public abstract class CompositeCommand( } } - @ExperimentalCommandDescriptors - public final override val provideOverloads: List by lazy { - // TODO 再加上额外的filter/validate? - overloads - } - /** * 自动根据带有 [SubCommand] 注解的函数签名生成 [usage]. 也可以被覆盖. */ @@ -123,6 +116,14 @@ public abstract class CompositeCommand( */ // open since 2.12 public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext + /** + * 标记一个属性为子指令集合 + */ + @Retention(RUNTIME) + @Target(PROPERTY) + protected annotation class CombinedCommand( + ) + /** * 标记一个函数为子指令, 当 [value] 为空时使用函数名. * @param value 子指令名 diff --git a/mirai-console/backend/mirai-console/src/command/SubCommandProvider.kt b/mirai-console/backend/mirai-console/src/command/SubCommandProvider.kt index 7cd4461807c..19793cbd572 100644 --- a/mirai-console/backend/mirai-console/src/command/SubCommandProvider.kt +++ b/mirai-console/backend/mirai-console/src/command/SubCommandProvider.kt @@ -4,7 +4,7 @@ import net.mamoe.mirai.console.command.descriptor.CommandSignatureFromKFunction import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.util.ConsoleExperimentalApi -interface SubCommandProvider { +public interface SubCommandProvider { /** * 被聚合时提供的子指令 @@ -12,4 +12,5 @@ interface SubCommandProvider { @ConsoleExperimentalApi("Property name is experimental") @ExperimentalCommandDescriptors public val provideOverloads: List<@JvmWildcard CommandSignatureFromKFunction> + } \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/command/descriptor/Exceptions.kt b/mirai-console/backend/mirai-console/src/command/descriptor/Exceptions.kt index bd9319a7e7c..1d113a758bd 100644 --- a/mirai-console/backend/mirai-console/src/command/descriptor/Exceptions.kt +++ b/mirai-console/backend/mirai-console/src/command/descriptor/Exceptions.kt @@ -42,6 +42,13 @@ public open class CommandDeclarationClashException( public val signatures: List, ) : CommandDeclarationException("Declaration clash for command '${command.primaryName}': \n${signatures.joinToString("\n")}") +@ExperimentalCommandDescriptors +public open class SubcommandDeclarationClashException( + public val owner: Any, + public val signatures: List, +) : CommandDeclarationException("Declaration clash for owner '${owner::class.qualifiedName}': \n${signatures.joinToString("\n")}") + + public open class CommandDeclarationException : RuntimeException { public constructor() : super() public constructor(message: String?) : super(message) diff --git a/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt b/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt index 0d14f1ca327..eb1459bb616 100644 --- a/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt +++ b/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt @@ -64,7 +64,7 @@ internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain { @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") internal object CompositeCommandSubCommandAnnotationResolver : - SubCommandAnnotationResolver { + SubCommandAnnotationResolver { override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = function.hasAnnotation() @@ -80,8 +80,8 @@ internal object CompositeCommandSubCommandAnnotationResolver : override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? = function.findAnnotation()?.value - override fun hasPropertyAnnotation(command: Command, property: KProperty<*>): Boolean = - property.hasAnnotation() + override fun hasPropertyAnnotation(command: Command, kProperty: KProperty<*>): Boolean = + kProperty.hasAnnotation() } @@ -92,30 +92,30 @@ internal object CompositeCommandSubCommandAnnotationResolver : */ @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") internal object GroupedCommandSubCommandAnnotationResolver : - SubCommandAnnotationResolver { - override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = - function.hasAnnotation() + SubCommandAnnotationResolver { + override fun hasAnnotation(ownerCommand: Any, function: KFunction<*>) = + function.hasAnnotation() - override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array { - val annotated = function.findAnnotation()!!.value + override fun getSubCommandNames(ownerCommand: Any, function: KFunction<*>): Array { + val annotated = function.findAnnotation()!!.value return if (annotated.isEmpty()) arrayOf(function.name) else annotated } - override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? = - parameter.findAnnotation()?.value + override fun getAnnotatedName(ownerCommand: Any, parameter: KParameter): String? = + parameter.findAnnotation()?.value - override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? = - function.findAnnotation()?.value + override fun getDescription(ownerCommand: Any, function: KFunction<*>): String? = + function.findAnnotation()?.value - override fun hasPropertyAnnotation(command: Command, property: KProperty<*>): Boolean = - property.hasAnnotation() + override fun hasPropertyAnnotation(command: Any, kProperty: KProperty<*>): Boolean = + kProperty.hasAnnotation() } @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") internal object SimpleCommandSubCommandAnnotationResolver : - SubCommandAnnotationResolver { + SubCommandAnnotationResolver { override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = function.hasAnnotation() @@ -131,12 +131,12 @@ internal object SimpleCommandSubCommandAnnotationResolver : override fun hasPropertyAnnotation(command: Command, kProperty: KProperty<*>): Boolean = false } -internal interface SubCommandAnnotationResolver { - fun hasAnnotation(ownerCommand: Command, function: KFunction<*>): Boolean - fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array - fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? - fun getDescription(ownerCommand: Command, function: KFunction<*>): String? - fun hasPropertyAnnotation(command: Command, kProperty: KProperty<*>): Boolean +internal interface SubCommandAnnotationResolver { + fun hasAnnotation(ownerCommand: T, function: KFunction<*>): Boolean + fun getSubCommandNames(ownerCommand: T, function: KFunction<*>): Array + fun getAnnotatedName(ownerCommand: T, parameter: KParameter): String? + fun getDescription(ownerCommand: T, function: KFunction<*>): String? + fun hasPropertyAnnotation(command: T, kProperty: KProperty<*>): Boolean } @ConsoleExperimentalApi @@ -144,10 +144,10 @@ public class IllegalCommandDeclarationException : Exception { public override val message: String? public constructor( - ownerCommand: Command, + owner: Any, correspondingFunction: KFunction<*>, message: String?, - ) : super("Illegal command declaration: ${correspondingFunction.name} declared in ${ownerCommand::class.qualifiedName}") { + ) : super("Illegal command declaration: ${correspondingFunction.name} declared in ${owner::class.qualifiedName}") { this.message = message } @@ -162,47 +162,8 @@ public class IllegalCommandDeclarationException : Exception { @OptIn(ExperimentalCommandDescriptors::class) internal class CommandReflector( val command: Command, - private val annotationResolver: SubCommandAnnotationResolver, -) { - - @Suppress("NOTHING_TO_INLINE") - private inline fun KFunction<*>.illegalDeclaration( - message: String, - ): Nothing { - throw IllegalCommandDeclarationException(command, this, message) - } - - private fun KProperty<*>.isSubCommandProviderProperty(): Boolean = annotationResolver.hasPropertyAnnotation(command, this) - private fun KFunction<*>.isSubCommandFunction(): Boolean = annotationResolver.hasAnnotation(command, this) - private fun KFunction<*>.checkExtensionReceiver() { - this.extensionReceiverParameter?.let { receiver -> - val classifier = receiver.type.classifierAsKClassOrNull() - if (classifier != null) { - if (!classifier.isSubclassOf(CommandSender::class) && !classifier.isSubclassOf(CommandContext::class)) { - illegalDeclaration("Extension receiver parameter type is not subclass of CommandSender nor CommandContext.") - } - } - } - } - - private fun KFunction<*>.checkNames() { - val names = annotationResolver.getSubCommandNames(command, this) - for (name in names) { - ILLEGAL_SUB_NAME_CHARS.find { it in name }?.let { - illegalDeclaration("'$it' is forbidden in command name.") - } - } - } - - private fun KFunction<*>.checkModifiers() { - if (isInline) illegalDeclaration("Command function cannot be inline") - if (visibility == KVisibility.PRIVATE) illegalDeclaration("Command function must be accessible from Mirai Console, that is, effectively public.") - if (this.hasAnnotation()) illegalDeclaration("Command function must not be static.") - - // should we allow abstract? - - // if (isAbstract) illegalDeclaration("Command function cannot be abstract") - } + private val annotationResolver: SubCommandAnnotationResolver, +) : SubCommandReflectible by SubCommandReflector(command, annotationResolver) { fun generateUsage(overloads: Iterable): String { return generateUsage(command, annotationResolver, overloads) @@ -211,7 +172,7 @@ internal class CommandReflector( companion object { fun generateUsage( command: Command, - annotationResolver: SubCommandAnnotationResolver?, + annotationResolver: SubCommandAnnotationResolver?, overloads: Iterable ): String { return overloads.joinToString("\n") { subcommand -> @@ -252,8 +213,61 @@ internal class CommandReflector( } } } +} + +@OptIn(ExperimentalCommandDescriptors::class) +internal interface SubCommandReflectible { + @Throws(IllegalCommandDeclarationException::class) + fun findSubCommands(): List + fun validate(signatures: List) +} - fun validate(signatures: List) { +@OptIn(ExperimentalCommandDescriptors::class) +internal class SubCommandReflector( + val owner: T, + private val annotationResolver: SubCommandAnnotationResolver, +) : SubCommandReflectible { + + @Suppress("NOTHING_TO_INLINE") + private inline fun KFunction<*>.illegalDeclaration( + message: String, + ): Nothing { + throw IllegalCommandDeclarationException(owner, this, message) + } + + private fun KProperty<*>.isSubCommandProviderProperty(): Boolean = annotationResolver.hasPropertyAnnotation(owner, this) + private fun KFunction<*>.isSubCommandFunction(): Boolean = annotationResolver.hasAnnotation(owner, this) + private fun KFunction<*>.checkExtensionReceiver() { + this.extensionReceiverParameter?.let { receiver -> + val classifier = receiver.type.classifierAsKClassOrNull() + if (classifier != null) { + if (!classifier.isSubclassOf(CommandSender::class) && !classifier.isSubclassOf(CommandContext::class)) { + illegalDeclaration("Extension receiver parameter type is not subclass of CommandSender nor CommandContext.") + } + } + } + } + + private fun KFunction<*>.checkNames() { + val names = annotationResolver.getSubCommandNames(owner, this) + for (name in names) { + ILLEGAL_SUB_NAME_CHARS.find { it in name }?.let { + illegalDeclaration("'$it' is forbidden in command name.") + } + } + } + + private fun KFunction<*>.checkModifiers() { + if (isInline) illegalDeclaration("Command function cannot be inline") + if (visibility == KVisibility.PRIVATE) illegalDeclaration("Command function must be accessible from Mirai Console, that is, effectively public.") + if (this.hasAnnotation()) illegalDeclaration("Command function must not be static.") + + // should we allow abstract? + + // if (isAbstract) illegalDeclaration("Command function cannot be abstract") + } + + override fun validate(signatures: List) { data class ErasedParameterInfo( val index: Int, @@ -289,19 +303,20 @@ internal class CommandReflector( value.size > 1 } ?: return - throw CommandDeclarationClashException(command, clashes.value.map { it.first }) + + throw SubcommandDeclarationClashException(owner, clashes.value.map { it.first }) } @Throws(IllegalCommandDeclarationException::class) - fun findSubCommands(): List { - val fromMemberFunctions = command::class.functions // exclude static later + override fun findSubCommands(): List { + val fromMemberFunctions = owner::class.functions // exclude static later .asSequence() .filter { it.isSubCommandFunction() } .onEach { it.checkExtensionReceiver() } .onEach { it.checkModifiers() } .onEach { it.checkNames() } .flatMap { function -> - val names = annotationResolver.getSubCommandNames(command, function) + val names = annotationResolver.getSubCommandNames(owner, function) if (names.isEmpty()) sequenceOf(createMapEntry(null, function)) else names.associateWith { function }.asSequence() } @@ -345,11 +360,11 @@ internal class CommandReflector( val instanceParameter = function.instanceParameter if (instanceParameter != null) { - check(instanceParameter.type.classifierAsKClass().isInstance(command)) { + check(instanceParameter.type.classifierAsKClass().isInstance(owner)) { "Bad command call resolved. " + - "Function expects instance parameter ${instanceParameter.type} whereas actual instance is ${command::class}." + "Function expects instance parameter ${instanceParameter.type} whereas actual instance is ${owner::class}." } - args[instanceParameter] = command + args[instanceParameter] = owner } if (receiverParameter != null) { @@ -377,10 +392,10 @@ internal class CommandReflector( } }.toList() - val fromMemberProperties = command::class.declaredMemberProperties + val fromMemberProperties = owner::class.declaredMemberProperties .asSequence() .filter { it.isSubCommandProviderProperty() } - .map { it.getter.call(command) } + .map { it.getter.call(owner) } .filter { it is SubCommandProvider } .flatMap { property -> property as SubCommandProvider @@ -435,5 +450,5 @@ internal class CommandReflector( } private fun KParameter.nameForCommandParameter(): String? = - annotationResolver.getAnnotatedName(command, this) ?: this.name + annotationResolver.getAnnotatedName(owner, this) ?: this.name } \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt b/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt index fdcec4fae03..bfe0f7ca911 100644 --- a/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt +++ b/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt @@ -38,36 +38,35 @@ import kotlin.test.* -val compositeCommandAsProvider = object : CompositeCommand(owner, "useless") { - @CompositeCommand.SubCommand - fun foo(seconds: Int) { - Testing.ok(seconds) - } -} - -val groupedCommandAsProvider = object : GroupedCommand(owner, "useless") { - @AsSubCommandProvider - val child1: SubCommandProvider = object : CompositeCommand(owner, "useless") { - @CompositeCommand.SubCommand - fun bar(seconds: Int) { - Testing.ok(seconds) - } - } -} class TestContainerCompositeCommand : CompositeCommand( owner, "testContainerComposite", "tsPC" ) { - @AsSubCommandProvider - val child1: SubCommandProvider = compositeCommandAsProvider + class TopProvider : AbstractSubCommandProvider(owner) { - @AsSubCommandProvider - val child2: SubCommandProvider = groupedCommandAsProvider + class NestProvider : AbstractSubCommandProvider(owner) { + @AnotherSubCommand + fun foo2(seconds: Int) { + Testing.ok(seconds) + } + } + + @AnotherCombinedCommand + val provider: SubCommandProvider = NestProvider() + + @AnotherSubCommand + fun foo1(seconds: Int) { + Testing.ok(seconds) + } + } + + @CombinedCommand + val provider: SubCommandProvider = TopProvider() @SubCommand - fun containerFoo(seconds: Int) { + fun foo0(seconds: Int) { Testing.ok(seconds) } @@ -547,17 +546,14 @@ internal class InstanceTestCommand : AbstractConsoleInstanceTest() { @Test fun `container composite command executing`() = runBlocking { containerCompositeCommand.withRegistration { - assertEquals(1, withTesting { - assertSuccess(containerCompositeCommand.execute(sender, "containerFoo 1")) + assertEquals(0, withTesting { + assertSuccess(containerCompositeCommand.execute(sender, "foo0 0")) }) assertEquals(1, withTesting { - assertSuccess(containerCompositeCommand.execute(sender, "containerBar 1")) - }) - assertEquals(2, withTesting { - assertSuccess(containerCompositeCommand.execute(sender, "foo 2")) + assertSuccess(containerCompositeCommand.execute(sender, "foo1 1")) }) assertEquals(2, withTesting { - assertSuccess(containerCompositeCommand.execute(sender, "bar 2")) + assertSuccess(containerCompositeCommand.execute(sender, "foo2 2")) }) } }