From 4aa996a41703aca2885c37df647027a44b89d11e Mon Sep 17 00:00:00 2001 From: Him188 Date: Fri, 23 Oct 2020 21:32:04 +0800 Subject: [PATCH] Rework command reflection: - Remove AbstractReflectionCommand - Introduce CommandReflector - Misc improvements --- .../mirai/console/command/CommandSender.kt | 2 +- .../mirai/console/command/CompositeCommand.kt | 46 +-- .../mamoe/mirai/console/command/RawCommand.kt | 11 +- .../mirai/console/command/SimpleCommand.kt | 49 ++- .../command/descriptor/CommandDescriptor.kt | 69 +++- .../console/command/descriptor/Exceptions.kt | 2 +- .../mirai/console/data/AutoSavePluginData.kt | 2 +- .../internal/command/CommandManagerImpl.kt | 6 +- .../internal/command/CommandReflector.kt | 215 +++++++++++ .../command/CompositeCommandInternal.kt | 334 ------------------ .../data/MultiFilePluginDataStorageImpl.kt | 1 - .../console/internal/data/reflectionUtils.kt | 11 +- .../internal/data/valueFromKTypeImpl.kt | 1 - .../mamoe/mirai/console/util/ContactUtils.kt | 2 +- .../mirai/console/command/TestCommand.kt | 2 +- 15 files changed, 332 insertions(+), 421 deletions(-) create mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandReflector.kt delete mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt index 8c3b1d8ed..f3f9b55ee 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt @@ -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 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 7f29eb4a7..c0b75ec89 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 @@ -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 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 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 } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt index 75facfed8..a0af65241 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt @@ -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 = listOf( - CommandSignatureVariantImpl(listOf(CommandValueParameter.UserDefinedType.createRequired("args", true))) { call -> + CommandSignatureVariantImpl( + receiverParameter = CommandReceiverParameter(false, typeOf0()), + valueParameters = listOf(CommandValueParameter.UserDefinedType.createRequired("args", true)) + ) { call -> val sender = call.caller val arguments = call.rawValueArguments sender.onCommand(arguments.mapTo(MessageChainBuilder()) { it.value }.build()) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt index 9288f3430..14861f717 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt @@ -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 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 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) { - super.checkSubCommand(subCommands) - check(subCommands.size == 1) { "There can only be exactly one function annotated with Handler at this moment as overloading is not yet supported." } - } - - @Deprecated("prohibited", level = DeprecationLevel.HIDDEN) - internal override suspend fun CommandSender.onDefault(rawArgs: MessageChain) { - sendMessage(usage) - } - - internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver - get() = SimpleCommandSubCommandAnnotationResolver } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt index 3a59735ad..723a4d94f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandDescriptor.kt @@ -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? + public val valueParameters: List> 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?, override val valueParameters: List>, 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?, + override val valueParameters: List>, + 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 { - 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 : ICommandParameter { + + /** + * 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( + override val isOptional: Boolean, + override val type: KType, +) : ICommandParameter { + override val name: String get() = PARAMETER_NAME + + public companion object { + public const val PARAMETER_NAME: String = "" + } +} + @ExperimentalCommandDescriptors -public sealed class CommandValueParameter : ICommandParameter { +public sealed class CommandValueParameter : ICommandValueParameter { internal fun validate() { // // TODO: 2020/10/18 net.mamoe.mirai.console.command.descriptor.CommandValueParameter.validate$mirai_console_mirai_console_main require(type.classifier?.safeCast>()?.isInstance(defaultValue) == true) { "defaultValue is not instance of type" @@ -132,8 +177,10 @@ public sealed class CommandValueParameter : ICommandParameter { return ArgumentAcceptance.Impossible } + @ConsoleExperimentalApi public class StringConstant( - public override val name: String, + @ConsoleExperimentalApi + public override val name: String?, public val expectingValue: String, ) : CommandValueParameter() { public override val type: KType get() = STRING_TYPE @@ -152,7 +199,7 @@ public sealed class CommandValueParameter : ICommandParameter { * @see createRequired */ public class UserDefinedType( - 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, diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt index f67e712bd..30d167657 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/Exceptions.kt @@ -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 diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt index 0ab74fc19..f081af4d2 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt @@ -14,7 +14,7 @@ package net.mamoe.mirai.console.data import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip +import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip import net.mamoe.mirai.console.internal.plugin.updateWhen import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.* diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt index dcffd9aaa..25476b2b2 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt @@ -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 diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandReflector.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandReflector.kt new file mode 100644 index 000000000..ed4b17f34 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandReflector.kt @@ -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() + + override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array = + function.findAnnotation()!!.value + + override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? = + parameter.findAnnotation()?.value + + override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? = + function.findAnnotation()?.value +} + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +internal object SimpleCommandSubCommandAnnotationResolver : + SubCommandAnnotationResolver { + override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) = + function.hasAnnotation() + + override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array = + ownerCommand.secondaryNames + + override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? = + parameter.findAnnotation()?.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 + 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()) illegalDeclaration("Command function must not be static.") + + // should we allow abstract? + + // if (isAbstract) illegalDeclaration("Command function cannot be abstract") + } + + fun generateUsage(overloads: Iterable): 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 CommandValueParameter.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 { + 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? { + 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 { + return CommandValueParameter.UserDefinedType(nameForCommandParameter(), this, this.isOptional, this.isVararg, this.type) + } + + private fun KParameter.nameForCommandParameter(): String? = annotationResolver.getAnnotatedName(command, this) ?: this.name +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt deleted file mode 100644 index a401e3c19..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt +++ /dev/null @@ -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() - - override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array = - function.findAnnotation()!!.value -} - -internal object SimpleCommandSubCommandAnnotationResolver : - AbstractReflectionCommand.SubCommandAnnotationResolver { - override fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>) = - function.hasAnnotation() - - override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array = - baseCommand.secondaryNames -} - -internal abstract class AbstractReflectionCommand -@JvmOverloads constructor( - owner: CommandOwner, - primaryName: String, - secondaryNames: Array, - description: String = "", - 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 = "" - - 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) { - - } - - @OptIn(ExperimentalCommandDescriptors::class) - private fun CommandParameter.toCommandValueParameter(): CommandValueParameter { - return CommandValueParameter.UserDefinedType(name, null, false, false, type) - } - - - @OptIn(ExperimentalCommandDescriptors::class) - override val overloads: List 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 - } - - internal val subCommands: Array 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, 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 { 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, - val params: Array>, - val description: String, - val permission: Permission, - val onCommand: suspend (sender: CommandSender, parsedArgs: List) -> Boolean, - val context: CommandArgumentContext, - val argumentBuilder: (sender: CommandSender) -> MutableMap, - ) { - 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> = names.map { it.bakeSubName() }.toTypedArray() - } -} - -internal fun Array.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 = 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 KAnnotatedElement.hasAnnotation(): Boolean = - findAnnotation() != null - -internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "" - -internal fun Array.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).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() - //val overridePermission = null//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})") - } - var argumentBuilder: (sender: CommandSender) -> MutableMap = { 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()?.value ?: param.name ?: "unknown" - CommandParameter( - paramName, - param.type, - param - ) - }.toTypedArray() - - // TODO: 2020/09/19 检查 optional/nullable 是否都在最后 - - @Suppress("UNCHECKED_CAST") - return SubCommandDescriptor( - commandName, - params as Array>, - 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 - ) -} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt index 45fa24f88..800229946 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt @@ -12,7 +12,6 @@ package net.mamoe.mirai.console.internal.data import kotlinx.serialization.json.Json import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.data.* -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.SilentLogger diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt index 226dce6cc..54818777e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt @@ -11,14 +11,15 @@ package net.mamoe.mirai.console.internal.data import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.ValueName -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip -import kotlin.reflect.KClass -import kotlin.reflect.KParameter -import kotlin.reflect.KProperty -import kotlin.reflect.KType +import kotlin.reflect.* import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.isSubclassOf +internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "" + +internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = + findAnnotation() != null + @Suppress("UNCHECKED_CAST") internal inline fun KType.toKClass(): KClass { val clazz = requireNotNull(classifier as? KClass) { "Unsupported classifier: $classifier" } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt index 037c65ba5..02e5ddbe9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt @@ -16,7 +16,6 @@ import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.SerializableValue.Companion.serializableValueWith import net.mamoe.mirai.console.data.SerializerAwareValue import net.mamoe.mirai.console.data.valueFromKType -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import kotlin.contracts.contract import kotlin.reflect.KClass import kotlin.reflect.KType diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt index 9b3aef0ed..bdd37219d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ContactUtils.kt @@ -12,7 +12,7 @@ package net.mamoe.mirai.console.util import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip +import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip import net.mamoe.mirai.contact.* /** diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt index 4e11719bc..1e5d26f64 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 @@ -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)