mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-10 10:30:13 +08:00
Support vararg in command
This commit is contained in:
parent
87b56ade12
commit
d10f2b4bea
@ -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<CommandSignatureVariant> = listOf(
|
||||
CommandSignatureVariantImpl(
|
||||
receiverParameter = CommandReceiverParameter(false, typeOf0<CommandSender>()),
|
||||
valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired<MessageChain>("args", true))
|
||||
valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired<Array<out Message>>("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 } })
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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<T : CommandSender>(
|
||||
}
|
||||
|
||||
|
||||
internal val ANY_TYPE = typeOf0<Any>()
|
||||
internal val ARRAY_OUT_ANY_TYPE = typeOf0<Array<out Any?>>()
|
||||
|
||||
@ExperimentalCommandDescriptors
|
||||
public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>, AbstractCommandParameter<T>() {
|
||||
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 {
|
||||
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
|
||||
|
||||
argument.typeVariants.associateWith { typeVariant ->
|
||||
@ -239,8 +254,12 @@ public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>,
|
||||
) : AbstractCommandValueParameter<T>() {
|
||||
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 {
|
||||
|
@ -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<out OutType> {
|
||||
*/
|
||||
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 <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> {
|
||||
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> {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
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
|
||||
public object MessageChainTypeVariant : TypeVariant<MessageChain> {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override val outType: KType = typeOf<MessageChain>()
|
||||
override fun mapValue(valueParameter: MessageContent): MessageChain = valueParameter.asMessageChain()
|
||||
override fun mapValue(valueParameter: Message): MessageChain = valueParameter.asMessageChain()
|
||||
}
|
||||
|
||||
@ExperimentalCommandDescriptors
|
||||
public object ContentStringTypeVariant : TypeVariant<String> {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override val outType: KType = typeOf<String>()
|
||||
override fun mapValue(valueParameter: MessageContent): String = valueParameter.content
|
||||
override fun mapValue(valueParameter: Message): String = valueParameter.content
|
||||
}
|
||||
|
@ -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<TypeVariant<*>>
|
||||
}
|
||||
|
||||
@ -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<MessageContent>()
|
||||
@ -73,6 +78,38 @@ public fun <T> CommandValueArgument.mapToType(type: KType): T =
|
||||
|
||||
@ExperimentalCommandDescriptors
|
||||
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)
|
||||
val result = typeVariants
|
||||
.filter { it.outType.isSubtypeOf(expectingType) }
|
||||
@ -85,7 +122,7 @@ public fun <T> CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? {
|
||||
else typeVariant
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result.mapValue(value) as T
|
||||
return result.mapValue(value)
|
||||
}
|
||||
|
||||
@ExperimentalCommandDescriptors
|
||||
|
@ -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<Pair<AbstractCommandValueParameter<*>, CommandValueArgument>>,
|
||||
val argumentAcceptances: List<ArgumentAcceptanceWithIndex>,
|
||||
val remainingParameters: List<AbstractCommandValueParameter<*>>,
|
||||
) {
|
||||
@ -46,32 +54,60 @@ public object BuiltInCommandCallResolver : CommandCallResolver {
|
||||
callee: Command,
|
||||
valueArguments: List<CommandValueArgument>,
|
||||
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
|
||||
/*
|
||||
|
@ -89,7 +89,7 @@ internal object Testing {
|
||||
internal var cont: Continuation<Any?>? = null
|
||||
|
||||
@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
|
||||
return if (timeout != -1L) {
|
||||
withTimeout<R>(timeout) {
|
||||
|
@ -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<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)
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user