Support vararg in command

This commit is contained in:
Him188 2020-10-24 13:14:25 +08:00
parent 87b56ade12
commit d10f2b4bea
8 changed files with 187 additions and 49 deletions

View File

@ -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.command.createOrFindCommandPermission
import net.mamoe.mirai.console.internal.data.typeOf0 import net.mamoe.mirai.console.internal.data.typeOf0
import net.mamoe.mirai.console.permission.Permission 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.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<CommandSignatureVariant> = listOf( override val overloads: List<CommandSignatureVariant> = listOf(
CommandSignatureVariantImpl( CommandSignatureVariantImpl(
receiverParameter = CommandReceiverParameter(false, typeOf0<CommandSender>()), receiverParameter = CommandReceiverParameter(false, typeOf0<CommandSender>()),
valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired<MessageChain>("args", true)) valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired<Array<out Message>>("args", true))
) { call -> ) { call ->
val sender = call.caller val sender = call.caller
val arguments = call.rawValueArguments val arguments = call.rawValueArguments
sender.onCommand(arguments.mapTo(MessageChainBuilder()) { it.value }.build()) sender.onCommand(buildMessageChain { arguments.forEach { +it.value } })
} }
) )

View File

@ -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.command.resolve.ResolvedCommandCall
import net.mamoe.mirai.console.internal.data.classifierAsKClass import net.mamoe.mirai.console.internal.data.classifierAsKClass
import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.internal.data.typeOf0
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
@ -173,6 +174,9 @@ public class CommandReceiverParameter<T : CommandSender>(
} }
internal val ANY_TYPE = typeOf0<Any>()
internal val ARRAY_OUT_ANY_TYPE = typeOf0<Array<out Any?>>()
@ExperimentalCommandDescriptors @ExperimentalCommandDescriptors
public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>, AbstractCommandParameter<T>() { public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>, AbstractCommandParameter<T>() {
override fun toString(): String = buildString { override fun toString(): String = buildString {
@ -184,8 +188,19 @@ public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>,
} }
public override fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance { public override fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance {
val expectingType = this.type if (isVararg) {
val arrayElementType = this.type.arguments.single() // Array<T>
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 if (argument.type.isSubtypeOf(expectingType)) return ArgumentAcceptance.Direct
argument.typeVariants.associateWith { typeVariant -> argument.typeVariants.associateWith { typeVariant ->
@ -239,8 +254,12 @@ public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>,
) : AbstractCommandValueParameter<T>() { ) : AbstractCommandValueParameter<T>() {
init { init {
requireNotNull(type.classifierAsKClassOrNull()) { 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 { public companion object {

View File

@ -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.CommandCall
import net.mamoe.mirai.console.command.parse.CommandCallParser import net.mamoe.mirai.console.command.parse.CommandCallParser
import net.mamoe.mirai.console.command.parse.RawCommandArgument import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.console.internal.data.castOrNull
import net.mamoe.mirai.message.data.MessageContent import net.mamoe.mirai.console.internal.data.kClassQualifiedName
import net.mamoe.mirai.message.data.asMessageChain import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.content
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -31,15 +30,18 @@ public interface TypeVariant<out OutType> {
*/ */
public val outType: KType public val outType: KType
public fun mapValue(valueParameter: MessageContent): OutType /**
* @see CommandValueArgument.value
*/
public fun mapValue(valueParameter: Message): OutType
public companion object { public companion object {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
@JvmSynthetic @JvmSynthetic
public inline operator fun <reified OutType> invoke(crossinline block: (valueParameter: RawCommandArgument) -> OutType): TypeVariant<OutType> { public inline operator fun <reified OutType> invoke(crossinline block: (valueParameter: Message) -> OutType): TypeVariant<OutType> {
return object : TypeVariant<OutType> { return object : TypeVariant<OutType> {
override val outType: KType = typeOf<OutType>() override val outType: KType = typeOf<OutType>()
override fun mapValue(valueParameter: MessageContent): OutType = block(valueParameter) override fun mapValue(valueParameter: Message): OutType = block(valueParameter)
} }
} }
} }
@ -49,19 +51,20 @@ public interface TypeVariant<out OutType> {
public object MessageContentTypeVariant : TypeVariant<MessageContent> { public object MessageContentTypeVariant : TypeVariant<MessageContent> {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override val outType: KType = typeOf<MessageContent>() override val outType: KType = typeOf<MessageContent>()
override fun mapValue(valueParameter: MessageContent): MessageContent = valueParameter override fun mapValue(valueParameter: Message): MessageContent =
valueParameter.castOrNull<MessageContent>() ?: error("Accepts MessageContent only but given ${valueParameter.kClassQualifiedName}")
} }
@ExperimentalCommandDescriptors @ExperimentalCommandDescriptors
public object MessageChainTypeVariant : TypeVariant<MessageChain> { public object MessageChainTypeVariant : TypeVariant<MessageChain> {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override val outType: KType = typeOf<MessageChain>() override val outType: KType = typeOf<MessageChain>()
override fun mapValue(valueParameter: MessageContent): MessageChain = valueParameter.asMessageChain() override fun mapValue(valueParameter: Message): MessageChain = valueParameter.asMessageChain()
} }
@ExperimentalCommandDescriptors @ExperimentalCommandDescriptors
public object ContentStringTypeVariant : TypeVariant<String> { public object ContentStringTypeVariant : TypeVariant<String> {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override val outType: KType = typeOf<String>() override val outType: KType = typeOf<String>()
override fun mapValue(valueParameter: MessageContent): String = valueParameter.content override fun mapValue(valueParameter: Message): String = valueParameter.content
} }

View File

@ -12,18 +12,18 @@
package net.mamoe.mirai.console.command.parse package net.mamoe.mirai.console.command.parse
import net.mamoe.mirai.console.command.descriptor.* 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.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.MessageContent
import net.mamoe.mirai.message.data.SingleMessage
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
/**
* For developing use, to be inlined in the future.
*/
public typealias RawCommandArgument = MessageContent
/** /**
* @see CommandValueArgument * @see CommandValueArgument
*/ */
@ -36,7 +36,12 @@ public interface CommandArgument
@ExperimentalCommandDescriptors @ExperimentalCommandDescriptors
public interface CommandValueArgument : CommandArgument { public interface CommandValueArgument : CommandArgument {
public val type: KType public val type: KType
public val value: RawCommandArgument
/**
* [MessageContent] if single argument
* [MessageChain] is vararg
*/
public val value: Message
public val typeVariants: List<TypeVariant<*>> public val typeVariants: List<TypeVariant<*>>
} }
@ -46,7 +51,7 @@ public interface CommandValueArgument : CommandArgument {
@ConsoleExperimentalApi @ConsoleExperimentalApi
@ExperimentalCommandDescriptors @ExperimentalCommandDescriptors
public data class DefaultCommandValueArgument( public data class DefaultCommandValueArgument(
public override val value: RawCommandArgument, public override val value: Message,
) : CommandValueArgument { ) : CommandValueArgument {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override val type: KType = typeOf<MessageContent>() override val type: KType = typeOf<MessageContent>()
@ -73,6 +78,38 @@ public fun <T> CommandValueArgument.mapToType(type: KType): T =
@ExperimentalCommandDescriptors @ExperimentalCommandDescriptors
public fun <T> CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? { public fun <T> CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? {
if (expectingType.isSubtypeOf(ARRAY_OUT_ANY_TYPE)) {
val arrayElementType = expectingType.arguments.single().type ?: ANY_TYPE
val result = ArrayList<Any?>()
when (val value = value) {
is MessageChain -> {
for (message in value) {
result.add(mapToTypeOrNullImpl(arrayElementType, message))
}
}
else -> { // single
value.castOrInternalError<SingleMessage>()
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<Any?> {
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) @OptIn(ExperimentalStdlibApi::class)
val result = typeVariants val result = typeVariants
.filter { it.outType.isSubtypeOf(expectingType) } .filter { it.outType.isSubtypeOf(expectingType) }
@ -85,7 +122,7 @@ public fun <T> CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? {
else typeVariant else typeVariant
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return result.mapValue(value) as T return result.mapValue(value)
} }
@ExperimentalCommandDescriptors @ExperimentalCommandDescriptors

View File

@ -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.descriptor.ArgumentAcceptance.Companion.isNotAcceptable
import net.mamoe.mirai.console.command.parse.CommandCall import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.parse.CommandValueArgument 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.extensions.CommandCallResolverProvider
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.safeCast 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] * Builtin implementation of [CommandCallResolver]
@ -26,11 +29,16 @@ public object BuiltInCommandCallResolver : CommandCallResolver {
val signature = resolveImpl(callee, valueArguments, context) ?: return null 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( private data class ResolveData(
val variant: CommandSignatureVariant, val variant: CommandSignatureVariant,
val zippedArguments: List<Pair<AbstractCommandValueParameter<*>, CommandValueArgument>>,
val argumentAcceptances: List<ArgumentAcceptanceWithIndex>, val argumentAcceptances: List<ArgumentAcceptanceWithIndex>,
val remainingParameters: List<AbstractCommandValueParameter<*>>, val remainingParameters: List<AbstractCommandValueParameter<*>>,
) { ) {
@ -46,32 +54,60 @@ public object BuiltInCommandCallResolver : CommandCallResolver {
callee: Command, callee: Command,
valueArguments: List<CommandValueArgument>, valueArguments: List<CommandValueArgument>,
context: CommandArgumentContext?, context: CommandArgumentContext?,
): CommandSignatureVariant? { ): ResolveData? {
callee.overloads callee.overloads
.mapNotNull l@{ signature -> .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( if (remainingParameters.any { !it.isOptional && !it.isVararg }) return@l null // not enough args. // vararg can be empty.
variant = signature,
argumentAcceptances = zipped.mapIndexed { index, (parameter, argument) -> if (zipped.isEmpty()) {
val accepting = parameter.accepting(argument, context) ResolveData(
if (accepting.isNotAcceptable) { variant = signature,
return@l null // argument type not assignable 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() .takeLongestMatches()
.ifEmpty { return null } .ifEmpty { return null }
.also { result -> result.singleOrNull()?.let { return it.variant } } .also { result -> result.singleOrNull()?.let { return it } }
// take single ArgumentAcceptance.Direct // take single ArgumentAcceptance.Direct
.also { list -> .also { list ->
@ -79,7 +115,7 @@ public object BuiltInCommandCallResolver : CommandCallResolver {
.flatMap { phase -> .flatMap { phase ->
phase.argumentAcceptances.filter { it.acceptance is ArgumentAcceptance.Direct }.map { phase to it } 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) { if (candidates.distinctBy { it.second.index }.size != candidates.size) {
// Resolution ambiguity // Resolution ambiguity
/* /*

View File

@ -89,7 +89,7 @@ internal object Testing {
internal var cont: Continuation<Any?>? = null internal var cont: Continuation<Any?>? = null
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
suspend fun <R> withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R { suspend fun <R> withTesting(timeout: Long = 50000L, block: suspend () -> Unit): R {
@Suppress("RemoveExplicitTypeArguments") // bug @Suppress("RemoveExplicitTypeArguments") // bug
return if (timeout != -1L) { return if (timeout != -1L) {
withTimeout<R>(timeout) { withTimeout<R>(timeout) {

View File

@ -31,10 +31,7 @@ import net.mamoe.mirai.message.data.*
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import kotlin.test.assertEquals import kotlin.test.*
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
object TestCompositeCommand : CompositeCommand( object TestCompositeCommand : CompositeCommand(
ConsoleCommandOwner, 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<String>(),
withTesting {
assertSuccess(sender.executeCommand("/test vararg 1"))
}
)
assertArrayEquals(
arrayOf("s"),
withTesting<Array<String>> {
assertSuccess(sender.executeCommand("/test vararg 1 s"))
}
)
assertArrayEquals(
arrayOf("s", "s", "s"),
withTesting {
assertSuccess(sender.executeCommand("/test vararg 1 s s s"))
}
)
}
}
}
}
fun <T> assertArrayEquals(expected: Array<out T>, actual: Array<out T>, message: String? = null) {
asserter.assertEquals(message, expected.contentToString(), actual.contentToString())
} }
@OptIn(ExperimentalCommandDescriptors::class) @OptIn(ExperimentalCommandDescriptors::class)

View File

@ -15,8 +15,12 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.mamoe.mirai.console.MiraiConsole 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.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.terminal.noconsole.NoConsole
import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.ConsoleInternalApi
import net.mamoe.mirai.console.util.requestInput import net.mamoe.mirai.console.util.requestInput
@ -26,7 +30,7 @@ import org.jline.reader.UserInterruptException
val consoleLogger by lazy { DefaultLogger("console") } val consoleLogger by lazy { DefaultLogger("console") }
@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class) @OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class, ExperimentalCommandDescriptors::class)
internal fun startupConsoleThread() { internal fun startupConsoleThread() {
if (terminal is NoConsole) return if (terminal is NoConsole) return