diff --git a/backend/mirai-console/build.gradle.kts b/backend/mirai-console/build.gradle.kts index 0de899038..060caa6f3 100644 --- a/backend/mirai-console/build.gradle.kts +++ b/backend/mirai-console/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI") + useExperimentalAnnotation("net.mamoe.mirai.console.utils.ConsoleExperimentalAPI") useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt index 75d81047d..74d6b379b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt @@ -22,8 +22,8 @@ import net.mamoe.mirai.console.plugin.PluginLoader import net.mamoe.mirai.console.plugin.center.CuiPluginCenter import net.mamoe.mirai.console.plugin.center.PluginCenter import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI import net.mamoe.mirai.utils.DefaultLogger -import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiLogger import java.io.ByteArrayOutputStream import java.io.File @@ -64,7 +64,7 @@ interface MiraiConsole : CoroutineScope { val pluginCenter: PluginCenter - @MiraiExperimentalAPI + @ConsoleExperimentalAPI fun newLogger(identity: String?): MiraiLogger companion object INSTANCE : MiraiConsole by MiraiConsoleInternal @@ -102,7 +102,7 @@ internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConso override val rootDir: File get() = instance.rootDir override val frontEnd: MiraiConsoleFrontEnd get() = instance.frontEnd - @MiraiExperimentalAPI + @ConsoleExperimentalAPI override val mainLogger: MiraiLogger get() = instance.mainLogger override val coroutineContext: CoroutineContext get() = instance.coroutineContext @@ -114,7 +114,7 @@ internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConso DefaultLogger = { identity -> this.newLogger(identity) } } - @MiraiExperimentalAPI + @ConsoleExperimentalAPI override fun newLogger(identity: String?): MiraiLogger = frontEnd.loggerFor(identity) internal fun initialize() { 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 8a4e0cf7f..16df71fa8 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 @@ -9,11 +9,11 @@ package net.mamoe.mirai.console.command -import net.mamoe.mirai.utils.MiraiExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI interface BuiltInCommand : Command -@MiraiExperimentalAPI +@ConsoleExperimentalAPI object BuiltInCommands /* 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 237dbb2c1..5dfceb470 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,6 +11,7 @@ package net.mamoe.mirai.console.command +import net.mamoe.mirai.console.command.internal.isValidSubName import net.mamoe.mirai.message.data.SingleMessage /** @@ -45,10 +46,28 @@ interface Command { /** * @param args 指令参数. 可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. - */ + */ // TODO: 2020/6/28 Java-friendly bridges suspend fun CommandSender.onCommand(args: Array) } +/** + * [Command] 的基础实现 + */ +abstract class AbstractCommand @JvmOverloads constructor( + final override val owner: CommandOwner, + vararg names: String, + description: String = "", + final override val permission: CommandPermission = CommandPermission.Default, + final override val prefixOptional: Boolean = false +) : Command { + final override val description = description.trimIndent() + final override val names: Array = + names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> + list.firstOrNull { !it.isValidSubName() }?.let { error("Invalid name: $it") } + }.toTypedArray() + +} + suspend inline fun Command.onCommand(sender: CommandSender, args: Array) = sender.run { onCommand(args) } /** 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 6af46e2ed..3e987e2a3 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 @@ -7,70 +7,66 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE") +@file:Suppress( + "EXPOSED_SUPER_CLASS", + "NOTHING_TO_INLINE", + "unused", + "WRONG_MODIFIER_TARGET", + "WRONG_MODIFIER_CONTAINING_DECLARATION" +) package net.mamoe.mirai.console.command -import net.mamoe.mirai.console.command.description.CommandArgParser -import net.mamoe.mirai.console.command.description.CommandParserContext -import net.mamoe.mirai.console.command.description.EmptyCommandParserContext -import net.mamoe.mirai.console.command.description.plus -import net.mamoe.mirai.console.command.internal.CompositeCommandImpl -import net.mamoe.mirai.console.command.internal.isValidSubName +import net.mamoe.mirai.console.command.description.* +import net.mamoe.mirai.console.command.internal.AbstractReflectionCommand +import net.mamoe.mirai.console.command.internal.CompositeCommandSubCommandAnnotationResolver +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.FUNCTION import kotlin.reflect.KClass - /** - * 功能最集中的 Commend - * 只支持有sub的指令 - * 例: - * /mute add - * /mute remove - * /mute addandremove (sub is case insensitive, lowercase are recommend) - * /mute add and remove('add and remove' consider as a sub) + * 复合指令. */ +@ConsoleExperimentalAPI abstract class CompositeCommand @JvmOverloads constructor( - final override val owner: CommandOwner, + owner: CommandOwner, vararg names: String, description: String = "no description available", - final override val permission: CommandPermission = CommandPermission.Default, - final override val prefixOptional: Boolean = false, + permission: CommandPermission = CommandPermission.Default, + prefixOptional: Boolean = false, overrideContext: CommandParserContext = EmptyCommandParserContext -) : Command, CompositeCommandImpl() { - final override val description = description.trimIndent() - final override val names: Array = - names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> - list.firstOrNull { !it.isValidSubName() }?.let { error("Name is not valid: $it") } - }.toTypedArray() - +) : Command, AbstractReflectionCommand(owner, names, description, permission, prefixOptional), + CommandParserContextAware { /** * [CommandArgParser] 的环境 */ - val context: CommandParserContext = CommandParserContext.Builtins + overrideContext + final override val context: CommandParserContext = CommandParserContext.Builtins + overrideContext - final override val usage: String get() = super.usage + /** + * 标记一个函数为子指令, 当 [value] 为空时使用函数名. + * @param value 子指令名 + */ + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class SubCommand(vararg val value: String) /** 指定子指令要求的权限 */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FUNCTION) - annotation class Permission(val permission: KClass) - - /** 标记一个函数为子指令, 当 [names] 为空时使用函数名. */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FUNCTION) - annotation class SubCommand(vararg val names: String) + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class Permission(val value: KClass) /** 指令描述 */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FUNCTION) - annotation class Description(val description: String) + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class Description(val value: String) /** 参数名, 将参与构成 [usage] */ - @Retention(AnnotationRetention.RUNTIME) + @Retention(RUNTIME) @Target(AnnotationTarget.VALUE_PARAMETER) - annotation class Name(val name: String) + protected annotation class Name(val value: String) - public override suspend fun CommandSender.onDefault(rawArgs: Array) { + override suspend fun CommandSender.onDefault(rawArgs: Array) { sendMessage(usage) } @@ -79,4 +75,7 @@ abstract class CompositeCommand @JvmOverloads constructor( defaultSubCommand.onCommand(this, args) } } + + final override val subCommandAnnotationResolver: SubCommandAnnotationResolver + get() = CompositeCommandSubCommandAnnotationResolver } 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 new file mode 100644 index 000000000..58adce745 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt @@ -0,0 +1,50 @@ +/* + * 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( + "EXPOSED_SUPER_CLASS", + "NOTHING_TO_INLINE", + "unused", + "WRONG_MODIFIER_TARGET", + "WRONG_MODIFIER_CONTAINING_DECLARATION" +) + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.description.CommandParserContextAware +import net.mamoe.mirai.console.command.description.EmptyCommandParserContext +import net.mamoe.mirai.console.command.internal.AbstractReflectionCommand +import net.mamoe.mirai.console.command.internal.SimpleCommandSubCommandAnnotationResolver + +abstract class SimpleCommand @JvmOverloads constructor( + owner: CommandOwner, + vararg names: String, + description: String = "no description available", + permission: CommandPermission = CommandPermission.Default, + prefixOptional: Boolean = false, + overrideContext: CommandParserContext = EmptyCommandParserContext +) : Command, AbstractReflectionCommand(owner, names, description, permission, prefixOptional), + CommandParserContextAware { + + /** + * 标注指令处理器 + */ + protected annotation class Handler + + final override val context: CommandParserContext + get() = TODO("Not yet implemented") + + final override suspend fun CommandSender.onCommand(args: Array) { + + } + + final override val subCommandAnnotationResolver: SubCommandAnnotationResolver + get() = SimpleCommandSubCommandAnnotationResolver +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt index 3b3a3f127..459e25f54 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt @@ -14,10 +14,10 @@ package net.mamoe.mirai.console.command.description import net.mamoe.mirai.Bot import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.description.CommandParserContext.ParserPair +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.utils.MiraiExperimentalAPI import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf @@ -57,6 +57,16 @@ interface CommandParserContext { }) } +/** + * 拥有 [CommandParserContext] 的类 + */ +interface CommandParserContextAware { + /** + * [CommandArgParser] 的环境 + */ + val context: CommandParserContext +} + object EmptyCommandParserContext : CommandParserContext by CustomCommandParserContext(listOf()) /** @@ -151,7 +161,7 @@ class CommandParserContextBuilder : MutableList> by mutableListOf( /** * 添加一个指令解析器 */ - @MiraiExperimentalAPI + @ConsoleExperimentalAPI @JvmSynthetic inline infix fun add( crossinline parser: CommandArgParser<*>.(s: String) -> T @@ -160,7 +170,7 @@ class CommandParserContextBuilder : MutableList> by mutableListOf( /** * 添加一个指令解析器 */ - @MiraiExperimentalAPI + @ConsoleExperimentalAPI @JvmSynthetic @LowPriorityInOverloadResolution inline infix fun add( diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt index 2ac138bc5..0a641e763 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt @@ -7,11 +7,10 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("unused") +@file:Suppress("unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") package net.mamoe.mirai.console.command.description -import net.mamoe.mirai.console.command.Command import net.mamoe.mirai.console.command.CompositeCommand import java.lang.reflect.Parameter import kotlin.reflect.KClass @@ -19,7 +18,8 @@ import kotlin.reflect.KClass internal fun Parameter.toCommandParam(): CommandParam<*> { val name = getAnnotation(CompositeCommand.Name::class.java) return CommandParam( - name?.name ?: this.name ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"), + name?.value ?: this.name + ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"), this.type.kotlin ) } @@ -34,7 +34,7 @@ internal data class CommandParam( */ val name: String, /** - * 参数类型. 将从 [CommandDescriptor.context] 中寻找 [CommandArgParser] 解析. + * 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgParser] 解析. */ val type: KClass // exact type ) { @@ -51,8 +51,6 @@ internal data class CommandParam( * 覆盖的 [CommandArgParser]. * * 如果非 `null`, 将不会从 [CommandParserContext] 寻找 [CommandArgParser] - * - * @see Command.parserFor */ val overrideParser: CommandArgParser? get() = _overrideParser } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt deleted file mode 100644 index 0c2cfeccc..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.command.internal - -import net.mamoe.mirai.console.command.* -import net.mamoe.mirai.console.command.description.CommandParam -import net.mamoe.mirai.message.data.PlainText -import net.mamoe.mirai.message.data.SingleMessage -import kotlin.reflect.KAnnotatedElement -import kotlin.reflect.KClass -import kotlin.reflect.full.* - -internal abstract class CompositeCommandImpl : Command { - @JvmField - @Suppress("PropertyName") - internal var _usage: String = "" - - override val usage: String // initialized by subCommand reflection - get() = _usage - - internal abstract suspend fun CommandSender.onDefault(rawArgs: Array) - - internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { - DefaultSubCommandDescriptor( - "", - CommandPermission.Default, - onCommand = block2 { sender: CommandSender, args: Array -> - sender.onDefault(args) - } - ) - } - - internal val subCommands: Array by lazy { - this@CompositeCommandImpl as CompositeCommand - - val buildUsage = StringBuilder(this.description).append(": \n") - - this::class.declaredFunctions.filter { it.hasAnnotation() } - .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 -> - val notStatic = !function.hasAnnotation() - val overridePermission = function.findAnnotation()//optional - val subDescription = - function.findAnnotation()?.description ?: "" - - 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 = - function.findAnnotation()!!.names - .let { namesFromAnnotation -> - if (namesFromAnnotation.isNotEmpty()) { - namesFromAnnotation - } else arrayOf(function.name) - }.also { names -> - names.forEach { - check(it.isValidSubName()) { - "Name of sub command ${function.name} is invalid" - } - } - } - - //map parameter - val params = parameters.map { param -> - buildUsage.append("/$primaryName ") - - if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") - - val argName = param.findAnnotation()?.name ?: param.name ?: "unknown" - buildUsage.append("<").append(argName).append("> ").append(" ") - CommandParam( - argName, - (param.type.classifier as? KClass<*>) - ?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") - ) - }.toTypedArray() - - buildUsage.append(subDescription).append("\n") - - SubCommandDescriptor( - commandName, - params, - subDescription, - overridePermission?.permission?.getInstance() ?: permission, - onCommand = block { sender: CommandSender, args: Array -> - 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. - } - ) - }.toTypedArray().also { - _usage = buildUsage.toString() - } - } - - private fun block(block: suspend (CommandSender, Array) -> Boolean): suspend (CommandSender, Array) -> Boolean { - return block - } - - private fun block2(block: suspend (CommandSender, Array) -> Unit): suspend (CommandSender, Array) -> Unit { - return block - } - - internal val bakedCommandNameToSubDescriptorArray: Map, SubCommandDescriptor> by lazy { - kotlin.run { - val map = LinkedHashMap, SubCommandDescriptor>(subCommands.size * 2) - for (descriptor in subCommands) { - for (name in descriptor.bakedSubNames) { - map[name] = descriptor - } - } - map.toSortedMap(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }) - } - } - - internal class DefaultSubCommandDescriptor( - val description: String, - val permission: CommandPermission, - val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Unit - ) - - internal inner class SubCommandDescriptor( - val names: Array, - val params: Array>, - val description: String, - val permission: CommandPermission, - val onCommand: suspend (sender: CommandSender, parsedArgs: Array) -> Boolean - ) { - internal suspend inline fun parseAndExecute( - sender: CommandSender, - argsWithSubCommandNameNotRemoved: Array - ) { - if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) { - sender.sendMessage(usage) - } - } - - @JvmField - internal val bakedSubNames: Array> = names.map { it.bakeSubName() }.toTypedArray() - private fun parseArgs(sender: CommandSender, rawArgs: Array, offset: Int): Array { - this@CompositeCommandImpl as CompositeCommand - 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 String -> context[param.type]?.parse(rawArg, sender) - is SingleMessage -> context[param.type]?.parse(rawArg, sender) - else -> throw IllegalArgumentException("Illegal argument type: ${rawArg::class.qualifiedName}") - } ?: error("Cannot find a parser for $rawArg") - } - } - } - - /** - * @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] - */ - internal fun matchSubCommand(rawArgs: Array): SubCommandDescriptor? { - val maxCount = rawArgs.size - 1 - 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 Array.contentEqualsOffset(other: Array, length: Int): Boolean { - repeat(length) { index -> - if (other[index].toString() != this[index]) { - 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 = split(' ').filterNot { it.isBlank() }.toTypedArray() - -internal fun Any.flattenCommandComponents(): ArrayList { - val list = ArrayList() - when (this) { - is PlainText -> this.content.splitToSequence(' ').filterNot { it.isBlank() } - .forEach { list.add(it) } - is CharSequence -> this.splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) } - is SingleMessage -> list.add(this) - is Array<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } - is Iterable<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } - else -> list.add(this.toString()) - } - return list -} - -internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = - findAnnotation() != null - -internal inline fun KClass.getInstance(): T { - return this.objectInstance ?: this.createInstance() -} - -internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "" diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandInternal.kt new file mode 100644 index 000000000..d56e9a7b6 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandInternal.kt @@ -0,0 +1,311 @@ +/* + * 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("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package net.mamoe.mirai.console.command.internal + +import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.description.CommandParam +import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.description.CommandParserContextAware +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.message.data.SingleMessage +import kotlin.reflect.KAnnotatedElement +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.full.* + +internal object CompositeCommandSubCommandAnnotationResolver : + AbstractReflectionCommand.SubCommandAnnotationResolver { + override fun hasAnnotation(function: KFunction<*>) = function.hasAnnotation() + override fun getSubCommandNames(function: KFunction<*>): Array = + function.findAnnotation()!!.value +} + +internal object SimpleCommandSubCommandAnnotationResolver : + AbstractReflectionCommand.SubCommandAnnotationResolver { + override fun hasAnnotation(function: KFunction<*>) = function.hasAnnotation() + override fun getSubCommandNames(function: KFunction<*>): Array = arrayOf("") +} + +internal abstract class AbstractReflectionCommand @JvmOverloads constructor( + owner: CommandOwner, + names: Array, + description: String = "", + permission: CommandPermission = CommandPermission.Default, + prefixOptional: Boolean = false +) : Command, AbstractCommand( + owner, + names = *names, + description = description, + permission = permission, + prefixOptional = prefixOptional +), CommandParserContextAware { + internal abstract val subCommandAnnotationResolver: SubCommandAnnotationResolver + + @JvmField + @Suppress("PropertyName") + internal var _usage: String = "" + + final override val usage: String // initialized by subCommand reflection + get() = _usage + + abstract suspend fun CommandSender.onDefault(rawArgs: Array) + + internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { + DefaultSubCommandDescriptor( + "", + CommandPermission.Default, + onCommand = block2 { sender: CommandSender, args: Array -> + sender.onDefault(args) + } + ) + } + + interface SubCommandAnnotationResolver { + fun hasAnnotation(function: KFunction<*>): Boolean + fun getSubCommandNames(function: KFunction<*>): Array + } + + internal val subCommands: Array by lazy { + this::class.declaredFunctions.filter { subCommandAnnotationResolver.hasAnnotation(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.firstOrNull()?.usage ?: description + } + } + + internal val bakedCommandNameToSubDescriptorArray: Map, SubCommandDescriptor> by lazy { + kotlin.run { + val map = LinkedHashMap, SubCommandDescriptor>(subCommands.size * 2) + for (descriptor in subCommands) { + for (name in descriptor.bakedSubNames) { + map[name] = descriptor + } + } + map.toSortedMap(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }) + } + } + + internal class DefaultSubCommandDescriptor( + val description: String, + val permission: CommandPermission, + val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Unit + ) + + internal class SubCommandDescriptor( + val names: Array, + val params: Array>, + val description: String, + val permission: CommandPermission, + val onCommand: suspend (sender: CommandSender, parsedArgs: Array) -> Boolean, + val context: CommandParserContext, + val usage: String + ) { + internal suspend inline fun parseAndExecute( + sender: CommandSender, + argsWithSubCommandNameNotRemoved: Array + ) { + if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) { + sender.sendMessage(usage) + } + } + + @JvmField + internal val bakedSubNames: Array> = names.map { it.bakeSubName() }.toTypedArray() + private fun parseArgs(sender: CommandSender, rawArgs: Array, offset: Int): Array { + 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 String -> context[param.type]?.parse(rawArg, sender) + is SingleMessage -> context[param.type]?.parse(rawArg, sender) + else -> throw IllegalArgumentException("Illegal argument type: ${rawArg::class.qualifiedName}") + } ?: error("Cannot find a parser for $rawArg") + } + } + } + + /** + * @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] + */ + internal fun matchSubCommand(rawArgs: Array): SubCommandDescriptor? { + val maxCount = rawArgs.size - 1 + 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 Array.contentEqualsOffset(other: Array, length: Int): Boolean { + repeat(length) { index -> + if (other[index].toString() != this[index]) { + 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 = split(' ').filterNot { it.isBlank() }.toTypedArray() + +internal fun Any.flattenCommandComponents(): ArrayList { + val list = ArrayList() + when (this) { + is PlainText -> this.content.splitToSequence(' ').filterNot { it.isBlank() } + .forEach { list.add(it) } + is CharSequence -> this.splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) } + is SingleMessage -> list.add(this) + is Array<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + is Iterable<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + else -> list.add(this.toString()) + } + return list +} + +internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = + findAnnotation() != null + +internal inline fun KClass.getInstance(): T { + return this.objectInstance ?: this.createInstance() +} + +internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "" + +internal fun AbstractReflectionCommand.createSubCommand( + function: KFunction<*>, + context: CommandParserContext +): AbstractReflectionCommand.SubCommandDescriptor { + val notStatic = !function.hasAnnotation() + val overridePermission = function.findAnnotation()//optional + val subDescription = + function.findAnnotation()?.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 = + function.findAnnotation()!!.value + .let { namesFromAnnotation -> + if (namesFromAnnotation.isNotEmpty()) { + namesFromAnnotation + } else arrayOf(function.name) + }.also { names -> + names.forEach { + check(it.isValidSubName()) { + "Name of sub command ${function.name} is invalid" + } + } + } + + val buildUsage = StringBuilder(this.description).append(": \n") + + //map parameter + val params = parameters.map { param -> + buildUsage.append("/$primaryName ") + + if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") + + val argName = param.findAnnotation()?.value ?: param.name ?: "unknown" + buildUsage.append("<").append(argName).append("> ").append(" ") + CommandParam( + argName, + (param.type.classifier as? KClass<*>) + ?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") + ) + }.toTypedArray() + + buildUsage.append(subDescription).append("\n") + + return AbstractReflectionCommand.SubCommandDescriptor( + commandName, + params, + subDescription, + overridePermission?.value?.getInstance() ?: permission, + onCommand = block { sender: CommandSender, args: Array -> + 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, + usage = buildUsage.toString() + ) +} + + +private fun block(block: suspend (CommandSender, Array) -> Boolean): suspend (CommandSender, Array) -> Boolean { + return block +} + +private fun block2(block: suspend (CommandSender, Array) -> Unit): suspend (CommandSender, Array) -> Unit { + return block +} 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 c5e5e3144..a41404493 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 @@ -10,7 +10,7 @@ package net.mamoe.mirai.console.plugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin -import net.mamoe.mirai.utils.MiraiExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI import java.io.File /** @@ -38,7 +38,7 @@ inline val

P.safeLoader: PluginLoader * * @see JvmPlugin */ -@MiraiExperimentalAPI("classname is subject to change") +@ConsoleExperimentalAPI("classname is subject to change") interface PluginFileExtensions { /** * 数据目录 diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/CuiPluginCenter.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/CuiPluginCenter.kt index d135a9301..ba2f9bb1e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/CuiPluginCenter.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/CuiPluginCenter.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:OptIn(MiraiExperimentalAPI::class) +@file:OptIn(ConsoleExperimentalAPI::class) package net.mamoe.mirai.console.plugin.center @@ -19,8 +19,8 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UnstableDefault import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI import net.mamoe.mirai.console.utils.retryCatching -import net.mamoe.mirai.utils.MiraiExperimentalAPI import java.io.File @OptIn(UnstableDefault::class) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/PluginCenter.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/PluginCenter.kt index eeca0f43e..685fd5bba 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/PluginCenter.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/PluginCenter.kt @@ -11,10 +11,10 @@ package net.mamoe.mirai.console.plugin.center import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import net.mamoe.mirai.utils.MiraiExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI import java.io.File -@MiraiExperimentalAPI +@ConsoleExperimentalAPI interface PluginCenter { @Serializable diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt index 62332426c..db4e5c3eb 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt @@ -16,7 +16,7 @@ import net.mamoe.mirai.console.plugin.PluginLoadException import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal import net.mamoe.mirai.console.plugin.internal.PluginsLoader import net.mamoe.mirai.console.setting.SettingStorage -import net.mamoe.mirai.utils.MiraiExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.yamlkt.Yaml import java.io.File @@ -32,7 +32,7 @@ object JarPluginLoader : AbstractFilePluginLoader load(holder: SettingHolder, settingClass: Class): T - @MiraiExperimentalAPI + @ConsoleExperimentalAPI fun store(holder: SettingHolder, setting: Setting) } -@MiraiExperimentalAPI +@ConsoleExperimentalAPI interface SettingHolder { val name: String } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Value.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Value.kt index 6e73d9289..16d615474 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Value.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Value.kt @@ -16,7 +16,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.StringFormat import net.mamoe.mirai.console.setting.internal.map import net.mamoe.mirai.console.setting.internal.setValueBySerializer -import net.mamoe.mirai.utils.MiraiExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI import kotlin.reflect.KProperty /** @@ -145,7 +145,7 @@ interface StringValue : PrimitiveValue //// endregion PrimitiveValues CODEGEN //// -@MiraiExperimentalAPI +@ConsoleExperimentalAPI interface CompositeValue : Value diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaFriendlyAPI.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaFriendlyAPI.kt index 23db91223..beb8f9966 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaFriendlyAPI.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaFriendlyAPI.kt @@ -1,8 +1,38 @@ package net.mamoe.mirai.console.utils +import kotlin.annotation.AnnotationTarget.* + /** * 表明这个 API 是为了让 Java 使用者调用更方便. Kotlin 使用者不应该使用这些 API. */ @RequiresOptIn(level = RequiresOptIn.Level.ERROR) -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.CLASS) +@Target(PROPERTY, FUNCTION, TYPE, CLASS) internal annotation class JavaFriendlyAPI + +/** + * 标记为一个仅供 mirai-console 内部使用的 API. + * + * 这些 API 可能会在任意时刻更改, 且不会发布任何预警. + * 非常不建议在发行版本中使用这些 API. + */ +@Retention(AnnotationRetention.SOURCE) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR, CLASS, FUNCTION, PROPERTY) +@MustBeDocumented +annotation class ConsoleInternalAPI( + val message: String = "" +) + +/** + * 标记一个实验性的 API. + * + * 这些 API 不具有稳定性, 且可能会在任意时刻更改. + * 不建议在发行版本中使用这些 API. + */ +@Retention(AnnotationRetention.SOURCE) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR) +@MustBeDocumented +annotation class ConsoleExperimentalAPI( + val message: String = "" +) 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 3e219ac55..a16a58594 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 @@ -36,6 +36,7 @@ object TestCompositeCommand : CompositeCommand( } } + object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") { override suspend fun CommandSender.onCommand(args: Array) { Testing.ok(args) @@ -63,7 +64,7 @@ internal class TestCommand { assertEquals(1, ConsoleCommandOwner.instance.registeredCommands.size) assertEquals(1, InternalCommandManager.registeredCommands.size) - assertEquals(1, InternalCommandManager.requiredPrefixCommandMap.size) + assertEquals(2, InternalCommandManager.requiredPrefixCommandMap.size) } finally { TestCompositeCommand.unregister() } @@ -112,11 +113,11 @@ internal class TestCommand { @Test fun `executing command by string command`() = runBlocking { TestCompositeCommand.register() - val result = withTesting> { - assertNotNull(sender.executeCommand("testComposite", "test")) + val result = withTesting { + assertNotNull(sender.executeCommand("/testComposite", "mute 1")) } - assertEquals("test", result.single()) + assertEquals(1, result) } @Test diff --git a/frontend/mirai-console-pure/build.gradle.kts b/frontend/mirai-console-pure/build.gradle.kts index 40c8cade5..faaf24d57 100644 --- a/frontend/mirai-console-pure/build.gradle.kts +++ b/frontend/mirai-console-pure/build.gradle.kts @@ -16,6 +16,7 @@ kotlin { languageSettings.progressiveMode = true languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI") + languageSettings.useExperimentalAnnotation("net.mamoe.mirai.console.utils.ConsoleExperimentalAPI") languageSettings.useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") languageSettings.useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")