From df461290c03812f4edaf9fb517dcddcf76cbef12 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 18 Oct 2020 12:26:54 +0800 Subject: [PATCH] Command resolving --- .../mamoe/mirai/console/command/Command.kt | 35 ++-- .../console/command/CommandExecuteResult.kt | 4 + .../mirai/console/command/CommandManager.kt | 173 ++++++++++-------- .../mirai/console/command/CommandSender.kt | 1 - .../mirai/console/command/CompositeCommand.kt | 21 ++- .../mamoe/mirai/console/command/RawCommand.kt | 17 +- .../mirai/console/command/SimpleCommand.kt | 12 +- .../command/descriptor/CommandDescriptor.kt | 109 +++++++++-- .../mirai/console/command/java/JCommand.kt | 21 +-- .../console/command/java/JCompositeCommand.kt | 2 + .../mirai/console/command/java/JRawCommand.kt | 27 +-- .../console/command/java/JSimpleCommand.kt | 2 + .../command/parse/CommandCallParser.kt | 4 +- .../command/parse/CommandValueArgument.kt | 19 +- .../parse/SpaceSeparatedCommandCallParser.kt | 23 +++ .../resolve/BuiltInCommandCallResolver.kt | 121 ++++++++++++ .../command/resolve/CommandCallResolver.kt | 15 -- .../command/resolve/ResolvedCommandCall.kt | 46 ++++- .../internal/command/CommandManagerImpl.kt | 56 +----- .../command/CompositeCommand.CommandParam.kt | 6 +- .../command/CompositeCommandInternal.kt | 97 +++------- .../command/executeCommandInternal.kt | 67 ------- .../mamoe/mirai/console/util/MessageUtils.kt | 25 +++ .../mirai/console/command/TestCommand.kt | 1 - 24 files changed, 527 insertions(+), 377 deletions(-) create mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/SpaceSeparatedCommandCallParser.kt create mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt delete mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/executeCommandInternal.kt create mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageUtils.kt diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt index 43346dcb9..5aac67c2d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt @@ -11,15 +11,16 @@ package net.mamoe.mirai.console.command -import net.mamoe.kjbb.JvmBlockingBridge -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register +import net.mamoe.mirai.console.command.descriptor.CommandArgumentContextAware +import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariant +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.java.JCommand import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.PermissionId -import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.console.util.ConsoleExperimentalApi /** * 指令 @@ -30,6 +31,8 @@ import net.mamoe.mirai.message.data.MessageChain * @see CompositeCommand 复合指令 * @see SimpleCommand 简单的, 支持参数自动解析的指令 * + * @see CommandArgumentContextAware + * * @see JCommand 为 Java 用户添加协程帮助的 [Command] */ public interface Command { @@ -48,6 +51,13 @@ public interface Command { @ResolveContext(COMMAND_NAME) public val secondaryNames: Array + /** + * + */ + @ConsoleExperimentalApi("Property name is experimental") + @ExperimentalCommandDescriptors + public val overloads: List + /** * 用法说明, 用于发送给用户. [usage] 一般包含 [description]. */ @@ -80,16 +90,6 @@ public interface Command { */ public val owner: CommandOwner - /** - * 在指令被执行时调用. - * - * @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数. - * - * @see CommandManager.executeCommand 查看更多信息 - */ - @JvmBlockingBridge - public suspend fun CommandSender.onCommand(args: MessageChain) - public companion object { /** @@ -116,12 +116,3 @@ public interface Command { } } } - -/** - * 调用 [Command.onCommand] - * @see Command.onCommand - */ -@JvmSynthetic -public suspend inline fun Command.onCommand(sender: CommandSender, args: MessageChain): Unit = - sender.onCommand(args) - diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt index 9076951b4..0d084810a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt @@ -12,6 +12,8 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandExecuteResult.CommandExecuteStatus +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain import kotlin.contracts.contract @@ -21,6 +23,8 @@ import kotlin.contracts.contract * * @see CommandExecuteStatus */ +@ConsoleExperimentalApi("Not yet implemented") +@ExperimentalCommandDescriptors public sealed class CommandExecuteResult { /** 指令最终执行状态 */ public abstract val status: CommandExecuteStatus diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt index b2c058d78..8d99ab6cc 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt @@ -8,16 +8,26 @@ */ @file:Suppress( - "NOTHING_TO_INLINE", "unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE", + "NOTHING_TO_INLINE", "unused", "MemberVisibilityCanBePrivate", "INAPPLICABLE_JVM_NAME" ) @file:JvmName("CommandManagerKt") package net.mamoe.mirai.console.command -import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.parse.CommandCall +import net.mamoe.mirai.console.command.parse.CommandCallParser +import net.mamoe.mirai.console.command.parse.CommandCallParser.Companion.parseCommandCall +import net.mamoe.mirai.console.command.resolve.CommandCallResolver +import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall +import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall.Companion.call +import net.mamoe.mirai.console.extensions.CommandCallResolverProvider import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.CommandManagerImpl.executeCommand +import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage +import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission import net.mamoe.mirai.message.data.* /** @@ -101,19 +111,45 @@ public interface CommandManager { * 注意: 字符串与消息元素之间不需要空格, 会被强制分割. 如 "bar[mirai:image:]" 会被分割为 "bar" 和 [Image] 类型的消息元素. * 3. 参数解析. 各类型指令实现不同. 详见 [RawCommand], [CompositeCommand], [SimpleCommand] * - * ### 未来的扩展 - * 在将来, 参数语法分析过程可能会被扩展, 允许插件自定义处理方式, 因此可能不会简单地使用空格分隔. + * ### 扩展 + * 参数语法分析过程可能会被扩展, 插件可以自定义处理方式, 因此可能不会简单地使用空格分隔. * * @param message 一条完整的指令. 如 "/managers add 123456.123456" * @param checkPermission 为 `true` 时检查权限 * + * @see CommandCallParser + * @see CommandCallResolver + * * @return 执行结果 */ - @JvmBlockingBridge - public suspend fun CommandSender.executeCommand( + // @JvmBlockingBridge + @OptIn(ExperimentalCommandDescriptors::class) + public suspend fun executeCommand( + caller: CommandSender, message: Message, checkPermission: Boolean = true, - ): CommandExecuteResult + ): CommandExecuteResult { + val call = message.asMessageChain().parseCommandCall(caller) ?: return CommandExecuteResult.CommandNotFound("") + val resolved = call.resolve() ?: return CommandExecuteResult.CommandNotFound(call.calleeName) + + val command = resolved.callee + + if (checkPermission && !command.permission.testPermission(caller)) { + return CommandExecuteResult.PermissionDenied(command, call.calleeName) + } + + return kotlin.runCatching { + resolved.call() + }.fold( + onSuccess = { + CommandExecuteResult.Success(resolved.callee, call.calleeName, EmptyMessageChain) + }, + onFailure = { + CommandExecuteResult.ExecutionFailed(it, resolved.callee, call.calleeName, EmptyMessageChain) + } + ) + } + /** * 解析并执行一个指令 @@ -124,35 +160,33 @@ public interface CommandManager { * @return 执行结果 * @see executeCommand */ - @JvmBlockingBridge + // @JvmBlockingBridge public suspend fun CommandSender.executeCommand( message: String, checkPermission: Boolean = true, - ): CommandExecuteResult = executeCommand(PlainText(message).asMessageChain(), checkPermission) + ): CommandExecuteResult = executeCommand(this, PlainText(message).asMessageChain(), checkPermission) + + @JvmName("resolveCall") + @ExperimentalCommandDescriptors + public fun CommandCall.resolve(): ResolvedCommandCall? { + GlobalComponentStorage.run { + CommandCallResolverProvider.useExtensions { provider -> + provider.instance.resolve(this@resolve)?.let { return it } + } + } + return null + } /** - * 执行一个确切的指令 - * @see executeCommand 获取更多信息 + * 从 [指令名称][commandName] 匹配对应的 [Command]. + * + * #### 实现细节 + * - [commandName] 带有 [commandPrefix] 时可以匹配到所有指令 + * - [commandName] 不带有 [commandPrefix] 时只能匹配到 [Command.prefixOptional] 的指令 + * + * @param commandName 可能带有或不带有 [commandPrefix]. */ - @JvmBlockingBridge - @JvmName("executeCommand") - public suspend fun Command.execute( - sender: CommandSender, - arguments: Message = EmptyMessageChain, - checkPermission: Boolean = true, - ): CommandExecuteResult - - /** - * 执行一个确切的指令 - * @see executeCommand 获取更多信息 - */ - @JvmBlockingBridge - @JvmName("executeCommand") - public suspend fun Command.execute( - sender: CommandSender, - arguments: String = "", - checkPermission: Boolean = true, - ): CommandExecuteResult = execute(sender, PlainText(arguments).asMessageChain(), checkPermission) + public fun matchCommand(commandName: String): Command? public companion object INSTANCE : CommandManager by CommandManagerImpl { // TODO: 2020/8/20 https://youtrack.jetbrains.com/issue/KT-41191 @@ -167,52 +201,37 @@ public interface CommandManager { override val allRegisteredCommands: List get() = CommandManagerImpl.allRegisteredCommands - - override suspend fun Command.execute( - sender: CommandSender, - arguments: Message, - checkPermission: Boolean, - ): CommandExecuteResult = - CommandManagerImpl.run { execute(sender, arguments = arguments, checkPermission = checkPermission) } - - override suspend fun CommandSender.executeCommand( - message: String, - checkPermission: Boolean, - ): CommandExecuteResult = CommandManagerImpl.run { executeCommand(message, checkPermission) } - - override suspend fun Command.execute( - sender: CommandSender, - arguments: String, - checkPermission: Boolean, - ): CommandExecuteResult = CommandManagerImpl.run { execute(sender, arguments, checkPermission) } - - override suspend fun CommandSender.executeCommand( - message: Message, - checkPermission: Boolean, - ): CommandExecuteResult = CommandManagerImpl.run { executeCommand(message, checkPermission) } - - /** - * 执行一个确切的指令 - * @see execute 获取更多信息 - */ - public suspend fun CommandSender.execute( - command: Command, - arguments: Message, - checkPermission: Boolean = true, - ): CommandExecuteResult { - return command.execute(this, arguments, checkPermission) - } - - /** - * 执行一个确切的指令 - * @see execute 获取更多信息 - */ - public suspend fun CommandSender.execute( - command: Command, - arguments: String, - checkPermission: Boolean = true, - ): CommandExecuteResult { - return command.execute(this, arguments, checkPermission) - } } +} + +/** + * 执行一个确切的指令 + * @see executeCommand 获取更多信息 + */ +// @JvmBlockingBridge +// @JvmName("executeCommand") +public suspend fun Command.execute( + sender: CommandSender, + arguments: String = "", + checkPermission: Boolean = true, +): CommandExecuteResult = execute(sender, PlainText(arguments).asMessageChain(), checkPermission) + +/** + * 执行一个确切的指令 + * @see executeCommand 获取更多信息 + */ +// @JvmBlockingBridge +// @JvmName("executeCommand") +public suspend fun Command.execute( + sender: CommandSender, + arguments: Message = EmptyMessageChain, + checkPermission: Boolean = true, +): CommandExecuteResult { + // TODO: 2020/10/18 net.mamoe.mirai.console.command.CommandManager.execute + val chain = buildMessageChain { + append(this@execute.primaryName) + append(' ') + append(arguments) + } + return CommandManager.executeCommand(sender, chain, checkPermission) } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt index 219c2485e..91b1e6d59 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.launch import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.Bot import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandSender.Companion.asCommandSender import net.mamoe.mirai.console.command.CommandSender.Companion.asMemberCommandSender import net.mamoe.mirai.console.command.CommandSender.Companion.asTempCommandSender diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt index 77126b96b..7f29eb4a7 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt @@ -124,13 +124,26 @@ public abstract class CompositeCommand( @Target(AnnotationTarget.VALUE_PARAMETER) protected annotation class Name(val value: String) - public final override suspend fun CommandSender.onCommand(args: MessageChain) { - matchSubCommand(args)?.parseAndExecute(this, args, true) ?: kotlin.run { - defaultSubCommand.onCommand(this, args) + @OptIn(ExperimentalCommandDescriptors::class) + override val overloads: List by lazy { + subCommands.flatMap { desc -> + desc.bakedSubNames.map { names -> + CommandSignatureVariantImpl( + valueParameters = + names.mapIndexed { index, s -> CommandValueParameter.StringConstant("p$index", s) } + desc.params.map { + CommandValueParameter.UserDefinedType(it.name, null, + isOptional = false, + isVararg = false, + type = it.type) + }, + onCall = { resolvedCommandCall -> + desc.onCommand(resolvedCommandCall.caller, resolvedCommandCall.resolvedValueArguments.drop(names.size)) + } + ) + } } } - protected override suspend fun CommandSender.onDefault(rawArgs: MessageChain) { sendMessage(usage) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt index a924e6982..75facfed8 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt @@ -11,14 +11,18 @@ package net.mamoe.mirai.console.command -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand +import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariant +import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariantImpl +import net.mamoe.mirai.console.command.descriptor.CommandValueParameter +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.java.JRawCommand import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageChainBuilder /** * 无参数解析, 接收原生参数的指令. @@ -52,6 +56,15 @@ public abstract class RawCommand( ) : Command { public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) } + @ExperimentalCommandDescriptors + override val overloads: List = listOf( + CommandSignatureVariantImpl(listOf(CommandValueParameter.UserDefinedType.createRequired("args", true))) { call -> + val sender = call.caller + val arguments = call.rawValueArguments + sender.onCommand(arguments.mapTo(MessageChainBuilder()) { it.value }.build()) + } + ) + /** * 在指令被执行时调用. * @@ -59,7 +72,7 @@ public abstract class RawCommand( * * @see CommandManager.execute 查看更多信息 */ - public abstract override suspend fun CommandSender.onCommand(args: MessageChain) + public abstract suspend fun CommandSender.onCommand(args: MessageChain) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt index 7a4a0e985..60e67ca32 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt @@ -61,6 +61,14 @@ public abstract class SimpleCommand( ) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), CommandArgumentContextAware { + @ExperimentalCommandDescriptors + override val overloads: List = listOf( + CommandSignatureVariantImpl(listOf(CommandValueParameter.UserDefinedType.createRequired("args", true))) { call -> + val sender = call.caller + subCommands.single().onCommand(sender, call.resolvedValueArguments) + } + ) + /** * 自动根据带有 [Handler] 注解的函数签名生成 [usage]. 也可以被覆盖. */ @@ -76,10 +84,6 @@ public abstract class SimpleCommand( */ public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext - public final override suspend fun CommandSender.onCommand(args: MessageChain) { - subCommands.single().parseAndExecute(this, args, false) - } - internal override fun checkSubCommand(subCommands: Array) { super.checkSubCommand(subCommands) check(subCommands.size == 1) { "There can only be exactly one function annotated with Handler at this moment as overloading is not yet supported." } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt index 690694d4e..2fdc5f424 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt @@ -9,19 +9,39 @@ package net.mamoe.mirai.console.command.descriptor +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.console.command.descriptor.ArgumentAcceptance.Companion.isAcceptable +import net.mamoe.mirai.console.command.descriptor.CommandValueParameter.UserDefinedType.Companion.createOptional +import net.mamoe.mirai.console.command.descriptor.CommandValueParameter.UserDefinedType.Companion.createRequired +import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall +import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull +import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.safeCast import kotlin.reflect.KClass import kotlin.reflect.KType +import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.typeOf -@ExperimentalCommandDescriptors -public interface CommandDescriptor { - public val overloads: List -} - +/** + * @see CommandSignatureVariantImpl + */ @ExperimentalCommandDescriptors public interface CommandSignatureVariant { public val valueParameters: List> + + @JvmBlockingBridge + public suspend fun call(resolvedCommandCall: ResolvedCommandCall) +} + +@ExperimentalCommandDescriptors +public class CommandSignatureVariantImpl( + override val valueParameters: List>, + private val onCall: suspend CommandSignatureVariantImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit, +) : CommandSignatureVariant { + override suspend fun call(resolvedCommandCall: ResolvedCommandCall) { + return onCall(resolvedCommandCall) + } } @@ -43,23 +63,85 @@ public interface ICommandParameter { * Reified type of [T] */ public val type: KType + + public val isVararg: Boolean + + public fun accepts(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): Boolean = + accepting(argument, commandArgumentContext).isAcceptable + + public fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance } +@ExperimentalCommandDescriptors +public sealed class ArgumentAcceptance( + /** + * Higher means more acceptable + */ + @ConsoleExperimentalApi + public val acceptanceLevel: Int, +) { + public object Direct : ArgumentAcceptance(Int.MAX_VALUE) + + public class WithTypeConversion( + public val typeVariant: TypeVariant<*>, + ) : ArgumentAcceptance(20) + + public class WithContextualConversion( + public val parser: CommandValueArgumentParser<*>, + ) : ArgumentAcceptance(10) + + public class ResolutionAmbiguity( + public val candidates: List>, + ) : ArgumentAcceptance(0) + + public object Impossible : ArgumentAcceptance(-1) + + public companion object { + @JvmStatic + public val ArgumentAcceptance.isAcceptable: Boolean + get() = acceptanceLevel > 0 + + @JvmStatic + public val ArgumentAcceptance.isNotAcceptable: Boolean + get() = acceptanceLevel <= 0 + } +} + + @ExperimentalCommandDescriptors public sealed class CommandValueParameter : ICommandParameter { - init { - @Suppress("LeakingThis") + internal fun validate() { // // TODO: 2020/10/18 net.mamoe.mirai.console.command.descriptor.CommandValueParameter.validate$mirai_console_mirai_console_main require(type.classifier?.safeCast>()?.isInstance(defaultValue) == true) { "defaultValue is not instance of type" } } + + public override fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance { + val expectingType = this.type + + if (argument.type.isSubtypeOf(expectingType)) return ArgumentAcceptance.Direct + + argument.typeVariants.associateWith { typeVariant -> + if (typeVariant.outType.isSubtypeOf(expectingType)) { + // TODO: 2020/10/11 resolution ambiguity + return ArgumentAcceptance.WithTypeConversion(typeVariant) + } + } + expectingType.classifierAsKClassOrNull()?.let { commandArgumentContext?.get(it) }?.let { parser -> + return ArgumentAcceptance.WithContextualConversion(parser) + } + return ArgumentAcceptance.Impossible + } + public class StringConstant( public override val name: String, + public val expectingValue: String, ) : CommandValueParameter() { public override val type: KType get() = STRING_TYPE public override val defaultValue: Nothing? get() = null public override val isOptional: Boolean get() = false + public override val isVararg: Boolean get() = false private companion object { @OptIn(ExperimentalStdlibApi::class) @@ -67,23 +149,28 @@ public sealed class CommandValueParameter : ICommandParameter { } } + /** + * @see createOptional + * @see createRequired + */ public class UserDefinedType( public override val name: String, public override val defaultValue: T?, public override val isOptional: Boolean, + public override val isVararg: Boolean, public override val type: KType, ) : CommandValueParameter() { public companion object { @JvmStatic - public inline fun createOptional(name: String, defaultValue: T): UserDefinedType { + public inline fun createOptional(name: String, isVararg: Boolean, defaultValue: T): UserDefinedType { @OptIn(ExperimentalStdlibApi::class) - return UserDefinedType(name, defaultValue, true, typeOf()) + return UserDefinedType(name, defaultValue, true, isVararg, typeOf()) } @JvmStatic - public inline fun createRequired(name: String): UserDefinedType { + public inline fun createRequired(name: String, isVararg: Boolean): UserDefinedType { @OptIn(ExperimentalStdlibApi::class) - return UserDefinedType(name, null, false, typeOf()) + return UserDefinedType(name, null, false, isVararg, typeOf()) } } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt index 1fbf748c0..af4fbbe0e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt @@ -9,13 +9,8 @@ package net.mamoe.mirai.console.command.java -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import net.mamoe.mirai.console.command.Command -import net.mamoe.mirai.console.command.CommandManager -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand -import net.mamoe.mirai.console.command.CommandSender -import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.console.util.ConsoleExperimentalApi /** * 为 Java 用户添加协程帮助的 [Command]. @@ -24,17 +19,7 @@ import net.mamoe.mirai.message.data.MessageChain * * @see Command */ +@ConsoleExperimentalApi("Not yet supported") public interface JCommand : Command { - public override suspend fun CommandSender.onCommand(args: MessageChain) { - withContext(Dispatchers.IO) { onCommand(this@onCommand, args) } - } - - /** - * 在指令被执行时调用. - * - * @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数. - * - * @see CommandManager.executeCommand 查看更多信息 - */ - public fun onCommand(sender: CommandSender, args: MessageChain) // overrides blocking bridge + // TODO: 2020/10/18 JCommand } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt index 215944426..fa0b9203a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt @@ -17,6 +17,7 @@ import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.util.ConsoleExperimentalApi /** * 复合指令. 指令注册时候会通过反射构造指令解析器. @@ -68,6 +69,7 @@ import net.mamoe.mirai.console.permission.Permission * * @see buildCommandArgumentContext */ +@ConsoleExperimentalApi("Not yet supported") public abstract class JCompositeCommand @JvmOverloads constructor( owner: CommandOwner, diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt index 84b06ea5c..19cf70d2a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt @@ -9,16 +9,15 @@ package net.mamoe.mirai.console.command.java -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.mamoe.mirai.console.command.* -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute +import net.mamoe.mirai.console.command.BuiltInCommands +import net.mamoe.mirai.console.command.Command +import net.mamoe.mirai.console.command.CommandManager +import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.permission.Permission -import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.console.util.ConsoleExperimentalApi /** * 供 Java 用户继承 @@ -46,6 +45,7 @@ import net.mamoe.mirai.message.data.SingleMessage * * @see JRawCommand */ +@ConsoleExperimentalApi("Not yet supported") public abstract class JRawCommand @JvmOverloads constructor( /** @@ -72,19 +72,4 @@ public abstract class JRawCommand /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ public final override var prefixOptional: Boolean = false protected set - - /** - * 在指令被执行时调用. - * - * @param args 指令参数. 数组元素类型可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. - * - * @see CommandManager.execute 查看更多信息 - */ - @Suppress("INAPPLICABLE_JVM_NAME") - @JvmName("onCommand") - public abstract fun onCommand(sender: CommandSender, args: MessageChain) - - public final override suspend fun CommandSender.onCommand(args: MessageChain) { - withContext(Dispatchers.IO) { onCommand(this@onCommand, args) } - } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt index f0e9b9784..4333bfa45 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt @@ -17,6 +17,7 @@ import net.mamoe.mirai.console.command.descriptor.CommandArgumentContext import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.util.ConsoleExperimentalApi /** * Java 实现: @@ -42,6 +43,7 @@ import net.mamoe.mirai.console.permission.Permission * @see SimpleCommand * @see [CommandManager.executeCommand] */ +@ConsoleExperimentalApi("Not yet supported") public abstract class JSimpleCommand( owner: CommandOwner, @ResolveContext(COMMAND_NAME) primaryName: String, diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandCallParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandCallParser.kt index 5be371aa8..4d4bc6f59 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandCallParser.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandCallParser.kt @@ -14,6 +14,8 @@ import net.mamoe.mirai.message.data.MessageChain * * @see CommandCallResolver The call resolver for [CommandCall] to become [ResolvedCommandCall] * @see CommandCallParserProvider The extension point + * + * @see SpaceSeparatedCommandCallParser */ @ConsoleExperimentalApi @ExperimentalCommandDescriptors @@ -24,7 +26,7 @@ public interface CommandCallParser { * * @return `null` if unable to parse (i.e. due to syntax errors). */ - public fun parse(sender: CommandSender, message: MessageChain): CommandCall? + public fun parse(caller: CommandSender, message: MessageChain): CommandCall? public companion object { /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt index 3c473e21a..f9dfe4588 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt @@ -14,6 +14,7 @@ import net.mamoe.mirai.console.command.descriptor.MessageContentTypeVariant import net.mamoe.mirai.console.command.descriptor.NoValueArgumentMappingException import net.mamoe.mirai.console.command.descriptor.TypeVariant import net.mamoe.mirai.message.data.MessageContent +import kotlin.reflect.KType import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.typeOf @@ -34,6 +35,7 @@ public interface CommandArgument */ @ExperimentalCommandDescriptors public interface CommandValueArgument : CommandArgument { + public val type: KType public val value: RawCommandArgument public val typeVariants: List> } @@ -45,6 +47,8 @@ public interface CommandValueArgument : CommandArgument { public data class InvariantCommandValueArgument( public override val value: RawCommandArgument, ) : CommandValueArgument { + @OptIn(ExperimentalStdlibApi::class) + override val type: KType = typeOf() override val typeVariants: List> = listOf(MessageContentTypeVariant) } @@ -57,10 +61,14 @@ public fun CommandValueArgument.mapValue(typeVariant: TypeVariant): T = t public inline fun CommandValueArgument.mapToType(): T = mapToTypeOrNull() ?: throw NoValueArgumentMappingException(this, typeOf()) +@OptIn(ExperimentalStdlibApi::class) @ExperimentalCommandDescriptors -public inline fun CommandValueArgument.mapToTypeOrNull(): T? { +public fun CommandValueArgument.mapToType(type: KType): T = + mapToTypeOrNull(type) ?: throw NoValueArgumentMappingException(this, type) + +@ExperimentalCommandDescriptors +public fun CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? { @OptIn(ExperimentalStdlibApi::class) - val expectingType = typeOf() val result = typeVariants .filter { it.outType.isSubtypeOf(expectingType) } .also { @@ -71,5 +79,12 @@ public inline fun CommandValueArgument.mapToTypeOrNull(): T? { acc else typeVariant } + @Suppress("UNCHECKED_CAST") return result.mapValue(value) as T +} + +@ExperimentalCommandDescriptors +public inline fun CommandValueArgument.mapToTypeOrNull(): T? { + @OptIn(ExperimentalStdlibApi::class) + return mapToTypeOrNull(typeOf()) } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/SpaceSeparatedCommandCallParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/SpaceSeparatedCommandCallParser.kt new file mode 100644 index 000000000..62e6a5370 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/SpaceSeparatedCommandCallParser.kt @@ -0,0 +1,23 @@ +package net.mamoe.mirai.console.command.parse + +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.internal.command.flattenCommandComponents +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageContent +import net.mamoe.mirai.message.data.content + +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public object SpaceSeparatedCommandCallParser : CommandCallParser { + override fun parse(caller: CommandSender, message: MessageChain): CommandCall? { + val flatten = message.flattenCommandComponents().filterIsInstance() + if (flatten.isEmpty()) return null + return CommandCallImpl( + caller = caller, + calleeName = flatten.first().content, + valueArguments = flatten.drop(1).map(::InvariantCommandValueArgument) + ) + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt new file mode 100644 index 000000000..5ff77e7dc --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt @@ -0,0 +1,121 @@ +package net.mamoe.mirai.console.command.resolve + +import net.mamoe.mirai.console.command.Command +import net.mamoe.mirai.console.command.CommandManager +import net.mamoe.mirai.console.command.descriptor.* +import net.mamoe.mirai.console.command.descriptor.ArgumentAcceptance.Companion.isNotAcceptable +import net.mamoe.mirai.console.command.parse.CommandCall +import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.console.util.cast +import net.mamoe.mirai.console.util.safeCast +import java.util.* + +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public object BuiltInCommandCallResolver : CommandCallResolver { + override fun resolve(call: CommandCall): ResolvedCommandCall? { + val callee = CommandManager.matchCommand(call.calleeName) ?: return null + + val valueArguments = call.valueArguments + val context = callee.safeCast()?.context + + val signature = resolveImpl(callee, valueArguments, context) ?: return null + + return ResolvedCommandCallImpl(call.caller, callee, signature, call.valueArguments) + } + + private data class ResolveData( + val variant: CommandSignatureVariant, + val argumentAcceptances: List, + ) + + private data class ArgumentAcceptanceWithIndex( + val index: Int, + val acceptance: ArgumentAcceptance, + ) + + private fun resolveImpl( + callee: Command, + valueArguments: List, + context: CommandArgumentContext?, + ): CommandSignatureVariant? { + + callee.overloads + .mapNotNull l@{ signature -> + val zipped = signature.valueParameters.zip(valueArguments) + + if (signature.valueParameters.drop(zipped.size).any { !it.isOptional }) return@l null // not enough args + + ResolveData(signature, zipped.mapIndexed { index, (parameter, argument) -> + val accepting = parameter.accepting(argument, context) + if (accepting.isNotAcceptable) { + return@l null // argument type not assignable + } + ArgumentAcceptanceWithIndex(index, accepting) + }) + } + .also { result -> result.singleOrNull()?.let { return it.variant } } + .takeLongestMatches() + .ifEmpty { return null } + .also { result -> result.singleOrNull()?.let { return it.variant } } + // take single ArgumentAcceptance.Direct + .also { list -> + + val candidates = list + .flatMap { phase -> + phase.argumentAcceptances.filter { it.acceptance is ArgumentAcceptance.Direct }.map { phase to it } + } + candidates.singleOrNull()?.let { return it.first.variant } // single Direct + if (candidates.distinctBy { it.second.index }.size != candidates.size) { + // Resolution ambiguity + /* + open class A + open class AA: A() + + open class C + open class CC: C() + + fun foo(a: A, c: CC) = 1 + fun foo(a: AA, c: C) = 1 + */ + // The call is foo(AA(), C()) or foo(A(), CC()) + return null + } + } + + return null + } + + /* + + +open class A +open class B : A() +open class C : A() +open class D : C() +open class BB : B() + +fun foo(a: A, c: C) = 1 +//fun foo(a: A, c: A) = 1 +//fun foo(a: A, c: C, def: Int = 0) = 1 +fun foo(a: B, c: C, d: D) = "" + +fun foo(b: BB, a: A, d: C) = 1.0 + + +fun main() { + val a = foo(D(), D()) // int + val b = foo(A(), C()) // int + val d = foo(BB(), c = C(), D()) // string +} + */ + + private fun List.takeLongestMatches(): List { + if (isEmpty()) return emptyList() + return associateByTo(TreeMap(Comparator.reverseOrder())) { it.variant.valueParameters.size }.let { m -> + val firstKey = m.keys.first().cast() + m.filter { it.key == firstKey }.map { it.value.cast() } + } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/CommandCallResolver.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/CommandCallResolver.kt index e8767cc8e..6e8eda94e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/CommandCallResolver.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/CommandCallResolver.kt @@ -11,8 +11,6 @@ package net.mamoe.mirai.console.command.resolve import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.parse.CommandCall -import net.mamoe.mirai.console.extensions.CommandCallResolverProvider -import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage /** * The resolver converting a [CommandCall] into [ResolvedCommandCall] based on registered [] @@ -20,17 +18,4 @@ import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage @ExperimentalCommandDescriptors public interface CommandCallResolver { public fun resolve(call: CommandCall): ResolvedCommandCall? - - public companion object { - @JvmStatic - @JvmName("resolveCall") - public fun CommandCall.resolve(): ResolvedCommandCall? { - GlobalComponentStorage.run { - CommandCallResolverProvider.useExtensions { provider -> - provider.instance.resolve(this@resolve)?.let { return it } - } - } - return null - } - } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/ResolvedCommandCall.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/ResolvedCommandCall.kt index 476ef273d..e0505279e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/ResolvedCommandCall.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/ResolvedCommandCall.kt @@ -7,19 +7,23 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.console.command.resolve; +package net.mamoe.mirai.console.command.resolve import net.mamoe.mirai.console.command.Command import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.CompositeCommand -import net.mamoe.mirai.console.command.descriptor.CommandDescriptor import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariant import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.parse.CommandCall import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.command.parse.mapToType +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.LazyThreadSafetyMode.PUBLICATION /** * The resolved [CommandCall]. + * + * @see ResolvedCommandCallImpl */ @ExperimentalCommandDescriptors public interface ResolvedCommandCall { @@ -31,17 +35,41 @@ public interface ResolvedCommandCall { public val callee: Command /** - * The callee [CommandDescriptor], specifically a sub command from [CompositeCommand] - */ - public val calleeDescriptor: CommandDescriptor - - /** - * The callee [CommandSignatureVariant] + * The callee [CommandSignatureVariant], specifically a sub command from [CompositeCommand] */ public val calleeSignature: CommandSignatureVariant + /** + * Original arguments + */ + public val rawValueArguments: List + /** * Resolved value arguments arranged mapping the [CommandSignatureVariant.valueParameters] by index. */ - public val valueArguments: List + @ConsoleExperimentalApi + public val resolvedValueArguments: List + + public companion object { + @JvmStatic + @ExperimentalCommandDescriptors + public suspend fun ResolvedCommandCall.call() { + return this.calleeSignature.call(this) + } + } +} + +@ExperimentalCommandDescriptors +public class ResolvedCommandCallImpl( + override val caller: CommandSender, + override val callee: Command, + override val calleeSignature: CommandSignatureVariant, + override val rawValueArguments: List, +) : ResolvedCommandCall { + override val resolvedValueArguments: List by lazy(PUBLICATION) { + calleeSignature.valueParameters.zip(rawValueArguments).map { (parameter, argument) -> + argument.mapToType(parameter.type) + // TODO: 2020/10/17 consider vararg and optional + } + } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt index 14dfa51b2..11b55aeb1 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt @@ -16,17 +16,15 @@ import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.Command.Companion.allNames import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender +import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.message.MessageEvent -import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.MessageContent -import net.mamoe.mirai.message.data.asMessageChain import net.mamoe.mirai.message.data.content import net.mamoe.mirai.utils.MiraiLogger import java.util.concurrent.locks.ReentrantLock -internal object CommandManagerImpl : CommandManager, CoroutineScope by CoroutineScope(MiraiConsole.job) { +internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiConsole.childScope("CommandManagerImpl") { private val logger: MiraiLogger by lazy { MiraiConsole.createLogger("command") } @@ -48,11 +46,11 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine /** * 从原始的 command 中解析出 Command 对象 */ - internal fun matchCommand(rawCommand: String): Command? { - if (rawCommand.startsWith(commandPrefix)) { - return requiredPrefixCommandMap[rawCommand.substringAfter(commandPrefix).toLowerCase()] + override fun matchCommand(commandName: String): Command? { + if (commandName.startsWith(commandPrefix)) { + return requiredPrefixCommandMap[commandName.substringAfter(commandPrefix).toLowerCase()] } - return optionalPrefixCommandMap[rawCommand.toLowerCase()] + return optionalPrefixCommandMap[commandName.toLowerCase()] } internal val commandListener: Listener by lazy { @@ -65,7 +63,7 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine ) { val sender = this.toCommandSender() - when (val result = sender.executeCommand(message)) { + when (val result = executeCommand(sender, message)) { is CommandExecuteResult.PermissionDenied -> { if (!result.command.prefixOptional || message.content.startsWith(CommandManager.commandPrefix)) { sender.sendMessage("权限不足") @@ -152,44 +150,4 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine } override fun Command.isRegistered(): Boolean = this in _registeredCommands - - override suspend fun Command.execute( - sender: CommandSender, - arguments: Message, - checkPermission: Boolean - ): CommandExecuteResult { - return sender.executeCommandInternal( - this, - arguments.flattenCommandComponents(), - primaryName, - checkPermission - ) - } - - override suspend fun Command.execute( - sender: CommandSender, - arguments: String, - checkPermission: Boolean - ): CommandExecuteResult { - return sender.executeCommandInternal( - this, - arguments.flattenCommandComponents(), - primaryName, - checkPermission - ) - } - - override suspend fun CommandSender.executeCommand( - message: Message, - checkPermission: Boolean - ): CommandExecuteResult { - val msg = message.asMessageChain().filterIsInstance() - if (msg.isEmpty()) return CommandExecuteResult.CommandNotFound("") - return executeCommandInternal(msg, msg[0].content.substringBefore(' '), checkPermission) - } - - override suspend fun CommandSender.executeCommand(message: String, checkPermission: Boolean): CommandExecuteResult { - if (message.isBlank()) return CommandExecuteResult.CommandNotFound("") - return executeCommandInternal(message, message.substringBefore(' '), checkPermission) - } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt index 16bae5667..8279d9f1c 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt @@ -13,8 +13,8 @@ package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.CompositeCommand import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser -import kotlin.reflect.KClass import kotlin.reflect.KParameter +import kotlin.reflect.KType /* internal fun Parameter.toCommandParam(): CommandParameter<*> { @@ -39,10 +39,10 @@ internal data class CommandParameter( /** * 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandValueArgumentParser] 解析. */ - val type: KClass, // exact type + val type: KType, // exact type val parameter: KParameter, // source parameter ) { - constructor(name: String, type: KClass, parameter: KParameter, parser: CommandValueArgumentParser) : this( + constructor(name: String, type: KType, parameter: KParameter, parser: CommandValueArgumentParser) : this( name, type, parameter ) { this._overrideParser = parser diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt index ea9249e5b..ede2cef02 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt @@ -12,12 +12,12 @@ package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.* -import net.mamoe.mirai.console.command.descriptor.CommandArgumentContext -import net.mamoe.mirai.console.command.descriptor.CommandArgumentContextAware -import net.mamoe.mirai.console.internal.data.kClassQualifiedNameOrTip +import net.mamoe.mirai.console.command.descriptor.* import net.mamoe.mirai.console.permission.Permission -import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission -import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.buildMessageChain import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -89,6 +89,21 @@ internal abstract class AbstractReflectionCommand } + @OptIn(ExperimentalCommandDescriptors::class) + private fun CommandParameter.toCommandValueParameter(): CommandValueParameter { + return CommandValueParameter.UserDefinedType(name, null, false, false, type) + } + + + @OptIn(ExperimentalCommandDescriptors::class) + override val overloads: List by lazy { + subCommands.map { desc -> + CommandSignatureVariantImpl(desc.params.map { it.toCommandValueParameter() }) { call -> + desc.onCommand(call.caller, call.resolvedValueArguments) + } + } + } + interface SubCommandAnnotationResolver { fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Boolean fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array @@ -132,27 +147,12 @@ internal abstract class AbstractReflectionCommand val params: Array>, val description: String, val permission: Permission, - val onCommand: suspend (sender: CommandSender, parsedArgs: Map) -> Boolean, + val onCommand: suspend (sender: CommandSender, parsedArgs: List) -> Boolean, val context: CommandArgumentContext, val argumentBuilder: (sender: CommandSender) -> MutableMap, ) { val usage: String = createUsage(this@AbstractReflectionCommand) - internal suspend fun parseAndExecute( - sender: CommandSender, - argsWithSubCommandNameNotRemoved: MessageChain, - removeSubName: Boolean, - ) { - val args = parseArgs(sender, argsWithSubCommandNameNotRemoved, if (removeSubName) 1 else 0) - if (!this.permission.testPermission(sender)) { - sender.sendMessage(usage) // TODO: 2020/8/26 #127 - return - } - if (args == null || !onCommand(sender, args)) { - sender.sendMessage(usage) - } - } - private fun KParameter.isOptional(): Boolean { return isOptional || this.type.isMarkedNullable } @@ -163,49 +163,6 @@ internal abstract class AbstractReflectionCommand @JvmField internal val bakedSubNames: Array> = names.map { it.bakeSubName() }.toTypedArray() - private fun parseArgs(sender: CommandSender, rawArgs: MessageChain, offset: Int): MutableMap? { - if (rawArgs.size < offset + minimalArgumentsSize) - return null - //require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } - return argumentBuilder(sender).also { result -> - params.forEachIndexed { index, parameter -> - val rawArg = rawArgs.getOrNull(offset + index) - result[parameter.parameter] = when (rawArg) { - null -> { - val p = parameter.parameter - when { - p.isOptional -> return@forEachIndexed - p.type.isMarkedNullable -> { - result[parameter.parameter] = null - return@forEachIndexed - } - else -> null - } - } - is PlainText -> context[parameter.type]?.parse(rawArg.content, sender) - is MessageContent -> context[parameter.type]?.parse(rawArg, sender) - else -> throw IllegalArgumentException("Illegal Message kind: ${rawArg.kClassQualifiedNameOrTip}") - } ?: error("Cannot find a parser for $rawArg") - } - } - } - } - - /** - * @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] - */ - internal fun matchSubCommand(rawArgs: MessageChain): SubCommandDescriptor? { - val maxCount = rawArgs.size - var cur = 0 - bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) -> - if (name.size != cur) { - if (cur++ == maxCount) return null - } - if (name.contentEqualsOffset(rawArgs, length = cur)) { - return descriptor - } - } - return null } } @@ -349,23 +306,23 @@ internal fun AbstractReflectionCommand.createSubCommand( // if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") val paramName = param.findAnnotation()?.value ?: param.name ?: "unknown" - CommandParameter( + CommandParameter( paramName, - (param.type.classifier as? KClass<*>) - ?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)"), + param.type, param ) }.toTypedArray() // TODO: 2020/09/19 检查 optional/nullable 是否都在最后 + @Suppress("UNCHECKED_CAST") return SubCommandDescriptor( commandName, - params, + params as Array>, subDescription, // overridePermission?.value permission,//overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission, - onCommand = { sender: CommandSender, args: Map -> - val result = function.callSuspendBy(args) + onCommand = { _: CommandSender, args -> + val result = function.callSuspendBy(parameters.zip(args).toMap()) checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/executeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/executeCommandInternal.kt deleted file mode 100644 index a9ff54cca..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/executeCommandInternal.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.console.internal.command - -import net.mamoe.mirai.console.command.* -import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission -import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.asMessageChain - -@JvmSynthetic -@Throws(CommandExecutionException::class) -internal suspend fun CommandSender.executeCommandInternal( - command: Command, - args: MessageChain, - commandName: String, - checkPermission: Boolean, -): CommandExecuteResult { - if (checkPermission && !command.permission.testPermission(this)) { - return CommandExecuteResult.PermissionDenied(command, commandName) - } - - kotlin.runCatching { - command.onCommand(this, args) - }.fold( - onSuccess = { - return CommandExecuteResult.Success( - commandName = commandName, - command = command, - args = args - ) - }, - onFailure = { exception -> - return if (exception is IllegalArgumentException) CommandExecuteResult.IllegalArgument( - commandName = commandName, - command = command, - exception = exception, - args = args - ) else CommandExecuteResult.ExecutionFailed( - commandName = commandName, - command = command, - exception = exception, - args = args - ) - } - ) -} - - -@JvmSynthetic -internal suspend fun CommandSender.executeCommandInternal( - messages: Any, - commandName: String, - checkPermission: Boolean, -): CommandExecuteResult { - val command = - CommandManagerImpl.matchCommand(commandName) ?: return CommandExecuteResult.CommandNotFound(commandName) - val args = messages.flattenCommandComponents() - - return executeCommandInternal(command, args.drop(1).asMessageChain(), commandName, checkPermission) -} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageUtils.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageUtils.kt new file mode 100644 index 000000000..475481b08 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageUtils.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2020 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/master/LICENSE + */ + +package net.mamoe.mirai.console.util + +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageContent + +@ConsoleExperimentalApi +public object MessageUtils { + @JvmStatic + public fun MessageChain.messageContentsSequence(): Sequence = asSequence().filterIsInstance() + + @JvmStatic + public fun MessageChain.firstContent(): MessageContent = messageContentsSequence().first() + + @JvmStatic + public fun MessageChain.firstContentOrNull(): MessageContent? = messageContentsSequence().firstOrNull() +} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt index b7937225a..bd3b1487f 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.runBlocking import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.Testing import net.mamoe.mirai.console.Testing.withTesting -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.CommandManager.INSTANCE.registeredCommands