Review command:

Add `CommandReflector.validate` to check declaration clashes;
Rename CommandSignatureVariant to CommandSignature;
Add docs;
Cleanup code;
This commit is contained in:
Him188 2020-10-25 14:49:08 +08:00
parent d1ebe44f3e
commit 291035f978
12 changed files with 109 additions and 140 deletions

View File

@ -12,7 +12,7 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.descriptor.CommandArgumentContextAware
import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariant
import net.mamoe.mirai.console.command.descriptor.CommandSignature
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
@ -52,7 +52,7 @@ public interface Command {
*/
@ConsoleExperimentalApi("Property name is experimental")
@ExperimentalCommandDescriptors
public val overloads: List<CommandSignatureVariant>
public val overloads: List<CommandSignature>
/**
* 用法说明, 用于发送给用户. [usage] 一般包含 [description].

View File

@ -95,8 +95,10 @@ public abstract class CompositeCommand(
private val reflector by lazy { CommandReflector(this, CompositeCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<CommandSignatureVariantFromKFunction> by lazy {
reflector.findSubCommands()
public final override val overloads: List<CommandSignatureFromKFunction> by lazy {
reflector.findSubCommands().also {
reflector.validate(it)
}
}
/**

View File

@ -56,8 +56,8 @@ public abstract class RawCommand(
public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) }
@ExperimentalCommandDescriptors
override val overloads: List<CommandSignatureVariant> = listOf(
CommandSignatureVariantImpl(
override val overloads: List<CommandSignature> = listOf(
CommandSignatureImpl(
receiverParameter = CommandReceiverParameter(false, typeOf0<CommandSender>()),
valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired<Array<out Message>>("args", true))
) { call ->

View File

@ -17,7 +17,6 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.java.JSimpleCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
@ -67,8 +66,9 @@ public abstract class SimpleCommand(
private val reflector by lazy { CommandReflector(this, SimpleCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<CommandSignatureVariantFromKFunction> by lazy {
public final override val overloads: List<CommandSignatureFromKFunction> by lazy {
reflector.findSubCommands().also {
reflector.validate(it)
if (it.isEmpty())
throw IllegalCommandDeclarationException(this, "SimpleCommand must have at least one subcommand, whereas zero present.")
}

View File

@ -1,31 +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("unused")
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.console.command.IllegalCommandArgumentException
import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueArgumentParser.Companion.illegalArgument
/**
* 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范等.
*
* [message] 将会发送给指令调用方.
*
* @see IllegalCommandArgumentException
* @see CommandValueArgumentParser
* @see AbstractCommandValueArgumentParser.illegalArgument
*/
public class CommandArgumentParserException : IllegalCommandArgumentException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}

View File

@ -26,26 +26,46 @@ import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf
/**
* @see CommandSignatureVariantImpl
* 指令签名. 表示指令定义的需要的参数.
*
* @see AbstractCommandSignature
*/
@ExperimentalCommandDescriptors
public interface CommandSignatureVariant {
public interface CommandSignature {
/**
* 接收者参数, [CommandSender] 子类
*/
@ConsoleExperimentalApi
public val receiverParameter: CommandReceiverParameter<out CommandSender>?
/**
* 形式 值参数.
*/
public val valueParameters: List<AbstractCommandValueParameter<*>>
/**
* 调用这个指令.
*/
public suspend fun call(resolvedCommandCall: ResolvedCommandCall)
}
/**
* 来自 [KFunction] 反射得到的 [CommandSignature]
*
* @see CommandSignatureFromKFunctionImpl
*/
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public interface CommandSignatureVariantFromKFunction : CommandSignatureVariant {
public interface CommandSignatureFromKFunction : CommandSignature {
public val originFunction: KFunction<*>
}
/**
* @see CommandSignatureImpl
* @see CommandSignatureFromKFunctionImpl
*/
@ExperimentalCommandDescriptors
public abstract class AbstractCommandSignatureVariant : CommandSignatureVariant {
public abstract class AbstractCommandSignature : CommandSignature {
override fun toString(): String {
val receiverParameter = receiverParameter
return if (receiverParameter == null) {
@ -57,11 +77,11 @@ public abstract class AbstractCommandSignatureVariant : CommandSignatureVariant
}
@ExperimentalCommandDescriptors
public open class CommandSignatureVariantImpl(
public open class CommandSignatureImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<AbstractCommandValueParameter<*>>,
private val onCall: suspend CommandSignatureVariantImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureVariant, AbstractCommandSignatureVariant() {
private val onCall: suspend CommandSignatureImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignature, AbstractCommandSignature() {
override suspend fun call(resolvedCommandCall: ResolvedCommandCall) {
return onCall(resolvedCommandCall)
}
@ -69,12 +89,12 @@ public open class CommandSignatureVariantImpl(
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public open class CommandSignatureVariantFromKFunctionImpl(
public open class CommandSignatureFromKFunctionImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<AbstractCommandValueParameter<*>>,
override val originFunction: KFunction<*>,
private val onCall: suspend CommandSignatureVariantFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureVariantFromKFunction, AbstractCommandSignatureVariant() {
private val onCall: suspend CommandSignatureFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureFromKFunction, AbstractCommandSignature() {
override suspend fun call(resolvedCommandCall: ResolvedCommandCall) {
return onCall(resolvedCommandCall)
}

View File

@ -11,7 +11,9 @@
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.IllegalCommandArgumentException
import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueArgumentParser.Companion.illegalArgument
import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip
@ -28,9 +30,10 @@ public open class NoValueArgumentMappingException(
) : CommandResolutionException("Cannot find a CommandArgument mapping for ${forType.qualifiedName}")
@ExperimentalCommandDescriptors
public open class UnresolvedCommandCallException(
public val call: CommandCall,
) : CommandResolutionException("Unresolved call: $call")
public open class CommandDeclarationClashException(
public val command: Command,
public val signatures: List<CommandSignature>,
) : CommandResolutionException("Command declaration clash: \n${signatures.joinToString("\n")}")
public open class CommandResolutionException : RuntimeException {
public constructor() : super()
@ -38,3 +41,19 @@ public open class CommandResolutionException : RuntimeException {
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}
/**
* 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范等.
*
* [message] 将会发送给指令调用方.
*
* @see IllegalCommandArgumentException
* @see CommandValueArgumentParser
* @see AbstractCommandValueArgumentParser.illegalArgument
*/
public class CommandArgumentParserException : IllegalCommandArgumentException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}

View File

@ -31,13 +31,13 @@ public object BuiltInCommandCallResolver : CommandCallResolver {
return ResolvedCommandCallImpl(call.caller,
callee,
signature.variant,
signature.signature,
signature.zippedArguments.map { it.second },
context ?: EmptyCommandArgumentContext)
}
private data class ResolveData(
val variant: CommandSignatureVariant,
val signature: CommandSignature,
val zippedArguments: List<Pair<AbstractCommandValueParameter<*>, CommandValueArgument>>,
val argumentAcceptances: List<ArgumentAcceptanceWithIndex>,
val remainingParameters: List<AbstractCommandValueParameter<*>>,
@ -69,7 +69,7 @@ public object BuiltInCommandCallResolver : CommandCallResolver {
if (zipped.isEmpty()) {
ResolveData(
variant = signature,
signature = signature,
zippedArguments = emptyList(),
argumentAcceptances = emptyList(),
remainingParameters = remainingParameters,
@ -91,7 +91,7 @@ public object BuiltInCommandCallResolver : CommandCallResolver {
}
ResolveData(
variant = signature,
signature = signature,
zippedArguments = zipped,
argumentAcceptances = zipped.mapIndexed { index, (parameter, argument) ->
val accepting = parameter.accepting(argument, context)
@ -163,7 +163,7 @@ fun main() {
private fun List<ResolveData>.takeLongestMatches(): Collection<ResolveData> {
if (isEmpty()) return emptyList()
return associateWith {
it.variant.valueParameters.size - it.remainingOptionalCount * 1.001 // slightly lower priority with optional defaults.
it.signature.valueParameters.size - it.remainingOptionalCount * 1.001 // slightly lower priority with optional defaults.
}.let { m ->
val maxMatch = m.values.maxByOrNull { it }
m.filter { it.value == maxMatch }.keys

View File

@ -37,9 +37,9 @@ public interface ResolvedCommandCall {
public val callee: Command
/**
* The callee [CommandSignatureVariant], specifically a sub command from [CompositeCommand]
* The callee [CommandSignature], specifically a sub command from [CompositeCommand]
*/
public val calleeSignature: CommandSignatureVariant
public val calleeSignature: CommandSignature
/**
* Original arguments
@ -47,7 +47,7 @@ public interface ResolvedCommandCall {
public val rawValueArguments: List<CommandValueArgument>
/**
* Resolved value arguments arranged mapping the [CommandSignatureVariant.valueParameters] by index.
* Resolved value arguments arranged mapping the [CommandSignature.valueParameters] by index.
*
* **Implementation details**: Lazy calculation.
*/
@ -73,7 +73,7 @@ public suspend inline fun ResolvedCommandCall.call() {
public class ResolvedCommandCallImpl(
override val caller: CommandSender,
override val callee: Command,
override val calleeSignature: CommandSignatureVariant,
override val calleeSignature: CommandSignature,
override val rawValueArguments: List<CommandValueArgument>,
private val context: CommandArgumentContext,
) : ResolvedCommandCall {

View File

@ -11,6 +11,7 @@ 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.KType
import kotlin.reflect.KVisibility
import kotlin.reflect.full.*
@ -133,7 +134,7 @@ internal class CommandReflector(
// if (isAbstract) illegalDeclaration("Command function cannot be abstract")
}
fun generateUsage(overloads: Iterable<CommandSignatureVariantFromKFunction>): String {
fun generateUsage(overloads: Iterable<CommandSignatureFromKFunction>): String {
return overloads.joinToString("\n") { subcommand ->
buildString {
if (command.prefixOptional) {
@ -173,17 +174,45 @@ internal class CommandReflector(
}
}
fun validate(variants: List<CommandSignatureVariantFromKFunctionImpl>) {
fun validate(signatures: List<CommandSignatureFromKFunctionImpl>) {
data class ErasedParameters(
val name: String,
val x: String,
data class ErasedParameterInfo(
val index: Int,
val name: String?,
val type: KType, // ignore nullability
val additional: String?,
)
variants
data class ErasedVariantInfo(
val receiver: ErasedParameterInfo?,
val valueParameters: List<ErasedParameterInfo>,
)
fun CommandParameter<*>.toErasedParameterInfo(index: Int): ErasedParameterInfo {
return ErasedParameterInfo(index,
this.name,
this.type.withNullability(false),
if (this is AbstractCommandValueParameter.StringConstant) this.expectingValue else null)
}
val candidates = signatures.map { variant ->
variant to ErasedVariantInfo(
variant.receiverParameter?.toErasedParameterInfo(0),
variant.valueParameters.mapIndexed { index, parameter -> parameter.toErasedParameterInfo(index) }
)
}
val groups = candidates.groupBy { it.second }
val clashes = groups.entries.find { (_, value) ->
value.size > 1
} ?: return
throw CommandDeclarationClashException(command, clashes.value.map { it.first })
}
@Throws(IllegalCommandDeclarationException::class)
fun findSubCommands(): List<CommandSignatureVariantFromKFunctionImpl> {
fun findSubCommands(): List<CommandSignatureFromKFunctionImpl> {
return command::class.functions // exclude static later
.asSequence()
.filter { it.isSubCommandFunction() }
@ -198,7 +227,7 @@ internal class CommandReflector(
val functionValueParameters =
function.valueParameters.associateBy { it.toUserDefinedCommandParameter() }
CommandSignatureVariantFromKFunctionImpl(
CommandSignatureFromKFunctionImpl(
receiverParameter = function.extensionReceiverParameter?.toCommandReceiverParameter(),
valueParameters = functionNameAsValueParameter + functionValueParameters.keys,
originFunction = function

View File

@ -1,63 +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("unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser
import kotlin.reflect.KParameter
import kotlin.reflect.KType
/*
internal fun Parameter.toCommandParam(): CommandParameter<*> {
val name = getAnnotation(CompositeCommand.Name::class.java)
return CommandParameter(
name?.value ?: this.name
?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"),
this.type.kotlin,
null
)
}
*/
/**
* 指令形式参数.
*/
internal data class CommandParameter<T : Any>(
/**
* 参数名. 不允许重复.
*/
val name: String,
/**
* 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandValueArgumentParser] 解析.
*/
val type: KType, // exact type
val parameter: KParameter, // source parameter
) {
constructor(name: String, type: KType, parameter: KParameter, parser: CommandValueArgumentParser<T>) : this(
name, type, parameter
) {
this._overrideParser = parser
}
@Suppress("PropertyName")
@JvmField
internal var _overrideParser: CommandValueArgumentParser<T>? = null
/**
* 覆盖的 [CommandValueArgumentParser].
*
* 如果非 `null`, 将不会从 [CommandArgumentContext] 寻找 [CommandValueArgumentParser]
*/
val overrideParser: CommandValueArgumentParser<T>? get() = _overrideParser
}

View File

@ -18,13 +18,6 @@ import kotlin.math.max
import kotlin.math.min
internal infix fun Array<String>.matchesBeginning(list: List<Any>): Boolean {
this.forEachIndexed { index, any ->
if (list[index] != any) return false
}
return true
}
internal infix fun Array<out String>.intersectsIgnoringCase(other: Array<out String>): Boolean {
val max = this.size.coerceAtMost(other.size)
for (i in 0 until max) {