Rework command reflection:

- Remove AbstractReflectionCommand
- Introduce CommandReflector
- Misc improvements
This commit is contained in:
Him188 2020-10-23 21:32:04 +08:00
parent 8b75e47f58
commit 4aa996a417
15 changed files with 332 additions and 421 deletions

View File

@ -26,8 +26,8 @@ import net.mamoe.mirai.console.command.CommandSender.Companion.asTempCommandSend
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
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

View File

@ -20,11 +20,10 @@ package net.mamoe.mirai.console.command
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.CompositeCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.internal.command.CommandReflector
import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver
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,13 +89,23 @@ 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, SimpleCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<CommandSignatureVariantFromKFunction> by lazy {
reflector.findSubCommands()
}
/**
* 自动根据带有 [SubCommand] 注解的函数签名生成 [usage]. 也可以被覆盖.
*/
public override val usage: String get() = super.usage
public override val usage: String by lazy {
@OptIn(ExperimentalCommandDescriptors::class)
reflector.generateUsage(overloads)
}
/**
* [CommandValueArgumentParser] 的环境
@ -123,33 +132,6 @@ public abstract class CompositeCommand(
@Retention(RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
protected annotation class Name(val value: String)
@OptIn(ExperimentalCommandDescriptors::class)
override val overloads: List<CommandSignatureVariant> by lazy {
subCommands.flatMap { desc ->
desc.bakedSubNames.map { names ->
CommandSignatureVariantImpl(
valueParameters =
names.mapIndexed { index, s -> CommandValueParameter.StringConstant("p$index", s) } + desc.params.map {
CommandValueParameter.UserDefinedType(it.name, null,
isOptional = false,
isVararg = false,
type = it.type)
},
onCall = { resolvedCommandCall ->
desc.onCommand(resolvedCommandCall.caller, resolvedCommandCall.resolvedValueArguments.drop(names.size))
}
)
}
}
}
protected override suspend fun CommandSender.onDefault(rawArgs: MessageChain) {
sendMessage(usage)
}
internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
get() = CompositeCommandSubCommandAnnotationResolver
}

View File

@ -12,14 +12,12 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariant
import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariantImpl
import net.mamoe.mirai.console.command.descriptor.CommandValueParameter
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.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.MessageChain
import net.mamoe.mirai.message.data.MessageChainBuilder
@ -58,7 +56,10 @@ public abstract class RawCommand(
@ExperimentalCommandDescriptors
override val overloads: List<CommandSignatureVariant> = listOf(
CommandSignatureVariantImpl(listOf(CommandValueParameter.UserDefinedType.createRequired<MessageChain>("args", true))) { call ->
CommandSignatureVariantImpl(
receiverParameter = CommandReceiverParameter(false, typeOf0<CommandSender>()),
valueParameters = listOf(CommandValueParameter.UserDefinedType.createRequired<MessageChain>("args", true))
) { call ->
val sender = call.caller
val arguments = call.rawValueArguments
sender.onCommand(arguments.mapTo(MessageChainBuilder()) { it.value }.build())

View File

@ -22,10 +22,13 @@ 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
/**
* 简单的, 支持参数自动解析的指令.
@ -58,47 +61,41 @@ public abstract class SimpleCommand(
parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware {
private val reflector by lazy { CommandReflector(this, SimpleCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
override val overloads: List<CommandSignatureVariant> by lazy {
CommandSignatureVariantImpl(
valueParameters = subCommands.single().params.map {
CommandValueParameter.UserDefinedType(it.name, null, isOptional = false, isVararg = false, type = it.type)
}
) { call ->
val sender = call.caller
subCommands.single().onCommand(sender, call.resolvedValueArguments)
}.let { listOf(it) }
public final override val overloads: List<CommandSignatureVariantFromKFunction> by lazy {
reflector.findSubCommands().also {
if (it.isEmpty())
throw IllegalCommandDeclarationException(this, "SimpleCommand must have at least one subcommand, whereas zero present.")
}
}
/**
* 自动根据带有 [Handler] 注解的函数签名生成 [usage]. 也可以被覆盖.
*/
public override val usage: String get() = super.usage
public override val usage: String by lazy {
@OptIn(ExperimentalCommandDescriptors::class)
reflector.generateUsage(overloads)
}
/**
* 标注指令处理器
*/
@Target(FUNCTION)
protected annotation class Handler
/** 参数名, 将参与构成 [usage] */
@ConsoleExperimentalApi("Classname might change")
@Target(VALUE_PARAMETER)
protected annotation class Name(val value: String)
/**
* 指令参数环境. 默认为 [CommandArgumentContext.Builtins] `+` `overrideContext`
*/
public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext
internal override fun checkSubCommand(subCommands: Array<SubCommandDescriptor>) {
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
}

View File

@ -9,6 +9,7 @@
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.descriptor.ArgumentAcceptance.Companion.isAcceptable
import net.mamoe.mirai.console.command.descriptor.CommandValueParameter.UserDefinedType.Companion.createOptional
import net.mamoe.mirai.console.command.descriptor.CommandValueParameter.UserDefinedType.Companion.createRequired
@ -18,6 +19,7 @@ import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.safeCast
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf
@ -27,13 +29,23 @@ import kotlin.reflect.typeOf
*/
@ExperimentalCommandDescriptors
public interface CommandSignatureVariant {
@ConsoleExperimentalApi
public val receiverParameter: CommandReceiverParameter<out CommandSender>?
public val valueParameters: List<CommandValueParameter<*>>
public suspend fun call(resolvedCommandCall: ResolvedCommandCall)
}
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public class CommandSignatureVariantImpl(
public interface CommandSignatureVariantFromKFunction : CommandSignatureVariant {
public val originFunction: KFunction<*>
}
@ExperimentalCommandDescriptors
public open class CommandSignatureVariantImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<CommandValueParameter<*>>,
private val onCall: suspend CommandSignatureVariantImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureVariant {
@ -42,25 +54,46 @@ public class CommandSignatureVariantImpl(
}
}
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public open class CommandSignatureVariantFromKFunctionImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<CommandValueParameter<*>>,
override val originFunction: KFunction<*>,
private val onCall: suspend CommandSignatureVariantFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureVariantFromKFunction {
override suspend fun call(resolvedCommandCall: ResolvedCommandCall) {
return onCall(resolvedCommandCall)
}
}
/**
* Inherited instances must be [CommandValueParameter]
* Inherited instances must be [ICommandValueParameter] or [CommandReceiverParameter]
*/
@ExperimentalCommandDescriptors
public interface ICommandParameter<T : Any?> {
public val name: String
public val name: String?
/**
* If [isOptional] is `false`, [defaultValue] is always `null`.
* Otherwise [defaultValue] may be `null` iff [T] is nullable.
*/
public val defaultValue: T?
public val isOptional: Boolean
/**
* Reified type of [T]
*/
public val type: KType
}
/**
* Inherited instances must be [CommandValueParameter]
*/
@ExperimentalCommandDescriptors
public interface ICommandValueParameter<T : Any?> : ICommandParameter<T> {
/**
* If [isOptional] is `false`, [defaultValue] is always `null`.
* Otherwise [defaultValue] may be `null` iff [T] is nullable.
*/
public val defaultValue: T?
public val isVararg: Boolean
@ -105,9 +138,21 @@ public sealed class ArgumentAcceptance(
}
}
@ExperimentalCommandDescriptors
public class CommandReceiverParameter<T : CommandSender>(
override val isOptional: Boolean,
override val type: KType,
) : ICommandParameter<T> {
override val name: String get() = PARAMETER_NAME
public companion object {
public const val PARAMETER_NAME: String = "<receiver>"
}
}
@ExperimentalCommandDescriptors
public sealed class CommandValueParameter<T> : ICommandParameter<T> {
public sealed class CommandValueParameter<T> : ICommandValueParameter<T> {
internal fun validate() { // // TODO: 2020/10/18 net.mamoe.mirai.console.command.descriptor.CommandValueParameter.validate$mirai_console_mirai_console_main
require(type.classifier?.safeCast<KClass<*>>()?.isInstance(defaultValue) == true) {
"defaultValue is not instance of type"
@ -132,8 +177,10 @@ public sealed class CommandValueParameter<T> : ICommandParameter<T> {
return ArgumentAcceptance.Impossible
}
@ConsoleExperimentalApi
public class StringConstant(
public override val name: String,
@ConsoleExperimentalApi
public override val name: String?,
public val expectingValue: String,
) : CommandValueParameter<String>() {
public override val type: KType get() = STRING_TYPE
@ -152,7 +199,7 @@ public sealed class CommandValueParameter<T> : ICommandParameter<T> {
* @see createRequired
*/
public class UserDefinedType<T>(
public override val name: String,
public override val name: String?,
public override val defaultValue: T?,
public override val isOptional: Boolean,
public override val isVararg: Boolean,

View File

@ -13,8 +13,8 @@ package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip
import kotlin.reflect.KType

View File

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

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.Command.Companion.allNames
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.subscribeAlways
@ -24,6 +25,7 @@ import net.mamoe.mirai.message.data.content
import net.mamoe.mirai.utils.MiraiLogger
import java.util.concurrent.locks.ReentrantLock
@OptIn(ExperimentalCommandDescriptors::class)
internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiConsole.childScope("CommandManagerImpl") {
private val logger: MiraiLogger by lazy {
MiraiConsole.createLogger("command")
@ -102,7 +104,9 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiCons
}
override fun Command.register(override: Boolean): Boolean {
if (this is CompositeCommand) this.subCommands // init lazy
if (this is CompositeCommand) {
this.overloads // init lazy
}
kotlin.runCatching {
this.permission // init lazy
this.secondaryNames // init lazy

View File

@ -0,0 +1,215 @@
package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.internal.data.classifierAsKClass
import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.buildMessageChain
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KVisibility
import kotlin.reflect.full.*
internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray()
internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain {
when (this@flattenCommandComponents) {
is PlainText -> this@flattenCommandComponents.content.splitToSequence(' ').filterNot { it.isBlank() }
.forEach { +PlainText(it) }
is CharSequence -> this@flattenCommandComponents.splitToSequence(' ').filterNot { it.isBlank() }
.forEach { +PlainText(it) }
is SingleMessage -> add(this@flattenCommandComponents)
is Array<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
is Iterable<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
else -> add(this@flattenCommandComponents.toString())
}
}
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
internal object CompositeCommandSubCommandAnnotationResolver :
SubCommandAnnotationResolver {
override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) =
function.hasAnnotation<CompositeCommand.SubCommand>()
override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String> =
function.findAnnotation<CompositeCommand.SubCommand>()!!.value
override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? =
parameter.findAnnotation<CompositeCommand.Name>()?.value
override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? =
function.findAnnotation<CompositeCommand.Description>()?.value
}
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
internal object SimpleCommandSubCommandAnnotationResolver :
SubCommandAnnotationResolver {
override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) =
function.hasAnnotation<SimpleCommand.Handler>()
override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String> =
ownerCommand.secondaryNames
override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? =
parameter.findAnnotation<SimpleCommand.Name>()?.value
override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? =
ownerCommand.description
}
internal interface SubCommandAnnotationResolver {
fun hasAnnotation(ownerCommand: Command, function: KFunction<*>): Boolean
fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String>
fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String?
fun getDescription(ownerCommand: Command, function: KFunction<*>): String?
}
@ConsoleExperimentalApi
public class IllegalCommandDeclarationException : Exception {
public override val message: String?
public constructor(
ownerCommand: Command,
correspondingFunction: KFunction<*>,
message: String?,
) : super("Illegal command declaration: ${correspondingFunction.name} declared in ${ownerCommand::class.qualifiedName}") {
this.message = message
}
public constructor(
ownerCommand: Command,
message: String?,
) : super("Illegal command declaration: ${ownerCommand::class.qualifiedName}") {
this.message = message
}
}
@OptIn(ExperimentalCommandDescriptors::class)
internal class CommandReflector(
val command: Command,
val annotationResolver: SubCommandAnnotationResolver,
) {
@Suppress("NOTHING_TO_INLINE")
private inline fun KFunction<*>.illegalDeclaration(
message: String,
): Nothing {
throw IllegalCommandDeclarationException(command, this, message)
}
private fun KFunction<*>.isSubCommandFunction(): Boolean = annotationResolver.hasAnnotation(command, this)
private fun KFunction<*>.checkExtensionReceiver() {
this.extensionReceiverParameter?.let { receiver ->
if (receiver.type.classifierAsKClassOrNull()?.isSubclassOf(CommandSender::class) != true) {
illegalDeclaration("Extension receiver parameter type is not subclass of CommandSender.")
}
}
}
private fun KFunction<*>.checkNames() {
val names = annotationResolver.getSubCommandNames(command, this)
for (name in names) {
ILLEGAL_SUB_NAME_CHARS.find { it in name }?.let {
illegalDeclaration("'$it' is forbidden in command name.")
}
}
}
private fun KFunction<*>.checkModifiers() {
if (isInline) illegalDeclaration("Command function cannot be inline")
if (visibility == KVisibility.PRIVATE) illegalDeclaration("Command function must be accessible from Mirai Console, that is, effectively public.")
if (this.hasAnnotation<JvmStatic>()) illegalDeclaration("Command function must not be static.")
// should we allow abstract?
// if (isAbstract) illegalDeclaration("Command function cannot be abstract")
}
fun generateUsage(overloads: Iterable<CommandSignatureVariantFromKFunction>): String {
return overloads.joinToString("\n") { subcommand ->
buildString {
if (command.prefixOptional) {
append("(")
append(CommandManager.commandPrefix)
append(")")
} else {
append(CommandManager.commandPrefix)
}
if (command is CompositeCommand) {
append(command.primaryName)
append(" ")
}
append(subcommand.valueParameters.joinToString(" ") { it.render() })
annotationResolver.getDescription(command, subcommand.originFunction).let { description ->
append(" ")
append(description)
}
}
}
}
companion object {
private fun <T> CommandValueParameter<T>.render(): String {
return when (this) {
is CommandValueParameter.Extended,
is CommandValueParameter.UserDefinedType<*>,
-> {
"<${this.name ?: this.type.classifierAsKClass().simpleName}>"
}
is CommandValueParameter.StringConstant -> {
this.expectingValue
}
}
}
}
@Throws(IllegalCommandDeclarationException::class)
fun findSubCommands(): List<CommandSignatureVariantFromKFunctionImpl> {
return command::class.functions // exclude static later
.asSequence()
.filter { it.isSubCommandFunction() }
.onEach { it.checkExtensionReceiver() }
.onEach { it.checkModifiers() }
.onEach { it.checkNames() }
.map { function ->
val functionNameAsValueParameter =
annotationResolver.getSubCommandNames(command, function).map { createStringConstantParameter(it) }
val functionValueParameters =
function.valueParameters.map { it.toUserDefinedCommandParameter() }
CommandSignatureVariantFromKFunctionImpl(
receiverParameter = function.extensionReceiverParameter?.toCommandReceiverParameter(),
valueParameters = functionNameAsValueParameter + functionValueParameters,
originFunction = function
) { call ->
function.callSuspend(command, *call.resolvedValueArguments.toTypedArray())
}
}.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(expectingValue: String): CommandValueParameter.StringConstant {
return CommandValueParameter.StringConstant(null, expectingValue)
}
private fun KParameter.toUserDefinedCommandParameter(): CommandValueParameter.UserDefinedType<KParameter> {
return CommandValueParameter.UserDefinedType(nameForCommandParameter(), this, this.isOptional, this.isVararg, this.type)
}
private fun KParameter.nameForCommandParameter(): String? = annotationResolver.getAnnotatedName(command, this) ?: this.name
}

View File

@ -1,334 +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.descriptor.*
import net.mamoe.mirai.console.internal.command.hasAnnotation
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.buildMessageChain
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.*
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>) {
}
@OptIn(ExperimentalCommandDescriptors::class)
private fun <T : Any> CommandParameter<T>.toCommandValueParameter(): CommandValueParameter<T> {
return CommandValueParameter.UserDefinedType<T>(name, null, false, false, type)
}
@OptIn(ExperimentalCommandDescriptors::class)
override val overloads: List<CommandSignatureVariant> by lazy {
subCommands.map { desc ->
CommandSignatureVariantImpl(desc.params.map { it.toCommandValueParameter() }) { call ->
desc.onCommand(call.caller, call.resolvedValueArguments)
}
}
}
interface SubCommandAnnotationResolver {
fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Boolean
fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<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: List<Any?>) -> Boolean,
val context: CommandArgumentContext,
val argumentBuilder: (sender: CommandSender) -> MutableMap<KParameter, Any?>,
) {
val usage: String = createUsage(this@AbstractReflectionCommand)
private fun KParameter.isOptional(): Boolean {
return isOptional || this.type.isMarkedNullable
}
val minimalArgumentsSize = params.count {
!it.parameter.isOptional()
}
@JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
}
}
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 <T1, R1, R2> ((T1) -> R1).then(then: (T1, R1) -> R2): ((T1) -> R2) {
return { a -> then.invoke(a, (this@then(a))) }
}
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})")
}
var argumentBuilder: (sender: CommandSender) -> MutableMap<KParameter, Any?> = { HashMap() }
val parameters = function.parameters.toMutableList()
if (notStatic) {
val type = parameters.removeAt(0) // instance
argumentBuilder = argumentBuilder.then { _, map ->
map[type] = this@createSubCommand
map
}
}
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) {
val senderType = parameters.removeAt(0)
argumentBuilder = argumentBuilder.then { sender, map ->
map[senderType] = sender
map
}
}
}
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<Any>(
paramName,
param.type,
param
)
}.toTypedArray()
// TODO: 2020/09/19 检查 optional/nullable 是否都在最后
@Suppress("UNCHECKED_CAST")
return SubCommandDescriptor(
commandName,
params as Array<CommandParameter<*>>,
subDescription, // overridePermission?.value
permission,//overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission,
onCommand = { _: CommandSender, args ->
val p = parameters.zip(args).toMap(LinkedHashMap())
if (notStatic) p[function.instanceParameter!!] = this@createSubCommand
val result = function.callSuspendBy(p)
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,
argumentBuilder = argumentBuilder
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -261,7 +261,7 @@ internal class TestCommand {
"testOptional"
) {
@SubCommand
fun optional(arg1: String, arg2: String = "Here is optional", arg3: String?) {
fun optional(arg1: String, arg2: String = "Here is optional", arg3: String? = null) {
println(arg1)
println(arg2)
println(arg3)