Rework CompositeCommand, SimpleCommand, RawCommand;

Add ConsoleExperimentalAPI annotation;
Add CommandParserContextAware;
This commit is contained in:
Him188 2020-06-28 07:43:58 +08:00
parent 32e77ca420
commit e25a818942
19 changed files with 495 additions and 342 deletions

View File

@ -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")

View File

@ -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() {

View File

@ -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
/*

View File

@ -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<out Any>)
}
/**
* [Command] 的基础实现
*/
abstract class AbstractCommand @JvmOverloads constructor(
final override val owner: CommandOwner,
vararg names: String,
description: String = "<no description available>",
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<out String> =
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<out Any>) = sender.run { onCommand(args) }
/**

View File

@ -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<out String> =
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<out CommandPermission>)
/** 标记一个函数为子指令, 当 [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<out CommandPermission>)
/** 指令描述 */
@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<out Any>) {
override suspend fun CommandSender.onDefault(rawArgs: Array<out Any>) {
sendMessage(usage)
}
@ -79,4 +75,7 @@ abstract class CompositeCommand @JvmOverloads constructor(
defaultSubCommand.onCommand(this, args)
}
}
final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
get() = CompositeCommandSubCommandAnnotationResolver
}

View File

@ -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<out Any>) {
}
final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
get() = SimpleCommandSubCommandAnnotationResolver
}

View File

@ -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<ParserPair<*>> by mutableListOf(
/**
* 添加一个指令解析器
*/
@MiraiExperimentalAPI
@ConsoleExperimentalAPI
@JvmSynthetic
inline infix fun <reified T : Any> add(
crossinline parser: CommandArgParser<*>.(s: String) -> T
@ -160,7 +170,7 @@ class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf(
/**
* 添加一个指令解析器
*/
@MiraiExperimentalAPI
@ConsoleExperimentalAPI
@JvmSynthetic
@LowPriorityInOverloadResolution
inline infix fun <reified T : Any> add(

View File

@ -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<T : Any>(
*/
val name: String,
/**
* 参数类型. 将从 [CommandDescriptor.context] 中寻找 [CommandArgParser] 解析.
* 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgParser] 解析.
*/
val type: KClass<T> // exact type
) {
@ -51,8 +51,6 @@ internal data class CommandParam<T : Any>(
* 覆盖的 [CommandArgParser].
*
* 如果非 `null`, 将不会从 [CommandParserContext] 寻找 [CommandArgParser]
*
* @see Command.parserFor
*/
val overrideParser: CommandArgParser<T>? get() = _overrideParser
}

View File

@ -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 = "<command build failed>"
override val usage: String // initialized by subCommand reflection
get() = _usage
internal abstract suspend fun CommandSender.onDefault(rawArgs: Array<out Any>)
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
DefaultSubCommandDescriptor(
"",
CommandPermission.Default,
onCommand = block2 { sender: CommandSender, args: Array<out Any> ->
sender.onDefault(args)
}
)
}
internal val subCommands: Array<SubCommandDescriptor> by lazy {
this@CompositeCommandImpl as CompositeCommand
val buildUsage = StringBuilder(this.description).append(": \n")
this::class.declaredFunctions.filter { it.hasAnnotation<CompositeCommand.SubCommand>() }
.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<JvmStatic>()
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
val subDescription =
function.findAnnotation<CompositeCommand.Description>()?.description ?: "<no description available>"
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<CompositeCommand.SubCommand>()!!.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<CompositeCommand.Name>()?.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<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.
}
)
}.toTypedArray().also {
_usage = buildUsage.toString()
}
}
private fun block(block: suspend (CommandSender, Array<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
return block
}
private fun block2(block: suspend (CommandSender, Array<out Any>) -> Unit): suspend (CommandSender, Array<out Any>) -> Unit {
return block
}
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(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() })
}
}
internal class DefaultSubCommandDescriptor(
val description: String,
val permission: CommandPermission,
val onCommand: suspend (sender: CommandSender, rawArgs: Array<out Any>) -> Unit
)
internal inner class SubCommandDescriptor(
val names: Array<out String>,
val params: Array<CommandParam<*>>,
val description: String,
val permission: CommandPermission,
val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean
) {
internal suspend inline fun parseAndExecute(
sender: CommandSender,
argsWithSubCommandNameNotRemoved: Array<out Any>
) {
if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) {
sender.sendMessage(usage)
}
}
@JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any> {
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<out Any>): 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 <T> Array<T>.contentEqualsOffset(other: Array<out Any>, 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<String> = split(' ').filterNot { it.isBlank() }.toTypedArray()
internal fun Any.flattenCommandComponents(): ArrayList<Any> {
val list = ArrayList<Any>()
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 <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
findAnnotation<T>() != null
internal inline fun <T : Any> KClass<out T>.getInstance(): T {
return this.objectInstance ?: this.createInstance()
}
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"

View File

@ -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<CompositeCommand.SubCommand>()
override fun getSubCommandNames(function: KFunction<*>): Array<out String> =
function.findAnnotation<CompositeCommand.SubCommand>()!!.value
}
internal object SimpleCommandSubCommandAnnotationResolver :
AbstractReflectionCommand.SubCommandAnnotationResolver {
override fun hasAnnotation(function: KFunction<*>) = function.hasAnnotation<SimpleCommand.Handler>()
override fun getSubCommandNames(function: KFunction<*>): Array<out String> = arrayOf("")
}
internal abstract class AbstractReflectionCommand @JvmOverloads constructor(
owner: CommandOwner,
names: Array<out String>,
description: String = "<no description available>",
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 = "<command build failed>"
final override val usage: String // initialized by subCommand reflection
get() = _usage
abstract suspend fun CommandSender.onDefault(rawArgs: Array<out Any>)
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
DefaultSubCommandDescriptor(
"",
CommandPermission.Default,
onCommand = block2 { sender: CommandSender, args: Array<out Any> ->
sender.onDefault(args)
}
)
}
interface SubCommandAnnotationResolver {
fun hasAnnotation(function: KFunction<*>): Boolean
fun getSubCommandNames(function: KFunction<*>): Array<out String>
}
internal val subCommands: Array<SubCommandDescriptor> 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<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(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() })
}
}
internal class DefaultSubCommandDescriptor(
val description: String,
val permission: CommandPermission,
val onCommand: suspend (sender: CommandSender, rawArgs: Array<out Any>) -> Unit
)
internal class SubCommandDescriptor(
val names: Array<out String>,
val params: Array<CommandParam<*>>,
val description: String,
val permission: CommandPermission,
val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean,
val context: CommandParserContext,
val usage: String
) {
internal suspend inline fun parseAndExecute(
sender: CommandSender,
argsWithSubCommandNameNotRemoved: Array<out Any>
) {
if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) {
sender.sendMessage(usage)
}
}
@JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any> {
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<out Any>): 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 <T> Array<T>.contentEqualsOffset(other: Array<out Any>, 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<String> = split(' ').filterNot { it.isBlank() }.toTypedArray()
internal fun Any.flattenCommandComponents(): ArrayList<Any> {
val list = ArrayList<Any>()
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 <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
findAnnotation<T>() != null
internal inline fun <T : Any> KClass<out T>.getInstance(): T {
return this.objectInstance ?: this.createInstance()
}
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"
internal fun AbstractReflectionCommand.createSubCommand(
function: KFunction<*>,
context: CommandParserContext
): AbstractReflectionCommand.SubCommandDescriptor {
val notStatic = !function.hasAnnotation<JvmStatic>()
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
val subDescription =
function.findAnnotation<CompositeCommand.Description>()?.value ?: "<no description available>"
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<CompositeCommand.SubCommand>()!!.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<CompositeCommand.Name>()?.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<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,
usage = buildUsage.toString()
)
}
private fun block(block: suspend (CommandSender, Array<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
return block
}
private fun block2(block: suspend (CommandSender, Array<out Any>) -> Unit): suspend (CommandSender, Array<out Any>) -> Unit {
return block
}

View File

@ -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 : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
*
* @see JvmPlugin
*/
@MiraiExperimentalAPI("classname is subject to change")
@ConsoleExperimentalAPI("classname is subject to change")
interface PluginFileExtensions {
/**
* 数据目录

View File

@ -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)

View File

@ -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

View File

@ -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<JvmPlugin, JvmPluginDescriptio
MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
}
@MiraiExperimentalAPI
@ConsoleExperimentalAPI
val settingStorage: SettingStorage by lazy { TODO() }
override val coroutineContext: CoroutineContext by lazy {

View File

@ -1,6 +1,6 @@
package net.mamoe.mirai.console.setting
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI
import kotlin.reflect.KClass
interface SettingStorage {
@ -9,11 +9,11 @@ interface SettingStorage {
fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T
@MiraiExperimentalAPI
@ConsoleExperimentalAPI
fun store(holder: SettingHolder, setting: Setting)
}
@MiraiExperimentalAPI
@ConsoleExperimentalAPI
interface SettingHolder {
val name: String
}

View File

@ -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<String>
//// endregion PrimitiveValues CODEGEN ////
@MiraiExperimentalAPI
@ConsoleExperimentalAPI
interface CompositeValue<T> : Value<T>

View File

@ -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 = ""
)

View File

@ -36,6 +36,7 @@ object TestCompositeCommand : CompositeCommand(
}
}
object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") {
override suspend fun CommandSender.onCommand(args: Array<out Any>) {
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<Array<String>> {
assertNotNull(sender.executeCommand("testComposite", "test"))
val result = withTesting<Int> {
assertNotNull(sender.executeCommand("/testComposite", "mute 1"))
}
assertEquals("test", result.single())
assertEquals(1, result)
}
@Test

View File

@ -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")