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 9ff25a02d4..005474e569 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 @@ -94,6 +94,14 @@ 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/AbstractSubCommandGroup : net/mamoe/mirai/console/command/SubCommandGroup, net/mamoe/mirai/console/command/descriptor/CommandArgumentContextAware { + public fun ()V + public fun (Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;)V + public synthetic fun (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 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; @@ -371,7 +379,7 @@ 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/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/SubCommandGroup, 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; @@ -379,14 +387,6 @@ public abstract class net/mamoe/mirai/console/command/CompositeCommand : net/mam public fun getUsage ()Ljava/lang/String; } -protected abstract interface annotation class net/mamoe/mirai/console/command/CompositeCommand$Description : java/lang/annotation/Annotation { - public abstract fun value ()Ljava/lang/String; -} - -protected abstract interface annotation class net/mamoe/mirai/console/command/CompositeCommand$SubCommand : java/lang/annotation/Annotation { - public abstract fun value ()[Ljava/lang/String; -} - public final class net/mamoe/mirai/console/command/ConsoleCommandOwner : net/mamoe/mirai/console/command/CommandOwner { public static final field INSTANCE Lnet/mamoe/mirai/console/command/ConsoleCommandOwner; public fun getParentPermission ()Lnet/mamoe/mirai/console/permission/Permission; @@ -605,6 +605,21 @@ public final class net/mamoe/mirai/console/command/StrangerCommandSenderOnMessag public fun getFromEvent ()Lnet/mamoe/mirai/event/events/StrangerMessageEvent; } +public abstract interface class net/mamoe/mirai/console/command/SubCommandGroup { + public abstract fun getOverloads ()Ljava/util/List; +} + +public abstract interface annotation class net/mamoe/mirai/console/command/SubCommandGroup$Description : java/lang/annotation/Annotation { + public abstract fun value ()Ljava/lang/String; +} + +public abstract interface annotation class net/mamoe/mirai/console/command/SubCommandGroup$FlattenSubCommands : java/lang/annotation/Annotation { +} + +public abstract interface annotation class net/mamoe/mirai/console/command/SubCommandGroup$SubCommand : java/lang/annotation/Annotation { + public abstract fun value ()[Ljava/lang/String; +} + public abstract interface class net/mamoe/mirai/console/command/SystemCommandSender : net/mamoe/mirai/console/command/CommandSender { public abstract fun isAnsiSupported ()Z } diff --git a/mirai-console/backend/mirai-console/src/command/AbstractSubCommandGroup.kt b/mirai-console/backend/mirai-console/src/command/AbstractSubCommandGroup.kt new file mode 100644 index 0000000000..5e51cb95e9 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/command/AbstractSubCommandGroup.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.descriptor.* +import net.mamoe.mirai.console.internal.command.GroupedCommandSubCommandAnnotationResolver +import net.mamoe.mirai.console.internal.command.SubCommandReflector + +public abstract class AbstractSubCommandGroup( + overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, +) : CommandArgumentContextAware, SubCommandGroup { + + private val reflector by lazy { SubCommandReflector(this, GroupedCommandSubCommandAnnotationResolver) } + + @ExperimentalCommandDescriptors + public final override val overloads: List by lazy { + reflector.findSubCommands().also { + reflector.validate(it) + } + } + + /** + * 智能参数解析环境 + */ // open since 2.12 + public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext + +} + + diff --git a/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt b/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt index c2a27bb9d8..581186fc42 100644 --- a/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt +++ b/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt @@ -23,6 +23,9 @@ import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.MiraiConsoleImplementation.ConsoleDataScope.Companion.get import net.mamoe.mirai.console.command.CommandManager.INSTANCE.allRegisteredCommands import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register +import net.mamoe.mirai.console.command.SubCommandGroup.Description +import net.mamoe.mirai.console.command.SubCommandGroup.Name +import net.mamoe.mirai.console.command.SubCommandGroup.SubCommand import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser.Companion.map import net.mamoe.mirai.console.command.descriptor.PermissionIdValueArgumentParser diff --git a/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt b/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt index 79c1facaed..d619a17adf 100644 --- a/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt @@ -18,10 +18,10 @@ import net.mamoe.mirai.console.internal.command.CommandReflector import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.DeprecationLevel.* import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION - /** * 复合指令. 指令注册时候会通过反射构造指令解析器. * @@ -92,7 +92,7 @@ public abstract class CompositeCommand( parentPermission: Permission = owner.parentPermission, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, ) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission), - CommandArgumentContextAware { + CommandArgumentContextAware, SubCommandGroup { private val reflector by lazy { CommandReflector(this, CompositeCommandSubCommandAnnotationResolver) } @@ -116,26 +116,30 @@ public abstract class CompositeCommand( */ // open since 2.12 public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext - /** + +/* *//** * 标记一个函数为子指令, 当 [value] 为空时使用函数名. * @param value 子指令名 - */ + *//* @Retention(RUNTIME) @Target(FUNCTION) + @Deprecated(level = HIDDEN, message = "use SubCommandGroup.SubCommand") protected annotation class SubCommand( @ResolveContext(COMMAND_NAME) vararg val value: String = [], ) - /** 指令描述 */ + *//** 指令描述 *//* @Retention(RUNTIME) @Target(FUNCTION) + @Deprecated(level = HIDDEN, message = "use SubCommandGroup.Description") protected annotation class Description(val value: String) - /** 参数名, 将参与构成 [usage] */ + *//** 参数名, 将参与构成 [usage] *//* @ConsoleExperimentalApi("Classname might change") @Retention(RUNTIME) @Target(AnnotationTarget.VALUE_PARAMETER) - protected annotation class Name(val value: String) + @Deprecated(level = HIDDEN, message = "use SubCommandGroup.Name") + protected annotation class Name(val value: String)*/ } diff --git a/mirai-console/backend/mirai-console/src/command/SubCommandGroup.kt b/mirai-console/backend/mirai-console/src/command/SubCommandGroup.kt new file mode 100644 index 0000000000..c84c754161 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/command/SubCommandGroup.kt @@ -0,0 +1,46 @@ +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.descriptor.CommandSignatureFromKFunction +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.util.ConsoleExperimentalApi + +public interface SubCommandGroup { + + /** + * 被聚合时提供的子指令 + */ + @ExperimentalCommandDescriptors + public val overloads: List<@JvmWildcard CommandSignatureFromKFunction> + + /** + * 标记一个属性为子指令集合,且使用flat策略 + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.PROPERTY) + public annotation class FlattenSubCommands( + ) + + /** + * 1. 标记一个函数为子指令, 当 [value] 为空时使用函数名. + * 2. 标记一个属性为子指令集合,且使用sub策略 + * @param value 子指令名 + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) + public annotation class SubCommand( + @ResolveContext(ResolveContext.Kind.COMMAND_NAME) vararg val value: String = [], + ) + + /** 指令描述 */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FUNCTION) + public annotation class Description(val value: String) + + /** 参数名, 由具体Command决定用途 */ + @ConsoleExperimentalApi("Classname might change") + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.VALUE_PARAMETER) + public annotation class Name(val value: String) + +} \ 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 bd9319a7e7..1d113a758b 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 c13a70c320..06503d2598 100644 --- a/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt +++ b/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt @@ -66,30 +66,76 @@ internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain { @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") internal object CompositeCommandSubCommandAnnotationResolver : - SubCommandAnnotationResolver { - override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = - function.hasAnnotation() + SubCommandAnnotationResolver { + override fun isDeclaredSubCommand(ownerCommand: Command, function: KFunction<*>) = + function.hasAnnotation() - override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array { - val annotated = function.findAnnotation()!!.value + override fun getDeclaredSubCommandNames(ownerCommand: Command, 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 + parameter.findAnnotation()?.value override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? = - function.findAnnotation()?.value + function.findAnnotation()?.value + + override fun isCombinedSubCommands(command: Command, kProperty: KProperty<*>): Boolean = + kProperty.hasAnnotation() || kProperty.hasAnnotation() + + override fun getCombinedAdditionNames(command: Command, kProperty: KProperty<*>): Array { + return if (kProperty.hasAnnotation()) { + emptyArray() + } else { + val annotated = kProperty.findAnnotation()!!.value + if (annotated.isEmpty()) arrayOf(kProperty.name) + else annotated + } + } +} + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +internal object GroupedCommandSubCommandAnnotationResolver : + SubCommandAnnotationResolver { + override fun isDeclaredSubCommand(ownerCommand: Any, function: KFunction<*>) = + function.hasAnnotation() + + override fun getDeclaredSubCommandNames(ownerCommand: Any, function: KFunction<*>): Array { + val annotated = function.findAnnotation()!!.value + return if (annotated.isEmpty()) arrayOf(function.name) + else annotated + } + + override fun getAnnotatedName(ownerCommand: Any, parameter: KParameter): String? = + parameter.findAnnotation()?.value + + override fun getDescription(ownerCommand: Any, function: KFunction<*>): String? = + function.findAnnotation()?.value + + override fun isCombinedSubCommands(command: Any, kProperty: KProperty<*>): Boolean = + kProperty.hasAnnotation() || kProperty.hasAnnotation() + + override fun getCombinedAdditionNames(command: Any, kProperty: KProperty<*>): Array { + return if (kProperty.hasAnnotation()) { + emptyArray() + } else { + val annotated = kProperty.findAnnotation()!!.value + if (annotated.isEmpty()) arrayOf(kProperty.name) + else annotated + } + } + } @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") internal object SimpleCommandSubCommandAnnotationResolver : - SubCommandAnnotationResolver { - override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = + SubCommandAnnotationResolver { + override fun isDeclaredSubCommand(ownerCommand: Command, function: KFunction<*>) = function.hasAnnotation() - override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array = + override fun getDeclaredSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array = emptyArray() override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? = @@ -97,13 +143,21 @@ internal object SimpleCommandSubCommandAnnotationResolver : override fun getDescription(ownerCommand: Command, function: KFunction<*>): String = ownerCommand.description + + override fun isCombinedSubCommands(command: Command, kProperty: KProperty<*>): Boolean = false + + override fun getCombinedAdditionNames(command: Command, kProperty: KProperty<*>): Array = + emptyArray() + } -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? +internal interface SubCommandAnnotationResolver { + fun isDeclaredSubCommand(ownerCommand: T, function: KFunction<*>): Boolean + fun getDeclaredSubCommandNames(ownerCommand: T, function: KFunction<*>): Array + fun getAnnotatedName(ownerCommand: T, parameter: KParameter): String? + fun getDescription(ownerCommand: T, function: KFunction<*>): String? + fun isCombinedSubCommands(command: T, kProperty: KProperty<*>): Boolean + fun getCombinedAdditionNames(command: T, kProperty: KProperty<*>): Array } @ConsoleExperimentalApi @@ -111,10 +165,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 } @@ -129,46 +183,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 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) @@ -177,7 +193,7 @@ internal class CommandReflector( companion object { fun generateUsage( command: Command, - annotationResolver: SubCommandAnnotationResolver?, + annotationResolver: SubCommandAnnotationResolver?, overloads: Iterable ): String { return overloads.joinToString("\n") { subcommand -> @@ -218,8 +234,62 @@ internal class CommandReflector( } } } +} - fun validate(signatures: List) { +@OptIn(ExperimentalCommandDescriptors::class) +internal interface SubCommandReflectible { + @Throws(IllegalCommandDeclarationException::class) + fun findSubCommands(): 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.isCombinedSubCommands(owner, this) + private fun KFunction<*>.isSubCommandFunction(): Boolean = annotationResolver.isDeclaredSubCommand(owner, this) + private fun KProperty<*>.getCombinedAdditionNames(): Array = annotationResolver.getCombinedAdditionNames(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.getDeclaredSubCommandNames(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, @@ -255,93 +325,136 @@ 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 { - return command::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) - if (names.isEmpty()) sequenceOf(createMapEntry(null, function)) - else names.associateWith { function }.asSequence() + private fun generateCommandSignatureFromKFunctionImplWithAdditionName(name: String?, origin: CommandSignatureFromKFunction): CommandSignatureFromKFunctionImpl { + val functionNameAsValueParameter = + name?.split(' ')?.mapIndexed { index, s -> createStringConstantParameterForName(index, s) } + .orEmpty() + + return CommandSignatureFromKFunctionImpl( + receiverParameter = origin.receiverParameter, + valueParameters = functionNameAsValueParameter + origin.valueParameters, + originFunction = origin.originFunction + ) { call -> + origin.call(call) + } + } + + private fun generateCommandSignatureFromKFunctionImplWithAdditionName(name: String?, function: KFunction<*>): CommandSignatureFromKFunctionImpl { + val functionNameAsValueParameter = + name?.split(' ')?.mapIndexed { index, s -> createStringConstantParameterForName(index, s) } + .orEmpty() + + val valueParameters = function.valueParameters.toMutableList() + var receiverParameter = function.extensionReceiverParameter + if (receiverParameter == null && valueParameters.isNotEmpty()) { + val valueFirstParameter = valueParameters[0] + val classifier = valueFirstParameter.type.classifierAsKClassOrNull() + if (classifier != null && isAcceptableReceiverType(classifier) + ) { + receiverParameter = valueFirstParameter + valueParameters.removeAt(0) } - .map { (name, function) -> + } - val functionNameAsValueParameter = - name?.split(' ')?.mapIndexed { index, s -> createStringConstantParameterForName(index, s) } - .orEmpty() - - val valueParameters = function.valueParameters.toMutableList() - var receiverParameter = function.extensionReceiverParameter - if (receiverParameter == null && valueParameters.isNotEmpty()) { - val valueFirstParameter = valueParameters[0] - val classifier = valueFirstParameter.type.classifierAsKClassOrNull() - if (classifier != null && isAcceptableReceiverType(classifier) - ) { - receiverParameter = valueFirstParameter - valueParameters.removeAt(0) - } + val functionValueParameters = + valueParameters.associateBy { it.toUserDefinedCommandParameter() } + + return CommandSignatureFromKFunctionImpl( + receiverParameter = receiverParameter?.toCommandReceiverParameter(), + valueParameters = functionNameAsValueParameter + functionValueParameters.keys, + originFunction = function + ) { call -> + val args = LinkedHashMap() + + for ((commandParameter, value) in call.resolvedValueArguments) { + if (commandParameter is AbstractCommandValueParameter.StringConstant) { + continue } + val functionParameter = + functionValueParameters[commandParameter] + ?: error("Could not find a corresponding function parameter '${commandParameter.name}'") + args[functionParameter] = value + } - val functionValueParameters = - valueParameters.associateBy { it.toUserDefinedCommandParameter() } + val instanceParameter = function.instanceParameter + if (instanceParameter != null) { + check(instanceParameter.type.classifierAsKClass().isInstance(owner)) { + "Bad command call resolved. " + + "Function expects instance parameter ${instanceParameter.type} whereas actual instance is ${owner::class}." + } + args[instanceParameter] = owner + } - CommandSignatureFromKFunctionImpl( - receiverParameter = receiverParameter?.toCommandReceiverParameter(), - valueParameters = functionNameAsValueParameter + functionValueParameters.keys, - originFunction = function - ) { call -> - val args = LinkedHashMap() + if (receiverParameter != null) { - for ((commandParameter, value) in call.resolvedValueArguments) { - if (commandParameter is AbstractCommandValueParameter.StringConstant) { - continue - } - val functionParameter = - functionValueParameters[commandParameter] - ?: error("Could not find a corresponding function parameter '${commandParameter.name}'") - args[functionParameter] = value - } + val receiverType = receiverParameter.type.classifierAsKClass() - val instanceParameter = function.instanceParameter - if (instanceParameter != null) { - check(instanceParameter.type.classifierAsKClass().isInstance(command)) { - "Bad command call resolved. " + - "Function expects instance parameter ${instanceParameter.type} whereas actual instance is ${command::class}." - } - args[instanceParameter] = command + if (receiverType.isSubclassOf(CommandContext::class)) { + args[receiverParameter] = CommandContextImpl(call.caller, call.originalMessage) + } else { + check(receiverType.isInstance(call.caller)) { + "Bad command call resolved. " + + "Function expects receiver parameter ${receiverParameter.type} whereas actual is ${call.caller::class}." } + args[receiverParameter] = call.caller + } - if (receiverParameter != null) { + } - val receiverType = receiverParameter.type.classifierAsKClass() + // mirai-console#341 + if (function.isSuspend) { + function.callSuspendBy(args) + } else { + runBIO { function.callBy(args) } + } + } + } - if (receiverType.isSubclassOf(CommandContext::class)) { - args[receiverParameter] = CommandContextImpl(call.caller, call.originalMessage) - } else { - check(receiverType.isInstance(call.caller)) { - "Bad command call resolved. " + - "Function expects receiver parameter ${receiverParameter.type} whereas actual is ${call.caller::class}." - } - args[receiverParameter] = call.caller - } - } + @Throws(IllegalCommandDeclarationException::class) + 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.getDeclaredSubCommandNames(owner, function) + if (names.isEmpty()) sequenceOf(createMapEntry(null, function)) + else names.associateWith { function }.asSequence() + } + .map { (name, function) -> + generateCommandSignatureFromKFunctionImplWithAdditionName(name, function) + }.toList() - // mirai-console#341 - if (function.isSuspend) { - function.callSuspendBy(args) - } else { - runBIO { function.callBy(args) } + val fromMemberProperties = owner::class.declaredMemberProperties + .asSequence() + .filter { it.isSubCommandProviderProperty() } + .filter { it.getter.call(owner) is SubCommandGroup } + .flatMap { + val names = it.getCombinedAdditionNames() + val originOverloads = (it.getter.call(owner) as SubCommandGroup).overloads + if (names.isEmpty()) sequenceOf(createMapEntry(null, originOverloads)) + else names.associateWith { originOverloads }.asSequence() + } + .map { (name, originOverloads) -> + originOverloads + .map { originOverload -> + generateCommandSignatureFromKFunctionImplWithAdditionName(name, originOverload) } - } - }.toList() + } + .flatten() + .toList() + + val list: MutableList = ArrayList() + list.addAll(fromMemberFunctions) + list.addAll(fromMemberProperties) + return list } private fun isAcceptableReceiverType(classifier: KClass) = @@ -386,5 +499,5 @@ internal class CommandReflector( } private fun KParameter.nameForCommandParameter(): String? = - annotationResolver.getAnnotatedName(command, this) ?: this.name -} \ No newline at end of file + annotationResolver.getAnnotatedName(owner, this) ?: this.name +} diff --git a/mirai-console/backend/mirai-console/test/command/CommandContextTest.kt b/mirai-console/backend/mirai-console/test/command/CommandContextTest.kt index 1a59e80b60..761c1137d2 100644 --- a/mirai-console/backend/mirai-console/test/command/CommandContextTest.kt +++ b/mirai-console/backend/mirai-console/test/command/CommandContextTest.kt @@ -18,6 +18,7 @@ import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.java.JCompositeCommand import net.mamoe.mirai.console.command.java.JRawCommand import net.mamoe.mirai.console.command.java.JSimpleCommand +import net.mamoe.mirai.console.command.SubCommandGroup.SubCommand import net.mamoe.mirai.console.internal.data.classifierAsKClass import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.safeCast diff --git a/mirai-console/backend/mirai-console/test/command/CommandValueArgumentContextTest.kt b/mirai-console/backend/mirai-console/test/command/CommandValueArgumentContextTest.kt index c67ed82642..3b163904db 100644 --- a/mirai-console/backend/mirai-console/test/command/CommandValueArgumentContextTest.kt +++ b/mirai-console/backend/mirai-console/test/command/CommandValueArgumentContextTest.kt @@ -19,6 +19,7 @@ import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext import net.mamoe.mirai.console.command.java.JCompositeCommand import net.mamoe.mirai.console.command.java.JSimpleCommand +import net.mamoe.mirai.console.command.SubCommandGroup.SubCommand import net.mamoe.mirai.console.internal.data.classifierAsKClass import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.MessageContent diff --git a/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt b/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt index 2cc6f3fd9c..31d1f91e53 100644 --- a/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt +++ b/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt @@ -24,6 +24,7 @@ import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterCommand import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext + import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.flattenCommandComponents import net.mamoe.mirai.console.permission.PermissionService.Companion.permit @@ -34,6 +35,46 @@ import java.time.temporal.TemporalAccessor import kotlin.reflect.KClass import kotlin.test.* +import net.mamoe.mirai.console.command.SubCommandGroup.SubCommand +import net.mamoe.mirai.console.command.SubCommandGroup.FlattenSubCommands + +class MyUnifiedCommand : CompositeCommand( + owner, "testMyUnifiedCommand", "tsMUC" +) { + // 插件一个模块的部分功能 + class ModuleAPart1 : AbstractSubCommandGroup() { + @SubCommand + fun function1(arg0: Int) { + Testing.ok(arg0) + } + } + + // 插件一个模块的另一部分功能 + class ModuleAPart2 : AbstractSubCommandGroup() { + @SubCommand + fun function2(arg0: Int) { + Testing.ok(arg0) + } + } + + class ModuleACommandGroup: AbstractSubCommandGroup() { + @SubCommand // 与在函数上标注这个注解类似, 它会带 `part1` 这个名称前缀来注册指令. 需要执行 /base part1 function1 + val part1 = ModuleAPart1() + @SubCommand("part1NewName") // 也可以使用 SubCommand 的参数来覆盖名称 /base part1NewName function1 + val part1b = ModuleAPart1() + @FlattenSubCommands // 新增, 不带前缀注册指令, 执行 /base function2 + val part2 = ModuleAPart2() + } + + @FlattenSubCommands + val moduleA = ModuleACommandGroup() + + @SubCommand + fun about(arg0: Int) { + Testing.ok(arg0) + } +} + class TestCompositeCommand : CompositeCommand( owner, "testComposite", "tsC" @@ -163,6 +204,7 @@ internal class InstanceTestCommand : AbstractConsoleInstanceTest() { private val simpleCommand by lazy { TestSimpleCommand() } private val rawCommand by lazy { TestRawCommand() } private val compositeCommand by lazy { TestCompositeCommand() } + private val unifiedCompositeCommand by lazy { MyUnifiedCommand() } @BeforeTest fun grantPermission() { @@ -499,6 +541,24 @@ internal class InstanceTestCommand : AbstractConsoleInstanceTest() { } } + @Test + fun `container composite command executing`() = runBlocking { + unifiedCompositeCommand.withRegistration { + assertEquals(0, withTesting { + assertSuccess(unifiedCompositeCommand.execute(sender, "part1 function1 0")) + }) + assertEquals(0, withTesting { + assertSuccess(unifiedCompositeCommand.execute(sender, "part1NewName function1 0")) + }) + assertEquals(0, withTesting { + assertSuccess(unifiedCompositeCommand.execute(sender, "function2 0")) + }) + assertEquals(0, withTesting { + assertSuccess(unifiedCompositeCommand.execute(sender, "about 0")) + }) + } + } + @Test fun `test first param command sender`() = runTest { object : CompositeCommand(owner, "cmd") {