diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt index 9fcc73a41..9916f025c 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt @@ -31,6 +31,7 @@ import java.util.* import java.util.concurrent.locks.ReentrantLock import kotlin.annotation.AnnotationTarget.* import kotlin.coroutines.CoroutineContext +import kotlin.system.exitProcess /** @@ -176,7 +177,7 @@ public interface MiraiConsoleImplementation : CoroutineScope { /** * 可由前端调用, 获取当前的 [MiraiConsoleImplementation] 实例 * - * 必须在 [start] 之后才能使用. + * 必须在 [start] 之后才能使用, 否则抛出 [UninitializedPropertyAccessException] */ @JvmStatic @ConsoleFrontEndImplementation @@ -189,7 +190,22 @@ public interface MiraiConsoleImplementation : CoroutineScope { public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock { if (::instance.isInitialized) error("Mirai Console is already initialized.") this@Companion.instance = this - MiraiConsoleImplementationBridge.doStart() + kotlin.runCatching { + MiraiConsoleImplementationBridge.doStart() + }.onFailure { e -> + kotlin.runCatching { + MiraiConsole.mainLogger.error("Failed to init MiraiConsole.", e) + }.onFailure { + e.printStackTrace() + } + + kotlin.runCatching { + MiraiConsole.cancel() + }.onFailure { + it.printStackTrace() + } + exitProcess(1) + } } } } 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..fc4c406b6 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,16 +15,20 @@ 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.CommandArgumentParserException +import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser.Companion.map +import net.mamoe.mirai.console.command.descriptor.PermissionIdValueArgumentParser +import net.mamoe.mirai.console.command.descriptor.PermitteeIdValueArgumentParser +import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext 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 import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.PermissionService -import net.mamoe.mirai.console.permission.PermissionService.Companion.denyPermission +import net.mamoe.mirai.console.permission.PermissionService.Companion.cancel import net.mamoe.mirai.console.permission.PermissionService.Companion.findCorrespondingPermissionOrFail import net.mamoe.mirai.console.permission.PermissionService.Companion.getPermittedPermissions -import net.mamoe.mirai.console.permission.PermissionService.Companion.grantPermission +import net.mamoe.mirai.console.permission.PermissionService.Companion.permit import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleInternalApi @@ -150,11 +154,11 @@ 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) } + }.getOrElse { throw CommandArgumentParserException("指令不存在: $id", it) } } }, ), BuiltInCommandInternal { @@ -166,7 +170,7 @@ public object BuiltInCommands { @Name("被许可人 ID") target: PermitteeId, @Name("权限 ID") permission: Permission, ) { - target.grantPermission(permission) + target.permit(permission) sendMessage("OK") } @@ -176,7 +180,7 @@ public object BuiltInCommands { @Name("被许可人 ID") target: PermitteeId, @Name("权限 ID") permission: Permission, ) { - target.denyPermission(permission, false) + target.cancel(permission, false) sendMessage("OK") } @@ -186,7 +190,7 @@ public object BuiltInCommands { @Name("被许可人 ID") target: PermitteeId, @Name("权限 ID") permission: Permission, ) { - target.denyPermission(permission, true) + target.cancel(permission, true) sendMessage("OK") } 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..a73e2f842 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,26 +11,25 @@ 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.java.JCommand +import net.mamoe.mirai.console.command.descriptor.CommandArgumentContextAware +import net.mamoe.mirai.console.command.descriptor.CommandSignature +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors 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 JCommand 为 Java 用户添加协程帮助的 [Command] + * @see CommandArgumentContextAware */ public interface Command { /** @@ -48,18 +47,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 +78,8 @@ public interface Command { * * 会影响聊天语境中的解析. */ + @ExperimentalCommandDescriptors + @ConsoleExperimentalApi public val prefixOptional: Boolean /** @@ -80,16 +88,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 +107,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..efec2b5d1 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,28 @@ 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().also { + reflector.validate(it) + } + } + /** * 自动根据带有 [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 +134,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/description/CommandArgumentParserException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/IllegalCommandArgumentException.kt similarity index 68% 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/IllegalCommandArgumentException.kt index 033d642df..0e8936524 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/IllegalCommandArgumentException.kt @@ -5,23 +5,25 @@ * 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.description +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException /** - * 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范等. + * 在处理参数时遇到的 _正常_ 错误. 如参数不符合规范, 参数值越界等. * * [message] 将会发送给指令调用方. * - * @see CommandArgumentParser - * @see CommandArgumentParser.illegalArgument + * @see CommandArgumentParserException */ -public class CommandArgumentParserException : RuntimeException { +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) -} \ No newline at end of file +} 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..4b17e51e2 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,16 @@ 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 /** * 无参数解析, 接收原生参数的指令. @@ -48,10 +50,23 @@ public abstract class RawCommand( /** 指令父权限 */ parentPermission: Permission = owner.parentPermission, /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ + @OptIn(ExperimentalCommandDescriptors::class) public override val prefixOptional: Boolean = false, ) : Command { public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) } + @ExperimentalCommandDescriptors + override val overloads: List = listOf( + CommandSignatureImpl( + 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..48ce42f7c 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 @@ -17,21 +17,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 +60,42 @@ 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 { + reflector.validate(it) + 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/CommandArgumentParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt deleted file mode 100644 index 08f7eee05..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt +++ /dev/null @@ -1,145 +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", "unused") - -package net.mamoe.mirai.console.command.description - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.command.CommandManager -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.contact.* -import net.mamoe.mirai.message.data.MessageContent -import net.mamoe.mirai.message.data.SingleMessage -import net.mamoe.mirai.message.data.content -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -/** - * 指令参数解析器. 用于解析字符串或 [SingleMessage] 到特定参数类型. - * - * ### 参数解析 - * - * 如 [SimpleCommand] 中的示例: - * ``` - * suspend fun CommandSender.mute(target: Member, duration: Int) - * ``` - * [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] 为 [Member] 的 [CommandArgumentParser], 并调用其 [CommandArgumentParser.parse] - * - * ### 内建指令解析器 - * - 基础类型: [ByteArgumentParser], [ShortArgumentParser], [IntArgumentParser], [LongArgumentParser] - * [FloatArgumentParser], [DoubleArgumentParser], - * [BooleanArgumentParser], [StringArgumentParser] - * - * - [Bot]: [ExistingBotArgumentParser] - * - [Friend]: [ExistingFriendArgumentParser] - * - [Group]: [ExistingGroupArgumentParser] - * - [Member]: [ExistingMemberArgumentParser] - * - [User]: [ExistingUserArgumentParser] - * - [Contact]: [ExistingContactArgumentParser] - * - * - * @see SimpleCommand 简单指令 - * @see CompositeCommand 复合指令 - * - * @see buildCommandArgumentContext 指令参数环境, 即 [CommandArgumentParser] 的集合 - */ -public interface CommandArgumentParser { - /** - * 解析一个字符串为 [T] 类型参数 - * - * **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException]. - * 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户. - * - * @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出. - * - * @see CommandArgumentParserException - */ - @Throws(CommandArgumentParserException::class) - public fun parse(raw: String, sender: CommandSender): T - - /** - * 解析一个消息内容元素为 [T] 类型参数 - * - * **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException]. - * 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户. - * - * @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出. - * - * @see CommandArgumentParserException - */ - @Throws(CommandArgumentParserException::class) - public fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender) -} - -/** - * 使用原 [this] 解析, 成功后使用 [mapper] 映射为另一个类型. - */ -public fun CommandArgumentParser.map( - mapper: CommandArgumentParser.(T) -> R -): CommandArgumentParser = MappingCommandArgumentParser(this, mapper) - -private class MappingCommandArgumentParser( - private val original: CommandArgumentParser, - private val mapper: CommandArgumentParser.(T) -> R -) : CommandArgumentParser { - 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)) -} - -/** - * 解析一个字符串或 [SingleMessage] 为 [T] 类型参数 - * - * @throws IllegalArgumentException 当 [raw] 既不是 [SingleMessage], 也不是 [String] 时抛出. - */ -@JvmSynthetic -@Throws(IllegalArgumentException::class) -public fun CommandArgumentParser.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) - else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}") - } -} - -/** - * 抛出一个 [CommandArgumentParserException] 的捷径 - * - * @throws CommandArgumentParserException - */ -@Suppress("unused") -@JvmSynthetic -@Throws(CommandArgumentParserException::class) -public inline fun CommandArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing { - throw CommandArgumentParserException(message, cause) -} - -/** - * 检查参数 [condition]. 当它为 `false` 时调用 [message] 并以其返回值作为消息, 抛出异常 [CommandArgumentParserException] - * - * @throws CommandArgumentParserException - */ -@Throws(CommandArgumentParserException::class) -@JvmSynthetic -public inline fun CommandArgumentParser<*>.checkArgument( - condition: Boolean, - crossinline message: () -> String = { "Check failed." } -) { - contract { - returns() implies condition - callsInPlace(message, InvocationKind.AT_MOST_ONCE) - } - if (!condition) illegalArgument(message()) -} \ No newline at end of file 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 65% 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..c662a2997 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,26 +9,29 @@ @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.contracts.InvocationKind.EXACTLY_ONCE +import kotlin.contracts.contract import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf /** - * 指令参数环境, 即 [CommandArgumentParser] 的集合, 用于 [CompositeCommand] 和 [SimpleCommand]. + * 指令参数环境, 即 [CommandValueArgumentParser] 的集合, 用于 [CompositeCommand] 和 [SimpleCommand]. * * 在指令解析时, 总是从 [CommandArgumentContextAware.context] 搜索相关解析器 * @@ -37,20 +40,28 @@ 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 companion object { + @JvmStatic + public fun ParserPair.toPair(): Pair, CommandValueArgumentParser> = klass to parser + } + } - public operator fun get(klass: KClass): CommandArgumentParser? + /** + * 获取一个 [kClass] 类型的解析器. + */ + public operator fun get(kClass: KClass): CommandValueArgumentParser? public fun toList(): List> @@ -58,37 +69,39 @@ public interface CommandArgumentContext { /** * For Java callers. * - * @see [EmptyCommandArgumentContext] + * @see EmptyCommandArgumentContext */ @JvmStatic public val EMPTY: CommandArgumentContext = EmptyCommandArgumentContext } /** - * 内建的默认 [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,11 +113,14 @@ public interface CommandArgumentContext { */ public interface CommandArgumentContextAware { /** - * [CommandArgumentParser] 的集合 + * [CommandValueArgumentParser] 的集合 */ public val context: CommandArgumentContext } +/** + * @see CommandArgumentContext.EMPTY + */ public object EmptyCommandArgumentContext : CommandArgumentContext by SimpleCommandArgumentContext(listOf()) /** @@ -114,8 +130,8 @@ 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? = - replacer[klass] ?: this@plus[klass] + override fun get(kClass: KClass): CommandValueArgumentParser? = + replacer[kClass] ?: this@plus[kClass] override fun toList(): List> = replacer.toList() + this@plus.toList() } @@ -129,9 +145,9 @@ 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? - ?: this@plus[klass] + override fun get(kClass: KClass): CommandValueArgumentParser? = + replacer.firstOrNull { kClass.isSubclassOf(it.klass) }?.parser as CommandValueArgumentParser? + ?: this@plus[kClass] override fun toList(): List> = replacer.toList() + this@plus.toList() } @@ -146,9 +162,9 @@ public operator fun CommandArgumentContext.plus(replacer: List>): public class SimpleCommandArgumentContext( public val list: List>, ) : CommandArgumentContext { - override fun get(klass: KClass): CommandArgumentParser? = - (this.list.firstOrNull { klass == it.klass }?.parser - ?: this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser) as CommandArgumentParser? + override fun get(kClass: KClass): CommandValueArgumentParser? = + (this.list.firstOrNull { kClass == it.klass }?.parser + ?: this.list.firstOrNull { kClass.isSubclassOf(it.klass) }?.parser) as CommandValueArgumentParser? override fun toList(): List> = list } @@ -160,7 +176,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()) * } @@ -189,6 +205,9 @@ public class SimpleCommandArgumentContext( */ @JvmSynthetic public fun buildCommandArgumentContext(block: CommandArgumentContextBuilder.() -> Unit): CommandArgumentContext { + contract { + callsInPlace(block, EXACTLY_ONCE) + } return CommandArgumentContextBuilder().apply(block).build() } @@ -200,14 +219,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 +237,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 +250,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 +270,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 +282,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 76% 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..20d0665b6 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,9 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.console.command.description +@file:Suppress("EXPOSED_SUPER_CLASS") + +package net.mamoe.mirai.console.command.descriptor import net.mamoe.mirai.Bot import net.mamoe.mirai.console.command.* @@ -25,47 +27,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 +75,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 +97,7 @@ public object ImageArgumentParser : InternalCommandArgumentParserExtensions { +public object PlainTextValueArgumentParser : InternalCommandValueArgumentParserExtensions() { public override fun parse(raw: String, sender: CommandSender): PlainText { return PlainText(raw) } @@ -109,7 +111,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 +123,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 +138,7 @@ public object ExistingBotArgumentParser : InternalCommandArgumentParserExtension /** * 解析任意一个存在的好友. */ -public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtensions<Friend> { +public object ExistingFriendValueArgumentParser : InternalCommandValueArgumentParserExtensions<Friend>() { private val syntax = """ - `botId.friendId` - `botId.friendNick` (模糊搜索, 寻找最优匹配) @@ -175,7 +177,7 @@ public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtens /** * 解析任意一个存在的群. */ -public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensions<Group> { +public object ExistingGroupValueArgumentParser : InternalCommandValueArgumentParserExtensions<Group>() { private val syntax = """ - `botId.groupId` - `~` (指代指令调用人自己所在群. 仅群聊天环境下) @@ -202,7 +204,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 +217,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 +248,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 +261,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 +288,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 +335,7 @@ public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtens } } -public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> { +public object PermissionIdValueArgumentParser : InternalCommandValueArgumentParserExtensions<PermissionId>() { override fun parse(raw: String, sender: CommandSender): PermissionId { return kotlin.runCatching { PermissionId.parseFromString(raw) }.getOrElse { illegalArgument("无法解析 $raw 为 PermissionId") @@ -341,7 +343,7 @@ public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> { } } -public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> { +public object PermitteeIdValueArgumentParser : InternalCommandValueArgumentParserExtensions<PermitteeId>() { override fun parse(raw: String, sender: CommandSender): PermitteeId { return if (raw == "~") sender.permitteeId else kotlin.runCatching { AbstractPermitteeId.parseFromString(raw) }.getOrElse { @@ -351,32 +353,38 @@ 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> { - fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数") +/** 直接返回原始参数 [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 +} - fun Long.findBotOrFail(): Bot = Bot.getInstanceOrNull(this) ?: illegalArgument("无法找到 Bot: $this") +internal abstract class InternalCommandValueArgumentParserExtensions<T : Any> : AbstractCommandValueArgumentParser<T>() { + private fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数") - fun String.findBotOrFail(): Bot = + protected fun Long.findBotOrFail(): Bot = Bot.getInstanceOrNull(this) ?: illegalArgument("无法找到 Bot: $this") + + protected fun String.findBotOrFail(): Bot = Bot.getInstanceOrNull(this.parseToLongOrFail()) ?: illegalArgument("无法找到 Bot: $this") - fun Bot.findGroupOrFail(id: Long): Group = getGroupOrNull(id) ?: illegalArgument("无法找到群: $this") + protected fun Bot.findGroupOrFail(id: Long): Group = getGroupOrNull(id) ?: illegalArgument("无法找到群: $this") - fun Bot.findGroupOrFail(id: String): Group = + protected fun Bot.findGroupOrFail(id: String): Group = getGroupOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到群: $this") - fun Bot.findFriendOrFail(id: String): Friend = + protected fun Bot.findFriendOrFail(id: String): Friend = getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到好友: $this") - fun Bot.findMemberOrFail(id: String): Friend = + protected fun Bot.findMemberOrFail(id: String): Friend = getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到群员: $this") - fun Group.findMemberOrFail(idOrCard: String): Member { + protected fun Group.findMemberOrFail(idOrCard: String): Member { if (idOrCard == "\$") return members.randomOrNull() ?: illegalArgument("当前语境下无法推断随机群员") idOrCard.toLongOrNull()?.let { getOrNull(it) }?.let { return it } this.members.singleOrNull { it.nameCardOrNick.contains(idOrCard) }?.let { return it } @@ -399,23 +407,21 @@ internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArg } } - fun CommandSender.inferBotOrFail(): Bot = + protected fun CommandSender.inferBotOrFail(): Bot = (this as? UserCommandSender)?.bot ?: Bot.botInstancesSequence.singleOrNull() ?: illegalArgument("当前语境下无法推断目标 Bot, 因为目前有多个 Bot 在线.") - fun CommandSender.inferGroupOrFail(): Group = + protected fun CommandSender.inferGroupOrFail(): Group = inferGroup() ?: illegalArgument("当前语境下无法推断目标群") - fun CommandSender.inferGroup(): Group? = (this as? GroupAwareCommandSender)?.group + protected fun CommandSender.inferGroup(): Group? = (this as? GroupAwareCommandSender)?.group - fun CommandSender.inferFriendOrFail(): Friend = + protected fun CommandSender.inferFriendOrFail(): Friend = (this as? FriendCommandSender)?.user ?: illegalArgument("当前语境下无法推断目标好友") } -internal fun Double.toDecimalPlace(n: Int): String { - return "%.${n}f".format(this) -} +internal fun Double.toDecimalPlace(n: Int): String = "%.${n}f".format(this) internal fun String.truncate(lengthLimit: Int, replacement: String = "..."): String = buildString { var lengthSum = 0 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..b83e6907a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt @@ -0,0 +1,307 @@ +/* + * 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 AbstractCommandSignature + */ +@ExperimentalCommandDescriptors +public interface CommandSignature { + /** + * 接收者参数, 为 [CommandSender] 子类 + */ + @ConsoleExperimentalApi + public val receiverParameter: CommandReceiverParameter<out CommandSender>? + + /** + * 形式 值参数. + */ + public val valueParameters: List<AbstractCommandValueParameter<*>> + + /** + * 调用这个指令. + */ + public suspend fun call(resolvedCommandCall: ResolvedCommandCall) +} + +/** + * 来自 [KFunction] 反射得到的 [CommandSignature] + * + * @see CommandSignatureFromKFunctionImpl + */ +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public interface CommandSignatureFromKFunction : CommandSignature { + public val originFunction: KFunction<*> +} + +/** + * @see CommandSignatureImpl + * @see CommandSignatureFromKFunctionImpl + */ +@ExperimentalCommandDescriptors +public abstract class AbstractCommandSignature : CommandSignature { + override fun toString(): String { + val receiverParameter = receiverParameter + return if (receiverParameter == null) { + "CommandSignatureVariant(${valueParameters.joinToString()})" + } else { + "CommandSignatureVariant($receiverParameter, ${valueParameters.joinToString()})" + } + } +} + +@ExperimentalCommandDescriptors +public open class CommandSignatureImpl( + override val receiverParameter: CommandReceiverParameter<out CommandSender>?, + override val valueParameters: List<AbstractCommandValueParameter<*>>, + private val onCall: suspend CommandSignatureImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit, +) : CommandSignature, AbstractCommandSignature() { + override suspend fun call(resolvedCommandCall: ResolvedCommandCall) { + return onCall(resolvedCommandCall) + } +} + +@ConsoleExperimentalApi +@ExperimentalCommandDescriptors +public open class CommandSignatureFromKFunctionImpl( + override val receiverParameter: CommandReceiverParameter<out CommandSender>?, + override val valueParameters: List<AbstractCommandValueParameter<*>>, + override val originFunction: KFunction<*>, + private val onCall: suspend CommandSignatureFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit, +) : CommandSignatureFromKFunction, AbstractCommandSignature() { + 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/descriptor/CommandValueArgumentParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandValueArgumentParser.kt new file mode 100644 index 000000000..75c373519 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandValueArgumentParser.kt @@ -0,0 +1,152 @@ +/* + * 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", "unused") + +package net.mamoe.mirai.console.command.descriptor + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.command.CommandManager +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.descriptor.CommandValueArgumentParser.Companion.parse +import net.mamoe.mirai.contact.* +import net.mamoe.mirai.message.data.* +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * 指令参数解析器. 用于解析字符串或 [SingleMessage] 到特定参数类型. + * + * ### 参数解析 + * + * 如 [SimpleCommand] 中的示例: + * ``` + * suspend fun CommandSender.mute(target: Member, duration: Int) + * ``` + * [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] 为 [Member] 的 [CommandValueArgumentParser], 并调用其 [CommandValueArgumentParser.parse] + * + * ### 内建指令解析器 + * - 基础类型: [ByteValueArgumentParser], [ShortValueArgumentParser], [IntValueArgumentParser], [LongValueArgumentParser] + * [FloatValueArgumentParser], [DoubleValueArgumentParser], + * [BooleanValueArgumentParser], [StringValueArgumentParser] + * + * - [Bot]: [ExistingBotValueArgumentParser] + * - [Friend]: [ExistingFriendValueArgumentParser] + * - [Group]: [ExistingGroupValueArgumentParser] + * - [Member]: [ExistingMemberValueArgumentParser] + * - [User]: [ExistingUserValueArgumentParser] + * - [Contact]: [ExistingContactValueArgumentParser] + * + * + * @see SimpleCommand 简单指令 + * @see CompositeCommand 复合指令 + * + * @see buildCommandArgumentContext 指令参数环境, 即 [CommandValueArgumentParser] 的集合 + */ +public interface CommandValueArgumentParser<out T : Any> { + /** + * 解析一个字符串为 [T] 类型参数 + * + * **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException]. + * 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户. + * + * @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出. + * + * @see CommandArgumentParserException + */ + @Throws(CommandArgumentParserException::class) + public fun parse(raw: String, sender: CommandSender): T + + /** + * 解析一个消息内容元素为 [T] 类型参数 + * + * **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException]. + * 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户. + * + * @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出. + * + * @see CommandArgumentParserException + */ + @Throws(CommandArgumentParserException::class) + public fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender) + + public companion object { + /** + * 解析一个字符串或 [SingleMessage] 为 [T] 类型参数 + * + * @throws IllegalArgumentException 当 [raw] 既不是 [SingleMessage], 也不是 [String] 时抛出. + * + * @see CommandValueArgumentParser.parse + */ + @JvmStatic + @Throws(IllegalArgumentException::class) + public fun <T : Any> CommandValueArgumentParser<T>.parse(raw: Message, sender: CommandSender): T { + return when (raw) { + is PlainText -> parse(raw.content, sender) + is MessageContent -> parse(raw, sender) + else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}") + } + } + + /** + * 使用原 [this] 解析, 成功后使用 [mapper] 映射为另一个类型. + */ + @JvmStatic + public fun <Original : Any, Result : Any> CommandValueArgumentParser<Original>.map( + mapper: MappingCommandValueArgumentParser<Original, Result>.(Original) -> Result, + ): CommandValueArgumentParser<Result> = MappingCommandValueArgumentParser(this, mapper) + } +} + +/** + * @see CommandValueArgumentParser 的基础实现. + */ +public abstract class AbstractCommandValueArgumentParser<T : Any> : CommandValueArgumentParser<T> { + public companion object { + /** + * 抛出一个 [CommandArgumentParserException] 的捷径 + * + * @throws CommandArgumentParserException + */ + @JvmStatic + @JvmSynthetic + @Throws(CommandArgumentParserException::class) + protected inline fun CommandValueArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing = + throw CommandArgumentParserException(message, cause) + + /** + * 检查参数 [condition]. 当它为 `false` 时调用 [message] 并以其返回值作为消息, 抛出异常 [CommandArgumentParserException] + * + * @throws CommandArgumentParserException + */ + @JvmStatic + @Throws(CommandArgumentParserException::class) + @JvmSynthetic + protected inline fun CommandValueArgumentParser<*>.checkArgument( + condition: Boolean, + crossinline message: () -> String = { "Check failed." }, + ) { + contract { + returns() implies condition + callsInPlace(message, InvocationKind.AT_MOST_ONCE) + } + if (!condition) illegalArgument(message()) + } + } +} + +public class MappingCommandValueArgumentParser<T : Any, R : Any>( + private val original: CommandValueArgumentParser<T>, + private val mapper: MappingCommandValueArgumentParser<T, R>.(T) -> R, +) : AbstractCommandValueArgumentParser<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)) +} \ No newline at end of file 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..b92ab8b68 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt @@ -0,0 +1,59 @@ +/* + * 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.Command +import net.mamoe.mirai.console.command.IllegalCommandArgumentException +import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueArgumentParser.Companion.illegalArgument +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 CommandDeclarationClashException( + public val command: Command, + public val signatures: List<CommandSignature>, +) : CommandResolutionException("Command declaration clash: \n${signatures.joinToString("\n")}") + +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) +} + +/** + * 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范等. + * + * [message] 将会发送给指令调用方. + * + * @see IllegalCommandArgumentException + * @see CommandValueArgumentParser + * @see AbstractCommandValueArgumentParser.illegalArgument + */ +public class CommandArgumentParserException : IllegalCommandArgumentException { + public constructor() : super() + public constructor(message: String?) : super(message) + public constructor(message: String?, cause: Throwable?) : super(message, cause) + public constructor(cause: Throwable?) : super(cause) +} \ No newline at end of file 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 deleted file mode 100644 index 1fbf748c0..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt +++ /dev/null @@ -1,40 +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.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 - -/** - * 为 Java 用户添加协程帮助的 [Command]. - * - * 注意, [JSimpleCommand], [JCompositeCommand], [JRawCommand] 都不实现这个接口. [JCommand] 只设计为 Java 使用者自己实现 [Command] 相关内容. - * - * @see Command - */ -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 -} \ 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..b3828418d 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,7 +13,8 @@ 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.ExperimentalCommandDescriptors +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 @@ -83,6 +84,7 @@ public abstract class JCompositeCommand protected set /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ + @ExperimentalCommandDescriptors public final override var prefixOptional: Boolean = false protected set 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..70b62e680 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.command.descriptor.ExperimentalCommandDescriptors 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 /** * 供 Java 用户继承 @@ -70,21 +69,7 @@ public abstract class JRawCommand protected set /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ + @ExperimentalCommandDescriptors 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..706a14df4 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 @@ -10,10 +10,10 @@ package net.mamoe.mirai.console.command.java 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.command.descriptor.ExperimentalCommandDescriptors 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 @@ -50,9 +50,10 @@ public abstract class JSimpleCommand( ) : SimpleCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = basePermission) { public override var description: String = super.description protected set - public override var permission: Permission = super.permission protected set + + @ExperimentalCommandDescriptors public override var prefixOptional: Boolean = super.prefixOptional protected set public override var context: CommandArgumentContext = super.context 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..b17cabdaf --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/SpaceSeparatedCommandCallParser.kt @@ -0,0 +1,27 @@ +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.extensions.CommandCallParserProviderImpl +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 by CommandCallParserProviderImpl(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..97774d55c --- /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.signature, + signature.zippedArguments.map { it.second }, + context ?: EmptyCommandArgumentContext) + } + + private data class ResolveData( + val signature: CommandSignature, + 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( + signature = signature, + zippedArguments = emptyList(), + argumentAcceptances = emptyList(), + remainingParameters = remainingParameters, + ) + } else { + if (valueArguments.size > valueParameters.size && zipped.last().first.isVararg) { + // merge vararg arguments + val (varargParameter, _) + = 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( + signature = 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.signature.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..5b48c74b9 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/ResolvedCommandCall.kt @@ -0,0 +1,88 @@ +/* + * 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.descriptor.CommandValueArgumentParser.Companion.parse +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 [CommandSignature], specifically a sub command from [CompositeCommand] + */ + public val calleeSignature: CommandSignature + + /** + * Original arguments + */ + public val rawValueArguments: List<CommandValueArgument> + + /** + * Resolved value arguments arranged mapping the [CommandSignature.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: CommandSignature, + 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/compiler/common/ResolveContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/ResolveContext.kt index a10aa00d8..350e20ada 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/ResolveContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/ResolveContext.kt @@ -11,22 +11,21 @@ package net.mamoe.mirai.console.compiler.common -import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.console.command.Command +import net.mamoe.mirai.console.data.PluginData +import net.mamoe.mirai.console.data.value +import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.plugin.description.PluginDescription +import net.mamoe.mirai.console.util.SemVersion import kotlin.annotation.AnnotationTarget.* /** * 标记一个参数的语境类型, 用于帮助编译器和 IntelliJ 插件进行语境推断. */ -@ConsoleExperimentalApi -@Target( - VALUE_PARAMETER, - PROPERTY, FIELD, - FUNCTION, - TYPE, TYPE_PARAMETER -) +@Target(VALUE_PARAMETER, PROPERTY, FIELD, FUNCTION, TYPE, TYPE_PARAMETER) @Retention(AnnotationRetention.BINARY) public annotation class ResolveContext( - val kind: Kind, + vararg val kinds: Kind, ) { /** * 元素数量可能在任意时间被改动 @@ -36,18 +35,57 @@ public annotation class ResolveContext( // ConstantKind /////////////////////////////////////////////////////////////////////////// - PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION - PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION - PLUGIN_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION + /* + * WARNING: IF YOU CHANGE NAMES HERE, + * YOU SHOULD ALSO CHANGE THEIR COUNTERPARTS AT net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind + */ + /** + * @see PluginDescription.id + */ + PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION + + /** + * @see PluginDescription.name + */ + PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION + + /** + * @see PluginDescription.version + * @see SemVersion.Companion.invoke + */ + SEMANTIC_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION + + /** + * @see SemVersion.Companion.parseRangeRequirement + */ VERSION_REQUIREMENT, // ILLEGAL_VERSION_REQUIREMENT // TODO + /** + * @see Command.allNames + */ COMMAND_NAME, // ILLEGAL_COMMAND_NAME - PERMISSION_NAMESPACE, // ILLEGAL_COMMAND_NAMESPACE - PERMISSION_NAME, // ILLEGAL_COMMAND_NAME - PERMISSION_ID, // ILLEGAL_COMMAND_ID + /** + * @see PermissionId.name + */ + PERMISSION_NAMESPACE, // ILLEGAL_PERMISSION_NAMESPACE + /** + * @see PermissionId.name + */ + PERMISSION_NAME, // ILLEGAL_PERMISSION_NAME + + /** + * @see PermissionId.parseFromString + */ + PERMISSION_ID, // ILLEGAL_PERMISSION_ID + + /** + * 标注一个泛型, 要求这个泛型必须拥有一个公开无参 (或所有参数都可选) 构造器. + * + * @see PluginData.value + */ RESTRICTED_NO_ARG_CONSTRUCTOR, // NOT_CONSTRUCTABLE_TYPE } } \ 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/data/ValueDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/ValueDescription.kt index f014df959..907944766 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/ValueDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/ValueDescription.kt @@ -9,6 +9,8 @@ package net.mamoe.mirai.console.data +import kotlinx.serialization.SerialInfo + /** * 序列化之后的注释. * @@ -30,6 +32,7 @@ package net.mamoe.mirai.console.data * a: b * ``` */ +@SerialInfo @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) public annotation class ValueDescription(val value: String) \ No newline at end of file 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..b5580db83 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 @@ -7,24 +7,72 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@file:Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "MemberVisibilityCanBePrivate") package net.mamoe.mirai.console.extension +import net.mamoe.mirai.console.extensions.SingletonExtensionSelector +import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage +import net.mamoe.mirai.console.util.ConsoleExperimentalApi import kotlin.reflect.KClass + /** - * 由 [Extension] 的 `companion` 实现. + * 由 [Extension] 的伴生对象实现. + * + * @see AbstractExtensionPoint */ public interface ExtensionPoint<T : Extension> { + /** + * 扩展实例 [T] 的类型 + */ public val extensionType: KClass<T> } -public open class AbstractExtensionPoint<T : Extension>( +public abstract class AbstractExtensionPoint<T : Extension>( public override val extensionType: KClass<T>, ) : ExtensionPoint<T> + /** * 表示一个 [SingletonExtension] 的 [ExtensionPoint] */ -public interface SingletonExtensionPoint<T : SingletonExtension<*>> : ExtensionPoint<T> \ No newline at end of file +public interface SingletonExtensionPoint<T : SingletonExtension<*>> : ExtensionPoint<T> + +/** + * 表示一个 [InstanceExtension] 的 [ExtensionPoint] + */ +public interface InstanceExtensionPoint<T : InstanceExtension<*>> : ExtensionPoint<T> + +/** + * 表示一个 [FunctionExtension] 的 [ExtensionPoint] + */ +public interface FunctionExtensionPoint<T : FunctionExtension> : ExtensionPoint<T> + + +public abstract class AbstractInstanceExtensionPoint<E : InstanceExtension<T>, T>( + extensionType: KClass<E>, + /** + * 内建的实现列表. + */ + @ConsoleExperimentalApi + public vararg val builtinImplementations: E, +) : AbstractExtensionPoint<E>(extensionType) + +public abstract class AbstractSingletonExtensionPoint<E : SingletonExtension<T>, T>( + extensionType: KClass<E>, + /** + * 内建的实现. + */ + @ConsoleExperimentalApi + public val builtinImplementation: T, +) : AbstractExtensionPoint<E>(extensionType), SingletonExtensionPoint<E> { + + /** + * 由 [SingletonExtensionSelector] 选择后的实例. + */ + @ConsoleExperimentalApi + public val selectedInstance: T by lazy { + GlobalComponentStorage.run { this@AbstractSingletonExtensionPoint.findSingletonInstance(extensionType, builtinImplementation) } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/PluginComponentStorage.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/PluginComponentStorage.kt index 2a6a8a63f..402b075df 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/PluginComponentStorage.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/PluginComponentStorage.kt @@ -9,6 +9,8 @@ package net.mamoe.mirai.console.extension +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.parse.CommandCallParser import net.mamoe.mirai.console.extensions.* import net.mamoe.mirai.console.internal.extension.AbstractConcurrentComponentStorage import net.mamoe.mirai.console.permission.PermissionService @@ -35,7 +37,7 @@ public class PluginComponentStorage( ): Unit = contribute(extensionPoint, plugin, lazyInstance) /** - * 注册一个扩展 + * 注册一个扩展. [E] 必须拥有伴生对象为 [ExtensionPoint]. */ public inline fun <reified E : Extension> contribute( noinline lazyInstance: () -> E, @@ -56,6 +58,7 @@ public class PluginComponentStorage( public fun contributeSingletonExtensionSelector(lazyInstance: () -> SingletonExtensionSelector): Unit = contribute(SingletonExtensionSelector, plugin, lazyInstance) + @Suppress("SpellCheckingInspection") // alterer /** 注册一个 [BotConfigurationAlterer] */ public fun contributeBotConfigurationAlterer(instance: BotConfigurationAlterer): Unit = contribute(BotConfigurationAlterer, plugin, lazyInstance = { instance }) @@ -73,16 +76,14 @@ public class PluginComponentStorage( /** 注册一个 [PermissionServiceProvider] */ @OverloadResolutionByLambdaReturnType - public fun contributePermissionService( - lazyInstance: () -> PermissionService<*>, - ): Unit = contribute(PermissionServiceProvider, plugin, LazyPermissionServiceProviderImpl(lazyInstance)) + public fun contributePermissionService(lazyInstance: () -> PermissionService<*>): Unit = + contribute(PermissionServiceProvider, plugin, LazyPermissionServiceProviderImpl(lazyInstance)) /** 注册一个 [PermissionServiceProvider] */ @JvmName("contributePermissionServiceProvider") @OverloadResolutionByLambdaReturnType - public fun contributePermissionService( - lazyProvider: () -> PermissionServiceProvider, - ): Unit = contribute(PermissionServiceProvider, plugin, lazyProvider) + public fun contributePermissionService(lazyProvider: () -> PermissionServiceProvider): Unit = + contribute(PermissionServiceProvider, plugin, lazyProvider) ///////////////////////////////////// @@ -95,5 +96,20 @@ public class PluginComponentStorage( @JvmName("contributePluginLoaderProvider") @OverloadResolutionByLambdaReturnType public fun contributePluginLoader(lazyProvider: () -> PluginLoaderProvider): Unit = - contribute(PluginLoaderProvider, plugin, lazyProvider) + contribute(PluginLoaderProvider, plugin, lazyProvider) // lazy for safety + + ///////////////////////////////////// + + /** 注册一个 [CommandCallParserProvider] */ + @ExperimentalCommandDescriptors + @OverloadResolutionByLambdaReturnType + public fun contributeCommandCallParser(lazyInstance: () -> CommandCallParser): Unit = + contribute(CommandCallParserProvider, plugin, LazyCommandCallParserProviderImpl(lazyInstance)) + + /** 注册一个 [CommandCallParserProvider] */ + @ExperimentalCommandDescriptors + @JvmName("contributeCommandCallParserProvider") + @OverloadResolutionByLambdaReturnType + public fun contributeCommandCallParser(provider: CommandCallParserProvider): Unit = + contribute(CommandCallParserProvider, plugin, provider) } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt index 45ce4f680..642d5ea9b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt @@ -21,6 +21,7 @@ import net.mamoe.mirai.utils.BotConfiguration * * @see MiraiConsole.addBot */ +@Suppress("SpellCheckingInspection") // alterer public fun interface BotConfigurationAlterer : FunctionExtension { /** 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..399d748dd --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallParserProvider.kt @@ -0,0 +1,35 @@ +/* + * 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.AbstractInstanceExtensionPoint +import net.mamoe.mirai.console.extension.InstanceExtension + +/** + * The provider of [CommandCallParser] + */ +@ExperimentalCommandDescriptors +public interface CommandCallParserProvider : InstanceExtension<CommandCallParser> { + public companion object ExtensionPoint : + AbstractInstanceExtensionPoint<CommandCallParserProvider, CommandCallParser>(CommandCallParserProvider::class, + SpaceSeparatedCommandCallParser.Provider) +} + + +@ExperimentalCommandDescriptors +public class CommandCallParserProviderImpl(override val instance: CommandCallParser) : CommandCallParserProvider + +@ExperimentalCommandDescriptors +public class LazyCommandCallParserProviderImpl(instanceCalculator: () -> CommandCallParser) : CommandCallParserProvider { + override val instance: CommandCallParser by lazy(instanceCalculator) +} \ 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..8be92c470 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/CommandCallResolverProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.AbstractInstanceExtensionPoint +import net.mamoe.mirai.console.extension.InstanceExtension + +@ExperimentalCommandDescriptors +public open class CommandCallResolverProvider(override val instance: CommandCallResolver) : InstanceExtension<CommandCallResolver> { + public companion object ExtensionPoint : + AbstractInstanceExtensionPoint<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/PermissionServiceProvider.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PermissionServiceProvider.kt index 0fa2b9f26..0415d1f8f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PermissionServiceProvider.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PermissionServiceProvider.kt @@ -9,9 +9,9 @@ package net.mamoe.mirai.console.extensions -import net.mamoe.mirai.console.extension.AbstractExtensionPoint +import net.mamoe.mirai.console.extension.AbstractSingletonExtensionPoint import net.mamoe.mirai.console.extension.SingletonExtension -import net.mamoe.mirai.console.extension.SingletonExtensionPoint +import net.mamoe.mirai.console.internal.permission.BuiltInPermissionService import net.mamoe.mirai.console.permission.PermissionService /** @@ -21,8 +21,7 @@ import net.mamoe.mirai.console.permission.PermissionService */ public interface PermissionServiceProvider : SingletonExtension<PermissionService<*>> { public companion object ExtensionPoint : - AbstractExtensionPoint<PermissionServiceProvider>(PermissionServiceProvider::class), - SingletonExtensionPoint<PermissionServiceProvider> + AbstractSingletonExtensionPoint<PermissionServiceProvider, PermissionService<*>>(PermissionServiceProvider::class, BuiltInPermissionService) } /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt index 510a1119b..3b96f4ac6 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt @@ -12,12 +12,19 @@ package net.mamoe.mirai.console.extensions import net.mamoe.mirai.console.extension.AbstractExtensionPoint import net.mamoe.mirai.console.extension.Extension import net.mamoe.mirai.console.extension.InstanceExtension +import net.mamoe.mirai.console.extension.PluginComponentStorage import net.mamoe.mirai.console.plugin.loader.PluginLoader /** * 提供扩展 [PluginLoader] * + * @see PluginComponentStorage.contributePluginLoader + * + * * @see Extension + * @see PluginLoader + * + * @see LazyPluginLoaderProviderImpl */ public interface PluginLoaderProvider : InstanceExtension<PluginLoader<*, *>> { public companion object ExtensionPoint : AbstractExtensionPoint<PluginLoaderProvider>(PluginLoaderProvider::class) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PostStartupExtension.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PostStartupExtension.kt index ee8a97f45..f18185e17 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PostStartupExtension.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PostStartupExtension.kt @@ -9,8 +9,11 @@ package net.mamoe.mirai.console.extensions +import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.extension.AbstractExtensionPoint +import net.mamoe.mirai.console.extension.ExtensionException import net.mamoe.mirai.console.extension.FunctionExtension +import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge /** * 在 Console 启动完成后立即在主线程调用的扩展. 用于进行一些必要的延迟初始化. @@ -20,7 +23,13 @@ import net.mamoe.mirai.console.extension.FunctionExtension public fun interface PostStartupExtension : FunctionExtension { /** * 将在 Console 主线程执行. + * + * @throws Exception 所有抛出的 [Exception] 都会被捕获并包装为 [ExtensionException] 抛出, 并停止 [MiraiConsole] + * + * #### 内部实现细节 + * 在 [MiraiConsoleImplementationBridge.doStart] 所有 [MiraiConsoleImplementationBridge.phase] 执行完成后顺序调用. */ + @Throws(Exception::class) public operator fun invoke() public companion object ExtensionPoint : AbstractExtensionPoint<PostStartupExtension>(PostStartupExtension::class) 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/MiraiConsoleImplementationBridge.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt index b414d596d..90024c888 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt @@ -41,7 +41,7 @@ import net.mamoe.mirai.console.internal.util.autoHexToBytes import net.mamoe.mirai.console.logging.* import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger import net.mamoe.mirai.console.permission.PermissionService -import net.mamoe.mirai.console.permission.PermissionService.Companion.grantPermission +import net.mamoe.mirai.console.permission.PermissionService.Companion.permit import net.mamoe.mirai.console.permission.RootPermission import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.center.PluginCenter @@ -177,9 +177,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI phase `load PermissionService`@{ mainLogger.verbose { "Loading PermissionService..." } - PermissionService.instanceField = GlobalComponentStorage.run { - PermissionServiceProvider.findSingletonInstance(BuiltInPermissionService) - } + PermissionServiceProvider.selectedInstance // init PermissionService.INSTANCE.let { ps -> if (ps is BuiltInPermissionService) { @@ -188,7 +186,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI } } - ConsoleCommandSender.grantPermission(RootPermission) + ConsoleCommandSender.permit(RootPermission) } phase `prepare commands`@{ @@ -231,7 +229,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI } GlobalComponentStorage.run { - PostStartupExtension.useExtensions { it() } + PostStartupExtension.useExtensions { it() } // exceptions thrown will be caught by caller of `doStart`. } mainLogger.info { "mirai-console started successfully." } 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..a85a7a8f8 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandReflector.kt @@ -0,0 +1,271 @@ +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.KType +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<CommandSignatureFromKFunction>): 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(signatures: List<CommandSignatureFromKFunctionImpl>) { + + data class ErasedParameterInfo( + val index: Int, + val name: String?, + val type: KType, // ignore nullability + val additional: String?, + ) + + data class ErasedVariantInfo( + val receiver: ErasedParameterInfo?, + val valueParameters: List<ErasedParameterInfo>, + ) + + fun CommandParameter<*>.toErasedParameterInfo(index: Int): ErasedParameterInfo { + return ErasedParameterInfo(index, + this.name, + this.type.withNullability(false), + if (this is AbstractCommandValueParameter.StringConstant) this.expectingValue else null) + } + + val candidates = signatures.map { variant -> + variant to ErasedVariantInfo( + variant.receiverParameter?.toErasedParameterInfo(0), + variant.valueParameters.mapIndexed { index, parameter -> parameter.toErasedParameterInfo(index) } + ) + } + + val groups = candidates.groupBy { it.second } + + val clashes = groups.entries.find { (_, value) -> + value.size > 1 + } ?: return + + throw CommandDeclarationClashException(command, clashes.value.map { it.first }) + } + + @Throws(IllegalCommandDeclarationException::class) + fun findSubCommands(): List<CommandSignatureFromKFunctionImpl> { + 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() } + + CommandSignatureFromKFunctionImpl( + 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 deleted file mode 100644 index 8506d6a4a..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt +++ /dev/null @@ -1,58 +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("unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") - -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 - -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 - ) -} - -/** - * 指令形式参数. - * @see toCommandParam - */ -internal data class CommandParameter<T : Any>( - /** - * 参数名. 不允许重复. - */ - val name: String, - /** - * 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgumentParser] 解析. - */ - val type: KClass<T> // exact type -) { - constructor(name: String, type: KClass<T>, parser: CommandArgumentParser<T>) : this(name, type) { - this._overrideParser = parser - } - - @Suppress("PropertyName") - @JvmField - internal var _overrideParser: CommandArgumentParser<T>? = null - - - /** - * 覆盖的 [CommandArgumentParser]. - * - * 如果非 `null`, 将不会从 [CommandArgumentContext] 寻找 [CommandArgumentParser] - */ - val overrideParser: CommandArgumentParser<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/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt index ac83357ac..a1ebfbf2e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt @@ -18,13 +18,6 @@ import kotlin.math.max import kotlin.math.min -internal infix fun Array<String>.matchesBeginning(list: List<Any>): Boolean { - this.forEachIndexed { index, any -> - if (list[index] != any) return false - } - return true -} - internal infix fun Array<out String>.intersectsIgnoringCase(other: Array<out String>): Boolean { val max = this.size.coerceAtMost(other.size) for (i in 0 until max) { 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..e8226315d 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 @@ -74,7 +73,9 @@ internal open class MultiFilePluginDataStorageImpl( public override fun store(holder: PluginDataHolder, instance: PluginData) { getPluginDataFile(holder, instance).writeText( kotlin.runCatching { - yaml.encodeToString(instance.updaterSerializer, Unit) + yaml.encodeToString(instance.updaterSerializer, Unit).also { + yaml.decodeAnyFromString(it) // test yaml + } }.recoverCatching { // Just use mainLogger for convenience. MiraiConsole.mainLogger.warning( 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..803ccc8d4 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 AbstractInstanceExtensionPoint<*, *>) { + 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..33ce08713 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, "*"), "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..d26feac23 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 @@ -20,6 +20,7 @@ import net.mamoe.mirai.console.internal.data.mkdir import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginManager +import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.jvm.JvmPlugin @@ -60,18 +61,17 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol override val pluginLoaders: List<PluginLoader<*, *>> get() = _pluginLoaders.toList() - override val Plugin.description: PluginDescription - get() = if (this is JvmPlugin) { - this.safeLoader.getPluginDescription(this) - } else resolvedPlugins.firstOrNull { it == this } - ?.loader?.cast<PluginLoader<Plugin, PluginDescription>>() - ?.getPluginDescription(this) - ?: error("Plugin is unloaded") + override fun getPluginDescription(plugin: Plugin): PluginDescription = if (plugin is JvmPlugin) { + plugin.safeLoader.getPluginDescription(plugin) + } else resolvedPlugins.firstOrNull { it == plugin } + ?.loader?.cast<PluginLoader<Plugin, PluginDescription>>() + ?.getPluginDescription(plugin) + ?: error("Plugin is unloaded") init { MiraiConsole.coroutineContext[Job]!!.invokeOnCompletion { - plugins.forEach { it.disable() } + plugins.forEach { disablePlugin(it) } } } @@ -98,10 +98,10 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol this.enable(plugin as P) }.fold( onSuccess = { - logger.info { "Successfully enabled plugin ${plugin.description.name}" } + logger.info { "Successfully enabled plugin ${getPluginDescription(plugin).name}" } }, onFailure = { - logger.info { "Cannot enable plugin ${plugin.description.name}" } + logger.info { "Cannot enable plugin ${getPluginDescription(plugin).name}" } throw it } ) @@ -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++ } @@ -166,7 +166,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol } internal fun enableAllLoadedPlugins() { - resolvedPlugins.forEach { it.enable() } + resolvedPlugins.forEach { enablePlugin(it) } } @kotlin.jvm.Throws(PluginLoadException::class) @@ -180,7 +180,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol private fun List<PluginLoader<*, *>>.listAndSortAllPlugins(): List<PluginDescriptionWithLoader> { return flatMap { loader -> - loader.listPlugins().map { plugin -> plugin.description.wrapWith(loader, plugin) } + loader.listPlugins().map { plugin -> getPluginDescription(plugin).wrapWith(loader, plugin) } }.sortByDependencies() } 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..b86f97612 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 @@ -17,8 +17,8 @@ import net.mamoe.mirai.console.extensions.PermissionServiceProvider import net.mamoe.mirai.console.internal.permission.checkType import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf import net.mamoe.mirai.console.plugin.Plugin -import net.mamoe.mirai.console.plugin.description -import net.mamoe.mirai.console.plugin.name +import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description +import net.mamoe.mirai.console.util.ConsoleExperimentalApi import kotlin.reflect.KClass /** @@ -27,7 +27,7 @@ import kotlin.reflect.KClass * ### 可扩展 * 权限服务可由插件扩展并覆盖默认实现. * - * [PermissionServiceProvider] + * @see PermissionServiceProvider 相应扩展 */ @PermissionImplementation public interface PermissionService<P : Permission> { @@ -50,11 +50,15 @@ public interface PermissionService<P : Permission> { /** * 获取所有已注册的指令列表. 应保证线程安全. + * + * 备注: Java 实现者使用 `CollectionsKt.asSequence(Collection)` 构造 [Sequence] */ public fun getRegisteredPermissions(): Sequence<P> /** * 获取 [PermitteeId] 和其父标识的所有被授予的所有直接和间接的权限列表 + * + * 备注: Java 实现者使用 `CollectionsKt.asSequence(Collection)` 构造 [Sequence] */ public fun getPermittedPermissions(permitteeId: PermitteeId): Sequence<P> @@ -83,7 +87,12 @@ public interface PermissionService<P : Permission> { * * @throws PermissionRegistryConflictException 当已存在一个 [PermissionId] 时抛出. * + * @param description 描述. 将会展示给用户. + * * @return 申请到的 [Permission] 实例 + * + * @see get 获取一个已注册的权限 + * @see getOrFail 获取一个已注册的权限 */ @Throws(PermissionRegistryConflictException::class) public fun register( @@ -93,11 +102,14 @@ public interface PermissionService<P : Permission> { ): P /** 为 [Plugin] 分配一个 [PermissionId] */ + @ConsoleExperimentalApi public fun allocatePermissionIdForPlugin( plugin: Plugin, @ResolveContext(COMMAND_NAME) permissionName: String, - reason: PluginPermissionIdRequestType - ): PermissionId = allocatePermissionIdForPluginDefaultImplement(plugin, permissionName, reason) + ): PermissionId = PermissionId( + plugin.description.id.toLowerCase(), + permissionName.toLowerCase() + ) /////////////////////////////////////////////////////////////////////////// @@ -126,102 +138,184 @@ public interface PermissionService<P : Permission> { @Throws(UnsupportedOperationException::class) public fun cancel(permitteeId: PermitteeId, permission: P, recursive: Boolean) - /** [Plugin] 尝试分配的 [PermissionId] 来源 */ - public enum class PluginPermissionIdRequestType { - /** For [Plugin.parentPermission] */ - ROOT_PERMISSION, - - /** For [Plugin.permissionId] */ - PERMISSION_ID - } - public companion object { - internal var instanceField: PermissionService<*>? = null - + /** + * [PermissionService] 实例 + * + * @see PermissionServiceProvider.selectedInstance + */ @get:JvmName("getInstance") @JvmStatic public val INSTANCE: PermissionService<out Permission> - get() = instanceField ?: error("PermissionService is not yet initialized therefore cannot be used.") + get() = PermissionServiceProvider.selectedInstance /** * 获取一个权限, 失败时抛出 [NoSuchElementException] + * + * @see register 申请并注册一个权限 */ + @JvmStatic @Throws(NoSuchElementException::class) public fun <P : Permission> PermissionService<P>.getOrFail(id: PermissionId): P = get(id) ?: throw NoSuchElementException("Permission not found: $id") - internal fun PermissionService<*>.allocatePermissionIdForPluginDefaultImplement( - plugin: Plugin, - @ResolveContext(COMMAND_NAME) permissionName: String, - reason: PluginPermissionIdRequestType - ) = PermissionId( - plugin.description.id.toLowerCase(), - permissionName.toLowerCase() - ) + /** + * @see findCorrespondingPermission + */ + @JvmStatic + public val PermissionId.correspondingPermission: Permission? + get() = findCorrespondingPermission() + /** + * @see get + */ + @JvmStatic public fun PermissionId.findCorrespondingPermission(): Permission? = INSTANCE[this] + /** + * @see getOrFail + * @throws NoSuchElementException + */ + @Throws(NoSuchElementException::class) + @JvmStatic public fun PermissionId.findCorrespondingPermissionOrFail(): Permission = INSTANCE.getOrFail(this) - public fun PermitteeId.grantPermission(permission: Permission) { + /** + * @see PermissionService.permit + */ + @JvmStatic + @JvmName("permit0") // clash, not JvmSynthetic to allow possible calls from Java. + public fun PermitteeId.permit(permission: Permission) { INSTANCE.checkType(permission::class).permit(this, permission) } - public fun PermitteeId.grantPermission(permissionId: PermissionId) { - grantPermission(permissionId.findCorrespondingPermissionOrFail()) + /** + * @see PermissionService.permit + * @throws NoSuchElementException + */ + @JvmStatic + @Throws(NoSuchElementException::class) + public fun PermitteeId.permit(permissionId: PermissionId) { + permit(permissionId.findCorrespondingPermissionOrFail()) } - public fun PermitteeId.denyPermission(permission: Permission, recursive: Boolean) { + /** + * @see PermissionService.cancel + */ + @JvmSynthetic + @JvmStatic + @JvmName("cancel0") // clash, not JvmSynthetic to allow possible calls from Java. + public fun PermitteeId.cancel(permission: Permission, recursive: Boolean) { INSTANCE.checkType(permission::class).cancel(this, permission, recursive) } - public fun PermitteeId.denyPermission(permissionId: PermissionId, recursive: Boolean) { - denyPermission(permissionId.findCorrespondingPermissionOrFail(), recursive) + /** + * @see PermissionService.cancel + * @throws NoSuchElementException + */ + @JvmStatic + @Throws(NoSuchElementException::class) + public fun PermitteeId.cancel(permissionId: PermissionId, recursive: Boolean) { + cancel(permissionId.findCorrespondingPermissionOrFail(), recursive) } + /** + * @see PermissionService.testPermission + */ + @JvmStatic public fun Permittee.hasPermission(permission: Permission): Boolean = permission.testPermission(this@hasPermission) + /** + * @see PermissionService.testPermission + */ + @JvmStatic public fun PermitteeId.hasPermission(permission: Permission): Boolean = permission.testPermission(this@hasPermission) + /** + * @see PermissionService.testPermission + * @throws NoSuchElementException + */ + @JvmStatic + @Throws(NoSuchElementException::class) public fun PermitteeId.hasPermission(permissionId: PermissionId): Boolean { val instance = permissionId.findCorrespondingPermissionOrFail() return INSTANCE.checkType(instance::class).testPermission(this@hasPermission, instance) } + /** + * @see PermissionService.testPermission + */ + @JvmStatic public fun Permittee.hasPermission(permissionId: PermissionId): Boolean = permissionId.testPermission(this@hasPermission) + + /** + * @see PermissionService.getPermittedPermissions + */ + @JvmStatic public fun Permittee.getPermittedPermissions(): Sequence<Permission> = INSTANCE.getPermittedPermissions(this@getPermittedPermissions.permitteeId) - public fun Permittee.grantPermission(vararg permissions: Permission) { + + /** + * @see PermissionService.permit + */ + @JvmStatic + public fun Permittee.permit(vararg permissions: Permission) { for (permission in permissions) { INSTANCE.checkType(permission::class).permit(this.permitteeId, permission) } } - public fun Permittee.denyPermission(vararg permissions: Permission, recursive: Boolean) { + /** + * @see PermissionService.cancel + */ + @JvmStatic + public fun Permittee.cancel(vararg permissions: Permission, recursive: Boolean) { for (permission in permissions) { INSTANCE.checkType(permission::class).cancel(this.permitteeId, permission, recursive) } } + /** + * @see PermissionService.getPermittedPermissions + */ + @JvmSynthetic + @JvmStatic + @JvmName("getPermittedPermissions0") // clash, not JvmSynthetic to allow possible calls from Java. public fun PermitteeId.getPermittedPermissions(): Sequence<Permission> = INSTANCE.getPermittedPermissions(this@getPermittedPermissions) + /** + * @see PermissionService.testPermission + */ + @JvmStatic public fun Permission.testPermission(permittee: Permittee): Boolean = INSTANCE.checkType(this::class).testPermission(permittee.permitteeId, this@testPermission) + /** + * @see PermissionService.testPermission + */ + @JvmStatic public fun Permission.testPermission(permitteeId: PermitteeId): Boolean = INSTANCE.checkType(this::class).testPermission(permitteeId, this@testPermission) + /** + * @see PermissionService.testPermission + */ + @JvmStatic public fun PermissionId.testPermission(permittee: Permittee): Boolean { val p = INSTANCE[this] ?: return false return p.testPermission(permittee) } + /** + * @see PermissionService.testPermission + */ + @JvmStatic public fun PermissionId.testPermission(permissible: PermitteeId): Boolean { val p = INSTANCE[this] ?: return false return p.testPermission(permissible) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt index 8db84c890..9fd35e08b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt @@ -12,23 +12,22 @@ package net.mamoe.mirai.console.plugin import net.mamoe.mirai.console.command.CommandOwner -import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable -import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable -import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader +import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.getPluginDescription import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.util.SemVersion +import kotlin.DeprecationLevel.ERROR /** * 表示一个 mirai-console 插件. * - * @see PluginManager.enable 启用一个插件 - * @see PluginManager.disable 禁用一个插件 + * @see PluginManager.enablePlugin 启用一个插件 + * @see PluginManager.disablePlugin 禁用一个插件 * @see PluginManager.description 获取一个插件的 [描述][PluginDescription] * - * @see PluginDescription 插件描述, 需由 [PluginLoader] 帮助提供([PluginLoader.description]) + * @see PluginDescription 插件描述, 需由 [PluginLoader] 帮助提供([PluginLoader.getPluginDescription]) * @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件 * @see PluginFileExtensions 支持文件系统存储的扩展 * @@ -38,8 +37,8 @@ public interface Plugin : CommandOwner { /** * 判断此插件是否已启用 * - * @see PluginManager.enable 启用一个插件 - * @see PluginManager.disable 禁用一个插件 + * @see PluginManager.enablePlugin 启用一个插件 + * @see PluginManager.disablePlugin 禁用一个插件 */ public val isEnabled: Boolean @@ -49,32 +48,42 @@ public interface Plugin : CommandOwner { public val loader: PluginLoader<*, *> } -/** - * 获取 [PluginDescription] - */ -public inline val Plugin.description: PluginDescription get() = this.safeLoader.getPluginDescription(this) +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@kotlin.internal.LowPriorityInOverloadResolution +@Deprecated( + "Moved to companion for a better Java API. ", + ReplaceWith("this.description", "net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description"), + level = ERROR +) +public inline val Plugin.description: PluginDescription + get() = getPluginDescription(this) // resolved to net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.getDescription /** - * 获取 [PluginDescription.name`] + * 获取 [PluginDescription.name] */ -public inline val Plugin.name: String get() = this.description.name +public inline val Plugin.name: String get() = getPluginDescription(this).name + +/** + * 获取 [PluginDescription.id] + */ +public inline val Plugin.id: String get() = getPluginDescription(this).id /** * 获取 [PluginDescription.version] */ -public inline val Plugin.version: SemVersion get() = this.description.version +public inline val Plugin.version: SemVersion get() = getPluginDescription(this).version /** * 获取 [PluginDescription.info] */ -public inline val Plugin.info: String get() = this.description.info +public inline val Plugin.info: String get() = getPluginDescription(this).info /** * 获取 [PluginDescription.author] */ -public inline val Plugin.author: String get() = this.description.author +public inline val Plugin.author: String get() = getPluginDescription(this).author /** * 获取 [PluginDescription.dependencies] */ -public inline val Plugin.dependencies: Set<PluginDependency> get() = this.description.dependencies +public inline val Plugin.dependencies: Set<PluginDependency> get() = getPluginDescription(this).dependencies diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt index 309f5acc1..e5ad8140f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt @@ -105,45 +105,52 @@ public interface PluginManager { /** * 获取插件的 [描述][PluginDescription], 通过 [PluginLoader.getPluginDescription] */ - public val Plugin.description: PluginDescription + public fun getPluginDescription(plugin: Plugin): PluginDescription /** * 禁用这个插件 * * @see PluginLoader.disable */ - public fun Plugin.disable(): Unit = safeLoader.disable(this) + public fun disablePlugin(plugin: Plugin): Unit = plugin.safeLoader.disable(plugin) /** * 加载这个插件 * * @see PluginLoader.load */ - public fun Plugin.load(): Unit = safeLoader.load(this) + public fun loadPlugin(plugin: Plugin): Unit = plugin.safeLoader.load(plugin) /** * 启用这个插件 * * @see PluginLoader.enable */ - public fun Plugin.enable(): Unit = safeLoader.enable(this) - - /** - * 经过泛型类型转换的 [Plugin.loader] - */ - @get:JvmSynthetic - @Suppress("UNCHECKED_CAST") - public val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription> - get() = this.loader as PluginLoader<P, PluginDescription> + public fun enablePlugin(plugin: Plugin): Unit = plugin.safeLoader.enable(plugin) // endregion public companion object INSTANCE : PluginManager by PluginManagerImpl { - // due to Kotlin's bug - public override val Plugin.description: PluginDescription get() = PluginManagerImpl.run { description } - public override fun Plugin.disable(): Unit = PluginManagerImpl.run { disable() } - public override fun Plugin.enable(): Unit = PluginManagerImpl.run { enable() } - public override fun Plugin.load(): Unit = PluginManagerImpl.run { load() } - public override val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription> get() = PluginManagerImpl.run { safeLoader } + /** + * 经过泛型类型转换的 [Plugin.loader] + */ + @get:JvmSynthetic + @Suppress("UNCHECKED_CAST") + public inline val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription> + get() = this.loader as PluginLoader<P, PluginDescription> + + + @get:JvmSynthetic + public inline val Plugin.description: PluginDescription + get() = getPluginDescription(this) + + @JvmSynthetic + public inline fun Plugin.disable(): Unit = disablePlugin(this) + + @JvmSynthetic + public inline fun Plugin.enable(): Unit = enablePlugin(this) + + @JvmSynthetic + public inline fun Plugin.load(): Unit = loadPlugin(this) } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt index ca9267495..7c84a2807 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt @@ -92,7 +92,7 @@ public interface PluginDescription { * * @see Semver 语义化版本. 允许 [宽松][Semver.SemverType.LOOSE] 类型版本. */ - @ResolveContext(PLUGIN_VERSION) + @ResolveContext(SEMANTIC_VERSION) public val version: SemVersion /** 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..1f19786fa 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) /** * 重载 [PluginData] diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt index edf5d99af..b147e8873 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt @@ -43,7 +43,7 @@ public interface JvmPluginDescription : PluginDescription { /** * @see [PluginDescription.version] */ - @ResolveContext(PLUGIN_VERSION) version: String, + @ResolveContext(SEMANTIC_VERSION) version: String, /** * @see [PluginDescription.name] */ @@ -102,7 +102,7 @@ public class JvmPluginDescriptionBuilder( ) { public constructor( @ResolveContext(PLUGIN_ID) id: String, - @ResolveContext(PLUGIN_VERSION) version: String, + @ResolveContext(SEMANTIC_VERSION) version: String, ) : this(id, SemVersion(version)) private var name: String = id @@ -115,7 +115,7 @@ public class JvmPluginDescriptionBuilder( apply { this.name = value.trim() } @ILoveKuriyamaMiraiForever - public fun version(@ResolveContext(PLUGIN_VERSION) value: String): JvmPluginDescriptionBuilder = + public fun version(@ResolveContext(SEMANTIC_VERSION) value: String): JvmPluginDescriptionBuilder = apply { this.version = SemVersion(value) } @ILoveKuriyamaMiraiForever diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt index 280ce144b..208f4e743 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt @@ -14,8 +14,7 @@ package net.mamoe.mirai.console.plugin.loader import net.mamoe.mirai.console.extensions.PluginLoaderProvider import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginManager -import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable -import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable +import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enablePlugin import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader @@ -38,6 +37,7 @@ import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader * 直接实现接口 [PluginLoader] 或 [FilePluginLoader], 并注册 [PluginLoaderProvider] * * @see JvmPluginLoader Jar 插件加载器 + * @see PluginLoaderProvider 扩展 */ public interface PluginLoader<P : Plugin, D : PluginDescription> { /** @@ -66,9 +66,9 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> { public fun getPluginDescription(plugin: P): D /** - * 主动加载一个插件 (实例), 但不 [启用][enable] 它. 返回加载成功的主类实例 + * 主动加载一个插件 (实例), 但不 [启用][enablePlugin] 它. 返回加载成功的主类实例 * - * **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] 或 [enable], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException]. + * **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] 或 [enablePlugin], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException]. * * **实现细节**: 此函数只允许抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误. * 当异常发生时, 插件将会直接被放弃加载, 并影响依赖它的其他插件. @@ -82,7 +82,7 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> { /** * 主动启用这个插件. * - * **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] 或 [enable], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException]. + * **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] 或 [enablePlugin], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException]. * * **实现细节**: 此函数可抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误. * 当异常发生时, 插件将会直接被放弃加载, 并影响依赖它的其他插件. @@ -90,7 +90,7 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> { * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等). * @throws IllegalStateException 在插件已经被加载时抛出. 这属于意料之外的情况. * - * @see PluginManager.enable + * @see PluginManager.enablePlugin */ @Throws(IllegalStateException::class, PluginLoadException::class) public fun enable(plugin: P) @@ -103,7 +103,7 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> { * * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等). * - * @see PluginManager.disable + * @see PluginManager.disablePlugin */ @Throws(IllegalStateException::class, PluginLoadException::class) public fun disable(plugin: P) 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/SemVersion.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/SemVersion.kt index 89519e813..779b97e5d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/SemVersion.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/SemVersion.kt @@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.builtins.serializer import net.mamoe.mirai.console.compiler.common.ResolveContext -import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_VERSION +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.SEMANTIC_VERSION import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.VERSION_REQUIREMENT import net.mamoe.mirai.console.internal.data.map import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal @@ -47,10 +47,10 @@ import kotlin.LazyThreadSafetyMode.PUBLICATION * ``` * 其中 identifier 和 metadata 都是可选的. * - * 对于核心版本号, 此实现稍微比 semver 宽松一些, 允许 x.y 的存在. + * 对于核心版本号, 此实现稍微比语义化版本规范宽松一些, 允许 x.y 的存在. * - * @see Requirement - * @see SemVersion.invoke + * @see Requirement 版本号要修 + * @see SemVersion.invoke 由字符串解析 */ @Serializable(with = SemVersion.SemVersionAsStringSerializer::class) public data class SemVersion @@ -69,6 +69,15 @@ internal constructor( /** 版本号元数据, 不参与版本号对比([compareTo]), 但是参与版本号严格对比([equals]) */ public val metadata: String? = null, ) : Comparable<SemVersion> { + + init { + require(major >= 0) { "major must >= 0" } + require(minor >= 0) { "minor must >= 0" } + if (patch != null) require(patch >= 0) { "patch must >= 0" } + if (identifier != null) require(identifier.none(Char::isWhitespace)) { "identifier must not contain whitespace" } + if (metadata != null) require(metadata.none(Char::isWhitespace)) { "metadata must not contain whitespace" } + } + /** * 一条依赖规则 * @see [parseRangeRequirement] @@ -103,10 +112,10 @@ internal constructor( * - 如果不确定版本号是否合法, 可以使用 [regex101.com](https://regex101.com/r/vkijKf/1/) 进行检查 * - 此实现使用的正则表达式为 `^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` */ - @Throws(IllegalArgumentException::class, NumberFormatException::class) @JvmStatic @JvmName("parse") - public operator fun invoke(@ResolveContext(PLUGIN_VERSION) version: String): SemVersion = SemVersionInternal.parse(version) + @Throws(IllegalArgumentException::class, NumberFormatException::class) + public operator fun invoke(@ResolveContext(SEMANTIC_VERSION) version: String): SemVersion = SemVersionInternal.parse(version) /** * 解析一条依赖需求描述, 在无法解析的时候抛出 [IllegalArgumentException] @@ -138,14 +147,15 @@ internal constructor( * - 如果目标版本号携带有先行版本号, 请不要忘记先行版本号 * - 因为 `()` 已经用于数学区间, 使用 `{}` 替代 `()` */ - @Throws(IllegalArgumentException::class) @JvmStatic + @Throws(IllegalArgumentException::class) public fun parseRangeRequirement(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Requirement = SemVersionInternal.parseRangeRequirement(requirement) /** @see [Requirement.test] */ @JvmStatic - public fun Requirement.test(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(invoke(version)) + @Throws(IllegalArgumentException::class, NumberFormatException::class) + public fun Requirement.test(@ResolveContext(SEMANTIC_VERSION) version: String): Boolean = test(invoke(version)) /** * 当满足 [requirement] 时返回 true, 否则返回 false @@ -157,6 +167,7 @@ internal constructor( * 当满足 [requirement] 时返回 true, 否则返回 false */ @JvmStatic + @Throws(IllegalArgumentException::class) public fun SemVersion.satisfies(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Boolean = parseRangeRequirement(requirement).test(this) /** for Kotlin only */ @@ -167,7 +178,7 @@ internal constructor( /** for Kotlin only */ @JvmStatic @JvmSynthetic - public operator fun Requirement.contains(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(version) + public operator fun Requirement.contains(@ResolveContext(SEMANTIC_VERSION) version: String): Boolean = test(version) } @Transient @@ -185,7 +196,10 @@ internal constructor( } } - override fun toString(): String = toString + /** + * 返回类似 `1.0.0-M4+c25733b8` 的字符串. + */ + public override fun toString(): String = toString /** * 将 [SemVersion] 转为 Kotlin data class 风格的 [String] @@ -194,27 +208,60 @@ internal constructor( return "SemVersion(major=$major, minor=$minor, patch=$patch, identifier=$identifier, metadata=$metadata)" } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as SemVersion - - return compareTo(other) == 0 && other.identifier == identifier && other.metadata == metadata + /** + * 比较 `this` 和 [other]. + * + * @param deep 为 `true` 时进行深度比较, 相当于 [equals]. 为 `false` 时相当于 `compareTo(other) == 0` + * @see compareTo + */ + public fun equals(other: SemVersion, deep: Boolean): Boolean { + return if (deep) { + (other.major == major + && other.minor == minor + && other.patch == patch + && other.identifier == identifier + && other.metadata == metadata) + } else { + this.compareTo(other) == 0 + } } - override fun hashCode(): Int { - var result = major shl minor - result *= (patch ?: 1) + /** + * 深度比较 `this` 和 [other], 当且仅当 [major], [patch], [minor], [identifier], [metadata] 完全相同时返回 `true`. + * + * 如: `1.0.0-RC` != `1.0-RC` + * + * @see compareTo + */ + public override fun equals(other: Any?): Boolean { + if (other === null) return false + if (this === other) return true + if (javaClass != other.javaClass) return false + return equals(other as SemVersion, deep = true) + } + + public override fun hashCode(): Int { + var result = major.hashCode() + result = 31 * result + minor.hashCode() + result = 31 * result + (patch?.hashCode() ?: 0) result = 31 * result + (identifier?.hashCode() ?: 0) result = 31 * result + (metadata?.hashCode() ?: 0) return result } /** - * Compares this object with the specified object for order. Returns zero if this object is equal - * to the specified [other] object, a negative number if it's less than [other], or a positive number - * if it's greater than [other]. + * 比较 `this` 和 [other] 的实际版本大小. + * + * 如: + * - `SemVersion("1.0.0-RC").compareTo(SemVersion("1.0-RC")) == 0` (然而对他们进行 [equals] 判断会返回 `false`) + * - `SemVersion("1.3.0") > SemVersion("1.1.0") == true` (因为 1.3.0 比 1.1.0 更高) + * + * + * @return 当 `this` 比 [other] 更高时返回一个正数. + * 当 `this` 比 [other] 更低时返回一个负数. + * 当 `this` 与 [other] 版本大小相等时返回 0. + * + * @see equals */ public override operator fun compareTo(other: SemVersion): Int { return SemVersionInternal.run { compareInternal(this@SemVersion, other) } 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 ce6708193..ca770aeb5 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/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/util/TestSemVersion.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/util/TestSemVersion.kt index 3f74dd191..60881b310 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/util/TestSemVersion.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/util/TestSemVersion.kt @@ -22,7 +22,8 @@ internal class TestSemVersion { internal fun testCompare() { fun String.sem(): SemVersion = SemVersion.invoke(this) assert("1.0".sem() < "1.0.1".sem()) - assert("1.0.0".sem() == "1.0".sem()) + assert("1.0.0".sem() != "1.0".sem()) + assert("1.0.0".sem().compareTo("1.0".sem()) == 0) assert("1.1".sem() > "1.0.0".sem()) assert("1.0-M4".sem() < "1.0-M5".sem()) assert("1.0-M5-dev-7".sem() < "1.0-M5-dev-15".sem()) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 37a6ea152..ac6523639 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -8,8 +8,8 @@ */ object Versions { - const val core = "1.3.0" - const val console = "1.0-RC-dev-31" + const val core = "1.3.2" + const val console = "1.0-RC-dev-32" const val consoleGraphical = "0.0.7" const val consoleTerminal = console @@ -19,7 +19,7 @@ object Versions { const val coroutines = "1.3.9" const val collectionsImmutable = "0.3.2" const val serialization = "1.0.0-RC" - const val ktor = "1.4.0" + const val ktor = "1.4.1" const val atomicFU = "0.14.4" const val androidGradle = "3.6.2" 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 diff --git a/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveTypes.kt b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveTypes.kt index 59f356cc9..b09530ae4 100644 --- a/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveTypes.kt +++ b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveTypes.kt @@ -13,6 +13,7 @@ import net.mamoe.mirai.console.compiler.common.castOrNull import net.mamoe.mirai.console.compiler.common.firstValue import org.jetbrains.kotlin.descriptors.annotations.Annotated import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.constants.ArrayValue import org.jetbrains.kotlin.resolve.constants.EnumValue /////////////////////////////////////////////////////////////////////////// @@ -73,11 +74,14 @@ enum class ResolveContextKind { } } -fun Annotated.isResolveContext(kind: ResolveContextKind) = this.resolveContextKind == kind - -val Annotated.resolveContextKind: ResolveContextKind? +val Annotated.resolveContextKinds: List<ResolveContextKind>? get() { val ann = this.findAnnotation(RESOLVE_CONTEXT_FQ_NAME) ?: return null - val (_, enumEntryName) = ann.allValueArguments.firstValue().castOrNull<EnumValue>()?.value ?: return null // undetermined kind - return ResolveContextKind.valueOf(enumEntryName.asString()) + val kinds = + ann.allValueArguments.firstValue().castOrNull<ArrayValue>()?.value?.mapNotNull { it.castOrNull<EnumValue>()?.value } + ?: return null // undetermined kind + + return kinds.map { (_, enumEntryName) -> + ResolveContextKind.valueOf(enumEntryName.asString()) + } } \ No newline at end of file diff --git a/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/VersionConstants.kt b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/VersionConstants.kt index 6c293ee10..8dd24fdf1 100644 --- a/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/VersionConstants.kt +++ b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/VersionConstants.kt @@ -10,6 +10,6 @@ package net.mamoe.mirai.console.gradle internal object VersionConstants { - const val CONSOLE_VERSION = "1.0-RC-dev-30" // value is written here automatically during build - const val CORE_VERSION = "1.3.0" // value is written here automatically during build + const val CONSOLE_VERSION = "1.0-RC-dev-32" // value is written here automatically during build + const val CORE_VERSION = "1.3.2" // value is written here automatically during build } \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/ContextualParametersChecker.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/ContextualParametersChecker.kt index ecf455dc4..ab82110a8 100644 --- a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/ContextualParametersChecker.kt +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/ContextualParametersChecker.kt @@ -12,7 +12,7 @@ package net.mamoe.mirai.console.intellij.diagnostics import com.intellij.psi.PsiElement import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.* import net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind -import net.mamoe.mirai.console.compiler.common.resolve.resolveContextKind +import net.mamoe.mirai.console.compiler.common.resolve.resolveContextKinds import net.mamoe.mirai.console.intellij.resolve.resolveAllCalls import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValues import net.mamoe.mirai.console.intellij.resolve.valueParametersWithArguments @@ -134,12 +134,18 @@ class ContextualParametersChecker : DeclarationChecker { context: DeclarationCheckerContext, ) { declaration.resolveAllCalls(context.bindingContext) + .asSequence() .flatMap { call -> call.valueParametersWithArguments().asSequence() } .mapNotNull { (p, a) -> - p.resolveContextKind?.let(checkersMap::get)?.let { it to a } + p.resolveContextKinds + ?.map(checkersMap::get) + ?.mapNotNull { + if (it == null) null else it to a + } } + .flatMap { it.asSequence() } .mapNotNull { (kind, argument) -> argument.resolveStringConstantValues()?.let { const -> Triple(kind, argument, const) diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDataValuesChecker.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDataValuesChecker.kt index 4130b6ce3..862e29d89 100644 --- a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDataValuesChecker.kt +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDataValuesChecker.kt @@ -31,12 +31,12 @@ class PluginDataValuesChecker : DeclarationChecker { declaration.resolveAllCallsWithElement(bindingContext) .filter { (call) -> call.isCalling(PLUGIN_DATA_VALUE_FUNCTIONS_FQ_FQ_NAME) } .filter { (call) -> - call.resultingDescriptor.resolveContextKind == ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR + call.resultingDescriptor.resolveContextKinds?.contains(ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) == true }.flatMap { (call, element) -> call.typeArguments.entries.associateWith { element }.asSequence() }.filter { (e, _) -> val (p, t) = e - (p.isReified || p.resolveContextKind == ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) + (p.isReified || p.resolveContextKinds?.contains(ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) == true) && t is SimpleType }.forEach { (e, callExpr) -> val (_, type) = e diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/diagnosticsUtil.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/diagnosticsUtil.kt index 6fd2874b4..1c7d4911f 100644 --- a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/diagnosticsUtil.kt +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/diagnosticsUtil.kt @@ -15,7 +15,6 @@ import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext -import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode fun DeclarationCheckerContext.report(diagnostic: Diagnostic) { return this.trace.report(diagnostic) @@ -25,7 +24,6 @@ val DeclarationCheckerContext.bindingContext get() = this.trace.bindingContext fun KtElement?.getResolvedCallOrResolveToCall( context: DeclarationCheckerContext, - bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL, ): ResolvedCall<out CallableDescriptor>? { - return this.getResolvedCallOrResolveToCall(context.bindingContext, bodyResolveMode) + return this.getResolvedCallOrResolveToCall(context.bindingContext) } \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/resolve/resolveIdea.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/resolve/resolveIdea.kt index 723da4408..a8afeb64b 100644 --- a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/resolve/resolveIdea.kt +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/resolve/resolveIdea.kt @@ -32,7 +32,6 @@ import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.constants.ArrayValue import org.jetbrains.kotlin.resolve.constants.ConstantValue import org.jetbrains.kotlin.resolve.constants.StringValue -import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance @@ -125,9 +124,8 @@ inline fun <reified E> PsiElement.findChild(): E? = this.children.find { it is E fun KtElement?.getResolvedCallOrResolveToCall( context: BindingContext, - bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL, ): ResolvedCall<out CallableDescriptor>? { - return this?.getCall(context)?.getResolvedCall(context)// ?: this?.resolveToCall(bodyResolveMode) + return this?.getCall(context)?.getResolvedCall(context) } val ResolvedCall<out CallableDescriptor>.valueParameters: List<ValueParameterDescriptor> get() = this.resultingDescriptor.valueParameters