diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt index 1b24675be..f37299e00 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register -import net.mamoe.mirai.console.command.description.* +import net.mamoe.mirai.console.command.descriptor.* import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.CommandManagerImpl.allRegisteredCommands import net.mamoe.mirai.console.internal.util.runIgnoreException @@ -150,8 +150,8 @@ public object BuiltInCommands { ConsoleCommandOwner, "permission", "权限", "perm", description = "管理权限", overrideContext = buildCommandArgumentContext { - PermitteeId::class with PermitteeIdArgumentParser - Permission::class with PermissionIdArgumentParser.map { id -> + PermitteeId::class with PermitteeIdValueArgumentParser + Permission::class with PermissionIdValueArgumentParser.map { id -> kotlin.runCatching { id.findCorrespondingPermissionOrFail() }.getOrElse { illegalArgument("指令不存在: $id", it) } 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..9d0036771 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,25 +11,27 @@ 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 /** * 指令 * - * @see CommandManager.register 注册这个指令 + * @see CommandManager.registerCommand 注册这个指令 * * @see RawCommand 无参数解析, 接收原生参数的指令 * @see CompositeCommand 复合指令 * @see SimpleCommand 简单的, 支持参数自动解析的指令 * + * @see CommandArgumentContextAware + * * @see JCommand 为 Java 用户添加协程帮助的 [Command] */ public interface Command { @@ -48,18 +50,25 @@ public interface Command { @ResolveContext(COMMAND_NAME) public val secondaryNames: Array + /** + * 指令可能的参数列表. + */ + @ConsoleExperimentalApi("Property name is experimental") + @ExperimentalCommandDescriptors + public val overloads: List + /** * 用法说明, 用于发送给用户. [usage] 一般包含 [description]. */ public val usage: String /** - * 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] + * 描述, 用于显示在 [BuiltInCommands.HelpCommand] */ public val description: String /** - * 此指令所分配的权限. + * 为此指令分配的权限. * * ### 实现约束 * - [Permission.id] 应由 [CommandOwner.permissionId] 创建. 因此保证相同的 [PermissionId.namespace] @@ -72,6 +81,8 @@ public interface Command { * * 会影响聊天语境中的解析. */ + @ExperimentalCommandDescriptors + @ConsoleExperimentalApi public val prefixOptional: Boolean /** @@ -80,16 +91,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 { /** @@ -109,19 +110,10 @@ public interface Command { public fun checkCommandName(@ResolveContext(COMMAND_NAME) name: String) { when { name.isBlank() -> throw IllegalArgumentException("Command name should not be blank.") - name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in command name.") + name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces are not yet allowed in command name.") name.contains(':') -> throw IllegalArgumentException("':' is forbidden in command name.") name.contains('.') -> throw IllegalArgumentException("'.' is forbidden in command name.") } } } } - -/** - * 调用 [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 f2195ae3b..9697dd4d7 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 @@ -55,6 +59,21 @@ public sealed class CommandExecuteResult { public override val status: CommandExecuteStatus get() = CommandExecuteStatus.SUCCESSFUL } + /** 执行执行时发生了一个非法参数错误 */ + public class IllegalArgument( + /** 指令执行时发生的错误 */ + public override val exception: IllegalCommandArgumentException, + /** 尝试执行的指令 */ + public override val command: Command, + /** 尝试执行的指令名 */ + public override val commandName: String, + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + public override val args: MessageChain + ) : CommandExecuteResult() { + /** 指令最终执行状态, 总是 [CommandExecuteStatus.EXECUTION_EXCEPTION] */ + public override val status: CommandExecuteStatus get() = CommandExecuteStatus.ILLEGAL_ARGUMENT + } + /** 指令执行过程出现了错误 */ public class ExecutionFailed( /** 指令执行时发生的错误 */ @@ -71,9 +90,9 @@ public sealed class CommandExecuteResult { } /** 没有匹配的指令 */ - public class CommandNotFound( + public class UnresolvedCall( /** 尝试执行的指令名 */ - public override val commandName: String + public override val commandName: String, ) : CommandExecuteResult() { /** 指令执行时发生的错误, 总是 `null` */ public override val exception: Nothing? get() = null @@ -119,7 +138,9 @@ public sealed class CommandExecuteResult { COMMAND_NOT_FOUND, /** 权限不足 */ - PERMISSION_DENIED + PERMISSION_DENIED, + /** 非法参数 */ + ILLEGAL_ARGUMENT, } } @@ -138,6 +159,18 @@ public fun CommandExecuteResult.isSuccess(): Boolean { return this is CommandExecuteResult.Success } +/** + * 当 [this] 为 [CommandExecuteResult.IllegalArgument] 时返回 `true` + */ +@JvmSynthetic +public fun CommandExecuteResult.isIllegalArgument(): Boolean { + contract { + returns(true) implies (this@isIllegalArgument is CommandExecuteResult.IllegalArgument) + returns(false) implies (this@isIllegalArgument !is CommandExecuteResult.IllegalArgument) + } + return this is CommandExecuteResult.IllegalArgument +} + /** * 当 [this] 为 [CommandExecuteResult.ExecutionFailed] 时返回 `true` */ @@ -151,7 +184,7 @@ public fun CommandExecuteResult.isExecutionException(): Boolean { } /** - * 当 [this] 为 [CommandExecuteResult.ExecutionFailed] 时返回 `true` + * 当 [this] 为 [CommandExecuteResult.PermissionDenied] 时返回 `true` */ @JvmSynthetic public fun CommandExecuteResult.isPermissionDenied(): Boolean { @@ -163,19 +196,19 @@ public fun CommandExecuteResult.isPermissionDenied(): Boolean { } /** - * 当 [this] 为 [CommandExecuteResult.ExecutionFailed] 时返回 `true` + * 当 [this] 为 [CommandExecuteResult.UnresolvedCall] 时返回 `true` */ @JvmSynthetic public fun CommandExecuteResult.isCommandNotFound(): Boolean { contract { - returns(true) implies (this@isCommandNotFound is CommandExecuteResult.CommandNotFound) - returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.CommandNotFound) + returns(true) implies (this@isCommandNotFound is CommandExecuteResult.UnresolvedCall) + returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.UnresolvedCall) } - return this is CommandExecuteResult.CommandNotFound + return this is CommandExecuteResult.UnresolvedCall } /** - * 当 [this] 为 [CommandExecuteResult.ExecutionFailed] 或 [CommandExecuteResult.CommandNotFound] 时返回 `true` + * 当 [this] 为 [CommandExecuteResult.ExecutionFailed], [CommandExecuteResult.IllegalArgument] 或 [CommandExecuteResult.UnresolvedCall] 时返回 `true` */ @JvmSynthetic public fun CommandExecuteResult.isFailure(): Boolean { 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..f837ed105 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,7 +8,7 @@ */ @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") @@ -16,21 +16,21 @@ 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.resolve.CommandCallResolver import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.CommandManagerImpl.executeCommand +import net.mamoe.mirai.console.internal.command.executeCommandImpl +import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.message.data.* /** * 指令管理器 */ public interface CommandManager { - /** - * 获取已经注册了的属于这个 [CommandOwner] 的指令列表. - * - * @return 这一时刻的浅拷贝. - */ - public val CommandOwner.registeredCommands: List - /** * 获取所有已经注册了指令列表. * @@ -44,9 +44,17 @@ public interface CommandManager { public val commandPrefix: String /** - * 取消注册所有属于 [this] 的指令 + * 获取已经注册了的属于这个 [CommandOwner] 的指令列表. + * + * @return 这一时刻的浅拷贝. */ - public fun CommandOwner.unregisterAllCommands() + public fun getRegisteredCommands(owner: CommandOwner): List + + + /** + * 取消注册所有属于 [owner] 的指令 + */ + public fun unregisterAllCommands(owner: CommandOwner) /** * 注册一个指令. @@ -65,35 +73,31 @@ public interface CommandManager { * * 注意: [内建指令][BuiltInCommands] 也可以被覆盖. */ - @JvmName("registerCommand") - public fun Command.register(override: Boolean = false): Boolean + public fun registerCommand(command: Command, override: Boolean = false): Boolean /** * 查找并返回重名的指令. 返回重名指令. */ - @JvmName("findCommandDuplicate") - public fun Command.findDuplicate(): Command? + public fun findDuplicateCommand(command: Command): Command? /** * 取消注册这个指令. * * 若指令未注册, 返回 `false`. */ - @JvmName("unregisterCommand") - public fun Command.unregister(): Boolean + public fun unregisterCommand(command: Command): Boolean /** - * 当 [this] 已经 [注册][register] 时返回 `true` + * 当 [command] 已经 [注册][registerCommand] 时返回 `true` */ - @JvmName("isCommandRegistered") - public fun Command.isRegistered(): Boolean + public fun isCommandRegistered(command: Command): Boolean /** * 解析并执行一个指令. * - * 如要避免参数解析, 请使用 [Command.onCommand] - * * ### 指令解析流程 + * 1. [CommandCallParser] 将 [MessageChain] 解析为 [CommandCall] + * 2. [CommandCallResolver] 将 [CommandCall] 解析为 [] * 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理) * 2. 参数语法分析. * 在当前的实现下, [message] 被以空格和 [SingleMessage] 分割. @@ -101,118 +105,154 @@ public interface CommandManager { * 注意: 字符串与消息元素之间不需要空格, 会被强制分割. 如 "bar[mirai:image:]" 会被分割为 "bar" 和 [Image] 类型的消息元素. * 3. 参数解析. 各类型指令实现不同. 详见 [RawCommand], [CompositeCommand], [SimpleCommand] * - * ### 未来的扩展 - * 在将来, 参数语法分析过程可能会被扩展, 允许插件自定义处理方式, 因此可能不会简单地使用空格分隔. + * ### 扩展 + * 参数语法分析过程可能会被扩展, 插件可以自定义处理方式 ([CommandCallParser]), 因此可能不会简单地使用空格分隔. * * @param message 一条完整的指令. 如 "/managers add 123456.123456" * @param checkPermission 为 `true` 时检查权限 * + * @see CommandCallParser + * @see CommandCallResolver + * + * @see CommandSender.executeCommand + * @see Command.execute + * * @return 执行结果 */ + @ExperimentalCommandDescriptors @JvmBlockingBridge - public suspend fun CommandSender.executeCommand( + public suspend fun executeCommand( + caller: CommandSender, message: Message, checkPermission: Boolean = true, - ): CommandExecuteResult - - /** - * 解析并执行一个指令 - * - * @param message 一条完整的指令. 如 "/managers add 123456.123456" - * @param checkPermission 为 `true` 时检查权限 - * - * @return 执行结果 - * @see executeCommand - */ - @JvmBlockingBridge - public suspend fun CommandSender.executeCommand( - message: String, - checkPermission: Boolean = true, - ): CommandExecuteResult = executeCommand(PlainText(message).asMessageChain(), checkPermission) + ): CommandExecuteResult { + return executeCommandImpl(message, caller, checkPermission) + } /** * 执行一个确切的指令 + * + * @param command 目标指令 + * @param arguments 参数列表 + * * @see executeCommand 获取更多信息 + * @see Command.execute */ - @JvmBlockingBridge + @ConsoleExperimentalApi @JvmName("executeCommand") - public suspend fun Command.execute( + @ExperimentalCommandDescriptors + @JvmSynthetic + public suspend fun executeCommand( sender: CommandSender, + command: Command, arguments: Message = EmptyMessageChain, checkPermission: Boolean = true, - ): CommandExecuteResult + ): CommandExecuteResult { + // TODO: 2020/10/18 net.mamoe.mirai.console.command.CommandManager.execute + val chain = buildMessageChain { + append(CommandManager.commandPrefix) + append(command.primaryName) + append(' ') + append(arguments) + } + return CommandManager.executeCommand(sender, chain, checkPermission) + } /** - * 执行一个确切的指令 - * @see executeCommand 获取更多信息 + * 从 [指令名称][commandName] 匹配对应的 [Command]. + * + * #### 实现细节 + * - [commandName] 带有 [commandPrefix] 时可以匹配到所有指令 + * - [commandName] 不带有 [commandPrefix] 时只能匹配到 [Command.prefixOptional] 的指令 + * + * @param commandName 可能带有或不带有 [commandPrefix]. */ - @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 - - override val CommandOwner.registeredCommands: List get() = CommandManagerImpl.run { this@registeredCommands.registeredCommands } - override fun CommandOwner.unregisterAllCommands(): Unit = CommandManagerImpl.run { unregisterAllCommands() } - override fun Command.register(override: Boolean): Boolean = CommandManagerImpl.run { register(override) } - override fun Command.findDuplicate(): Command? = CommandManagerImpl.run { findDuplicate() } - override fun Command.unregister(): Boolean = CommandManagerImpl.run { unregister() } - override fun Command.isRegistered(): Boolean = CommandManagerImpl.run { isRegistered() } - override val commandPrefix: String get() = CommandManagerImpl.commandPrefix - 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 获取更多信息 + * @see CommandManager.getRegisteredCommands */ - public suspend fun CommandSender.execute( - command: Command, - arguments: Message, - checkPermission: Boolean = true, - ): CommandExecuteResult { - return command.execute(this, arguments, checkPermission) - } + @get:JvmName("registeredCommands0") + @get:JvmSynthetic + public inline val CommandOwner.registeredCommands: List + get() = getRegisteredCommands(this) /** - * 执行一个确切的指令 - * @see execute 获取更多信息 + * @see CommandManager.registerCommand */ - public suspend fun CommandSender.execute( - command: Command, - arguments: String, - checkPermission: Boolean = true, - ): CommandExecuteResult { - return command.execute(this, arguments, checkPermission) - } + @JvmSynthetic + public inline fun Command.register(override: Boolean = false): Boolean = registerCommand(this, override) + + /** + * @see CommandManager.unregisterCommand + */ + @JvmSynthetic + public inline fun Command.unregister(): Boolean = unregisterCommand(this) + + /** + * @see CommandManager.isCommandRegistered + */ + @get:JvmSynthetic + public inline val Command.isRegistered: Boolean + get() = isCommandRegistered(this) + + /** + * @see CommandManager.unregisterAll + */ + @JvmSynthetic + public inline fun CommandOwner.unregisterAll(): Unit = unregisterAllCommands(this) + + /** + * @see CommandManager.findDuplicate + */ + @JvmSynthetic + public inline fun Command.findDuplicate(): Command? = findDuplicateCommand(this) + } -} \ No newline at end of file +} + +/** + * 解析并执行一个指令 + * + * @param message 一条完整的指令. 如 "/managers add 123456.123456" + * @param checkPermission 为 `true` 时检查权限 + * + * @return 执行结果 + * @see executeCommand + */ +@JvmName("execute0") +@ExperimentalCommandDescriptors +@JvmSynthetic +public suspend inline fun CommandSender.executeCommand( + message: String, + checkPermission: Boolean = true, +): CommandExecuteResult = CommandManager.executeCommand(this, PlainText(message).asMessageChain(), checkPermission) + + +/** + * 执行一个确切的指令 + * @see executeCommand 获取更多信息 + */ +@JvmName("execute0") +@ExperimentalCommandDescriptors +@JvmSynthetic +public suspend inline fun Command.execute( + sender: CommandSender, + arguments: Message = EmptyMessageChain, + checkPermission: Boolean = true, +): CommandExecuteResult = CommandManager.executeCommand(sender, this, arguments, checkPermission) + +/** + * 执行一个确切的指令 + * @see executeCommand 获取更多信息 + */ +@JvmName("execute0") +@ExperimentalCommandDescriptors +@JvmSynthetic +public suspend inline fun Command.execute( + sender: CommandSender, + arguments: String = "", + checkPermission: Boolean = true, +): CommandExecuteResult = execute(sender, PlainText(arguments), checkPermission) 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 08aaccac0..f3f9b55ee 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,15 +20,14 @@ 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 import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender -import net.mamoe.mirai.console.command.description.CommandArgumentParserException +import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.internal.data.castOrNull +import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip import net.mamoe.mirai.console.internal.plugin.rootCauseOrSelf import net.mamoe.mirai.console.permission.AbstractPermitteeId import net.mamoe.mirai.console.permission.Permittee @@ -281,6 +280,13 @@ public sealed class AbstractCommandSender : CommandSender, CoroutineScope { if (this is CommandSenderOnMessage<*>) { val cause = e.rootCauseOrSelf + // TODO: 2020/10/17 + // CommandArgumentParserException 作为 IllegalCommandArgumentException 不会再进入此函数 + // 已在 + // - [console] CommandManagerImpl.commandListener + // - [terminal] ConsoleThread.kt + // 处理 + val message = cause .takeIf { it is CommandArgumentParserException }?.message ?: "${cause::class.simpleName.orEmpty()}: ${cause.message}" 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 926fcd19a..a7582efc7 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 @@ -17,14 +17,13 @@ package net.mamoe.mirai.console.command -import net.mamoe.mirai.console.command.description.* +import net.mamoe.mirai.console.command.descriptor.* 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.AbstractReflectionCommand +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 net.mamoe.mirai.message.data.MessageChain import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION @@ -90,16 +89,26 @@ public abstract class CompositeCommand( parentPermission: Permission = owner.parentPermission, prefixOptional: Boolean = false, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, -) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), +) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), CommandArgumentContextAware { + private val reflector by lazy { CommandReflector(this, CompositeCommandSubCommandAnnotationResolver) } + + @ExperimentalCommandDescriptors + public final override val overloads: List by lazy { + reflector.findSubCommands() + } + /** * 自动根据带有 [SubCommand] 注解的函数签名生成 [usage]. 也可以被覆盖. */ - public override val usage: String get() = super.usage + public override val usage: String by lazy { + @OptIn(ExperimentalCommandDescriptors::class) + reflector.generateUsage(overloads) + } /** - * [CommandArgumentParser] 的环境 + * [CommandValueArgumentParser] 的环境 */ public final override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext @@ -123,20 +132,6 @@ public abstract class CompositeCommand( @Retention(RUNTIME) @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) - } - } - - - protected override suspend fun CommandSender.onDefault(rawArgs: MessageChain) { - sendMessage(usage) - } - - internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver - get() = CompositeCommandSubCommandAnnotationResolver } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/IllegalCommandArgumentException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/IllegalCommandArgumentException.kt new file mode 100644 index 000000000..0e8936524 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/IllegalCommandArgumentException.kt @@ -0,0 +1,29 @@ +/* + * 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 + * + */ + +@file:Suppress("unused") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException + +/** + * 在处理参数时遇到的 _正常_ 错误. 如参数不符合规范, 参数值越界等. + * + * [message] 将会发送给指令调用方. + * + * @see CommandArgumentParserException + */ +public open class IllegalCommandArgumentException : IllegalArgumentException { + public constructor() : super() + public constructor(message: String?) : super(message) + public constructor(message: String?, cause: Throwable?) : super(message, cause) + public constructor(cause: Throwable?) : super(cause) +} 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..2486f6f42 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,17 @@ 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.* 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.internal.data.typeOf0 import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.buildMessageChain /** * 无参数解析, 接收原生参数的指令. @@ -52,6 +55,18 @@ public abstract class RawCommand( ) : Command { public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) } + @ExperimentalCommandDescriptors + override val overloads: List = listOf( + CommandSignatureVariantImpl( + receiverParameter = CommandReceiverParameter(false, typeOf0()), + valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired>("args", true)) + ) { call -> + val sender = call.caller + val arguments = call.rawValueArguments + sender.onCommand(buildMessageChain { arguments.forEach { +it.value } }) + } + ) + /** * 在指令被执行时调用. * @@ -59,7 +74,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 27bb0f6e3..14861f717 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 @@ -18,20 +18,23 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand -import net.mamoe.mirai.console.command.description.* +import net.mamoe.mirai.console.command.descriptor.* import net.mamoe.mirai.console.command.java.JSimpleCommand 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.AbstractReflectionCommand +import net.mamoe.mirai.console.internal.command.CommandReflector +import net.mamoe.mirai.console.internal.command.IllegalCommandDeclarationException import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.permission.Permission -import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER /** * 简单的, 支持参数自动解析的指令. * * 要查看指令解析流程, 参考 [CommandManager.executeCommand] - * 要查看参数解析方式, 参考 [CommandArgumentParser] + * 要查看参数解析方式, 参考 [CommandValueArgumentParser] * * Kotlin 实现: * ``` @@ -58,39 +61,41 @@ public abstract class SimpleCommand( parentPermission: Permission = owner.parentPermission, prefixOptional: Boolean = false, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, -) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), +) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), CommandArgumentContextAware { + private val reflector by lazy { CommandReflector(this, SimpleCommandSubCommandAnnotationResolver) } + + @ExperimentalCommandDescriptors + public final override val overloads: List by lazy { + reflector.findSubCommands().also { + if (it.isEmpty()) + throw IllegalCommandDeclarationException(this, "SimpleCommand must have at least one subcommand, whereas zero present.") + } + } + /** * 自动根据带有 [Handler] 注解的函数签名生成 [usage]. 也可以被覆盖. */ - public override val usage: String get() = super.usage + public override val usage: String by lazy { + @OptIn(ExperimentalCommandDescriptors::class) + reflector.generateUsage(overloads) + } /** * 标注指令处理器 */ + @Target(FUNCTION) protected annotation class Handler + /** 参数名, 将参与构成 [usage] */ + @ConsoleExperimentalApi("Classname might change") + @Target(VALUE_PARAMETER) + protected annotation class Name(val value: String) + /** * 指令参数环境. 默认为 [CommandArgumentContext.Builtins] `+` `overrideContext` */ 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." } - } - - @Deprecated("prohibited", level = DeprecationLevel.HIDDEN) - internal override suspend fun CommandSender.onDefault(rawArgs: MessageChain) { - sendMessage(usage) - } - - internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver - get() = SimpleCommandSubCommandAnnotationResolver } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt similarity index 73% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt index cacea64e0..acc3d3d25 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt @@ -9,18 +9,19 @@ @file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "unused", "MemberVisibilityCanBePrivate") -package net.mamoe.mirai.console.command.description +package net.mamoe.mirai.console.command.descriptor import net.mamoe.mirai.Bot import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.CompositeCommand import net.mamoe.mirai.console.command.SimpleCommand -import net.mamoe.mirai.console.command.description.CommandArgumentContext.ParserPair +import net.mamoe.mirai.console.command.descriptor.CommandArgumentContext.ParserPair import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.contact.* import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.MessageContent import net.mamoe.mirai.message.data.PlainText import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KClass @@ -28,7 +29,7 @@ import kotlin.reflect.full.isSubclassOf /** - * 指令参数环境, 即 [CommandArgumentParser] 的集合, 用于 [CompositeCommand] 和 [SimpleCommand]. + * 指令参数环境, 即 [CommandValueArgumentParser] 的集合, 用于 [CompositeCommand] 和 [SimpleCommand]. * * 在指令解析时, 总是从 [CommandArgumentContextAware.context] 搜索相关解析器 * @@ -37,20 +38,20 @@ import kotlin.reflect.full.isSubclassOf * @see SimpleCommandArgumentContext 简单实现 * @see EmptyCommandArgumentContext 空实现, 类似 [emptyList] * - * @see CommandArgumentContext.Builtins 内建 [CommandArgumentParser] + * @see CommandArgumentContext.Builtins 内建 [CommandValueArgumentParser] * * @see buildCommandArgumentContext DSL 构造 */ public interface CommandArgumentContext { /** - * [KClass] 到 [CommandArgumentParser] 的匹配 + * [KClass] 到 [CommandValueArgumentParser] 的匹配 */ public data class ParserPair( val klass: KClass, - val parser: CommandArgumentParser, + val parser: CommandValueArgumentParser, ) - public operator fun get(klass: KClass): CommandArgumentParser? + public operator fun get(klass: KClass): CommandValueArgumentParser? public fun toList(): List> @@ -65,30 +66,32 @@ public interface CommandArgumentContext { } /** - * 内建的默认 [CommandArgumentParser] + * 内建的默认 [CommandValueArgumentParser] */ public object Builtins : CommandArgumentContext by (buildCommandArgumentContext { - Int::class with IntArgumentParser - Byte::class with ByteArgumentParser - Short::class with ShortArgumentParser - Boolean::class with BooleanArgumentParser - String::class with StringArgumentParser - Long::class with LongArgumentParser - Double::class with DoubleArgumentParser - Float::class with FloatArgumentParser + Int::class with IntValueArgumentParser + Byte::class with ByteValueArgumentParser + Short::class with ShortValueArgumentParser + Boolean::class with BooleanValueArgumentParser + String::class with StringValueArgumentParser + Long::class with LongValueArgumentParser + Double::class with DoubleValueArgumentParser + Float::class with FloatValueArgumentParser - Image::class with ImageArgumentParser - PlainText::class with PlainTextArgumentParser + Image::class with ImageValueArgumentParser + PlainText::class with PlainTextValueArgumentParser - Contact::class with ExistingContactArgumentParser - User::class with ExistingUserArgumentParser - Member::class with ExistingMemberArgumentParser - Group::class with ExistingGroupArgumentParser - Friend::class with ExistingFriendArgumentParser - Bot::class with ExistingBotArgumentParser + Contact::class with ExistingContactValueArgumentParser + User::class with ExistingUserValueArgumentParser + Member::class with ExistingMemberValueArgumentParser + Group::class with ExistingGroupValueArgumentParser + Friend::class with ExistingFriendValueArgumentParser + Bot::class with ExistingBotValueArgumentParser - PermissionId::class with PermissionIdArgumentParser - PermitteeId::class with PermitteeIdArgumentParser + PermissionId::class with PermissionIdValueArgumentParser + PermitteeId::class with PermitteeIdValueArgumentParser + + MessageContent::class with RawContentValueArgumentParser }) } @@ -100,7 +103,7 @@ public interface CommandArgumentContext { */ public interface CommandArgumentContextAware { /** - * [CommandArgumentParser] 的集合 + * [CommandValueArgumentParser] 的集合 */ public val context: CommandArgumentContext } @@ -114,7 +117,7 @@ public operator fun CommandArgumentContext.plus(replacer: CommandArgumentContext if (replacer == EmptyCommandArgumentContext) return this if (this == EmptyCommandArgumentContext) return replacer return object : CommandArgumentContext { - override fun get(klass: KClass): CommandArgumentParser? = + override fun get(klass: KClass): CommandValueArgumentParser? = replacer[klass] ?: this@plus[klass] override fun toList(): List> = replacer.toList() + this@plus.toList() @@ -129,8 +132,8 @@ public operator fun CommandArgumentContext.plus(replacer: List>): if (this == EmptyCommandArgumentContext) return SimpleCommandArgumentContext(replacer) return object : CommandArgumentContext { @Suppress("UNCHECKED_CAST") - override fun get(klass: KClass): CommandArgumentParser? = - replacer.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandArgumentParser? + override fun get(klass: KClass): CommandValueArgumentParser? = + replacer.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandValueArgumentParser? ?: this@plus[klass] override fun toList(): List> = replacer.toList() + this@plus.toList() @@ -146,9 +149,9 @@ public operator fun CommandArgumentContext.plus(replacer: List>): public class SimpleCommandArgumentContext( public val list: List>, ) : CommandArgumentContext { - override fun get(klass: KClass): CommandArgumentParser? = + override fun get(klass: KClass): CommandValueArgumentParser? = (this.list.firstOrNull { klass == it.klass }?.parser - ?: this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser) as CommandArgumentParser? + ?: this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser) as CommandValueArgumentParser? override fun toList(): List> = list } @@ -160,7 +163,7 @@ public class SimpleCommandArgumentContext( * ``` * val context = buildCommandArgumentContext { * Int::class with IntArgParser - * Member::class with ExistMemberArgParser + * Member::class with ExistingMemberArgParser * Group::class with { s: String, sender: CommandSender -> * Bot.getInstance(s.toLong()).getGroup(s.toLong()) * } @@ -200,14 +203,14 @@ public class CommandArgumentContextBuilder : MutableList> by mutab * 添加一个指令解析器. */ @JvmName("add") - public infix fun Class.with(parser: CommandArgumentParser): CommandArgumentContextBuilder = + public infix fun Class.with(parser: CommandValueArgumentParser): CommandArgumentContextBuilder = this.kotlin with parser /** * 添加一个指令解析器 */ @JvmName("add") - public inline infix fun KClass.with(parser: CommandArgumentParser): CommandArgumentContextBuilder { + public inline infix fun KClass.with(parser: CommandValueArgumentParser): CommandArgumentContextBuilder { add(ParserPair(this, parser)) return this@CommandArgumentContextBuilder } @@ -218,9 +221,9 @@ public class CommandArgumentContextBuilder : MutableList> by mutab @JvmSynthetic @LowPriorityInOverloadResolution public inline infix fun KClass.with( - crossinline parser: CommandArgumentParser.(s: String, sender: CommandSender) -> T, + crossinline parser: CommandValueArgumentParser.(s: String, sender: CommandSender) -> T, ): CommandArgumentContextBuilder { - add(ParserPair(this, object : CommandArgumentParser { + add(ParserPair(this, object : CommandValueArgumentParser { override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) })) return this@CommandArgumentContextBuilder @@ -231,16 +234,16 @@ public class CommandArgumentContextBuilder : MutableList> by mutab */ @JvmSynthetic public inline infix fun KClass.with( - crossinline parser: CommandArgumentParser.(s: String) -> T, + crossinline parser: CommandValueArgumentParser.(s: String) -> T, ): CommandArgumentContextBuilder { - add(ParserPair(this, object : CommandArgumentParser { + add(ParserPair(this, object : CommandValueArgumentParser { override fun parse(raw: String, sender: CommandSender): T = parser(raw) })) return this@CommandArgumentContextBuilder } @JvmSynthetic - public inline fun add(parser: CommandArgumentParser): CommandArgumentContextBuilder { + public inline fun add(parser: CommandValueArgumentParser): CommandArgumentContextBuilder { add(ParserPair(T::class, parser)) return this@CommandArgumentContextBuilder } @@ -251,8 +254,8 @@ public class CommandArgumentContextBuilder : MutableList> by mutab @ConsoleExperimentalApi @JvmSynthetic public inline infix fun add( - crossinline parser: CommandArgumentParser<*>.(s: String) -> T, - ): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser { + crossinline parser: CommandValueArgumentParser<*>.(s: String) -> T, + ): CommandArgumentContextBuilder = T::class with object : CommandValueArgumentParser { override fun parse(raw: String, sender: CommandSender): T = parser(raw) } @@ -263,8 +266,8 @@ public class CommandArgumentContextBuilder : MutableList> by mutab @JvmSynthetic @LowPriorityInOverloadResolution public inline infix fun add( - crossinline parser: CommandArgumentParser<*>.(s: String, sender: CommandSender) -> T, - ): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser { + crossinline parser: CommandValueArgumentParser<*>.(s: String, sender: CommandSender) -> T, + ): CommandArgumentContextBuilder = T::class with object : CommandValueArgumentParser { override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParserBuiltins.kt similarity index 82% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParserBuiltins.kt index 28d49cd11..a1fc279e3 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParserBuiltins.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.console.command.description +package net.mamoe.mirai.console.command.descriptor import net.mamoe.mirai.Bot import net.mamoe.mirai.console.command.* @@ -25,47 +25,47 @@ import net.mamoe.mirai.message.data.* /** * 使用 [String.toInt] 解析 */ -public object IntArgumentParser : InternalCommandArgumentParserExtensions { +public object IntValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Int = raw.toIntOrNull() ?: illegalArgument("无法解析 $raw 为整数") } /** - * 使用 [String.toInt] 解析 + * 使用 [String.toLong] 解析 */ -public object LongArgumentParser : InternalCommandArgumentParserExtensions { +public object LongValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Long = raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为长整数") } /** - * 使用 [String.toInt] 解析 + * 使用 [String.toShort] 解析 */ -public object ShortArgumentParser : InternalCommandArgumentParserExtensions { +public object ShortValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Short = raw.toShortOrNull() ?: illegalArgument("无法解析 $raw 为短整数") } /** - * 使用 [String.toInt] 解析 + * 使用 [String.toByte] 解析 */ -public object ByteArgumentParser : InternalCommandArgumentParserExtensions { +public object ByteValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Byte = raw.toByteOrNull() ?: illegalArgument("无法解析 $raw 为字节") } /** - * 使用 [String.toInt] 解析 + * 使用 [String.toDouble] 解析 */ -public object DoubleArgumentParser : InternalCommandArgumentParserExtensions { +public object DoubleValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Double = raw.toDoubleOrNull() ?: illegalArgument("无法解析 $raw 为小数") } /** - * 使用 [String.toInt] 解析 + * 使用 [String.toFloat] 解析 */ -public object FloatArgumentParser : InternalCommandArgumentParserExtensions { +public object FloatValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Float = raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数") } @@ -73,14 +73,14 @@ public object FloatArgumentParser : InternalCommandArgumentParserExtensions { +public object StringValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): String = raw } /** * 解析 [String] 通过 [Image]. */ -public object ImageArgumentParser : InternalCommandArgumentParserExtensions { +public object ImageValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Image { return kotlin.runCatching { Image(raw) @@ -95,7 +95,7 @@ public object ImageArgumentParser : InternalCommandArgumentParserExtensions { +public object PlainTextValueArgumentParser : InternalCommandValueArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): PlainText { return PlainText(raw) } @@ -109,7 +109,7 @@ public object PlainTextArgumentParser : InternalCommandArgumentParserExtensions< /** * 当字符串内容为(不区分大小写) "true", "yes", "enabled" */ -public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> { +public object BooleanValueArgumentParser : InternalCommandValueArgumentParserExtensions<Boolean> { public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str -> str.equals("true", ignoreCase = true) || str.equals("yes", ignoreCase = true) @@ -121,7 +121,7 @@ public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Bo /** * 根据 [Bot.id] 解析一个登录后的 [Bot] */ -public object ExistingBotArgumentParser : InternalCommandArgumentParserExtensions<Bot> { +public object ExistingBotValueArgumentParser : InternalCommandValueArgumentParserExtensions<Bot> { public override fun parse(raw: String, sender: CommandSender): Bot = if (raw == "~") sender.inferBotOrFail() else raw.findBotOrFail() @@ -136,7 +136,7 @@ public object ExistingBotArgumentParser : InternalCommandArgumentParserExtension /** * 解析任意一个存在的好友. */ -public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtensions<Friend> { +public object ExistingFriendValueArgumentParser : InternalCommandValueArgumentParserExtensions<Friend> { private val syntax = """ - `botId.friendId` - `botId.friendNick` (模糊搜索, 寻找最优匹配) @@ -175,7 +175,7 @@ public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtens /** * 解析任意一个存在的群. */ -public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensions<Group> { +public object ExistingGroupValueArgumentParser : InternalCommandValueArgumentParserExtensions<Group> { private val syntax = """ - `botId.groupId` - `~` (指代指令调用人自己所在群. 仅群聊天环境下) @@ -202,7 +202,7 @@ public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensi } } -public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensions<User> { +public object ExistingUserValueArgumentParser : InternalCommandValueArgumentParserExtensions<User> { private val syntax: String = """ - `botId.groupId.memberId` - `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配) @@ -215,11 +215,11 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio """.trimIndent() override fun parse(raw: String, sender: CommandSender): User { - return parseImpl(sender, raw, ExistingMemberArgumentParser::parse, ExistingFriendArgumentParser::parse) + return parseImpl(sender, raw, ExistingMemberValueArgumentParser::parse, ExistingFriendValueArgumentParser::parse) } override fun parse(raw: MessageContent, sender: CommandSender): User { - return parseImpl(sender, raw, ExistingMemberArgumentParser::parse, ExistingFriendArgumentParser::parse) + return parseImpl(sender, raw, ExistingMemberValueArgumentParser::parse, ExistingFriendValueArgumentParser::parse) } private fun <T> parseImpl( @@ -246,7 +246,7 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio } -public object ExistingContactArgumentParser : InternalCommandArgumentParserExtensions<Contact> { +public object ExistingContactValueArgumentParser : InternalCommandValueArgumentParserExtensions<Contact> { private val syntax: String = """ - `botId.groupId.memberId` - `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配) @@ -259,11 +259,11 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten """.trimIndent() override fun parse(raw: String, sender: CommandSender): Contact { - return parseImpl(sender, raw, ExistingUserArgumentParser::parse, ExistingGroupArgumentParser::parse) + return parseImpl(sender, raw, ExistingUserValueArgumentParser::parse, ExistingGroupValueArgumentParser::parse) } override fun parse(raw: MessageContent, sender: CommandSender): Contact { - return parseImpl(sender, raw, ExistingUserArgumentParser::parse, ExistingGroupArgumentParser::parse) + return parseImpl(sender, raw, ExistingUserValueArgumentParser::parse, ExistingGroupValueArgumentParser::parse) } private fun <T> parseImpl( @@ -286,7 +286,7 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten /** * 解析任意一个群成员. */ -public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtensions<Member> { +public object ExistingMemberValueArgumentParser : InternalCommandValueArgumentParserExtensions<Member> { private val syntax: String = """ - `botId.groupId.memberId` - `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配) @@ -333,7 +333,7 @@ public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtens } } -public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> { +public object PermissionIdValueArgumentParser : CommandValueArgumentParser<PermissionId> { override fun parse(raw: String, sender: CommandSender): PermissionId { return kotlin.runCatching { PermissionId.parseFromString(raw) }.getOrElse { illegalArgument("无法解析 $raw 为 PermissionId") @@ -341,7 +341,7 @@ public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> { } } -public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> { +public object PermitteeIdValueArgumentParser : CommandValueArgumentParser<PermitteeId> { override fun parse(raw: String, sender: CommandSender): PermitteeId { return if (raw == "~") sender.permitteeId else kotlin.runCatching { AbstractPermitteeId.parseFromString(raw) }.getOrElse { @@ -351,13 +351,19 @@ public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> { override fun parse(raw: MessageContent, sender: CommandSender): PermitteeId { if (raw is At) { - return ExistingUserArgumentParser.parse(raw, sender).asCommandSender(false).permitteeId + return ExistingUserValueArgumentParser.parse(raw, sender).asCommandSender(false).permitteeId } return super.parse(raw, sender) } } -internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArgumentParser<T> { +/** 直接返回原始参数 [MessageContent] */ +public object RawContentValueArgumentParser : CommandValueArgumentParser<MessageContent> { + override fun parse(raw: String, sender: CommandSender): MessageContent = PlainText(raw) + override fun parse(raw: MessageContent, sender: CommandSender): MessageContent = raw +} + +internal interface InternalCommandValueArgumentParserExtensions<T : Any> : CommandValueArgumentParser<T> { fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数") fun Long.findBotOrFail(): Bot = Bot.getInstanceOrNull(this) ?: illegalArgument("无法找到 Bot: $this") diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParserException.kt similarity index 70% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserException.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParserException.kt index 033d642df..25a59067d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserException.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParserException.kt @@ -9,17 +9,20 @@ @file:Suppress("unused") -package net.mamoe.mirai.console.command.description +package net.mamoe.mirai.console.command.descriptor + +import net.mamoe.mirai.console.command.IllegalCommandArgumentException /** * 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范等. * * [message] 将会发送给指令调用方. * - * @see CommandArgumentParser - * @see CommandArgumentParser.illegalArgument + * @see IllegalCommandArgumentException + * @see CommandValueArgumentParser + * @see CommandValueArgumentParser.illegalArgument */ -public class CommandArgumentParserException : RuntimeException { +public class CommandArgumentParserException : IllegalCommandArgumentException { public constructor() : super() public constructor(message: String?) : super(message) public constructor(message: String?, cause: Throwable?) : super(message, cause) 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 new file mode 100644 index 000000000..556a44e6c --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt @@ -0,0 +1,287 @@ +/* + * 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.command.descriptor + +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueParameter.UserDefinedType.Companion.createOptional +import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueParameter.UserDefinedType.Companion.createRequired +import net.mamoe.mirai.console.command.descriptor.ArgumentAcceptance.Companion.isAcceptable +import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall +import net.mamoe.mirai.console.internal.data.classifierAsKClass +import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull +import net.mamoe.mirai.console.internal.data.typeOf0 +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KType +import kotlin.reflect.full.isSubtypeOf +import kotlin.reflect.typeOf + +/** + * @see CommandSignatureVariantImpl + */ +@ExperimentalCommandDescriptors +public interface CommandSignatureVariant { + @ConsoleExperimentalApi + public val receiverParameter: CommandReceiverParameter<out CommandSender>? + + public val valueParameters: List<AbstractCommandValueParameter<*>> + + public suspend fun call(resolvedCommandCall: ResolvedCommandCall) +} + +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public interface CommandSignatureVariantFromKFunction : CommandSignatureVariant { + public val originFunction: KFunction<*> +} + +@ExperimentalCommandDescriptors +public abstract class AbstractCommandSignatureVariant : CommandSignatureVariant { + override fun toString(): String { + val receiverParameter = receiverParameter + return if (receiverParameter == null) { + "CommandSignatureVariant(${valueParameters.joinToString()})" + } else { + "CommandSignatureVariant($receiverParameter, ${valueParameters.joinToString()})" + } + } +} + +@ExperimentalCommandDescriptors +public open class CommandSignatureVariantImpl( + override val receiverParameter: CommandReceiverParameter<out CommandSender>?, + override val valueParameters: List<AbstractCommandValueParameter<*>>, + private val onCall: suspend CommandSignatureVariantImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit, +) : CommandSignatureVariant, AbstractCommandSignatureVariant() { + override suspend fun call(resolvedCommandCall: ResolvedCommandCall) { + return onCall(resolvedCommandCall) + } +} + +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public open class CommandSignatureVariantFromKFunctionImpl( + override val receiverParameter: CommandReceiverParameter<out CommandSender>?, + override val valueParameters: List<AbstractCommandValueParameter<*>>, + override val originFunction: KFunction<*>, + private val onCall: suspend CommandSignatureVariantFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit, +) : CommandSignatureVariantFromKFunction, AbstractCommandSignatureVariant() { + override suspend fun call(resolvedCommandCall: ResolvedCommandCall) { + return onCall(resolvedCommandCall) + } +} + + +/** + * Inherited instances must be [CommandValueParameter] or [CommandReceiverParameter] + */ +@ExperimentalCommandDescriptors +public interface CommandParameter<T : Any?> { + public val name: String? + + public val isOptional: Boolean + + /** + * Reified type of [T] + */ + public val type: KType +} + +@ExperimentalCommandDescriptors +public abstract class AbstractCommandParameter<T> : CommandParameter<T> { + override fun toString(): String = buildString { + append(name) + append(": ") + append(type.classifierAsKClass().simpleName) + append(if (type.isMarkedNullable) "?" else "") + } +} + +/** + * Inherited instances must be [AbstractCommandValueParameter] + */ +@ExperimentalCommandDescriptors +public interface CommandValueParameter<T : Any?> : CommandParameter<T> { + + 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<TypeVariant<*>>, + ) : 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 class CommandReceiverParameter<T : CommandSender>( + override val isOptional: Boolean, + override val type: KType, +) : CommandParameter<T>, AbstractCommandParameter<T>() { + override val name: String get() = PARAMETER_NAME + + init { + check(type.classifier is KClass<*>) { + "CommandReceiverParameter.type.classifier must be KClass." + } + } + + public companion object { + public const val PARAMETER_NAME: String = "<receiver>" + } +} + + +internal val ANY_TYPE = typeOf0<Any>() +internal val ARRAY_OUT_ANY_TYPE = typeOf0<Array<out Any?>>() + +@ExperimentalCommandDescriptors +public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>, AbstractCommandParameter<T>() { + override fun toString(): String = buildString { + if (isVararg) append("vararg ") + append(super.toString()) + if (isOptional) { + append(" = ...") + } + } + + public override fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance { + if (isVararg) { + val arrayElementType = this.type.arguments.single() // Array<T> + return acceptingImpl(arrayElementType.type ?: ANY_TYPE, argument, commandArgumentContext) + } + + return acceptingImpl(this.type, argument, commandArgumentContext) + } + + private fun acceptingImpl( + expectingType: KType, + argument: CommandValueArgument, + commandArgumentContext: CommandArgumentContext?, + ): ArgumentAcceptance { + 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 + } + + @ConsoleExperimentalApi + public class StringConstant( + @ConsoleExperimentalApi + public override val name: String?, + public val expectingValue: String, + ) : AbstractCommandValueParameter<String>() { + public override val type: KType get() = STRING_TYPE + public override val isOptional: Boolean get() = false + public override val isVararg: Boolean get() = false + + init { + require(expectingValue.isNotBlank()) { + "expectingValue must not be blank" + } + require(expectingValue.none(Char::isWhitespace)) { + "expectingValue must not contain whitespace" + } + } + + override fun toString(): String = "<$expectingValue>" + + private companion object { + @OptIn(ExperimentalStdlibApi::class) + val STRING_TYPE = typeOf<String>() + } + } + + /** + * @see createOptional + * @see createRequired + */ + public class UserDefinedType<T>( + public override val name: String?, + public override val isOptional: Boolean, + public override val isVararg: Boolean, + public override val type: KType, + ) : AbstractCommandValueParameter<T>() { + init { + requireNotNull(type.classifierAsKClassOrNull()) { + "type.classifier must be KClass." + } + if (isVararg) + check(type.isSubtypeOf(ARRAY_OUT_ANY_TYPE)) { + "type must be subtype of Array if vararg. Given $type." + } + } + + public companion object { + @JvmStatic + public inline fun <reified T : Any> createOptional(name: String, isVararg: Boolean): UserDefinedType<T> { + @OptIn(ExperimentalStdlibApi::class) + return UserDefinedType(name, true, isVararg, typeOf<T>()) + } + + @JvmStatic + public inline fun <reified T : Any> createRequired(name: String, isVararg: Boolean): UserDefinedType<T> { + @OptIn(ExperimentalStdlibApi::class) + return UserDefinedType(name, false, isVararg, typeOf<T>()) + } + } + } + + /** + * Extended by [CommandValueArgumentParser] + */ + @ConsoleExperimentalApi + public abstract class Extended<T> : AbstractCommandValueParameter<T>() { + abstract override fun toString(): String + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandValueArgumentParser.kt similarity index 72% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandValueArgumentParser.kt index 08f7eee05..ff82ed6db 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandValueArgumentParser.kt @@ -9,7 +9,7 @@ @file:Suppress("NOTHING_TO_INLINE", "unused") -package net.mamoe.mirai.console.command.description +package net.mamoe.mirai.console.command.descriptor import net.mamoe.mirai.Bot import net.mamoe.mirai.console.command.CommandManager @@ -32,27 +32,27 @@ import kotlin.contracts.contract * ``` * suspend fun CommandSender.mute(target: Member, duration: Int) * ``` - * [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] 为 [Member] 的 [CommandArgumentParser], 并调用其 [CommandArgumentParser.parse] + * [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] 为 [Member] 的 [CommandValueArgumentParser], 并调用其 [CommandValueArgumentParser.parse] * * ### 内建指令解析器 - * - 基础类型: [ByteArgumentParser], [ShortArgumentParser], [IntArgumentParser], [LongArgumentParser] - * [FloatArgumentParser], [DoubleArgumentParser], - * [BooleanArgumentParser], [StringArgumentParser] + * - 基础类型: [ByteValueArgumentParser], [ShortValueArgumentParser], [IntValueArgumentParser], [LongValueArgumentParser] + * [FloatValueArgumentParser], [DoubleValueArgumentParser], + * [BooleanValueArgumentParser], [StringValueArgumentParser] * - * - [Bot]: [ExistingBotArgumentParser] - * - [Friend]: [ExistingFriendArgumentParser] - * - [Group]: [ExistingGroupArgumentParser] - * - [Member]: [ExistingMemberArgumentParser] - * - [User]: [ExistingUserArgumentParser] - * - [Contact]: [ExistingContactArgumentParser] + * - [Bot]: [ExistingBotValueArgumentParser] + * - [Friend]: [ExistingFriendValueArgumentParser] + * - [Group]: [ExistingGroupValueArgumentParser] + * - [Member]: [ExistingMemberValueArgumentParser] + * - [User]: [ExistingUserValueArgumentParser] + * - [Contact]: [ExistingContactValueArgumentParser] * * * @see SimpleCommand 简单指令 * @see CompositeCommand 复合指令 * - * @see buildCommandArgumentContext 指令参数环境, 即 [CommandArgumentParser] 的集合 + * @see buildCommandArgumentContext 指令参数环境, 即 [CommandValueArgumentParser] 的集合 */ -public interface CommandArgumentParser<out T : Any> { +public interface CommandValueArgumentParser<out T : Any> { /** * 解析一个字符串为 [T] 类型参数 * @@ -83,14 +83,14 @@ public interface CommandArgumentParser<out T : Any> { /** * 使用原 [this] 解析, 成功后使用 [mapper] 映射为另一个类型. */ -public fun <T : Any, R : Any> CommandArgumentParser<T>.map( - mapper: CommandArgumentParser<R>.(T) -> R -): CommandArgumentParser<R> = MappingCommandArgumentParser(this, mapper) +public fun <T : Any, R : Any> CommandValueArgumentParser<T>.map( + mapper: CommandValueArgumentParser<R>.(T) -> R, +): CommandValueArgumentParser<R> = MappingCommandValueArgumentParser(this, mapper) -private class MappingCommandArgumentParser<T : Any, R : Any>( - private val original: CommandArgumentParser<T>, - private val mapper: CommandArgumentParser<R>.(T) -> R -) : CommandArgumentParser<R> { +private class MappingCommandValueArgumentParser<T : Any, R : Any>( + private val original: CommandValueArgumentParser<T>, + private val mapper: CommandValueArgumentParser<R>.(T) -> R, +) : CommandValueArgumentParser<R> { override fun parse(raw: String, sender: CommandSender): R = mapper(original.parse(raw, sender)) override fun parse(raw: MessageContent, sender: CommandSender): R = mapper(original.parse(raw, sender)) } @@ -102,14 +102,14 @@ private class MappingCommandArgumentParser<T : Any, R : Any>( */ @JvmSynthetic @Throws(IllegalArgumentException::class) -public fun <T : Any> CommandArgumentParser<T>.parse(raw: Any, sender: CommandSender): T { +public fun <T : Any> CommandValueArgumentParser<T>.parse(raw: Any, sender: CommandSender): T { contract { returns() implies (raw is String || raw is SingleMessage) } return when (raw) { is String -> parse(raw, sender) - is SingleMessage -> parse(raw, sender) + is MessageContent -> parse(raw, sender) else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}") } } @@ -122,7 +122,7 @@ public fun <T : Any> CommandArgumentParser<T>.parse(raw: Any, sender: CommandSen @Suppress("unused") @JvmSynthetic @Throws(CommandArgumentParserException::class) -public inline fun CommandArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing { +public inline fun CommandValueArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing { throw CommandArgumentParserException(message, cause) } @@ -133,9 +133,9 @@ public inline fun CommandArgumentParser<*>.illegalArgument(message: String, caus */ @Throws(CommandArgumentParserException::class) @JvmSynthetic -public inline fun CommandArgumentParser<*>.checkArgument( +public inline fun CommandValueArgumentParser<*>.checkArgument( condition: Boolean, - crossinline message: () -> String = { "Check failed." } + crossinline message: () -> String = { "Check failed." }, ) { contract { returns() implies condition diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt new file mode 100644 index 000000000..30d167657 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt @@ -0,0 +1,40 @@ +/* + * 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 + */ + +@file:Suppress("MemberVisibilityCanBePrivate", "unused") + +package net.mamoe.mirai.console.command.descriptor + +import net.mamoe.mirai.console.command.parse.CommandCall +import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull +import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip +import kotlin.reflect.KType + + +internal val KType.qualifiedName: String + get() = this.classifierAsKClassOrNull()?.qualifiedNameOrTip ?: classifier.toString() + +@ExperimentalCommandDescriptors +public open class NoValueArgumentMappingException( + public val argument: CommandValueArgument, + public val forType: KType, +) : CommandResolutionException("Cannot find a CommandArgument mapping for ${forType.qualifiedName}") + +@ExperimentalCommandDescriptors +public open class UnresolvedCommandCallException( + public val call: CommandCall, +) : CommandResolutionException("Unresolved call: $call") + +public open class CommandResolutionException : RuntimeException { + public constructor() : super() + public constructor(message: String?) : super(message) + public constructor(message: String?, cause: Throwable?) : super(message, cause) + public constructor(cause: Throwable?) : super(cause) +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/ExperimentalCommandDescriptors.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/ExperimentalCommandDescriptors.kt new file mode 100644 index 000000000..31ee99b7c --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/ExperimentalCommandDescriptors.kt @@ -0,0 +1,31 @@ +/* + * 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.command.descriptor + +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.annotation.AnnotationTarget.* + +/** + * 标记一个实验性的指令解释器 API. + * + * 这些 API 不具有稳定性, 且可能会在任意时刻更改. + * 不建议在发行版本中使用这些 API. + * + * @since 1.0-RC + */ +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR) +@MustBeDocumented +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public annotation class ExperimentalCommandDescriptors( + val message: String = "Command descriptors are an experimental API.", +) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/TypeVariant.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/TypeVariant.kt new file mode 100644 index 000000000..4e2792638 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/TypeVariant.kt @@ -0,0 +1,70 @@ +/* + * 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.command.descriptor + +import net.mamoe.mirai.console.command.parse.CommandCall +import net.mamoe.mirai.console.command.parse.CommandCallParser +import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.internal.data.castOrNull +import net.mamoe.mirai.console.internal.data.kClassQualifiedName +import net.mamoe.mirai.message.data.* +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Implicit type variant specified by [CommandCallParser]. + * + * [TypeVariant] is not necessary for all [CommandCall]s. + */ +@ExperimentalCommandDescriptors +public interface TypeVariant<out OutType> { + /** + * The reified type of [OutType] + */ + public val outType: KType + + /** + * @see CommandValueArgument.value + */ + public fun mapValue(valueParameter: Message): OutType + + public companion object { + @OptIn(ExperimentalStdlibApi::class) + @JvmSynthetic + public inline operator fun <reified OutType> invoke(crossinline block: (valueParameter: Message) -> OutType): TypeVariant<OutType> { + return object : TypeVariant<OutType> { + override val outType: KType = typeOf<OutType>() + override fun mapValue(valueParameter: Message): OutType = block(valueParameter) + } + } + } +} + +@ExperimentalCommandDescriptors +public object MessageContentTypeVariant : TypeVariant<MessageContent> { + @OptIn(ExperimentalStdlibApi::class) + override val outType: KType = typeOf<MessageContent>() + override fun mapValue(valueParameter: Message): MessageContent = + valueParameter.castOrNull<MessageContent>() ?: error("Accepts MessageContent only but given ${valueParameter.kClassQualifiedName}") +} + +@ExperimentalCommandDescriptors +public object MessageChainTypeVariant : TypeVariant<MessageChain> { + @OptIn(ExperimentalStdlibApi::class) + override val outType: KType = typeOf<MessageChain>() + override fun mapValue(valueParameter: Message): MessageChain = valueParameter.asMessageChain() +} + +@ExperimentalCommandDescriptors +public object ContentStringTypeVariant : TypeVariant<String> { + @OptIn(ExperimentalStdlibApi::class) + override val outType: KType = typeOf<String>() + override fun mapValue(valueParameter: Message): String = valueParameter.content +} 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 d09d2275c..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 @@ -13,10 +13,11 @@ import net.mamoe.mirai.console.command.BuiltInCommands import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.CompositeCommand -import net.mamoe.mirai.console.command.description.buildCommandArgumentContext +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 98e650ebd..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 @@ -13,10 +13,11 @@ import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.SimpleCommand -import net.mamoe.mirai.console.command.description.CommandArgumentContext +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/CommandCall.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandCall.kt new file mode 100644 index 000000000..4bb43bb9e --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandCall.kt @@ -0,0 +1,41 @@ +/* + * 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 + */ + +@file:OptIn(ExperimentalStdlibApi::class) + +package net.mamoe.mirai.console.command.parse + +import net.mamoe.mirai.console.command.Command +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors + +/** + * Unresolved [CommandCall]. + */ +@ExperimentalCommandDescriptors +public interface CommandCall { + public val caller: CommandSender + + /** + * One of callee [Command]'s [Command.allNames] + */ + public val calleeName: String + + /** + * Explicit value arguments + */ + public val valueArguments: List<CommandValueArgument> +} + +@ExperimentalCommandDescriptors +public class CommandCallImpl( + override val caller: CommandSender, + override val calleeName: String, + override val valueArguments: List<CommandValueArgument>, +) : CommandCall \ No newline at end of file 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 new file mode 100644 index 000000000..4d4bc6f59 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandCallParser.kt @@ -0,0 +1,46 @@ +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.command.resolve.CommandCallResolver +import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall +import net.mamoe.mirai.console.extensions.CommandCallParserProvider +import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.message.data.MessageChain + +/** + * Lexical and syntactical parser for transforming a [MessageChain] into [CommandCall] + * + * @see CommandCallResolver The call resolver for [CommandCall] to become [ResolvedCommandCall] + * @see CommandCallParserProvider The extension point + * + * @see SpaceSeparatedCommandCallParser + */ +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public interface CommandCallParser { + + /** + * Lexically and syntactically parse a [message] into [CommandCall], but performs nothing about resolving a call. + * + * @return `null` if unable to parse (i.e. due to syntax errors). + */ + public fun parse(caller: CommandSender, message: MessageChain): CommandCall? + + public companion object { + /** + * Calls [CommandCallParser]s provided by [CommandCallParserProvider] in [GlobalComponentStorage] sequentially, + * returning the first non-null result, `null` otherwise. + */ + @JvmStatic + public fun MessageChain.parseCommandCall(sender: CommandSender): CommandCall? { + GlobalComponentStorage.run { + CommandCallParserProvider.useExtensions { provider -> + provider.instance.parse(sender, this@parseCommandCall)?.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/parse/CommandValueArgument.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt new file mode 100644 index 000000000..30e908e60 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt @@ -0,0 +1,132 @@ +/* + * 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 + */ + +@file:Suppress("unused") + +package net.mamoe.mirai.console.command.parse + +import net.mamoe.mirai.console.command.descriptor.* +import net.mamoe.mirai.console.internal.data.castOrInternalError +import net.mamoe.mirai.console.internal.data.classifierAsKClass +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageContent +import net.mamoe.mirai.message.data.SingleMessage +import kotlin.reflect.KType +import kotlin.reflect.full.isSubtypeOf +import kotlin.reflect.typeOf + + +/** + * @see CommandValueArgument + */ +@ExperimentalCommandDescriptors +public interface CommandArgument + +/** + * @see DefaultCommandValueArgument + */ +@ExperimentalCommandDescriptors +public interface CommandValueArgument : CommandArgument { + public val type: KType + + /** + * [MessageContent] if single argument + * [MessageChain] is vararg + */ + public val value: Message + public val typeVariants: List<TypeVariant<*>> +} + +/** + * The [CommandValueArgument] that doesn't vary in type (remaining [MessageContent]). + */ +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public data class DefaultCommandValueArgument( + public override val value: Message, +) : CommandValueArgument { + @OptIn(ExperimentalStdlibApi::class) + override val type: KType = typeOf<MessageContent>() + override val typeVariants: List<TypeVariant<*>> = listOf( + MessageContentTypeVariant, + MessageChainTypeVariant, + ContentStringTypeVariant, + ) +} + +@ExperimentalCommandDescriptors +public fun <T> CommandValueArgument.mapValue(typeVariant: TypeVariant<T>): T = typeVariant.mapValue(this.value) + + +@OptIn(ExperimentalStdlibApi::class) +@ExperimentalCommandDescriptors +public inline fun <reified T> CommandValueArgument.mapToType(): T = + mapToTypeOrNull() ?: throw NoValueArgumentMappingException(this, typeOf<T>()) + +@OptIn(ExperimentalStdlibApi::class) +@ExperimentalCommandDescriptors +public fun <T> CommandValueArgument.mapToType(type: KType): T = + mapToTypeOrNull(type) ?: throw NoValueArgumentMappingException(this, type) + +@ExperimentalCommandDescriptors +public fun <T> CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? { + if (expectingType.isSubtypeOf(ARRAY_OUT_ANY_TYPE)) { + val arrayElementType = expectingType.arguments.single().type ?: ANY_TYPE + + val result = ArrayList<Any?>() + + when (val value = value) { + is MessageChain -> { + for (message in value) { + result.add(mapToTypeOrNullImpl(arrayElementType, message)) + } + } + else -> { // single + value.castOrInternalError<SingleMessage>() + result.add(mapToTypeOrNullImpl(arrayElementType, value)) + } + } + + + @Suppress("UNCHECKED_CAST") + return result.toArray(arrayElementType.createArray(result.size)) as T + } + + @Suppress("UNCHECKED_CAST") + return mapToTypeOrNullImpl(expectingType, value) as T +} + +private fun KType.createArray(size: Int): Array<Any?> { + return java.lang.reflect.Array.newInstance(this.classifierAsKClass().javaObjectType, size).castOrInternalError() +} + +@OptIn(ExperimentalCommandDescriptors::class) +private fun CommandValueArgument.mapToTypeOrNullImpl(expectingType: KType, value: Message): Any? { + @OptIn(ExperimentalStdlibApi::class) + val result = typeVariants + .filter { it.outType.isSubtypeOf(expectingType) } + .ifEmpty { + return null + } + .reduce { acc, typeVariant -> + if (acc.outType.isSubtypeOf(typeVariant.outType)) + acc + else typeVariant + } + @Suppress("UNCHECKED_CAST") + return result.mapValue(value) +} + +@ExperimentalCommandDescriptors +public inline fun <reified T> CommandValueArgument.mapToTypeOrNull(): T? { + @OptIn(ExperimentalStdlibApi::class) + return mapToTypeOrNull(typeOf<T>()) +} \ 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..8cb917344 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/SpaceSeparatedCommandCallParser.kt @@ -0,0 +1,26 @@ +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.extensions.CommandCallParserProvider +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<MessageContent>() + if (flatten.isEmpty()) return null + return CommandCallImpl( + caller = caller, + calleeName = flatten.first().content, + valueArguments = flatten.drop(1).map(::DefaultCommandValueArgument) + ) + } + + public object Provider : CommandCallParserProvider(SpaceSeparatedCommandCallParser) +} \ 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..98a203118 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt @@ -0,0 +1,172 @@ +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.command.parse.DefaultCommandValueArgument +import net.mamoe.mirai.console.extensions.CommandCallResolverProvider +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.console.util.safeCast +import net.mamoe.mirai.message.data.EmptyMessageChain +import net.mamoe.mirai.message.data.asMessageChain + +/** + * Builtin implementation of [CommandCallResolver] + */ +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public object BuiltInCommandCallResolver : CommandCallResolver { + public object Provider : CommandCallResolverProvider(BuiltInCommandCallResolver) + + override fun resolve(call: CommandCall): ResolvedCommandCall? { + val callee = CommandManager.matchCommand(call.calleeName) ?: return null + + val valueArguments = call.valueArguments + val context = callee.safeCast<CommandArgumentContextAware>()?.context + + val signature = resolveImpl(callee, valueArguments, context) ?: return null + + return ResolvedCommandCallImpl(call.caller, + callee, + signature.variant, + signature.zippedArguments.map { it.second }, + context ?: EmptyCommandArgumentContext) + } + + private data class ResolveData( + val variant: CommandSignatureVariant, + val zippedArguments: List<Pair<AbstractCommandValueParameter<*>, CommandValueArgument>>, + val argumentAcceptances: List<ArgumentAcceptanceWithIndex>, + val remainingParameters: List<AbstractCommandValueParameter<*>>, + ) { + val remainingOptionalCount: Int = remainingParameters.count { it.isOptional } + } + + private data class ArgumentAcceptanceWithIndex( + val index: Int, + val acceptance: ArgumentAcceptance, + ) + + private fun resolveImpl( + callee: Command, + valueArguments: List<CommandValueArgument>, + context: CommandArgumentContext?, + ): ResolveData? { + + + callee.overloads + .mapNotNull l@{ signature -> + val valueParameters = signature.valueParameters + + val zipped = valueParameters.zip(valueArguments).toMutableList() + + val remainingParameters = valueParameters.drop(zipped.size).toMutableList() + + if (remainingParameters.any { !it.isOptional && !it.isVararg }) return@l null // not enough args. // vararg can be empty. + + if (zipped.isEmpty()) { + ResolveData( + variant = signature, + zippedArguments = emptyList(), + argumentAcceptances = emptyList(), + remainingParameters = remainingParameters, + ) + } else { + if (valueArguments.size > valueParameters.size && zipped.last().first.isVararg) { + // merge vararg arguments + val (varargParameter, varargFirstArgument) + = zipped.removeLast() + + zipped.add(varargParameter to DefaultCommandValueArgument(valueArguments.drop(zipped.size).map { it.value }.asMessageChain())) + } else { + // add default empty vararg argument + val remainingVararg = remainingParameters.find { it.isVararg } + if (remainingVararg != null) { + zipped.add(remainingVararg to DefaultCommandValueArgument(EmptyMessageChain)) + remainingParameters.remove(remainingVararg) + } + } + + ResolveData( + variant = signature, + zippedArguments = zipped, + argumentAcceptances = zipped.mapIndexed { index, (parameter, argument) -> + val accepting = parameter.accepting(argument, context) + if (accepting.isNotAcceptable) { + return@l null // argument type not assignable + } + ArgumentAcceptanceWithIndex(index, accepting) + }, + remainingParameters = remainingParameters + ) + } + } + .also { result -> result.singleOrNull()?.let { return it } } + .takeLongestMatches() + .ifEmpty { return null } + .also { result -> result.singleOrNull()?.let { return it } } + // 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 } // 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<ResolveData>.takeLongestMatches(): Collection<ResolveData> { + if (isEmpty()) return emptyList() + return associateWith { + it.variant.valueParameters.size - it.remainingOptionalCount * 1.001 // slightly lower priority with optional defaults. + }.let { m -> + val maxMatch = m.values.maxByOrNull { it } + m.filter { it.value == maxMatch }.keys + } + } +} \ 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 new file mode 100644 index 000000000..8022f532b --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/CommandCallResolver.kt @@ -0,0 +1,41 @@ +/* + * 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.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 +import net.mamoe.mirai.console.util.ConsoleExperimentalApi + +/** + * The resolver converting a [CommandCall] into [ResolvedCommandCall] based on registered [] + * + * @see CommandCallResolverProvider The provider to instances of this class + * @see BuiltInCommandCallResolver The builtin implementation + */ +@ExperimentalCommandDescriptors +public interface CommandCallResolver { + public fun resolve(call: CommandCall): ResolvedCommandCall? + + public companion object { + @JvmName("resolveCall") + @ConsoleExperimentalApi + @ExperimentalCommandDescriptors + 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 new file mode 100644 index 000000000..547dd736a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/ResolvedCommandCall.kt @@ -0,0 +1,87 @@ +/* + * 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.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.* +import net.mamoe.mirai.console.command.parse.CommandCall +import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.command.parse.mapToTypeOrNull +import net.mamoe.mirai.console.internal.data.classifierAsKClass +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.console.util.cast +import kotlin.LazyThreadSafetyMode.PUBLICATION + +/** + * The resolved [CommandCall]. + * + * @see ResolvedCommandCallImpl + */ +@ExperimentalCommandDescriptors +public interface ResolvedCommandCall { + public val caller: CommandSender + + /** + * The callee [Command] + */ + public val callee: Command + + /** + * The callee [CommandSignatureVariant], specifically a sub command from [CompositeCommand] + */ + public val calleeSignature: CommandSignatureVariant + + /** + * Original arguments + */ + public val rawValueArguments: List<CommandValueArgument> + + /** + * Resolved value arguments arranged mapping the [CommandSignatureVariant.valueParameters] by index. + * + * **Implementation details**: Lazy calculation. + */ + @ConsoleExperimentalApi + public val resolvedValueArguments: List<ResolvedCommandValueArgument<*>> + + public companion object +} + +@ExperimentalCommandDescriptors +public data class ResolvedCommandValueArgument<T>( + val parameter: CommandValueParameter<T>, + val value: T, +) + +// Don't move into companion, compilation error +@ExperimentalCommandDescriptors +public suspend inline fun ResolvedCommandCall.call() { + return this@call.calleeSignature.call(this@call) +} + +@ExperimentalCommandDescriptors +public class ResolvedCommandCallImpl( + override val caller: CommandSender, + override val callee: Command, + override val calleeSignature: CommandSignatureVariant, + override val rawValueArguments: List<CommandValueArgument>, + private val context: CommandArgumentContext, +) : ResolvedCommandCall { + override val resolvedValueArguments: List<ResolvedCommandValueArgument<*>> by lazy(PUBLICATION) { + calleeSignature.valueParameters.zip(rawValueArguments).map { (parameter, argument) -> + val value = argument.mapToTypeOrNull(parameter.type) ?: context[parameter.type.classifierAsKClass()]?.parse(argument.value, caller) + ?: throw NoValueArgumentMappingException(argument, parameter.type) + // TODO: 2020/10/17 consider vararg and optional + ResolvedCommandValueArgument(parameter.cast(), value) + } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt index 0ab74fc19..f081af4d2 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt @@ -14,7 +14,7 @@ package net.mamoe.mirai.console.data import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip +import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip import net.mamoe.mirai.console.internal.plugin.updateWhen import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.* diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionPoint.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionPoint.kt index da9d0ff80..d076776c0 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionPoint.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionPoint.kt @@ -24,6 +24,12 @@ public open class AbstractExtensionPoint<T : Extension>( public override val extensionType: KClass<T>, ) : ExtensionPoint<T> +public open class InstanceExtensionPoint<E : InstanceExtension<T>, T>( + extensionType: KClass<E>, + public vararg val builtinImplementations: E, +) : AbstractExtensionPoint<E>(extensionType) + + /** * 表示一个 [SingletonExtension] 的 [ExtensionPoint] */ diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallParserProvider.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallParserProvider.kt new file mode 100644 index 000000000..4a19a0fac --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallParserProvider.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.extensions + +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.parse.CommandCallParser +import net.mamoe.mirai.console.command.parse.SpaceSeparatedCommandCallParser +import net.mamoe.mirai.console.extension.InstanceExtension +import net.mamoe.mirai.console.extension.InstanceExtensionPoint + +/** + * The provider of [CommandCallParser] + */ +@ExperimentalCommandDescriptors +public open class CommandCallParserProvider(override val instance: CommandCallParser) : InstanceExtension<CommandCallParser> { + public companion object ExtensionPoint : + InstanceExtensionPoint<CommandCallParserProvider, CommandCallParser>(CommandCallParserProvider::class, SpaceSeparatedCommandCallParser.Provider) +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallResolverProvider.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallResolverProvider.kt new file mode 100644 index 000000000..e6e390d99 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallResolverProvider.kt @@ -0,0 +1,22 @@ +/* + * 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.extensions + +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.resolve.BuiltInCommandCallResolver +import net.mamoe.mirai.console.command.resolve.CommandCallResolver +import net.mamoe.mirai.console.extension.InstanceExtension +import net.mamoe.mirai.console.extension.InstanceExtensionPoint + +@ExperimentalCommandDescriptors +public open class CommandCallResolverProvider(override val instance: CommandCallResolver) : InstanceExtension<CommandCallResolver> { + public companion object ExtensionPoint : + InstanceExtensionPoint<CommandCallResolverProvider, CommandCallResolver>(CommandCallResolverProvider::class, BuiltInCommandCallResolver.Provider) +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/SingletonExtensionSelector.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/SingletonExtensionSelector.kt index e5fd07fb8..2dfce8f6c 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/SingletonExtensionSelector.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/SingletonExtensionSelector.kt @@ -29,7 +29,7 @@ import kotlin.reflect.KClass */ public interface SingletonExtensionSelector : FunctionExtension { public data class Registry<T : Extension>( - val plugin: Plugin, + val plugin: Plugin?, val extension: T, ) @@ -55,11 +55,11 @@ public interface SingletonExtensionSelector : FunctionExtension { instances.isEmpty() -> BuiltInSingletonExtensionSelector instances.size == 1 -> { instances.single().also { (plugin, ext) -> - MiraiConsole.mainLogger.info { "Loaded SingletonExtensionSelector: $ext from ${plugin.name}" } + MiraiConsole.mainLogger.info { "Loaded SingletonExtensionSelector: $ext from ${plugin?.name ?: "<builtin>"}" } }.extension } else -> { - error("Found too many SingletonExtensionSelectors: ${instances.joinToString { (p, i) -> "'$i' from '${p.name}'" }}. Check your plugins and ensure there is only one external SingletonExtensionSelectors") + error("Found too many SingletonExtensionSelectors: ${instances.joinToString { (p, i) -> "'$i' from '${p?.name ?: "<builtin>"}'" }}. Check your plugins and ensure there is only one external SingletonExtensionSelectors") } } } 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 eb34fb34a..f0bc776ca 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 @@ -15,18 +15,25 @@ import kotlinx.coroutines.CoroutineScope 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.CommandManager.INSTANCE.findDuplicate import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.parse.CommandCallParser.Companion.parseCommandCall +import net.mamoe.mirai.console.command.resolve.CommandCallResolver.Companion.resolve +import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission +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.EmptyMessageChain 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) { +@OptIn(ExperimentalCommandDescriptors::class) +internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiConsole.childScope("CommandManagerImpl") { private val logger: MiraiLogger by lazy { MiraiConsole.createLogger("command") } @@ -48,11 +55,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<MessageEvent> by lazy { @@ -65,13 +72,17 @@ 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("权限不足") intercept() } } + is CommandExecuteResult.IllegalArgument -> { + result.exception.message?.let { sender.sendMessage(it) } + intercept() + } is CommandExecuteResult.Success -> { intercept() } @@ -79,7 +90,7 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine sender.catchExecutionException(result.exception) intercept() } - is CommandExecuteResult.CommandNotFound -> { + is CommandExecuteResult.UnresolvedCall -> { // noop } } @@ -90,102 +101,90 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine ///// IMPL - override val CommandOwner.registeredCommands: List<Command> get() = _registeredCommands.filter { it.owner == this } + override fun getRegisteredCommands(owner: CommandOwner): List<Command> = _registeredCommands.filter { it.owner == owner } override val allRegisteredCommands: List<Command> get() = _registeredCommands.toList() // copy override val commandPrefix: String get() = "/" - override fun CommandOwner.unregisterAllCommands() { - for (registeredCommand in registeredCommands) { - registeredCommand.unregister() + override fun unregisterAllCommands(owner: CommandOwner) { + for (registeredCommand in getRegisteredCommands(owner)) { + unregisterCommand(registeredCommand) } } - override fun Command.register(override: Boolean): Boolean { - if (this is CompositeCommand) this.subCommands // init lazy + override fun registerCommand(command: Command, override: Boolean): Boolean { + if (command is CompositeCommand) { + command.overloads // init lazy + } kotlin.runCatching { - this.permission // init lazy - this.secondaryNames // init lazy - this.description // init lazy - this.usage // init lazy + command.permission // init lazy + command.secondaryNames // init lazy + command.description // init lazy + command.usage // init lazy }.onFailure { - throw IllegalStateException("Failed to init command ${this@register}.", it) + throw IllegalStateException("Failed to init command ${command}.", it) } - modifyLock.withLock { + this@CommandManagerImpl.modifyLock.withLock { if (!override) { - if (findDuplicate() != null) return false + if (command.findDuplicate() != null) return false } - _registeredCommands.add(this@register) - if (this.prefixOptional) { - for (name in this.allNames) { + this@CommandManagerImpl._registeredCommands.add(command) + if (command.prefixOptional) { + for (name in command.allNames) { val lowerCaseName = name.toLowerCase() - optionalPrefixCommandMap[lowerCaseName] = this - requiredPrefixCommandMap[lowerCaseName] = this + this@CommandManagerImpl.optionalPrefixCommandMap[lowerCaseName] = command + this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command } } else { - for (name in this.allNames) { + for (name in command.allNames) { val lowerCaseName = name.toLowerCase() - optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency - requiredPrefixCommandMap[lowerCaseName] = this + this@CommandManagerImpl.optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency + this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command } } return true } } - override fun Command.findDuplicate(): Command? = - _registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase this.allNames } + override fun findDuplicateCommand(command: Command): Command? = + _registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase command.allNames } - override fun Command.unregister(): Boolean = modifyLock.withLock { - if (this.prefixOptional) { - this.allNames.forEach { - optionalPrefixCommandMap.remove(it) + override fun unregisterCommand(command: Command): Boolean = modifyLock.withLock { + if (command.prefixOptional) { + command.allNames.forEach { + optionalPrefixCommandMap.remove(it.toLowerCase()) } } - this.allNames.forEach { - requiredPrefixCommandMap.remove(it) + command.allNames.forEach { + requiredPrefixCommandMap.remove(it.toLowerCase()) } - _registeredCommands.remove(this) + _registeredCommands.remove(command) } - override fun Command.isRegistered(): Boolean = this in _registeredCommands + override fun isCommandRegistered(command: Command): Boolean = command in _registeredCommands +} - override suspend fun Command.execute( - sender: CommandSender, - arguments: Message, - checkPermission: Boolean - ): CommandExecuteResult { - return sender.executeCommandInternal( - this, - arguments.flattenCommandComponents(), - primaryName, - checkPermission - ) + +// Don't move into CommandManager, compilation error / VerifyError +@OptIn(ExperimentalCommandDescriptors::class) +internal suspend fun executeCommandImpl( + message: Message, + caller: CommandSender, + checkPermission: Boolean, +): CommandExecuteResult { + val call = message.asMessageChain().parseCommandCall(caller) ?: return CommandExecuteResult.UnresolvedCall("") + val resolved = call.resolve() ?: return CommandExecuteResult.UnresolvedCall(call.calleeName) + + val command = resolved.callee + + if (checkPermission && !command.permission.testPermission(caller)) { + return CommandExecuteResult.PermissionDenied(command, call.calleeName) } - override suspend fun Command.execute( - sender: CommandSender, - arguments: String, - checkPermission: Boolean - ): CommandExecuteResult { - return sender.executeCommandInternal( - this, - arguments.flattenCommandComponents(), - primaryName, - checkPermission - ) + return try { + resolved.calleeSignature.call(resolved) + CommandExecuteResult.Success(resolved.callee, call.calleeName, EmptyMessageChain) + } catch (e: Throwable) { + CommandExecuteResult.ExecutionFailed(e, resolved.callee, call.calleeName, EmptyMessageChain) } +} - override suspend fun CommandSender.executeCommand( - message: Message, - checkPermission: Boolean - ): CommandExecuteResult { - val msg = message.asMessageChain().filterIsInstance<MessageContent>() - 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/CommandReflector.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandReflector.kt new file mode 100644 index 000000000..ebba4b153 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandReflector.kt @@ -0,0 +1,242 @@ +package net.mamoe.mirai.console.internal.command + +import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.descriptor.* +import net.mamoe.mirai.console.internal.data.classifierAsKClass +import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +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.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KVisibility +import kotlin.reflect.full.* + + +internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray() + +internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain { + when (this@flattenCommandComponents) { + is PlainText -> this@flattenCommandComponents.content.splitToSequence(' ').filterNot { it.isBlank() } + .forEach { +PlainText(it) } + is CharSequence -> this@flattenCommandComponents.splitToSequence(' ').filterNot { it.isBlank() } + .forEach { +PlainText(it) } + is SingleMessage -> add(this@flattenCommandComponents) + is Array<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) } + is Iterable<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) } + else -> add(this@flattenCommandComponents.toString()) + } +} + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +internal object CompositeCommandSubCommandAnnotationResolver : + SubCommandAnnotationResolver { + override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = + function.hasAnnotation<CompositeCommand.SubCommand>() + + override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String> { + val annotated = function.findAnnotation<CompositeCommand.SubCommand>()!!.value + return if (annotated.isEmpty()) arrayOf(function.name) + else annotated + } + + override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? = + parameter.findAnnotation<CompositeCommand.Name>()?.value + + override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? = + function.findAnnotation<CompositeCommand.Description>()?.value +} + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +internal object SimpleCommandSubCommandAnnotationResolver : + SubCommandAnnotationResolver { + override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = + function.hasAnnotation<SimpleCommand.Handler>() + + override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String> = + ownerCommand.secondaryNames + + override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? = + parameter.findAnnotation<SimpleCommand.Name>()?.value + + override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? = + ownerCommand.description +} + +internal interface SubCommandAnnotationResolver { + fun hasAnnotation(ownerCommand: Command, function: KFunction<*>): Boolean + fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String> + fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? + fun getDescription(ownerCommand: Command, function: KFunction<*>): String? +} + +@ConsoleExperimentalApi +public class IllegalCommandDeclarationException : Exception { + public override val message: String? + + public constructor( + ownerCommand: Command, + correspondingFunction: KFunction<*>, + message: String?, + ) : super("Illegal command declaration: ${correspondingFunction.name} declared in ${ownerCommand::class.qualifiedName}") { + this.message = message + } + + public constructor( + ownerCommand: Command, + message: String?, + ) : super("Illegal command declaration: ${ownerCommand::class.qualifiedName}") { + this.message = message + } +} + +@OptIn(ExperimentalCommandDescriptors::class) +internal class CommandReflector( + val command: Command, + 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 -> + if (receiver.type.classifierAsKClassOrNull()?.isSubclassOf(CommandSender::class) != true) { + illegalDeclaration("Extension receiver parameter type is not subclass of CommandSender.") + } + } + } + + 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<JvmStatic>()) illegalDeclaration("Command function must not be static.") + + // should we allow abstract? + + // if (isAbstract) illegalDeclaration("Command function cannot be abstract") + } + + fun generateUsage(overloads: Iterable<CommandSignatureVariantFromKFunction>): String { + return overloads.joinToString("\n") { subcommand -> + buildString { + if (command.prefixOptional) { + append("(") + append(CommandManager.commandPrefix) + append(")") + } else { + append(CommandManager.commandPrefix) + } + if (command is CompositeCommand) { + append(command.primaryName) + append(" ") + } + append(subcommand.valueParameters.joinToString(" ") { it.render() }) + annotationResolver.getDescription(command, subcommand.originFunction).let { description -> + append(" ") + append(description) + } + } + } + } + + + companion object { + + private fun <T> AbstractCommandValueParameter<T>.render(): String { + return when (this) { + is AbstractCommandValueParameter.Extended, + is AbstractCommandValueParameter.UserDefinedType<*>, + -> { + "<${this.name ?: this.type.classifierAsKClass().simpleName}>" + } + is AbstractCommandValueParameter.StringConstant -> { + this.expectingValue + } + } + } + } + + fun validate(variants: List<CommandSignatureVariantFromKFunctionImpl>) { + + data class ErasedParameters( + val name: String, + val x: String, + ) + variants + } + + @Throws(IllegalCommandDeclarationException::class) + fun findSubCommands(): List<CommandSignatureVariantFromKFunctionImpl> { + return command::class.functions // exclude static later + .asSequence() + .filter { it.isSubCommandFunction() } + .onEach { it.checkExtensionReceiver() } + .onEach { it.checkModifiers() } + .onEach { it.checkNames() } + .map { function -> + + val functionNameAsValueParameter = + annotationResolver.getSubCommandNames(command, function).mapIndexed { index, s -> createStringConstantParameter(index, s) } + + val functionValueParameters = + function.valueParameters.associateBy { it.toUserDefinedCommandParameter() } + + CommandSignatureVariantFromKFunctionImpl( + receiverParameter = function.extensionReceiverParameter?.toCommandReceiverParameter(), + valueParameters = functionNameAsValueParameter + functionValueParameters.keys, + originFunction = function + ) { call -> + val args = LinkedHashMap<KParameter, Any?>() + + 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 instanceParameter = function.instanceParameter + if (instanceParameter != null) { + args[instanceParameter] = command + } + function.callSuspendBy(args) + } + }.toList() + } + + private fun KParameter.toCommandReceiverParameter(): CommandReceiverParameter<out CommandSender>? { + check(!this.isVararg) { "Receiver cannot be vararg." } + check(this.type.classifierAsKClass().isSubclassOf(CommandSender::class)) { "Receiver must be subclass of CommandSender" } + + return CommandReceiverParameter(this.type.isMarkedNullable, this.type) + } + + private fun createStringConstantParameter(index: Int, expectingValue: String): AbstractCommandValueParameter.StringConstant { + return AbstractCommandValueParameter.StringConstant("#$index", expectingValue) + } + + private fun KParameter.toUserDefinedCommandParameter(): AbstractCommandValueParameter.UserDefinedType<*> { + return AbstractCommandValueParameter.UserDefinedType<Any?>(nameForCommandParameter(), this.isOptional, this.isVararg, this.type) // Any? is erased + } + + private fun KParameter.nameForCommandParameter(): String? = annotationResolver.getAnnotatedName(command, this) ?: this.name +} \ 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 8506d6a4a..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 @@ -12,22 +12,24 @@ package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.CompositeCommand -import net.mamoe.mirai.console.command.description.CommandArgumentParser -import java.lang.reflect.Parameter -import kotlin.reflect.KClass +import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser +import kotlin.reflect.KParameter +import kotlin.reflect.KType +/* internal fun Parameter.toCommandParam(): CommandParameter<*> { val name = getAnnotation(CompositeCommand.Name::class.java) return CommandParameter( name?.value ?: this.name ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"), - this.type.kotlin + this.type.kotlin, + null ) } +*/ /** * 指令形式参数. - * @see toCommandParam */ internal data class CommandParameter<T : Any>( /** @@ -35,24 +37,27 @@ internal data class CommandParameter<T : Any>( */ val name: String, /** - * 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgumentParser] 解析. + * 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandValueArgumentParser] 解析. */ - val type: KClass<T> // exact type + val type: KType, // exact type + val parameter: KParameter, // source parameter ) { - constructor(name: String, type: KClass<T>, parser: CommandArgumentParser<T>) : this(name, type) { + constructor(name: String, type: KType, parameter: KParameter, parser: CommandValueArgumentParser<T>) : this( + name, type, parameter + ) { this._overrideParser = parser } @Suppress("PropertyName") @JvmField - internal var _overrideParser: CommandArgumentParser<T>? = null + internal var _overrideParser: CommandValueArgumentParser<T>? = null /** - * 覆盖的 [CommandArgumentParser]. + * 覆盖的 [CommandValueArgumentParser]. * - * 如果非 `null`, 将不会从 [CommandArgumentContext] 寻找 [CommandArgumentParser] + * 如果非 `null`, 将不会从 [CommandArgumentContext] 寻找 [CommandValueArgumentParser] */ - val overrideParser: CommandArgumentParser<T>? get() = _overrideParser + val overrideParser: CommandValueArgumentParser<T>? get() = _overrideParser } 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 deleted file mode 100644 index f922229d4..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt +++ /dev/null @@ -1,349 +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 - */ - -@file:Suppress("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") - -package net.mamoe.mirai.console.internal.command - -import net.mamoe.mirai.console.command.* -import net.mamoe.mirai.console.command.description.CommandArgumentContext -import net.mamoe.mirai.console.command.description.CommandArgumentContextAware -import net.mamoe.mirai.console.internal.data.kClassQualifiedNameOrTip -import net.mamoe.mirai.console.permission.Permission -import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission -import net.mamoe.mirai.message.data.* -import kotlin.reflect.KAnnotatedElement -import kotlin.reflect.KClass -import kotlin.reflect.KFunction -import kotlin.reflect.full.callSuspend -import kotlin.reflect.full.declaredFunctions -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.full.isSubclassOf - -internal object CompositeCommandSubCommandAnnotationResolver : - AbstractReflectionCommand.SubCommandAnnotationResolver { - override fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>) = - function.hasAnnotation<CompositeCommand.SubCommand>() - - override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> = - function.findAnnotation<CompositeCommand.SubCommand>()!!.value -} - -internal object SimpleCommandSubCommandAnnotationResolver : - AbstractReflectionCommand.SubCommandAnnotationResolver { - override fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>) = - function.hasAnnotation<SimpleCommand.Handler>() - - override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> = - baseCommand.secondaryNames -} - -internal abstract class AbstractReflectionCommand -@JvmOverloads constructor( - owner: CommandOwner, - primaryName: String, - secondaryNames: Array<out String>, - description: String = "<no description available>", - parentPermission: Permission = owner.parentPermission, - prefixOptional: Boolean = false, -) : Command, AbstractCommand( - owner, - primaryName = primaryName, - secondaryNames = secondaryNames, - description = description, - parentPermission = parentPermission, - prefixOptional = prefixOptional -), CommandArgumentContextAware { - internal abstract val subCommandAnnotationResolver: SubCommandAnnotationResolver - - @JvmField - @Suppress("PropertyName") - internal var _usage: String = "<not yet initialized>" - - override val usage: String // initialized by subCommand reflection - get() { - subCommands // ensure init - return _usage - } - - abstract suspend fun CommandSender.onDefault(rawArgs: MessageChain) - - internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { - DefaultSubCommandDescriptor( - "", - createOrFindCommandPermission(parentPermission), - onCommand = { sender: CommandSender, args: MessageChain -> - sender.onDefault(args) - } - ) - } - - internal open fun checkSubCommand(subCommands: Array<SubCommandDescriptor>) { - - } - - interface SubCommandAnnotationResolver { - fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Boolean - fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> - } - - internal val subCommands: Array<SubCommandDescriptor> by lazy { - this::class.declaredFunctions.filter { subCommandAnnotationResolver.hasAnnotation(this, it) } - .also { subCommandFunctions -> - // overloading not yet supported - val overloadFunction = subCommandFunctions.groupBy { it.name }.entries.firstOrNull { it.value.size > 1 } - if (overloadFunction != null) { - error("Sub command overloading is not yet supported. (at ${this::class.qualifiedNameOrTip}.${overloadFunction.key})") - } - }.map { function -> - createSubCommand(function, context) - }.toTypedArray().also { - _usage = it.createUsage(this) - }.also { checkSubCommand(it) } - } - - internal val bakedCommandNameToSubDescriptorArray: Map<Array<String>, SubCommandDescriptor> by lazy { - kotlin.run { - val map = LinkedHashMap<Array<String>, SubCommandDescriptor>(subCommands.size * 2) - for (descriptor in subCommands) { - for (name in descriptor.bakedSubNames) { - map[name] = descriptor - } - } - map.toSortedMap { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() } - } - } - - internal class DefaultSubCommandDescriptor( - val description: String, - val permission: Permission, - val onCommand: suspend (sender: CommandSender, rawArgs: MessageChain) -> Unit, - ) - - internal inner class SubCommandDescriptor( - val names: Array<out String>, - val params: Array<CommandParameter<*>>, - val description: String, - val permission: Permission, - val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean, - val context: CommandArgumentContext, - ) { - 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) - } - } - - @JvmField - internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray() - private fun parseArgs(sender: CommandSender, rawArgs: MessageChain, offset: Int): Array<out Any>? { - if (rawArgs.size < offset + this.params.size) - return null - //require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } - - return Array(this.params.size) { index -> - val param = params[index] - val rawArg = rawArgs[offset + index] - when (rawArg) { - is PlainText -> context[param.type]?.parse(rawArg.content, sender) - is MessageContent -> context[param.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 - } -} - -internal fun <T> Array<T>.contentEqualsOffset(other: MessageChain, length: Int): Boolean { - repeat(length) { index -> - if (!other[index].toString().equals(this[index].toString(), ignoreCase = true)) { - return false - } - } - return true -} - -internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray() -internal fun String.isValidSubName(): Boolean = ILLEGAL_SUB_NAME_CHARS.none { it in this } -internal fun String.bakeSubName(): Array<String> = split(' ').filterNot { it.isBlank() }.toTypedArray() - -internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain { - when (this@flattenCommandComponents) { - is PlainText -> this@flattenCommandComponents.content.splitToSequence(' ').filterNot { it.isBlank() } - .forEach { +PlainText(it) } - is CharSequence -> this@flattenCommandComponents.splitToSequence(' ').filterNot { it.isBlank() } - .forEach { +PlainText(it) } - is SingleMessage -> add(this@flattenCommandComponents) - is Array<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) } - is Iterable<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) } - else -> add(this@flattenCommandComponents.toString()) - } -} - -internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean = - findAnnotation<T>() != null - -internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>" - -internal fun Array<AbstractReflectionCommand.SubCommandDescriptor>.createUsage(baseCommand: AbstractReflectionCommand): String = - buildString { - appendLine(baseCommand.description) - appendLine() - - for (subCommandDescriptor in this@createUsage) { - appendLine(subCommandDescriptor.usage) - } - }.trimEnd() - -internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseCommand: AbstractReflectionCommand): String = - buildString { - if (baseCommand.prefixOptional) { - append("(") - append(CommandManager.commandPrefix) - append(")") - } else { - append(CommandManager.commandPrefix) - } - if (baseCommand is CompositeCommand) { - append(baseCommand.primaryName) - append(" ") - } - append(names.first()) - append(" ") - append(params.joinToString(" ") { "<${it.name}>" }) - append(" ") - append(description) - appendLine() - }.trimEnd() - -internal fun AbstractReflectionCommand.createSubCommand( - function: KFunction<*>, - context: CommandArgumentContext, -): AbstractReflectionCommand.SubCommandDescriptor { - val notStatic = !function.hasAnnotation<JvmStatic>() - //val overridePermission = null//function.findAnnotation<CompositeCommand.PermissionId>()//optional - val subDescription = - function.findAnnotation<CompositeCommand.Description>()?.value ?: "" - - fun KClass<*>.isValidReturnType(): Boolean { - return when (this) { - Boolean::class, Void::class, Unit::class, Nothing::class -> true - else -> false - } - } - - check((function.returnType.classifier as? KClass<*>)?.isValidReturnType() == true) { - error("Return type of sub command ${function.name} must be one of the following: kotlin.Boolean, java.lang.Boolean, kotlin.Unit (including implicit), kotlin.Nothing, boolean or void (at ${this::class.qualifiedNameOrTip}.${function.name})") - } - - check(!function.returnType.isMarkedNullable) { - error("Return type of sub command ${function.name} must not be marked nullable in Kotlin, and must be marked with @NotNull or @NonNull explicitly in Java. (at ${this::class.qualifiedNameOrTip}.${function.name})") - } - - val parameters = function.parameters.toMutableList() - - if (notStatic) parameters.removeAt(0) // instance - - var hasSenderParam = false - check(parameters.isNotEmpty()) { - "Parameters of sub command ${function.name} must not be empty. (Must have CommandSender as its receiver or first parameter or absent, followed by naturally typed params) (at ${this::class.qualifiedNameOrTip}.${function.name})" - } - - parameters.forEach { param -> - check(!param.isVararg) { - "Parameter $param must not be vararg. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)" - } - } - - (parameters.first()).let { receiver -> - if ((receiver.type.classifier as? KClass<*>)?.isSubclassOf(CommandSender::class) == true) { - hasSenderParam = true - parameters.removeAt(0) - } - } - - val commandName = - subCommandAnnotationResolver.getSubCommandNames(this, function) - .let { namesFromAnnotation -> - if (namesFromAnnotation.isNotEmpty()) { - namesFromAnnotation.map(String::toLowerCase).toTypedArray() - } else arrayOf(function.name.toLowerCase()) - }.also { names -> - names.forEach { - check(it.isValidSubName()) { - "Name of sub command ${function.name} is invalid" - } - } - } - - //map parameter - val params = parameters.map { param -> - - if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") - - val paramName = param.findAnnotation<CompositeCommand.Name>()?.value ?: param.name ?: "unknown" - CommandParameter( - paramName, - (param.type.classifier as? KClass<*>) - ?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") - ) - }.toTypedArray() - - return SubCommandDescriptor( - commandName, - params, - subDescription, // overridePermission?.value - permission,//overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission, - onCommand = { sender: CommandSender, args: Array<out Any> -> - val result = if (notStatic) { - if (hasSenderParam) { - function.isSuspend - function.callSuspend(this, sender, *args) - } else function.callSuspend(this, *args) - } else { - if (hasSenderParam) { - function.callSuspend(sender, *args) - } else function.callSuspend(*args) - } - - checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" } - - result as? Boolean ?: true // Unit, void is considered as true. - }, - context = context - ) -} \ No newline at end of file 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 703a7e062..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/executeCommandInternal.kt +++ /dev/null @@ -1,62 +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 = { - return CommandExecuteResult.ExecutionFailed( - commandName = commandName, - command = command, - exception = it, - 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/internal/data/MultiFilePluginDataStorageImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt index 45fa24f88..800229946 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt @@ -12,7 +12,6 @@ package net.mamoe.mirai.console.internal.data import kotlinx.serialization.json.Json import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.data.* -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.SilentLogger diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt index e9952dd86..54818777e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt @@ -11,14 +11,15 @@ package net.mamoe.mirai.console.internal.data import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.ValueName -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip -import kotlin.reflect.KClass -import kotlin.reflect.KParameter -import kotlin.reflect.KProperty -import kotlin.reflect.KType +import kotlin.reflect.* import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.isSubclassOf +internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>" + +internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean = + findAnnotation<T>() != null + @Suppress("UNCHECKED_CAST") internal inline fun <reified T : Any> KType.toKClass(): KClass<out T> { val clazz = requireNotNull(classifier as? KClass<T>) { "Unsupported classifier: $classifier" } @@ -41,8 +42,8 @@ internal inline fun <reified T : PluginData> newPluginDataInstanceUsingReflectio ?: createInstanceOrNull() ?: throw IllegalArgumentException( "Cannot create PluginData instance. " + - "PluginDataHolder supports PluginData implemented as an object " + - "or the ones with a constructor which either has no parameters or all parameters of which are optional, by default newPluginDataInstance implementation." + "PluginDataHolder supports PluginData implemented as an object " + + "or the ones with a constructor which either has no parameters or all parameters of which are optional, by default newPluginDataInstance implementation." ) } } @@ -54,6 +55,12 @@ internal fun KType.classifierAsKClass() = when (val t = classifier) { else -> error("Only KClass supported as classifier, got $t") } as KClass<Any> +@Suppress("UNCHECKED_CAST") +internal fun KType.classifierAsKClassOrNull() = when (val t = classifier) { + is KClass<*> -> t + else -> null +} as KClass<Any>? + @JvmSynthetic internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? { val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt index 037c65ba5..02e5ddbe9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt @@ -16,7 +16,6 @@ import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.SerializableValue.Companion.serializableValueWith import net.mamoe.mirai.console.data.SerializerAwareValue import net.mamoe.mirai.console.data.valueFromKType -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import kotlin.contracts.contract import kotlin.reflect.KClass import kotlin.reflect.KType diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt index 60bb09cd8..d096341ed 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt @@ -52,7 +52,7 @@ internal object BuiltInSingletonExtensionSelector : SingletonExtensionSelector { val candidatesList = candidates.toList() for ((index, candidate) in candidatesList.withIndex()) { - MiraiConsole.mainLogger.info { "${index + 1}. '${candidate.extension}' from '${candidate.plugin.name}'" } + MiraiConsole.mainLogger.info { "${index + 1}. '${candidate.extension}' from '${candidate.plugin?.name ?: "<builtin>"}'" } } MiraiConsole.mainLogger.info { "Please choose a number from 1 to ${candidatesList.count()}" } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/ComponentStorageInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/ComponentStorageInternal.kt index 6527aadb3..b3072ad49 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/ComponentStorageInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/ComponentStorageInternal.kt @@ -20,12 +20,15 @@ import java.util.concurrent.CopyOnWriteArraySet import kotlin.contracts.contract import kotlin.reflect.KClass +/** + * The [ComponentStorage] containing all components provided by Mirai Console internals and installed plugins. + */ internal object GlobalComponentStorage : AbstractConcurrentComponentStorage() internal interface ExtensionRegistry<out E : Extension> { - val plugin: Plugin + val plugin: Plugin? val extension: E - operator fun component1(): Plugin { + operator fun component1(): Plugin? { return this.plugin } @@ -35,21 +38,27 @@ internal interface ExtensionRegistry<out E : Extension> { } internal class LazyExtensionRegistry<out E : Extension>( - override val plugin: Plugin, + override val plugin: Plugin?, initializer: () -> E, ) : ExtensionRegistry<E> { override val extension: E by lazy { initializer() } } internal data class DataExtensionRegistry<out E : Extension>( - override val plugin: Plugin, + override val plugin: Plugin?, override val extension: E, ) : ExtensionRegistry<E> internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { @Suppress("UNCHECKED_CAST") internal fun <T : Extension> ExtensionPoint<out T>.getExtensions(): Set<ExtensionRegistry<T>> { - return instances.getOrPut(this, ::CopyOnWriteArraySet) as Set<ExtensionRegistry<T>> + val userDefined = instances.getOrPut(this, ::CopyOnWriteArraySet) as Set<ExtensionRegistry<T>> + + val builtins = if (this is InstanceExtensionPoint<*, *>) { + this.builtinImplementations.mapTo(HashSet()) { DataExtensionRegistry(null, it) } as Set<ExtensionRegistry<T>> + } else null + + return builtins?.plus(userDefined) ?: userDefined } internal fun mergeWith(another: AbstractConcurrentComponentStorage) { @@ -68,7 +77,7 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @kotlin.internal.LowPriorityInOverloadResolution - internal inline fun <T : Extension> ExtensionPoint<out T>.withExtensions(block: T.(plugin: Plugin) -> Unit) { + internal inline fun <T : Extension> ExtensionPoint<out T>.withExtensions(block: T.(plugin: Plugin?) -> Unit) { contract { callsInPlace(block) } @@ -128,11 +137,11 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { internal fun <T : Extension> ExtensionPoint<out T>.throwExtensionException( extension: T, - plugin: Plugin, + plugin: Plugin?, throwable: Throwable, ) { throw ExtensionException( - "Exception while executing extension '${extension.kClassQualifiedNameOrTip}' provided by plugin '${plugin.name}', registered for '${this.extensionType.qualifiedName}'", + "Exception while executing extension '${extension.kClassQualifiedNameOrTip}' provided by plugin '${plugin?.name ?: "<builtin>"}', registered for '${this.extensionType.qualifiedName}'", throwable ) } @@ -142,7 +151,7 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @kotlin.internal.LowPriorityInOverloadResolution - internal inline fun <T : Extension> ExtensionPoint<T>.useExtensions(block: (extension: T, plugin: Plugin) -> Unit): Unit = + internal inline fun <T : Extension> ExtensionPoint<T>.useExtensions(block: (extension: T, plugin: Plugin?) -> Unit): Unit = withExtensions(block) val instances: MutableMap<ExtensionPoint<*>, MutableSet<ExtensionRegistry<*>>> = ConcurrentHashMap() @@ -154,6 +163,15 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(DataExtensionRegistry(plugin, extensionInstance)) } + @JvmName("contribute1") + fun <T : Extension> contribute( + extensionPoint: ExtensionPoint<T>, + plugin: Plugin?, + extensionInstance: T, + ) { + instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(DataExtensionRegistry(plugin, extensionInstance)) + } + override fun <T : Extension> contribute( extensionPoint: ExtensionPoint<T>, plugin: Plugin, @@ -161,4 +179,13 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { ) { instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(LazyExtensionRegistry(plugin, lazyInstance)) } + + @JvmName("contribute1") + fun <T : Extension> contribute( + extensionPoint: ExtensionPoint<T>, + plugin: Plugin?, + lazyInstance: () -> T, + ) { + instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(LazyExtensionRegistry(plugin, lazyInstance)) + } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt index fbe937d23..448f3feba 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt @@ -48,7 +48,7 @@ internal abstract class JvmPluginInternal( final override val parentPermission: Permission by lazy { PermissionService.INSTANCE.register( - PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.ROOT_PERMISSION), + PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.PLUGIN_ROOT_PERMISSION), "The base permission" ) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt index 2f15ba42a..68045ff93 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt @@ -148,7 +148,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol var count = 0 GlobalComponentStorage.run { PluginLoaderProvider.useExtensions { ext, plugin -> - logger.info { "Loaded PluginLoader ${ext.instance} from ${plugin.name}" } + logger.info { "Loaded PluginLoader ${ext.instance} from ${plugin?.name ?: "<builtin>"}" } _pluginLoaders.add(ext.instance) count++ } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt index b7c7cd80f..6a8b72425 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt @@ -7,6 +7,8 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("unused") + package net.mamoe.mirai.console.permission import net.mamoe.mirai.console.command.BuiltInCommands @@ -65,10 +67,10 @@ public interface Permission { * @see RootPermission 推荐 Kotlin 用户使用. */ @JvmStatic - public fun getRootPermission(): Permission = PermissionService.INSTANCE.rootPermission + public fun getRootPermission(): Permission = RootPermission /** - * 递归获取 [Permission.parent], `permission.parent.parent`, permission.parent.parent` ... 直到 [Permission.parent] 为它自己. + * 递归获取 [Permission.parent], `permission.parent.parent`, permission.parent.parent.parent` ... 直到 [Permission.parent] 为它自己. */ @get:JvmStatic public val Permission.parentsWithSelf: Sequence<Permission> @@ -82,5 +84,5 @@ public interface Permission { * 根权限. 是所有权限的父权限. 权限 ID 为 "*:*" */ @get:JvmSynthetic -public val RootPermission: Permission +public inline val RootPermission: Permission // It might be removed in the future, so make it inline to avoid ABI changes. get() = PermissionService.INSTANCE.rootPermission \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt index b34171aba..551ca81a3 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt @@ -35,20 +35,20 @@ public data class PermissionId( "' ' is not allowed in namespace" } require(name.none { it.isWhitespace() }) { - "' ' is not allowed in id" + "' ' is not allowed in name" } require(!namespace.contains(':')) { "':' is not allowed in namespace" } require(!name.contains(':')) { - "':' is not allowed in id" + "':' is not allowed in name" } } public object PermissionIdAsStringSerializer : KSerializer<PermissionId> by String.serializer().map( serializer = { it.namespace + ":" + it.name }, - deserializer = { it.split(':').let { (namespace, id) -> PermissionId(namespace, id) } } + deserializer = ::parseFromString ) /** @@ -76,11 +76,11 @@ public data class PermissionId( */ @JvmStatic @Throws(IllegalArgumentException::class) - public fun checkPermissionIdName(@ResolveContext(PERMISSION_NAME) value: String) { + public fun checkPermissionIdName(@ResolveContext(PERMISSION_NAME) name: String) { when { - value.isBlank() -> throw IllegalArgumentException("PermissionId.name should not be blank.") - value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.name.") - value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.name.") + name.isBlank() -> throw IllegalArgumentException("PermissionId.name should not be blank.") + name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces are not yet allowed in PermissionId.name.") + name.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.name.") } } @@ -89,11 +89,11 @@ public data class PermissionId( */ @JvmStatic @Throws(IllegalArgumentException::class) - public fun checkPermissionIdNamespace(@ResolveContext(PERMISSION_NAME) value: String) { + public fun checkPermissionIdNamespace(@ResolveContext(PERMISSION_NAME) namespace: String) { when { - value.isBlank() -> throw IllegalArgumentException("PermissionId.namespace should not be blank.") - value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.namespace.") - value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.namespace.") + namespace.isBlank() -> throw IllegalArgumentException("PermissionId.namespace should not be blank.") + namespace.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces are not yet allowed in PermissionId.namespace.") + namespace.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.namespace.") } } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt index dc284650e..477afdc26 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt @@ -129,10 +129,10 @@ public interface PermissionService<P : Permission> { /** [Plugin] 尝试分配的 [PermissionId] 来源 */ public enum class PluginPermissionIdRequestType { /** For [Plugin.parentPermission] */ - ROOT_PERMISSION, + PLUGIN_ROOT_PERMISSION, /** For [Plugin.permissionId] */ - PERMISSION_ID + NORMAL } public companion object { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt index fb1f1849f..ce5f98ce3 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt @@ -39,7 +39,7 @@ public abstract class AbstractJvmPlugin @JvmOverloads constructor( public final override val loader: JvmPluginLoader get() = super<JvmPluginInternal>.loader public final override fun permissionId(name: String): PermissionId = - PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name, PermissionService.PluginPermissionIdRequestType.PERMISSION_ID) + PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name, PermissionService.PluginPermissionIdRequestType.NORMAL) /** * 重载 [PluginData] diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt index 9b3aef0ed..bdd37219d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt @@ -12,7 +12,7 @@ package net.mamoe.mirai.console.util import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip +import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip import net.mamoe.mirai.contact.* /** 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<MessageContent> = asSequence().filterIsInstance<MessageContent>() + + @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/main/kotlin/net/mamoe/mirai/console/util/StandardUtils.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/StandardUtils.kt new file mode 100644 index 000000000..0d7dc8630 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/StandardUtils.kt @@ -0,0 +1,34 @@ +/* + * 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 kotlin.contracts.contract + +/** + * Perform `this as? T`. + */ +@JvmSynthetic +public inline fun <reified T : Any> Any?.safeCast(): T? { + contract { + returnsNotNull() implies (this@safeCast is T) + } + return this as? T +} + +/** + * Perform `this as T`. + */ +@JvmSynthetic +public inline fun <reified T : Any> Any?.cast(): T { + contract { + returns() implies (this@cast is T) + } + return this as T +} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt index 969f03425..c1eb1145c 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt @@ -89,7 +89,7 @@ internal object Testing { internal var cont: Continuation<Any?>? = null @Suppress("UNCHECKED_CAST") - suspend fun <R> withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R { + suspend fun <R> withTesting(timeout: Long = 50000L, block: suspend () -> Unit): R { @Suppress("RemoveExplicitTypeArguments") // bug return if (timeout != -1L) { withTimeout<R>(timeout) { 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 122475bed..bf3b7c796 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,14 +16,14 @@ 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.getRegisteredCommands import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.registeredCommands -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.registerCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands -import net.mamoe.mirai.console.command.description.CommandArgumentParser -import net.mamoe.mirai.console.command.description.buildCommandArgumentContext +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.initTestEnvironment import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.flattenCommandComponents @@ -38,7 +38,12 @@ object TestCompositeCommand : CompositeCommand( "testComposite", "tsC" ) { @SubCommand - fun mute(seconds: Int) { + fun mute(seconds: Int = 60) { + Testing.ok(seconds) + } + + @SubCommand + fun mute(target: Long, seconds: Int) { Testing.ok(seconds) } } @@ -54,6 +59,7 @@ internal val sender by lazy { ConsoleCommandSender } internal val owner by lazy { ConsoleCommandOwner } +@OptIn(ExperimentalCommandDescriptors::class) internal class TestCommand { companion object { @JvmStatic @@ -72,25 +78,30 @@ internal class TestCommand { @Test fun testRegister() { try { - ConsoleCommandOwner.unregisterAllCommands() // builtins + unregisterAllCommands(ConsoleCommandOwner) // builtins + unregisterCommand(TestSimpleCommand) assertTrue(TestCompositeCommand.register()) assertFalse(TestCompositeCommand.register()) - assertEquals(1, ConsoleCommandOwner.registeredCommands.size) + assertEquals(1, getRegisteredCommands(ConsoleCommandOwner).size) assertEquals(1, CommandManagerImpl._registeredCommands.size) - assertEquals(2, CommandManagerImpl.requiredPrefixCommandMap.size) + assertEquals(2, + CommandManagerImpl.requiredPrefixCommandMap.size, + CommandManagerImpl.requiredPrefixCommandMap.entries.joinToString { it.toString() }) } finally { - TestCompositeCommand.unregister() + unregisterCommand(TestCompositeCommand) } } @Test fun testSimpleExecute() = runBlocking { - assertEquals("test", withTesting<MessageChain> { - assertSuccess(TestSimpleCommand.execute(sender, "test")) - }.contentToString()) + TestSimpleCommand.withRegistration { + assertEquals("test", withTesting<MessageChain> { + assertSuccess(TestSimpleCommand.execute(sender, "test")) + }.contentToString()) + } } @Test @@ -105,24 +116,28 @@ internal class TestCommand { @Test fun testSimpleArgsSplitting() = runBlocking { - assertEquals(arrayOf("test", "ttt", "tt").joinToString(), withTesting<MessageChain> { - assertSuccess(TestSimpleCommand.execute(sender, PlainText("test ttt tt"))) - }.joinToString()) + TestSimpleCommand.withRegistration { + assertEquals(arrayOf("test", "ttt", "tt").joinToString(), withTesting<MessageChain> { + assertSuccess(TestSimpleCommand.execute(sender, PlainText("test ttt tt"))) + }.joinToString()) + } } val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f") @Test fun `PlainText and Image args splitting`() = runBlocking { - val result = withTesting<MessageChain> { - assertSuccess(TestSimpleCommand.execute(sender, buildMessageChain { - +"test" - +image - +"tt" - })) + TestSimpleCommand.withRegistration { + val result = withTesting<MessageChain> { + assertSuccess(TestSimpleCommand.execute(sender, buildMessageChain { + +"test" + +image + +"tt" + })) + } + assertEquals<Any>(arrayOf("test", image, "tt").joinToString(), result.toTypedArray().joinToString()) + assertSame(image, result[1]) } - assertEquals<Any>(arrayOf("test", image, "tt").joinToString(), result.toTypedArray().joinToString()) - assertSame(image, result[1]) } @Test @@ -134,19 +149,29 @@ internal class TestCommand { @Test fun `executing command by string command`() = runBlocking { - TestCompositeCommand.register() - val result = withTesting<Int> { - assertSuccess(sender.executeCommand("/testComposite mute 1")) - } + TestCompositeCommand.withRegistration { + val result = withTesting<Int> { + assertSuccess(sender.executeCommand("/testComposite mute 1")) + } - assertEquals(1, result) + assertEquals(1, result) + } + } + + @Test + fun `composite command descriptors`() { + val overloads = TestCompositeCommand.overloads + assertEquals("CommandSignatureVariant(<mute>, seconds: Int = ...)", overloads[0].toString()) + assertEquals("CommandSignatureVariant(<mute>, target: Long, seconds: Int)", overloads[1].toString()) } @Test fun `composite command executing`() = runBlocking { - assertEquals(1, withTesting { - assertSuccess(TestCompositeCommand.execute(sender, "mute 1")) - }) + TestCompositeCommand.withRegistration { + assertEquals(1, withTesting { + assertSuccess(TestCompositeCommand.execute(sender, "mute 1")) + }) + } } @Test @@ -164,19 +189,19 @@ internal class TestCommand { @Suppress("UNUSED_PARAMETER") @SubCommand - fun mute(seconds: Int, arg2: Int) { + fun mute(seconds: Int, arg2: Int = 1) { Testing.ok(2) } } - assertFailsWith<IllegalStateException> { - composite.register() - } - /* + registerCommand(composite) + + println(composite.overloads.joinToString()) + composite.withRegistration { - assertEquals(1, withTesting { execute(sender, "tr", "mute 123") }) // one args, resolves to mute(Int) - assertEquals(2, withTesting { execute(sender, "tr", "mute 123 123") }) - }*/ + assertEquals(1, withTesting { assertSuccess(composite.execute(sender, "mute 123")) }) // one arg, resolves to mute(Int) + assertEquals(2, withTesting { assertSuccess(composite.execute(sender, "mute 123 1")) }) // two arg, resolved to mute(Int, Int) + } } } @@ -184,19 +209,20 @@ internal class TestCommand { fun `composite sub command parsing`() { runBlocking { class MyClass( - val value: Int + val value: Int, ) val composite = object : CompositeCommand( ConsoleCommandOwner, "test22", overrideContext = buildCommandArgumentContext { - add(object : CommandArgumentParser<MyClass> { + add(object : CommandValueArgumentParser<MyClass> { override fun parse(raw: String, sender: CommandSender): MyClass { return MyClass(raw.toInt()) } override fun parse(raw: MessageContent, sender: CommandSender): MyClass { + if (raw is PlainText) return parse(raw.content, sender) assertSame(image, raw) return MyClass(2) } @@ -210,12 +236,14 @@ internal class TestCommand { } composite.withRegistration { - assertEquals(333, withTesting<MyClass> { execute(sender, "mute 333") }.value) + assertEquals(333, withTesting<MyClass> { assertSuccess(execute(sender, "mute 333")) }.value) assertEquals(2, withTesting<MyClass> { - execute(sender, buildMessageChain { - +"mute" - +image - }) + assertSuccess( + execute(sender, buildMessageChain { + +"mute" + +image + }) + ) }.value) } } @@ -238,8 +266,76 @@ internal class TestCommand { } } } + + @Test + fun `test optional argument command`() { + runBlocking { + val optionCommand = object : CompositeCommand( + ConsoleCommandOwner, + "testOptional" + ) { + @SubCommand + fun optional(arg1: String, arg2: String = "Here is optional", arg3: String? = null) { + println(arg1) + println(arg2) + println(arg3) +// println(arg3) + Testing.ok(Unit) + } + } + optionCommand.withRegistration { + withTesting<Unit> { + assertSuccess(sender.executeCommand("/testOptional optional 1")) + } + } + } + } + + @Test + fun `test vararg`() { + runBlocking { + val optionCommand = object : CompositeCommand( + ConsoleCommandOwner, + "test" + ) { + @SubCommand + fun vararg(arg1: Int, vararg x: String) { + assertEquals(1, arg1) + Testing.ok(x) + } + } + optionCommand.withRegistration { + assertArrayEquals( + emptyArray<String>(), + withTesting { + assertSuccess(sender.executeCommand("/test vararg 1")) + } + ) + + assertArrayEquals( + arrayOf("s"), + withTesting<Array<String>> { + assertSuccess(sender.executeCommand("/test vararg 1 s")) + } + ) + assertArrayEquals( + arrayOf("s", "s", "s"), + withTesting { + assertSuccess(sender.executeCommand("/test vararg 1 s s s")) + } + ) + } + } + } } +fun <T> assertArrayEquals(expected: Array<out T>, actual: Array<out T>, message: String? = null) { + asserter.assertEquals(message, expected.contentToString(), actual.contentToString()) +} + +@OptIn(ExperimentalCommandDescriptors::class) internal fun assertSuccess(result: CommandExecuteResult) { - assertTrue(result.isSuccess(), result.toString()) + if (result.isFailure()) { + throw result.exception ?: AssertionError(result.toString()) + } } \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt index 758a7848b..8863af745 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt @@ -10,13 +10,13 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterCommand inline fun <T : Command, R> T.withRegistration(block: T.() -> R): R { this.register() try { return block() } finally { - this.unregister() + unregisterCommand(this) } } \ No newline at end of file diff --git a/docs/Commands.md b/docs/Commands.md index a1a9ce551..816884f77 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -37,9 +37,9 @@ [`RawCommand`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt [`CommandManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt [`CommandSender`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt -[`CommandArgumentParser`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt -[`CommandArgumentContext`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt -[`CommandArgumentContext.BuiltIns`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt#L66 +[`CommandArgumentParser`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParser.kt +[`CommandArgumentContext`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt +[`CommandArgumentContext.BuiltIns`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt#L66 [`MessageScope`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt @@ -113,7 +113,7 @@ interface CommandArgumentParser<out T : Any> { 支持原生数据类型,`Contact` 及其子类,`Bot`。 #### 构建 [`CommandArgumentContext`] -查看源码内注释:[CommandArgumentContext.kt: Line 146](../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt#L146-L183) +查看源码内注释:[CommandArgumentContext.kt: Line 146](../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt#L146-L183) ### 支持参数解析的 [`Command`] 实现 Mirai Console 内建 [`SimpleCommand`] 与 [`CompositeCommand`] 拥有 [`CommandArgumentContext`],在处理参数时会首先解析参数再传递给插件的实现。 diff --git a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt index a21b63c1e..5703eb512 100644 --- a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt @@ -15,11 +15,8 @@ import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.command.BuiltInCommands -import net.mamoe.mirai.console.command.CommandExecuteStatus -import net.mamoe.mirai.console.command.CommandManager -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand -import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.terminal.noconsole.NoConsole import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.requestInput @@ -29,7 +26,7 @@ import org.jline.reader.UserInterruptException val consoleLogger by lazy { DefaultLogger("console") } -@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class) +@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class, ExperimentalCommandDescriptors::class) internal fun startupConsoleThread() { if (terminal is NoConsole) return @@ -65,6 +62,9 @@ internal fun startupConsoleThread() { when (result.status) { CommandExecuteStatus.SUCCESSFUL -> { } + CommandExecuteStatus.ILLEGAL_ARGUMENT -> { + result.exception?.message?.let { consoleLogger.warning(it) } + } CommandExecuteStatus.EXECUTION_EXCEPTION -> { result.exception?.let(consoleLogger::error) } diff --git a/gradle.properties b/gradle.properties index 6f6711e1f..20544ecbe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ # style guide kotlin.code.style=official +org.gradle.vfs.watch=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7743a94c2..32b5708ed 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Wed Mar 04 22:27:09 CST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists