From d10f2b4bea2cf4ad147814c04358ebf7a66b51d8 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sat, 24 Oct 2020 13:14:25 +0800 Subject: [PATCH] Support vararg in command --- .../mamoe/mirai/console/command/RawCommand.kt | 7 +- .../command/descriptor/CommandDescriptor.kt | 23 +++++- .../console/command/descriptor/TypeVariant.kt | 25 ++++--- .../command/parse/CommandValueArgument.kt | 53 +++++++++++--- .../resolve/BuiltInCommandCallResolver.kt | 72 ++++++++++++++----- .../mamoe/mirai/console/TestMiraiConosle.kt | 2 +- .../mirai/console/command/TestCommand.kt | 46 ++++++++++-- .../mirai/console/terminal/ConsoleThread.kt | 8 ++- 8 files changed, 187 insertions(+), 49 deletions(-) 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 0da2747ec..2486f6f42 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 @@ -19,8 +19,9 @@ import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.internal.data.typeOf0 import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.MessageChainBuilder +import net.mamoe.mirai.message.data.buildMessageChain /** * 无参数解析, 接收原生参数的指令. @@ -58,11 +59,11 @@ public abstract class RawCommand( override val overloads: List = listOf( CommandSignatureVariantImpl( receiverParameter = CommandReceiverParameter(false, typeOf0()), - valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired("args", true)) + valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired>("args", true)) ) { call -> val sender = call.caller val arguments = call.rawValueArguments - sender.onCommand(arguments.mapTo(MessageChainBuilder()) { it.value }.build()) + sender.onCommand(buildMessageChain { arguments.forEach { +it.value } }) } ) 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 805111f4a..556a44e6c 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 @@ -17,6 +17,7 @@ import net.mamoe.mirai.console.command.parse.CommandValueArgument import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall import net.mamoe.mirai.console.internal.data.classifierAsKClass import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull +import net.mamoe.mirai.console.internal.data.typeOf0 import net.mamoe.mirai.console.util.ConsoleExperimentalApi import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -173,6 +174,9 @@ public class CommandReceiverParameter( } +internal val ANY_TYPE = typeOf0() +internal val ARRAY_OUT_ANY_TYPE = typeOf0>() + @ExperimentalCommandDescriptors public sealed class AbstractCommandValueParameter : CommandValueParameter, AbstractCommandParameter() { override fun toString(): String = buildString { @@ -184,8 +188,19 @@ public sealed class AbstractCommandValueParameter : CommandValueParameter, } public override fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance { - val expectingType = this.type + if (isVararg) { + val arrayElementType = this.type.arguments.single() // Array + return acceptingImpl(arrayElementType.type ?: ANY_TYPE, argument, commandArgumentContext) + } + return acceptingImpl(this.type, argument, commandArgumentContext) + } + + private fun acceptingImpl( + expectingType: KType, + argument: CommandValueArgument, + commandArgumentContext: CommandArgumentContext?, + ): ArgumentAcceptance { if (argument.type.isSubtypeOf(expectingType)) return ArgumentAcceptance.Direct argument.typeVariants.associateWith { typeVariant -> @@ -239,8 +254,12 @@ public sealed class AbstractCommandValueParameter : CommandValueParameter, ) : AbstractCommandValueParameter() { init { requireNotNull(type.classifierAsKClassOrNull()) { - "CommandReceiverParameter.type.classifier must be KClass." + "type.classifier must be KClass." } + if (isVararg) + check(type.isSubtypeOf(ARRAY_OUT_ANY_TYPE)) { + "type must be subtype of Array if vararg. Given $type." + } } public companion object { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/TypeVariant.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/TypeVariant.kt index 86761807d..4e2792638 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/TypeVariant.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/TypeVariant.kt @@ -11,11 +11,10 @@ package net.mamoe.mirai.console.command.descriptor import net.mamoe.mirai.console.command.parse.CommandCall import net.mamoe.mirai.console.command.parse.CommandCallParser -import net.mamoe.mirai.console.command.parse.RawCommandArgument -import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.MessageContent -import net.mamoe.mirai.message.data.asMessageChain -import net.mamoe.mirai.message.data.content +import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.internal.data.castOrNull +import net.mamoe.mirai.console.internal.data.kClassQualifiedName +import net.mamoe.mirai.message.data.* import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -31,15 +30,18 @@ public interface TypeVariant { */ public val outType: KType - public fun mapValue(valueParameter: MessageContent): OutType + /** + * @see CommandValueArgument.value + */ + public fun mapValue(valueParameter: Message): OutType public companion object { @OptIn(ExperimentalStdlibApi::class) @JvmSynthetic - public inline operator fun invoke(crossinline block: (valueParameter: RawCommandArgument) -> OutType): TypeVariant { + public inline operator fun invoke(crossinline block: (valueParameter: Message) -> OutType): TypeVariant { return object : TypeVariant { override val outType: KType = typeOf() - override fun mapValue(valueParameter: MessageContent): OutType = block(valueParameter) + override fun mapValue(valueParameter: Message): OutType = block(valueParameter) } } } @@ -49,19 +51,20 @@ public interface TypeVariant { public object MessageContentTypeVariant : TypeVariant { @OptIn(ExperimentalStdlibApi::class) override val outType: KType = typeOf() - override fun mapValue(valueParameter: MessageContent): MessageContent = valueParameter + override fun mapValue(valueParameter: Message): MessageContent = + valueParameter.castOrNull() ?: error("Accepts MessageContent only but given ${valueParameter.kClassQualifiedName}") } @ExperimentalCommandDescriptors public object MessageChainTypeVariant : TypeVariant { @OptIn(ExperimentalStdlibApi::class) override val outType: KType = typeOf() - override fun mapValue(valueParameter: MessageContent): MessageChain = valueParameter.asMessageChain() + override fun mapValue(valueParameter: Message): MessageChain = valueParameter.asMessageChain() } @ExperimentalCommandDescriptors public object ContentStringTypeVariant : TypeVariant { @OptIn(ExperimentalStdlibApi::class) override val outType: KType = typeOf() - override fun mapValue(valueParameter: MessageContent): String = valueParameter.content + override fun mapValue(valueParameter: Message): String = valueParameter.content } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt index b20c14e00..30e908e60 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/parse/CommandValueArgument.kt @@ -12,18 +12,18 @@ package net.mamoe.mirai.console.command.parse import net.mamoe.mirai.console.command.descriptor.* +import net.mamoe.mirai.console.internal.data.castOrInternalError +import net.mamoe.mirai.console.internal.data.classifierAsKClass import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageContent +import net.mamoe.mirai.message.data.SingleMessage import kotlin.reflect.KType import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.typeOf -/** - * For developing use, to be inlined in the future. - */ -public typealias RawCommandArgument = MessageContent - /** * @see CommandValueArgument */ @@ -36,7 +36,12 @@ public interface CommandArgument @ExperimentalCommandDescriptors public interface CommandValueArgument : CommandArgument { public val type: KType - public val value: RawCommandArgument + + /** + * [MessageContent] if single argument + * [MessageChain] is vararg + */ + public val value: Message public val typeVariants: List> } @@ -46,7 +51,7 @@ public interface CommandValueArgument : CommandArgument { @ConsoleExperimentalApi @ExperimentalCommandDescriptors public data class DefaultCommandValueArgument( - public override val value: RawCommandArgument, + public override val value: Message, ) : CommandValueArgument { @OptIn(ExperimentalStdlibApi::class) override val type: KType = typeOf() @@ -73,6 +78,38 @@ public fun CommandValueArgument.mapToType(type: KType): T = @ExperimentalCommandDescriptors public fun CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? { + if (expectingType.isSubtypeOf(ARRAY_OUT_ANY_TYPE)) { + val arrayElementType = expectingType.arguments.single().type ?: ANY_TYPE + + val result = ArrayList() + + when (val value = value) { + is MessageChain -> { + for (message in value) { + result.add(mapToTypeOrNullImpl(arrayElementType, message)) + } + } + else -> { // single + value.castOrInternalError() + result.add(mapToTypeOrNullImpl(arrayElementType, value)) + } + } + + + @Suppress("UNCHECKED_CAST") + return result.toArray(arrayElementType.createArray(result.size)) as T + } + + @Suppress("UNCHECKED_CAST") + return mapToTypeOrNullImpl(expectingType, value) as T +} + +private fun KType.createArray(size: Int): Array { + return java.lang.reflect.Array.newInstance(this.classifierAsKClass().javaObjectType, size).castOrInternalError() +} + +@OptIn(ExperimentalCommandDescriptors::class) +private fun CommandValueArgument.mapToTypeOrNullImpl(expectingType: KType, value: Message): Any? { @OptIn(ExperimentalStdlibApi::class) val result = typeVariants .filter { it.outType.isSubtypeOf(expectingType) } @@ -85,7 +122,7 @@ public fun CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? { else typeVariant } @Suppress("UNCHECKED_CAST") - return result.mapValue(value) as T + return result.mapValue(value) } @ExperimentalCommandDescriptors diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt index 0409808ac..98a203118 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/resolve/BuiltInCommandCallResolver.kt @@ -6,9 +6,12 @@ import net.mamoe.mirai.console.command.descriptor.* import net.mamoe.mirai.console.command.descriptor.ArgumentAcceptance.Companion.isNotAcceptable import net.mamoe.mirai.console.command.parse.CommandCall import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.command.parse.DefaultCommandValueArgument import net.mamoe.mirai.console.extensions.CommandCallResolverProvider import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.safeCast +import net.mamoe.mirai.message.data.EmptyMessageChain +import net.mamoe.mirai.message.data.asMessageChain /** * Builtin implementation of [CommandCallResolver] @@ -26,11 +29,16 @@ public object BuiltInCommandCallResolver : CommandCallResolver { val signature = resolveImpl(callee, valueArguments, context) ?: return null - return ResolvedCommandCallImpl(call.caller, callee, signature, call.valueArguments, context ?: EmptyCommandArgumentContext) + return ResolvedCommandCallImpl(call.caller, + callee, + signature.variant, + signature.zippedArguments.map { it.second }, + context ?: EmptyCommandArgumentContext) } private data class ResolveData( val variant: CommandSignatureVariant, + val zippedArguments: List, CommandValueArgument>>, val argumentAcceptances: List, val remainingParameters: List>, ) { @@ -46,32 +54,60 @@ public object BuiltInCommandCallResolver : CommandCallResolver { callee: Command, valueArguments: List, context: CommandArgumentContext?, - ): CommandSignatureVariant? { + ): ResolveData? { + callee.overloads .mapNotNull l@{ signature -> - val zipped = signature.valueParameters.zip(valueArguments) + val valueParameters = signature.valueParameters - val remaining = signature.valueParameters.drop(zipped.size) + val zipped = valueParameters.zip(valueArguments).toMutableList() - if (remaining.any { !it.isOptional }) return@l null // not enough args + val remainingParameters = valueParameters.drop(zipped.size).toMutableList() - ResolveData( - variant = signature, - argumentAcceptances = zipped.mapIndexed { index, (parameter, argument) -> - val accepting = parameter.accepting(argument, context) - if (accepting.isNotAcceptable) { - return@l null // argument type not assignable + if (remainingParameters.any { !it.isOptional && !it.isVararg }) return@l null // not enough args. // vararg can be empty. + + if (zipped.isEmpty()) { + ResolveData( + variant = signature, + zippedArguments = emptyList(), + argumentAcceptances = emptyList(), + remainingParameters = remainingParameters, + ) + } else { + if (valueArguments.size > valueParameters.size && zipped.last().first.isVararg) { + // merge vararg arguments + val (varargParameter, varargFirstArgument) + = zipped.removeLast() + + zipped.add(varargParameter to DefaultCommandValueArgument(valueArguments.drop(zipped.size).map { it.value }.asMessageChain())) + } else { + // add default empty vararg argument + val remainingVararg = remainingParameters.find { it.isVararg } + if (remainingVararg != null) { + zipped.add(remainingVararg to DefaultCommandValueArgument(EmptyMessageChain)) + remainingParameters.remove(remainingVararg) } - ArgumentAcceptanceWithIndex(index, accepting) - }, - remainingParameters = remaining - ) + } + + ResolveData( + variant = signature, + zippedArguments = zipped, + argumentAcceptances = zipped.mapIndexed { index, (parameter, argument) -> + val accepting = parameter.accepting(argument, context) + if (accepting.isNotAcceptable) { + return@l null // argument type not assignable + } + ArgumentAcceptanceWithIndex(index, accepting) + }, + remainingParameters = remainingParameters + ) + } } - .also { result -> result.singleOrNull()?.let { return it.variant } } + .also { result -> result.singleOrNull()?.let { return it } } .takeLongestMatches() .ifEmpty { return null } - .also { result -> result.singleOrNull()?.let { return it.variant } } + .also { result -> result.singleOrNull()?.let { return it } } // take single ArgumentAcceptance.Direct .also { list -> @@ -79,7 +115,7 @@ public object BuiltInCommandCallResolver : CommandCallResolver { .flatMap { phase -> phase.argumentAcceptances.filter { it.acceptance is ArgumentAcceptance.Direct }.map { phase to it } } - candidates.singleOrNull()?.let { return it.first.variant } // single Direct + candidates.singleOrNull()?.let { return it.first } // single Direct if (candidates.distinctBy { it.second.index }.size != candidates.size) { // Resolution ambiguity /* diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt index 969f03425..c1eb1145c 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt @@ -89,7 +89,7 @@ internal object Testing { internal var cont: Continuation? = null @Suppress("UNCHECKED_CAST") - suspend fun withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R { + suspend fun withTesting(timeout: Long = 50000L, block: suspend () -> Unit): R { @Suppress("RemoveExplicitTypeArguments") // bug return if (timeout != -1L) { withTimeout(timeout) { diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt index 70b65b298..f4943150d 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 @@ -31,10 +31,7 @@ import net.mamoe.mirai.message.data.* import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertSame -import kotlin.test.assertTrue +import kotlin.test.* object TestCompositeCommand : CompositeCommand( ConsoleCommandOwner, @@ -293,6 +290,47 @@ internal class TestCommand { } } } + + @Test + fun `test vararg`() { + runBlocking { + val optionCommand = object : CompositeCommand( + ConsoleCommandOwner, + "test" + ) { + @SubCommand + fun vararg(arg1: Int, vararg x: String) { + assertEquals(1, arg1) + Testing.ok(x) + } + } + optionCommand.withRegistration { + assertArrayEquals( + emptyArray(), + withTesting { + assertSuccess(sender.executeCommand("/test vararg 1")) + } + ) + + assertArrayEquals( + arrayOf("s"), + withTesting> { + assertSuccess(sender.executeCommand("/test vararg 1 s")) + } + ) + assertArrayEquals( + arrayOf("s", "s", "s"), + withTesting { + assertSuccess(sender.executeCommand("/test vararg 1 s s s")) + } + ) + } + } + } +} + +fun assertArrayEquals(expected: Array, actual: Array, message: String? = null) { + asserter.assertEquals(message, expected.contentToString(), actual.contentToString()) } @OptIn(ExperimentalCommandDescriptors::class) diff --git a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt index 021d58ae6..179901546 100644 --- a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt @@ -15,8 +15,12 @@ import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.BuiltInCommands +import net.mamoe.mirai.console.command.CommandExecuteStatus +import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.terminal.noconsole.NoConsole import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.requestInput @@ -26,7 +30,7 @@ import org.jline.reader.UserInterruptException val consoleLogger by lazy { DefaultLogger("console") } -@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class) +@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class, ExperimentalCommandDescriptors::class) internal fun startupConsoleThread() { if (terminal is NoConsole) return