diff --git a/backend/codegen/README.md b/backend/codegen/README.md index 013c05c7b..477674b6d 100644 --- a/backend/codegen/README.md +++ b/backend/codegen/README.md @@ -2,5 +2,5 @@ 后端代码生成模块,用于最小化重复代码的人工成本。 -- `MessageScope` 代码生成: [MessageScopeCodegen.kt: Line 33](src/main/kotlin/net/mamoe/mirai/console/codegen/MessageScopeCodegen.kt#L33) -- `Value` 和 `PluginData` 相关代码生成: [ValueSettingCodegen.kt: Line 18](src/main/kotlin/net/mamoe/mirai/console/codegen/ValuePluginDataCodegen.kt#L18) +- `MessageScope` 代码生成: [MessageScopeCodegen.kt: Line 33](src/MessageScopeCodegen.kt#L33) +- `Value` 和 `PluginData` 相关代码生成: [ValueSettingCodegen.kt: Line 18](src/ValuePluginDataCodegen.kt#L18) diff --git a/backend/mirai-console/src/MiraiConsole.kt b/backend/mirai-console/src/MiraiConsole.kt index 9f26892c2..8360bd388 100644 --- a/backend/mirai-console/src/MiraiConsole.kt +++ b/backend/mirai-console/src/MiraiConsole.kt @@ -24,11 +24,13 @@ import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.center.PluginCenter import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader +import net.mamoe.mirai.console.util.AnsiMessageBuilder import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScopeContext import net.mamoe.mirai.console.util.SemVersion import net.mamoe.mirai.utils.BotConfiguration +import net.mamoe.mirai.utils.DefaultLogger import net.mamoe.mirai.utils.MiraiLogger import java.io.File import java.nio.file.Path @@ -90,6 +92,17 @@ public interface MiraiConsole : CoroutineScope { @ConsoleExperimentalApi public fun createLogger(identity: String?): MiraiLogger + /** + * 是否支持使用 Ansi 输出彩色信息 + * + * 注: 不是每个前端都可能提供 `org.fusesource.jansi:jansi` 库支持, + * 请不要直接使用 `org.fusesource.jansi:jansi` + * + * @see [AnsiMessageBuilder] + */ + @ConsoleExperimentalApi + public val isAnsiSupported: Boolean + public companion object INSTANCE : MiraiConsole by MiraiConsoleImplementationBridge { /** * 获取 [MiraiConsole] 的 [Job] @@ -128,6 +141,9 @@ public interface MiraiConsole : CoroutineScope { var config = BotConfiguration().apply { fileBasedDeviceInfo() redirectNetworkLogToDirectory() + this.botLoggerSupplier = { + DefaultLogger("Bot.${it.id}") + } parentCoroutineContext = MiraiConsole.childScopeContext("Bot $id") this.loginSolver = MiraiConsoleImplementationBridge.createLoginSolver(id, this) @@ -151,6 +167,8 @@ public interface MiraiConsole : CoroutineScope { public val isActive: Boolean get() = job.isActive } + + } /** diff --git a/backend/mirai-console/src/MiraiConsoleImplementation.kt b/backend/mirai-console/src/MiraiConsoleImplementation.kt index 418b50536..0f5afd22e 100644 --- a/backend/mirai-console/src/MiraiConsoleImplementation.kt +++ b/backend/mirai-console/src/MiraiConsoleImplementation.kt @@ -18,8 +18,8 @@ import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.command.ConsoleCommandSender import net.mamoe.mirai.console.data.PluginDataStorage import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge -import net.mamoe.mirai.console.logging.LoggerController import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl +import net.mamoe.mirai.console.logging.LoggerController import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.util.ConsoleInput @@ -172,11 +172,19 @@ public interface MiraiConsoleImplementation : CoroutineScope { */ public fun createLogger(identity: String?): MiraiLogger + /** + * 该前端是否支持使用 Ansi 输出彩色信息 + * + * 注: 若为 `true`, 建议携带 `org.fusesource.jansi:jansi` + */ + public val isAnsiSupported: Boolean get() = false + /** * 前端预先定义的 [LoggerController], 以允许前端使用自己的配置系统 */ public val loggerController: LoggerController get() = LoggerControllerImpl + public companion object { internal lateinit var instance: MiraiConsoleImplementation private val initLock = ReentrantLock() diff --git a/backend/mirai-console/src/command/BuiltInCommands.kt b/backend/mirai-console/src/command/BuiltInCommands.kt index 9da243d3b..33141108d 100644 --- a/backend/mirai-console/src/command/BuiltInCommands.kt +++ b/backend/mirai-console/src/command/BuiltInCommands.kt @@ -41,8 +41,10 @@ import net.mamoe.mirai.console.permission.PermissionService.Companion.permit import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.plugin.version +import net.mamoe.mirai.console.util.AnsiMessageBuilder import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleInternalApi +import net.mamoe.mirai.console.util.sendAnsiMessage import net.mamoe.mirai.event.events.EventCancelledException import net.mamoe.mirai.message.nextMessageOrNull import net.mamoe.mirai.utils.secondsToMillis @@ -355,48 +357,102 @@ public object BuiltInCommands { ), BuiltInCommandInternal { @Handler public suspend fun CommandSender.handle() { - sendMessage(buildString { + sendAnsiMessage { val buildDateFormatted = MiraiConsoleBuildConstants.buildDate.atZone(ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - - append("Running MiraiConsole v${MiraiConsoleBuildConstants.versionConst}, built on ").append(buildDateFormatted) - .append(".\n") + append("Running MiraiConsole v") + gold().append(MiraiConsoleBuildConstants.versionConst) + reset().append(", built on ") + lightBlue().append(buildDateFormatted).reset().append(".\n") append(MiraiConsoleImplementationBridge.frontEndDescription.render()).append("\n\n") append("Permission Service: ").append( if (PermissionService.INSTANCE is BuiltInPermissionService) { + lightYellow() "Built In Permission Service" } else { val plugin = PermissionServiceProvider.providerPlugin if (plugin == null) { PermissionService.INSTANCE.toString() } else { - "${plugin.name} v${plugin.version}" + green().append(plugin.name).reset().append(" v").gold() + plugin.version.toString() } } ) - append("\n\n") + reset().append("\n\n") append("Plugins: ") if (PluginManagerImpl.resolvedPlugins.isEmpty()) { - append("") + gray().append("") } else { PluginManagerImpl.resolvedPlugins.joinTo(this) { plugin -> - "${plugin.name} v${plugin.version}" + green().append(plugin.name).reset().append(" v").gold() + plugin.version.toString() } } - append("\n\n") + reset().append("\n\n") + val memoryMXBean = ManagementFactory.getMemoryMXBean() append("Object Pending Finalization Count: ") + .emeraldGreen() .append(memoryMXBean.objectPendingFinalizationCount) + .reset() .append("\n") + val l1 = arrayOf("committed", "init", "used", "max") + val l2 = renderMemoryUsage(memoryMXBean.heapMemoryUsage) + val l3 = renderMemoryUsage(memoryMXBean.nonHeapMemoryUsage) + val lmax = calculateMax(l1, l2.first, l3.first) + + append(" ") + l1.forEachIndexed { index, s -> + if (index != 0) append(" | ") + renderMUNum(lmax[index], s.length) { append(s); reset() } + } + reset() + append("\n") + + fun rendMU(l: Pair, LongArray>) { + val max = l.second[3] + val e50 = max / 2 + val e90 = max * 90 / 100 + l.first.forEachIndexed { index, s -> + if (index != 0) append(" | ") + renderMUNum(lmax[index], s.length) { + if (index == 3) { + // MAX + append(s) + } else { + if (max < 0L) { + append(s) + } else { + val v = l.second[index] + when { + v < e50 -> { + green() + } + v < e90 -> { + lightRed() + } + else -> { + red() + } + } + append(s) + reset() + } + } + } + } + } append(" Heap Memory: ") - renderMemoryUsage(memoryMXBean.heapMemoryUsage) + rendMU(l2) append("\nNon-Heap Memory: ") + rendMU(l3) renderMemoryUsage(memoryMXBean.nonHeapMemoryUsage) - }) + } } private const val MEM_B = 1024L @@ -408,7 +464,7 @@ public object BuiltInCommands { private inline fun StringBuilder.appendDouble(number: Double): StringBuilder = append(floor(number * 100) / 100) - private fun StringBuilder.renderMemoryUsageNumber(num: Long) { + private fun renderMemoryUsageNumber(num: Long) = buildString { when { num == -1L -> { append(num) @@ -428,17 +484,39 @@ public object BuiltInCommands { } } + private fun AnsiMessageBuilder.renderMemoryUsage(usage: MemoryUsage) = arrayOf( + renderMemoryUsageNumber(usage.committed), + renderMemoryUsageNumber(usage.init), + renderMemoryUsageNumber(usage.used), + renderMemoryUsageNumber(usage.max), + ) to longArrayOf( + usage.committed, + usage.init, + usage.used, + usage.max, + ) - private fun StringBuilder.renderMemoryUsage(usage: MemoryUsage) { - append("(committed / init / used / max) [") - renderMemoryUsageNumber(usage.committed) - append(", ") - renderMemoryUsageNumber(usage.init) - append(", ") - renderMemoryUsageNumber(usage.used) - append(", ") - renderMemoryUsageNumber(usage.max) - append("]") + private var emptyLine = " ".repeat(10) + private fun Appendable.emptyLine(size: Int) { + if (emptyLine.length <= size) { + emptyLine = String(CharArray(size) { ' ' }) + } + append(emptyLine, 0, size) + } + + private inline fun AnsiMessageBuilder.renderMUNum(size: Int, contentLength: Int, code: () -> Unit) { + val s = size - contentLength + val left = s / 2 + val right = s - left + emptyLine(left) + code() + emptyLine(right) + } + + private fun calculateMax( + vararg lines: Array + ): IntArray = IntArray(lines[0].size) { r -> + lines.maxOf { it[r].length } } } } \ No newline at end of file diff --git a/backend/mirai-console/src/command/Command.kt b/backend/mirai-console/src/command/Command.kt index 6ea89734e..675481731 100644 --- a/backend/mirai-console/src/command/Command.kt +++ b/backend/mirai-console/src/command/Command.kt @@ -16,6 +16,7 @@ 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 +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.util.ConsoleExperimentalApi @@ -89,6 +90,7 @@ public interface Command { * 指令拥有者. * @see CommandOwner */ + @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) public val owner: CommandOwner public companion object { diff --git a/backend/mirai-console/src/command/CompositeCommand.kt b/backend/mirai-console/src/command/CompositeCommand.kt index 46be20fdc..b3598c6f2 100644 --- a/backend/mirai-console/src/command/CompositeCommand.kt +++ b/backend/mirai-console/src/command/CompositeCommand.kt @@ -20,6 +20,7 @@ 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.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.internal.command.CommandReflector import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.permission.Permission @@ -82,7 +83,7 @@ import kotlin.annotation.AnnotationTarget.FUNCTION * @see buildCommandArgumentContext */ public abstract class CompositeCommand( - owner: CommandOwner, + @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) owner: CommandOwner, @ResolveContext(COMMAND_NAME) primaryName: String, @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, description: String = "no description available", diff --git a/backend/mirai-console/src/command/RawCommand.kt b/backend/mirai-console/src/command/RawCommand.kt index fda863120..e68e34bad 100644 --- a/backend/mirai-console/src/command/RawCommand.kt +++ b/backend/mirai-console/src/command/RawCommand.kt @@ -15,6 +15,7 @@ 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.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.internal.command.findOrCreateCommandPermission import net.mamoe.mirai.console.internal.data.typeOf0 import net.mamoe.mirai.console.permission.Permission @@ -38,11 +39,14 @@ public abstract class RawCommand( * 指令拥有者. * @see CommandOwner */ + @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) public override val owner: CommandOwner, /** 主指令名. */ - @ResolveContext(COMMAND_NAME) public override val primaryName: String, + @ResolveContext(COMMAND_NAME) + public override val primaryName: String, /** 次要指令名. */ - @ResolveContext(COMMAND_NAME) public override vararg val secondaryNames: String, + @ResolveContext(COMMAND_NAME) + public override vararg val secondaryNames: String, /** 用法说明, 用于发送给用户 */ public override val usage: String = "", /** 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] */ diff --git a/backend/mirai-console/src/command/SimpleCommand.kt b/backend/mirai-console/src/command/SimpleCommand.kt index 6f78c8a42..46b0a2343 100644 --- a/backend/mirai-console/src/command/SimpleCommand.kt +++ b/backend/mirai-console/src/command/SimpleCommand.kt @@ -21,6 +21,7 @@ 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.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.internal.command.CommandReflector import net.mamoe.mirai.console.internal.command.IllegalCommandDeclarationException import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver @@ -53,7 +54,7 @@ import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER * @see [CommandManager.executeCommand] */ public abstract class SimpleCommand( - owner: CommandOwner, + @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) owner: CommandOwner, @ResolveContext(COMMAND_NAME) primaryName: String, @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, description: String = "no description available", diff --git a/backend/mirai-console/src/command/java/JCompositeCommand.kt b/backend/mirai-console/src/command/java/JCompositeCommand.kt index aba439d1c..c7be80672 100644 --- a/backend/mirai-console/src/command/java/JCompositeCommand.kt +++ b/backend/mirai-console/src/command/java/JCompositeCommand.kt @@ -17,6 +17,7 @@ import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.permission.Permission /** @@ -71,7 +72,7 @@ import net.mamoe.mirai.console.permission.Permission */ public abstract class JCompositeCommand @JvmOverloads constructor( - owner: CommandOwner, + @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) owner: CommandOwner, @ResolveContext(COMMAND_NAME) primaryName: String, @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, parentPermission: Permission = owner.parentPermission, diff --git a/backend/mirai-console/src/command/java/JRawCommand.kt b/backend/mirai-console/src/command/java/JRawCommand.kt index e3cf80512..a1652296b 100644 --- a/backend/mirai-console/src/command/java/JRawCommand.kt +++ b/backend/mirai-console/src/command/java/JRawCommand.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.console.command.CommandOwner 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 +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.internal.command.findOrCreateCommandPermission import net.mamoe.mirai.console.permission.Permission @@ -51,9 +52,12 @@ public abstract class JRawCommand * 指令拥有者. * @see CommandOwner */ + @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) public override val owner: CommandOwner, - @ResolveContext(COMMAND_NAME) public override val primaryName: String, - @ResolveContext(COMMAND_NAME) public override vararg val secondaryNames: String, + @ResolveContext(COMMAND_NAME) + public override val primaryName: String, + @ResolveContext(COMMAND_NAME) + public override vararg val secondaryNames: String, parentPermission: Permission = owner.parentPermission, ) : Command { /** 用法说明, 用于发送给用户 */ diff --git a/backend/mirai-console/src/command/java/JSimpleCommand.kt b/backend/mirai-console/src/command/java/JSimpleCommand.kt index d5aa6d3ed..cf80c1ab3 100644 --- a/backend/mirai-console/src/command/java/JSimpleCommand.kt +++ b/backend/mirai-console/src/command/java/JSimpleCommand.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.console.command.descriptor.CommandArgumentContext 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 +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.permission.Permission /** @@ -43,7 +44,7 @@ import net.mamoe.mirai.console.permission.Permission * @see [CommandManager.executeCommand] */ public abstract class JSimpleCommand( - owner: CommandOwner, + @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) owner: CommandOwner, @ResolveContext(COMMAND_NAME) primaryName: String, @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, basePermission: Permission, diff --git a/backend/mirai-console/src/common/ResolveContext.kt b/backend/mirai-console/src/common/ResolveContext.kt index 93a5006e3..592c2179f 100644 --- a/backend/mirai-console/src/common/ResolveContext.kt +++ b/backend/mirai-console/src/common/ResolveContext.kt @@ -59,7 +59,7 @@ public annotation class ResolveContext( /** * @see SemVersion.Companion.parseRangeRequirement */ - VERSION_REQUIREMENT, // ILLEGAL_VERSION_REQUIREMENT // TODO + VERSION_REQUIREMENT, // ILLEGAL_VERSION_REQUIREMENT /** * @see Command.allNames @@ -87,5 +87,7 @@ public annotation class ResolveContext( * @see PluginData.value */ RESTRICTED_NO_ARG_CONSTRUCTOR, // NOT_CONSTRUCTABLE_TYPE + + RESTRICTED_CONSOLE_COMMAND_OWNER, } } \ No newline at end of file diff --git a/backend/mirai-console/src/compiler/common/ResolveContext.kt b/backend/mirai-console/src/compiler/common/ResolveContext.kt new file mode 100644 index 000000000..592c2179f --- /dev/null +++ b/backend/mirai-console/src/compiler/common/ResolveContext.kt @@ -0,0 +1,93 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("unused") + +package net.mamoe.mirai.console.compiler.common + +import net.mamoe.mirai.console.command.Command +import net.mamoe.mirai.console.data.PluginData +import net.mamoe.mirai.console.data.value +import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.plugin.description.PluginDescription +import net.mamoe.mirai.console.util.SemVersion +import kotlin.annotation.AnnotationTarget.* + +/** + * 标记一个参数的语境类型, 用于帮助编译器和 IntelliJ 插件进行语境推断. + */ +@Target(VALUE_PARAMETER, PROPERTY, FIELD, FUNCTION, TYPE, TYPE_PARAMETER) +@Retention(AnnotationRetention.BINARY) +public annotation class ResolveContext( + vararg val kinds: Kind, +) { + /** + * 元素数量可能在任意时间被改动 + */ + public enum class Kind { + /////////////////////////////////////////////////////////////////////////// + // ConstantKind + /////////////////////////////////////////////////////////////////////////// + + /* + * WARNING: IF YOU CHANGE NAMES HERE, + * YOU SHOULD ALSO CHANGE THEIR COUNTERPARTS AT net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind + */ + + /** + * @see PluginDescription.id + */ + PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION + + /** + * @see PluginDescription.name + */ + PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION + + /** + * @see PluginDescription.version + * @see SemVersion.Companion.invoke + */ + SEMANTIC_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION + + /** + * @see SemVersion.Companion.parseRangeRequirement + */ + VERSION_REQUIREMENT, // ILLEGAL_VERSION_REQUIREMENT + + /** + * @see Command.allNames + */ + COMMAND_NAME, // ILLEGAL_COMMAND_NAME + + /** + * @see PermissionId.name + */ + PERMISSION_NAMESPACE, // ILLEGAL_PERMISSION_NAMESPACE + + /** + * @see PermissionId.name + */ + PERMISSION_NAME, // ILLEGAL_PERMISSION_NAME + + /** + * @see PermissionId.parseFromString + */ + PERMISSION_ID, // ILLEGAL_PERMISSION_ID + + /** + * 标注一个泛型, 要求这个泛型必须拥有一个公开无参 (或所有参数都可选) 构造器. + * + * @see PluginData.value + */ + RESTRICTED_NO_ARG_CONSTRUCTOR, // NOT_CONSTRUCTABLE_TYPE + + RESTRICTED_CONSOLE_COMMAND_OWNER, + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/compiler/common/RestrictedScope.kt b/backend/mirai-console/src/compiler/common/RestrictedScope.kt new file mode 100644 index 000000000..f4f4f1215 --- /dev/null +++ b/backend/mirai-console/src/compiler/common/RestrictedScope.kt @@ -0,0 +1,28 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.compiler.common + +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.annotation.AnnotationTarget.FUNCTION + +/** + * 标记一个函数, 在其函数体内限制特定一些函数的使用. + */ +@ConsoleExperimentalApi +@Target(FUNCTION) +@Retention(AnnotationRetention.BINARY) +public annotation class RestrictedScope( + vararg val kinds: Kind, +) { + public enum class Kind { + PERMISSION_REGISTER, // ILLEGAL_PERMISSION_REGISTER_USE + COMMAND_REGISTER, // ILLEGAL_COMMAND_REGISTER_USE + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/internal/MiraiConsoleBuildConstants.kt b/backend/mirai-console/src/internal/MiraiConsoleBuildConstants.kt index 7d8d8614b..778eb7221 100644 --- a/backend/mirai-console/src/internal/MiraiConsoleBuildConstants.kt +++ b/backend/mirai-console/src/internal/MiraiConsoleBuildConstants.kt @@ -14,8 +14,8 @@ import java.time.Instant internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants) @JvmStatic - val buildDate: Instant = Instant.ofEpochSecond(1606185513) - const val versionConst: String = "1.0.1-dev-1" + val buildDate: Instant = Instant.ofEpochSecond(1606580812) + const val versionConst: String = "1.1.0-dev-30" @JvmStatic val version: SemVersion = SemVersion(versionConst) diff --git a/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt b/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt index 67b45b9b7..ffe63cf51 100644 --- a/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt +++ b/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt @@ -34,7 +34,6 @@ import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.Pa import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.PasswordKind.PLAIN import net.mamoe.mirai.console.internal.data.builtins.ConsoleDataScope import net.mamoe.mirai.console.internal.data.builtins.LoggerConfig -import net.mamoe.mirai.console.internal.data.castOrNull import net.mamoe.mirai.console.internal.extension.BuiltInSingletonExtensionSelector import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl @@ -49,7 +48,6 @@ import net.mamoe.mirai.console.permission.PermissionService.Companion.permit import net.mamoe.mirai.console.permission.RootPermission import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.center.PluginCenter -import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.util.ConsoleExperimentalApi @@ -90,6 +88,8 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI override val dataStorageForBuiltIns: PluginDataStorage by instance::dataStorageForBuiltIns override val configStorageForBuiltIns: PluginDataStorage by instance::configStorageForBuiltIns override val consoleInput: ConsoleInput by instance::consoleInput + override val isAnsiSupported: Boolean by instance::isAnsiSupported + override fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver = instance.createLoginSolver(requesterBot, configuration) @@ -167,14 +167,6 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI mainLogger.verbose { "${PluginManager.plugins.size} plugin(s) loaded." } } - phase `collect extensions`@{ - for (resolvedPlugin in PluginManagerImpl.resolvedPlugins) { - resolvedPlugin.castOrNull()?.let { - GlobalComponentStorage.mergeWith(it.componentStorage) - } - } - } - phase `load SingletonExtensionSelector`@{ SingletonExtensionSelector.init() val instance = SingletonExtensionSelector.instance diff --git a/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt b/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt index 49375d866..447051689 100644 --- a/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt +++ b/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt @@ -27,6 +27,7 @@ internal object LoggerConfig : AutoSavePluginConfig("Logger") { mapOf( "example.logger" to AbstractLoggerController.LogPriority.NONE, "console.debug" to AbstractLoggerController.LogPriority.NONE, + "Bot" to AbstractLoggerController.LogPriority.ALL, ) ) diff --git a/backend/mirai-console/src/internal/extension/ComponentStorageInternal.kt b/backend/mirai-console/src/internal/extension/ComponentStorageInternal.kt index dedf18192..f8697dcc6 100644 --- a/backend/mirai-console/src/internal/extension/ComponentStorageInternal.kt +++ b/backend/mirai-console/src/internal/extension/ComponentStorageInternal.kt @@ -50,6 +50,8 @@ internal data class DataExtensionRegistry( ) : ExtensionRegistry internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { + private val instances: MutableMap, MutableSet>> = ConcurrentHashMap() + @Suppress("UNCHECKED_CAST") internal fun ExtensionPoint.getExtensions(): Set> { val userDefined = instances.getOrPut(this, ::CopyOnWriteArraySet) as Set> @@ -61,6 +63,13 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { return builtins?.plus(userDefined) ?: userDefined } + // unused for now + internal fun removeExtensionsRegisteredByPlugin(plugin: Plugin) { + instances.forEach { (_, u) -> + u.removeAll { it.plugin == plugin } + } + } + internal fun mergeWith(another: AbstractConcurrentComponentStorage) { for ((ep, list) in another.instances) { for (extensionRegistry in list) { @@ -154,7 +163,6 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { internal inline fun ExtensionPoint.useExtensions(block: (extension: T, plugin: Plugin?) -> Unit): Unit = withExtensions(block) - private val instances: MutableMap, MutableSet>> = ConcurrentHashMap() override fun contribute( extensionPoint: ExtensionPoint, plugin: Plugin, diff --git a/backend/mirai-console/src/internal/logging/LoggerControllerImpl.kt b/backend/mirai-console/src/internal/logging/LoggerControllerImpl.kt index 836583e7e..2f7273d79 100644 --- a/backend/mirai-console/src/internal/logging/LoggerControllerImpl.kt +++ b/backend/mirai-console/src/internal/logging/LoggerControllerImpl.kt @@ -16,15 +16,18 @@ import net.mamoe.mirai.console.util.ConsoleInternalApi @ConsoleFrontEndImplementation @ConsoleInternalApi -internal object LoggerControllerImpl : AbstractLoggerController() { +internal object LoggerControllerImpl : AbstractLoggerController.PathBased() { internal var initialized = false - override fun getPriority(identity: String?): LogPriority { + override fun findPriority(identity: String?): LogPriority? { if (!initialized) return LogPriority.NONE return if (identity == null) { LoggerConfig.defaultPriority } else { - LoggerConfig.loggers[identity] ?: LoggerConfig.defaultPriority + LoggerConfig.loggers[identity] } } + + override val defaultPriority: LogPriority + get() = if (initialized) LoggerConfig.defaultPriority else LogPriority.NONE } \ No newline at end of file diff --git a/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt b/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt index 46fe659ae..782de114f 100644 --- a/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt +++ b/backend/mirai-console/src/internal/plugin/BuiltInJvmPluginLoaderImpl.kt @@ -20,6 +20,7 @@ import net.mamoe.mirai.console.internal.util.PluginServiceHelper.loadAllServices import net.mamoe.mirai.console.plugin.jvm.* import net.mamoe.mirai.console.plugin.loader.AbstractFilePluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoadException +import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope import net.mamoe.mirai.utils.MiraiLogger import java.io.File @@ -93,20 +94,25 @@ internal object BuiltInJvmPluginLoaderImpl : return filePlugins.toSet().map { it.value } } + private val loadedPlugins = ConcurrentHashMap() + @Throws(PluginLoadException::class) override fun load(plugin: JvmPlugin) { ensureActive() + if (loadedPlugins.put(plugin, Unit) != null) { + error("Plugin '${plugin.name}' is already loaded and cannot be reloaded.") + } runCatching { - check(plugin is JvmPluginInternal) { "A JvmPlugin must extend AbstractJvmPlugin" } - plugin.internalOnLoad(plugin.componentStorage) + check(plugin is JvmPluginInternal) { "A JvmPlugin must extend AbstractJvmPlugin to be loaded by JvmPluginLoader.BuiltIn" } + plugin.internalOnLoad() }.getOrElse { throw PluginLoadException("Exception while loading ${plugin.description.name}", it) } } override fun enable(plugin: JvmPlugin) { - if (plugin.isEnabled) return + if (plugin.isEnabled) error("Plugin '${plugin.name}' is already enabled and cannot be re-enabled.") ensureActive() runCatching { if (plugin is JvmPluginInternal) { @@ -118,7 +124,7 @@ internal object BuiltInJvmPluginLoaderImpl : } override fun disable(plugin: JvmPlugin) { - if (!plugin.isEnabled) return + if (!plugin.isEnabled) error("Plugin '${plugin.name}' is not already disabled and cannot be re-disabled.") if (MiraiConsole.isActive) ensureActive() diff --git a/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt b/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt index 941fa1f95..3b7fe0c95 100644 --- a/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt +++ b/backend/mirai-console/src/internal/plugin/JvmPluginInternal.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.data.runCatchingLog import net.mamoe.mirai.console.extension.PluginComponentStorage import net.mamoe.mirai.console.internal.data.mkdir +import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.plugin.Plugin @@ -43,9 +44,6 @@ internal abstract class JvmPluginInternal( parentCoroutineContext: CoroutineContext, ) : JvmPlugin, CoroutineScope { - @Suppress("LeakingThis") - internal val componentStorage: PluginComponentStorage = PluginComponentStorage(this) - final override val parentPermission: Permission by lazy { PermissionService.INSTANCE.register( PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*"), @@ -54,6 +52,7 @@ internal abstract class JvmPluginInternal( } final override var isEnabled: Boolean = false + internal set private val resourceContainerDelegate by lazy { this::class.java.classLoader.asResourceContainer() } final override fun getResourceAsStream(path: String): InputStream? = @@ -100,13 +99,16 @@ internal abstract class JvmPluginInternal( } @Throws(Throwable::class) - internal fun internalOnLoad(componentStorage: PluginComponentStorage) { + internal fun internalOnLoad() { + val componentStorage = PluginComponentStorage(this) onLoad(componentStorage) + GlobalComponentStorage.mergeWith(componentStorage) } internal fun internalOnEnable(): Boolean { parentPermission if (!firstRun) refreshCoroutineContext() + kotlin.runCatching { onEnable() }.fold( diff --git a/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt b/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt index 70bbdd4c5..bd2d72257 100644 --- a/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt +++ b/backend/mirai-console/src/internal/plugin/PluginManagerImpl.kt @@ -71,7 +71,10 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol init { MiraiConsole.coroutineContext[Job]!!.invokeOnCompletion { - plugins.forEach { disablePlugin(it) } + plugins.forEach { plugin -> + if (plugin.isEnabled) + disablePlugin(plugin) + } } } diff --git a/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt b/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt deleted file mode 100644 index 17064132b..000000000 --- a/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt +++ /dev/null @@ -1,250 +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 through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.internal.util.semver - -import net.mamoe.mirai.console.util.SemVersion -import kotlin.math.max -import kotlin.math.min - -internal object RangeTokenReader { - enum class TokenType { - STRING, - - /* 左括号 */ - LEFT, - - /* 右括号 */ - RIGHT, - - /* || */ - OR, - - /* && */ - AND, - GROUP - } - - sealed class Token { - abstract val type: TokenType - abstract val value: String - abstract val position: Int - - class LeftBracket(override val position: Int) : Token() { - override val type: TokenType get() = TokenType.LEFT - override val value: String get() = "{" - - override fun toString(): String = "LB{" - } - - class RightBracket(override val position: Int) : Token() { - override val type: TokenType get() = TokenType.RIGHT - override val value: String get() = "}" - - override fun toString(): String = "RB}" - } - - class Or(override val position: Int) : Token() { - override val type: TokenType get() = TokenType.OR - override val value: String get() = "||" - override fun toString(): String = "OR||" - } - - class And(override val position: Int) : Token() { - override val type: TokenType get() = TokenType.AND - override val value: String get() = "&&" - - override fun toString(): String = "AD&&" - } - - class Group(val values: List, override val position: Int) : Token() { - override val type: TokenType get() = TokenType.GROUP - override val value: String get() = "" - } - - class Raw(val source: String, val start: Int, val end: Int) : Token() { - override val value: String get() = source.substring(start, end) - override val position: Int - get() = start - override val type: TokenType get() = TokenType.STRING - - override fun toString(): String = "R:$value" - } - } - - fun parseToTokens(source: String): List = ArrayList( - max(source.length / 3, 16) - ).apply { - var index = 0 - var position = 0 - fun flushOld() { - if (position > index) { - val id = index - index = position - for (i in id until position) { - if (!source[i].isWhitespace()) { - add(Token.Raw(source, id, position)) - return - } - } - } - } - - val iterator = source.indices.iterator() - for (i in iterator) { - position = i - when (source[i]) { - '{' -> { - flushOld() - add(Token.LeftBracket(i)) - index = i + 1 - } - '|' -> { - if (source.getOrNull(i + 1) == '|') { - flushOld() - add(Token.Or(i)) - index = i + 2 - iterator.nextInt() - } - } - '&' -> { - if (source.getOrNull(i + 1) == '&') { - flushOld() - add(Token.And(i)) - index = i + 2 - iterator.nextInt() - } - } - '}' -> { - flushOld() - add(Token.RightBracket(i)) - index = i + 1 - } - } - } - position = source.length - flushOld() - } - - fun collect(source: String, tokens: Iterator, root: Boolean): List = ArrayList().apply { - tokens.forEach { token -> - if (token is Token.LeftBracket) { - add(Token.Group(collect(source, tokens, false), token.position)) - } else if (token is Token.RightBracket) { - if (root) { - throw IllegalArgumentException("Syntax error: Unexpected }, ${buildMsg(source, token.position)}") - } else { - return@apply - } - } else add(token) - } - if (!root) { - throw IllegalArgumentException("Syntax error: Excepted }, ${buildMsg(source, source.length)}") - } - } - - private fun buildMsg(source: String, position: Int): String { - val ed = min(position + 10, source.length) - val st = max(0, position - 10) - return buildString { - append('`') - if (st != 0) append("...") - append(source, st, ed) - if (ed != source.length) append("...") - append("` at ").append(position) - } - } - - fun check(source: String, tokens: Iterator, group: Token.Group?) { - if (!tokens.hasNext()) { - throw IllegalArgumentException("Syntax error: empty rule, ${buildMsg(source, group?.position ?: 0)}") - } - var type = false - do { - val next = tokens.next() - if (type) { - if (next is Token.Group || next is Token.Raw) { - throw IllegalArgumentException("Syntax error: Except logic but got expression, ${buildMsg(source, next.position)}") - } - } else { - if (next is Token.Or || next is Token.And) { - throw IllegalArgumentException("Syntax error: Except expression but got logic, ${buildMsg(source, next.position)}") - } - if (next is Token.Group) { - check(source, next.values.iterator(), next) - } - } - type = !type - } while (tokens.hasNext()) - if (!type) { - throw IllegalArgumentException("Syntax error: Except more expression, ${buildMsg(source, group?.values?.last()?.position ?: source.length)}") - } - } - - fun parse(source: String, token: Token): RequirementInternal { - return when (token) { - is Token.Group -> { - if (token.values.size == 1) { - parse(source, token.values.first()) - } else { - val logic = token.values.asSequence().map { it.type }.filter { - it == TokenType.OR || it == TokenType.AND - }.toSet() - if (logic.size == 2) { - throw IllegalArgumentException("Syntax error: || and && cannot use in one group, ${buildMsg(source, token.position)}") - } - val rules = token.values.asSequence().filter { - it is Token.Raw || it is Token.Group - }.map { parse(source, it) }.toList() - when (logic.first()) { - TokenType.OR -> { - return object : RequirementInternal { - override fun test(version: SemVersion): Boolean { - rules.forEach { if (it.test(version)) return true } - return false - } - } - } - TokenType.AND -> { - return object : RequirementInternal { - override fun test(version: SemVersion): Boolean { - rules.forEach { if (!it.test(version)) return false } - return true - } - } - } - else -> throw AssertionError() - } - } - } - is Token.Raw -> SemVersionInternal.parseRule(token.value) - else -> throw AssertionError() - } - } - - fun StringBuilder.dump(prefix: String, token: Token) { - when (token) { - is Token.LeftBracket -> append("${prefix}LF {\n") - - is Token.RightBracket -> append("${prefix}LR }\n") - - is Token.Or -> append("${prefix}OR ||\n") - - is Token.And -> append("${prefix}AND &&\n") - is Token.Group -> { - append("${prefix}GROUP {\n") - token.values.forEach { dump("$prefix ", it) } - append("${prefix}}\n") - } - is Token.Raw -> append("${prefix}RAW ${token.value}\n") - } - } -} \ No newline at end of file diff --git a/backend/mirai-console/src/internal/util/semver/RequirementParser.kt b/backend/mirai-console/src/internal/util/semver/RequirementParser.kt new file mode 100644 index 000000000..011f18954 --- /dev/null +++ b/backend/mirai-console/src/internal/util/semver/RequirementParser.kt @@ -0,0 +1,335 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.internal.util.semver + +import kotlin.math.max +import kotlin.math.min + +internal class RequirementParser { + sealed class Token { + open var line: Int = -1 + open var pos: Int = -1 + open var sourcePos: Int = -1 + open lateinit var content: String + + sealed class GroupBod : Token() { + class Left : GroupBod() { + override var content: String + get() = "{" + set(_) {} + } + + class Right : GroupBod() { + override var content: String + get() = "}" + set(_) {} + } + } + + sealed class Logic : Token() { + class And : Logic() { + override var content: String + get() = "&&" + set(_) {} + } + + class Or : Logic() { + override var content: String + get() = "||" + set(_) {} + } + } + + class Content : Token() + class Ending : Token() { + override var content: String + get() = "" + set(_) {} + } + + object Begin : Token() { + override var content: String + get() = "" + set(_) {} + override var line: Int + get() = 0 + set(_) {} + override var pos: Int + get() = 0 + set(_) {} + override var sourcePos: Int + get() = 0 + set(_) {} + } + + override fun toString(): String { + return javaClass.canonicalName.substringAfterLast('.') + " - $content [$line, $pos]" + } + } + + companion object { + const val END = '\u0000' + } + + class TokenReader( + @JvmField val content: String + ) { + @JvmField + var pos: Int = 0 + + @JvmField + var line: Int = 0 + + @JvmField + var posi: Int = 0 + + @JvmField + var latestToken: Token = Token.Begin + + @JvmField + var insertToken: Token? = Token.Begin + fun peekChar(): Char { + if (pos < content.length) + return content[pos] + return END + } + + fun peekNextChar(): Char { + if (pos + 1 < content.length) + return content[pos + 1] + return END + } + + fun nextChar(): Char { + val char = peekChar() + pos++ + if (char == '\n') { + line++ + posi = 0 + } else { + posi++ + } + return char + } + + fun nextToken(): Token { + insertToken?.let { insertToken = null; return it } + return nextToken0().also { latestToken = it } + } + + private fun nextToken0(): Token { + if (pos < content.length) { + while (peekChar().isWhitespace()) { + nextChar() + } + val startIndex = pos + if (startIndex >= content.length) { + return Token.Ending().also { + it.line = line + it.pos = posi + it.sourcePos = content.length + } + } + val pline = line + val ppos = posi + nextChar() + when (content[startIndex]) { + '&' -> { + if (peekChar() == '&') { + return Token.Logic.And().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + nextChar() + } + } + } + '|' -> { + if (peekChar() == '|') { + return Token.Logic.Or().also { + nextChar() + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + } + '{' -> { + return Token.GroupBod.Left().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + '}' -> { + return Token.GroupBod.Right().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + } + while (true) { + when (val c = peekChar()) { + '&', '|' -> { + if (c == peekNextChar()) { + break + } + nextChar() + } + '{', '}' -> { + break + } + END -> break + else -> nextChar() + } + } + val endIndex = pos + return Token.Content().also { + it.content = content.substring(startIndex, endIndex) + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + return Token.Ending().also { + it.line = line + it.pos = posi + it.sourcePos = content.length + } + } + } + + interface TokensProcessor { + fun process(reader: TokenReader): R + fun processLine(reader: TokenReader): R + fun processLogic(isAnd: Boolean, chunks: Iterable): R + } + + abstract class ProcessorBase : TokensProcessor { + fun Token.ia(reader: TokenReader, msg: String, cause: Throwable? = null): Nothing { + throw IllegalArgumentException("$msg (at [$line, $pos], ${cutSource(reader, sourcePos)})", cause) + } + + fun cutSource(reader: TokenReader, index: Int): String { + val content = reader.content + val s = max(0, index - 10) + val e = min(content.length, index + 10) + return content.substring(s, e) + } + + override fun process(reader: TokenReader): R { + return when (val nextToken = reader.nextToken()) { + is Token.Begin, + is Token.GroupBod.Left -> { + val first = when (val next = reader.nextToken()) { + is Token.Content -> { + processString(reader, next) + } + is Token.GroupBod.Right -> { + nextToken.ia( + reader, if (nextToken is Token.Begin) + "Invalid token `}`" + else "The first token cannot be Group Ending" + ) + } + is Token.Logic -> { + nextToken.ia(reader, "The first token cannot be Token.Logic") + } + is Token.Ending -> { + nextToken.ia( + reader, if (nextToken is Token.Begin) + "Requirement cannot be blank" + else "Except more tokens" + ) + } + is Token.GroupBod.Left -> { + reader.insertToken = next + process(reader) + } + else -> { + next.ia(reader, "Bad token $next") + } + } + // null -> not set + // true -> AND mode + // false-> OR mode + var mode: Boolean? = null + val chunks = arrayListOf(first) + while (true) { + when (val next = reader.nextToken()) { + is Token.Ending, + is Token.GroupBod.Right -> { + val isEndingOfGroup = next is Token.GroupBod.Right + val isStartingOfGroup = nextToken is Token.GroupBod.Left + if (isStartingOfGroup != isEndingOfGroup) { + fun getType(type: Boolean) = if (type) "`}`" else "" + next.ia(reader, "Except ${getType(isStartingOfGroup)} but got ${getType(isEndingOfGroup)}") + } else { + // reader.insertToken = next + break + } + } + is Token.Logic -> { + val stx = next is Token.Logic.And + if (mode == null) mode = stx + else if (mode != stx) { + fun getMode(type: Boolean) = if (type) "`&&`" else "`||`" + next.ia( + reader, "Cannot change logic mode after setting. " + + "Except ${getMode(mode)} but got ${getMode(stx)}" + ) + } + chunks.add(process(reader)) + } + else -> { + next.ia( + reader, "Except ${ + when (mode) { + null -> "`&&` or `||`" + true -> "`&&`" + false -> "`||`" + } + } but get `${next.content}`" + ) + } + } + } + if (mode == null) { + first + } else { + processLogic(mode, chunks) + } + } + is Token.Content -> { + processString(reader, nextToken) + } + is Token.Ending -> { + nextToken.ia(reader, "Except more values.") + } + else -> { + nextToken.ia(reader, "Assert Error: $nextToken") + } + } + } + + abstract fun processString(reader: TokenReader, token: Token.Content): R + + + override fun processLine(reader: TokenReader): R { + return process(reader).also { + val tok = reader.nextToken() + if (reader.nextToken() !is Token.Ending) { + tok.ia(reader, "Token reader stream not done") + } + } + } + } +} diff --git a/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt b/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt index 1d9c1af03..bb9f9a947 100644 --- a/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt +++ b/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt @@ -9,7 +9,6 @@ package net.mamoe.mirai.console.internal.util.semver -import net.mamoe.mirai.console.internal.util.semver.RangeTokenReader.dump import net.mamoe.mirai.console.util.SemVersion import kotlin.math.max import kotlin.math.min @@ -166,21 +165,27 @@ internal object SemVersionInternal { @JvmStatic - fun parseRangeRequirement(requirement: String): RequirementInternal { - if (requirement.isBlank()) { - throw IllegalArgumentException("Invalid requirement: Empty requirement rule.") - } - val tokens = RangeTokenReader.parseToTokens(requirement) - val collected = RangeTokenReader.collect(requirement, tokens.iterator(), true) - RangeTokenReader.check(requirement, collected.iterator(), null) - return kotlin.runCatching { - RangeTokenReader.parse(requirement, RangeTokenReader.Token.Group(collected, 0)) - }.onFailure { error -> - throw IllegalArgumentException("Exception in parsing $requirement\n\n" + buildString { - collected.forEach { dump("", it) } - }, error) - }.getOrThrow() - } + fun parseRangeRequirement(requirement: String): RequirementInternal = + object : RequirementParser.ProcessorBase() { + override fun processLogic(isAnd: Boolean, chunks: Iterable): RequirementInternal { + return if (isAnd) object : RequirementInternal { + override fun test(version: SemVersion): Boolean { + return chunks.all { it.test(version) } + } + } else object : RequirementInternal { + override fun test(version: SemVersion): Boolean { + return chunks.any { it.test(version) } + } + } + } + + override fun processString( + reader: RequirementParser.TokenReader, + token: RequirementParser.Token.Content + ): RequirementInternal = kotlin.runCatching { + parseRule(token.content) + }.getOrElse { token.ia(reader, "Error in parsing rule `${token.content}`", it) } + }.processLine(RequirementParser.TokenReader(requirement)) @JvmStatic fun compareInternal(source: SemVersion, other: SemVersion): Int { diff --git a/backend/mirai-console/src/logging/AbstractLoggerController.kt b/backend/mirai-console/src/logging/AbstractLoggerController.kt index a3105bcac..c9a58e965 100644 --- a/backend/mirai-console/src/logging/AbstractLoggerController.kt +++ b/backend/mirai-console/src/logging/AbstractLoggerController.kt @@ -13,19 +13,33 @@ import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.SimpleLogger import java.util.* +/** + * 日志控制器的基本实现 + */ @ConsoleExperimentalApi public abstract class AbstractLoggerController : LoggerController { + /** + * @param priority 尝试判断的日志等级 + * @param settings 配置中的日志等级 (see [getPriority]) + */ protected open fun shouldLog( priority: LogPriority, settings: LogPriority, ): Boolean = settings <= priority + /** + * 获取配置中与 [identity] 对应的 [LogPriority] + */ protected abstract fun getPriority(identity: String?): LogPriority override fun shouldLog(identity: String?, priority: SimpleLogger.LogPriority): Boolean = shouldLog(LogPriority.by(priority), getPriority(identity)) + /** + * 便于进行配置存储的 [LogPriority], + * 等级优先级与 [SimpleLogger.LogPriority] 对应 + */ @Suppress("unused") @ConsoleExperimentalApi public enum class LogPriority { @@ -59,4 +73,64 @@ public abstract class AbstractLoggerController : LoggerController { } + /** + * 路径形式实现的基本日志控制器 + * + * Example: + * 配置文件: + * ``` + * defaultPriority: ALL + * loggers: + * t: NONE + * t.sub: VERBOSE + * t.sub.1: NONE + * ``` + * + * ``` + * "logger.1" + * -> "logger.1" << null + * -> "logger" << null + * -> defaultPriority << ALL + * + * "t.sub.1" + * -> "t.sub.1" << NONE + * + * "t.sub.2" + * -> "t.sub.2" << null + * -> "t.sub" << VERBOSE + * + * ...... + * ``` + */ + @ConsoleExperimentalApi + public abstract class PathBased + @JvmOverloads public constructor( + protected open val spliterator: Char = '.' + ) : AbstractLoggerController() { + protected abstract val defaultPriority: LogPriority + protected abstract fun findPriority(identity: String?): LogPriority? + + /** + * 从 [path] 析出下一次应该进行搜索的二次 path (@see [getPriority]) + * + * @return 如果返回了 `null`, 会令 [getPriority] 返回 `findPriority(null) ?: defaultPriority`) + */ + protected open fun nextPath(path: String): String? { + val lastIndex = path.lastIndexOf(spliterator) + if (lastIndex == -1) return null + return path.substring(0, lastIndex) + } + + override fun getPriority(identity: String?): LogPriority { + if (identity == null) { + return findPriority(null) ?: defaultPriority + } else { + var path: String = identity + while (true) { + findPriority(path)?.let { return it } + path = nextPath(path) ?: return (findPriority(null) ?: defaultPriority) + } + } + } + } } diff --git a/backend/mirai-console/src/logging/LoggerController.kt b/backend/mirai-console/src/logging/LoggerController.kt index 59b60457b..cdb96a203 100644 --- a/backend/mirai-console/src/logging/LoggerController.kt +++ b/backend/mirai-console/src/logging/LoggerController.kt @@ -9,14 +9,17 @@ package net.mamoe.mirai.console.logging -import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl +import net.mamoe.mirai.console.MiraiConsoleImplementation +import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.SimpleLogger /** * 日志控制系统 * - * @see [LoggerControllerImpl] + * @see [AbstractLoggerController] + * @see [MiraiConsoleImplementation.loggerController] + * @see [MiraiConsoleLogger] */ @ConsoleExperimentalApi public interface LoggerController { diff --git a/backend/mirai-console/src/plugin/loader/PluginLoader.kt b/backend/mirai-console/src/plugin/loader/PluginLoader.kt index e4b9e863f..22021a7df 100644 --- a/backend/mirai-console/src/plugin/loader/PluginLoader.kt +++ b/backend/mirai-console/src/plugin/loader/PluginLoader.kt @@ -76,7 +76,7 @@ public interface PluginLoader

{ * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等). * @throws IllegalStateException 在插件已经被加载时抛出. 这属于意料之外的情况. */ - @Throws(PluginLoadException::class) + @Throws(IllegalStateException::class, PluginLoadException::class) public fun load(plugin: P) /** diff --git a/backend/mirai-console/src/util/AnsiMessageBuilder.kt b/backend/mirai-console/src/util/AnsiMessageBuilder.kt new file mode 100644 index 000000000..0215a5bd2 --- /dev/null +++ b/backend/mirai-console/src/util/AnsiMessageBuilder.kt @@ -0,0 +1,230 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ +@file:Suppress("unused", "MemberVisibilityCanBePrivate", "FunctionName") + +package net.mamoe.mirai.console.util + +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge +import net.mamoe.mirai.console.util.AnsiMessageBuilder.Companion.dropAnsi + +public open class AnsiMessageBuilder public constructor( + public val delegate: StringBuilder +) : Appendable { + override fun toString(): String = delegate.toString() + + /** + * 同 [append] 方法, 在 `noAnsi=true` 的时候会忽略此函数的调用 + * + * 参考资料: + * - [ANSI转义序列](https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97) + * - [ANSI转义序列#颜色](https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97#%E9%A2%9C%E8%89%B2) + * + * @param code Ansi 操作码 + * + * @see from + * @see create + */ + public open fun ansi(code: String): AnsiMessageBuilder = append(code) + + public open fun reset(): AnsiMessageBuilder = append(Color.RESET) + public open fun white(): AnsiMessageBuilder = append(Color.WHITE) + public open fun red(): AnsiMessageBuilder = append(Color.RED) + public open fun emeraldGreen(): AnsiMessageBuilder = append(Color.EMERALD_GREEN) + public open fun gold(): AnsiMessageBuilder = append(Color.GOLD) + public open fun blue(): AnsiMessageBuilder = append(Color.BLUE) + public open fun purple(): AnsiMessageBuilder = append(Color.PURPLE) + public open fun green(): AnsiMessageBuilder = append(Color.GREEN) + public open fun gray(): AnsiMessageBuilder = append(Color.GRAY) + public open fun lightRed(): AnsiMessageBuilder = append(Color.LIGHT_RED) + public open fun lightGreen(): AnsiMessageBuilder = append(Color.LIGHT_GREEN) + public open fun lightYellow(): AnsiMessageBuilder = append(Color.LIGHT_YELLOW) + public open fun lightBlue(): AnsiMessageBuilder = append(Color.LIGHT_BLUE) + public open fun lightPurple(): AnsiMessageBuilder = append(Color.LIGHT_PURPLE) + public open fun lightCyan(): AnsiMessageBuilder = append(Color.LIGHT_CYAN) + + internal object Color { + const val RESET = "\u001b[0m" + const val WHITE = "\u001b[30m" + const val RED = "\u001b[31m" + const val EMERALD_GREEN = "\u001b[32m" + const val GOLD = "\u001b[33m" + const val BLUE = "\u001b[34m" + const val PURPLE = "\u001b[35m" + const val GREEN = "\u001b[36m" + const val GRAY = "\u001b[90m" + const val LIGHT_RED = "\u001b[91m" + const val LIGHT_GREEN = "\u001b[92m" + const val LIGHT_YELLOW = "\u001b[93m" + const val LIGHT_BLUE = "\u001b[94m" + const val LIGHT_PURPLE = "\u001b[95m" + const val LIGHT_CYAN = "\u001b[96m" + } + + internal class NoAnsiMessageBuilder(builder: StringBuilder) : AnsiMessageBuilder(builder) { + override fun reset(): AnsiMessageBuilder = this + override fun white(): AnsiMessageBuilder = this + override fun red(): AnsiMessageBuilder = this + override fun emeraldGreen(): AnsiMessageBuilder = this + override fun gold(): AnsiMessageBuilder = this + override fun blue(): AnsiMessageBuilder = this + override fun purple(): AnsiMessageBuilder = this + override fun green(): AnsiMessageBuilder = this + override fun gray(): AnsiMessageBuilder = this + override fun lightRed(): AnsiMessageBuilder = this + override fun lightGreen(): AnsiMessageBuilder = this + override fun lightYellow(): AnsiMessageBuilder = this + override fun lightBlue(): AnsiMessageBuilder = this + override fun lightPurple(): AnsiMessageBuilder = this + override fun lightCyan(): AnsiMessageBuilder = this + override fun ansi(code: String): AnsiMessageBuilder = this + } + + public companion object { + // CSI序列由ESC [、若干个(包括0个)“参数字节”、若干个“中间字节”,以及一个“最终字节”组成。各部分的字符范围如下: + // + // CSI序列在ESC [之后各个组成部分的字符范围[12]:5.4 + // 组成部分 字符范围 ASCII + // 参数字节 0x30–0x3F 0–9:;<=>? + // 中间字节 0x20–0x2F 空格、!"#$%&'()*+,-./ + // 最终字节 0x40–0x7E @A–Z[\]^_`a–z{|}~ + // + // @see https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97#CSI%E5%BA%8F%E5%88%97 + @Suppress("RegExpRedundantEscape") + private val DROP_CSI_PATTERN = """\u001b\[([\u0030-\u003F])*?([\u0020-\u002F])*?[\u0040-\u007E]""".toRegex() + + // 序列具有不同的长度。所有序列都以ASCII字符ESC(27 / 十六进制 0x1B)开头, + // 第二个字节则是0x40–0x5F(ASCII @A–Z[\]^_)范围内的字符。[12]:5.3.a + // + // 标准规定,在8位环境中,这两个字节的序列可以合并为0x80-0x9F范围内的单个字节(详情请参阅C1控制字符集)。 + // 但是,在现代设备上,这些代码通常用于其他目的,例如UTF-8的一部分或CP-1252字符,因此并不使用这种合并的方式。 + // + // 除ESC之外的其他C0代码(通常是BEL,BS,CR,LF,FF,TAB,VT,SO和SI)在输出时也可能会产生与某些控制序列相似或相同的效果。 + // + // @see https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97#%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97 + // + // 注: 缺少详细资料, 只能认定 ansi 长度固定为二字节 (CSI除外) + private val DROP_ANSI_PATTERN = """\u001b[\u0040–\u005F]""".toRegex() + + /** + * 从 [String] 中剔除 ansi 控制符 + */ + @JvmStatic + public fun String.dropAnsi(): String = this + .replace(DROP_CSI_PATTERN, "") // 先进行 CSI 剔除后进行 ANSI 剔除 + .replace(DROP_ANSI_PATTERN, "") + + /** + * 使用 [builder] 封装一个 [AnsiMessageBuilder] + * + * @param noAnsi 为 `true` 时忽略全部与 ansi 有关的方法的调用 + */ + @JvmStatic + @JvmOverloads + public fun from( + builder: StringBuilder, + noAnsi: Boolean = false + ): AnsiMessageBuilder = if (noAnsi) { + NoAnsiMessageBuilder(builder) + } else AnsiMessageBuilder(builder) + + /** + * @param capacity [StringBuilder] 的初始化大小 + * + * @param noAnsi 为 `true` 时忽略全部与 ansi 有关的方法的调用 + */ + @JvmStatic + @JvmOverloads + public fun create( + capacity: Int = 16, + noAnsi: Boolean = false + ): AnsiMessageBuilder = from(StringBuilder(capacity), noAnsi) + + /** + * 判断 [sender] 是否支持带 ansi 控制符的正确显示 + */ + @ConsoleExperimentalApi + @JvmStatic + public fun isAnsiSupported(sender: CommandSender): Boolean = + if (sender is ConsoleCommandSender) { + MiraiConsoleImplementationBridge.isAnsiSupported + } else false + + /** + * 往 [StringBuilder] 追加 ansi 控制符 + */ + public inline fun StringBuilder.appendAnsi( + action: AnsiMessageBuilder.() -> Unit + ): AnsiMessageBuilder = from(this).apply(action) + + } + + ///////////////////////////////////////////////////////////////////////////////// + override fun append(c: Char): AnsiMessageBuilder = apply { delegate.append(c) } + override fun append(csq: CharSequence?): AnsiMessageBuilder = apply { delegate.append(csq) } + override fun append(csq: CharSequence?, start: Int, end: Int): AnsiMessageBuilder = apply { delegate.append(csq, start, end) } + public fun append(any: Any?): AnsiMessageBuilder = apply { delegate.append(any) } + public fun append(value: String): AnsiMessageBuilder = apply { delegate.append(value) } + public fun append(value: String, start: Int, end: Int): AnsiMessageBuilder = apply { delegate.append(value, start, end) } + public fun append(value: Boolean): AnsiMessageBuilder = apply { delegate.append(value) } + public fun append(value: Float): AnsiMessageBuilder = apply { delegate.append(value) } + public fun append(value: Double): AnsiMessageBuilder = apply { delegate.append(value) } + public fun append(value: Int): AnsiMessageBuilder = apply { delegate.append(value) } + public fun append(value: Long): AnsiMessageBuilder = apply { delegate.append(value) } + public fun append(value: Short): AnsiMessageBuilder = apply { delegate.append(value) } + ///////////////////////////////////////////////////////////////////////////////// +} + +/** + * @param capacity [StringBuilder] 初始化大小 + */ +public fun AnsiMessageBuilder(capacity: Int = 16): AnsiMessageBuilder = AnsiMessageBuilder(StringBuilder(capacity)) + +/** + * 构建一条 ansi 信息 + * + * @see [AnsiMessageBuilder] + */ +public inline fun buildAnsiMessage( + capacity: Int = 16, + action: AnsiMessageBuilder.() -> Unit +): String = AnsiMessageBuilder.create(capacity, false).apply(action).toString() + +// 不在 top-level 使用者会得到 Internal error: Couldn't inline sendAnsiMessage + +/** + * 向 [CommandSender] 发送一条带有 ansi 控制符的信息 + * + * @see [AnsiMessageBuilder] + */ +public suspend inline fun CommandSender.sendAnsiMessage( + capacity: Int = 16, + builder: AnsiMessageBuilder.() -> Unit +) { + sendMessage( + AnsiMessageBuilder.create(capacity, noAnsi = !AnsiMessageBuilder.isAnsiSupported(this)) + .apply(builder) + .toString() + ) +} + +/** + * 向 [CommandSender] 发送一条带有 ansi 控制符的信息 + * + * @see [AnsiMessageBuilder.Companion.dropAnsi] + */ +public suspend inline fun CommandSender.sendAnsiMessage(message: String) { + sendMessage( + if (AnsiMessageBuilder.isAnsiSupported(this)) + message + else + message.dropAnsi() + ) +} diff --git a/backend/mirai-console/test/TestMiraiConosle.kt b/backend/mirai-console/test/TestMiraiConosle.kt index d5186fabe..2c919c5d5 100644 --- a/backend/mirai-console/test/TestMiraiConosle.kt +++ b/backend/mirai-console/test/TestMiraiConosle.kt @@ -14,7 +14,6 @@ import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.data.MemoryPluginDataStorage import net.mamoe.mirai.console.data.PluginDataStorage -import net.mamoe.mirai.console.logging.LoggerController import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.util.ConsoleExperimentalApi @@ -27,12 +26,13 @@ import java.nio.file.Path import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume +import kotlin.io.path.createTempDirectory import kotlin.test.assertNotNull -@OptIn(ConsoleInternalApi::class) +@OptIn(ConsoleInternalApi::class, kotlin.io.path.ExperimentalPathApi::class) fun initTestEnvironment() { object : MiraiConsoleImplementation { - override val rootPath: Path = createTempDir().toPath() + override val rootPath: Path = createTempDirectory() @ConsoleExperimentalApi override val frontEndDescription: MiraiConsoleFrontEndDescription diff --git a/backend/mirai-console/test/command/TestCommand.kt b/backend/mirai-console/test/command/TestCommand.kt index 7b93718b8..c2c060379 100644 --- a/backend/mirai-console/test/command/TestCommand.kt +++ b/backend/mirai-console/test/command/TestCommand.kt @@ -34,7 +34,7 @@ import org.junit.jupiter.api.Test import kotlin.test.* object TestCompositeCommand : CompositeCommand( - ConsoleCommandOwner, + owner, "testComposite", "tsC" ) { @SubCommand @@ -49,7 +49,7 @@ object TestCompositeCommand : CompositeCommand( } object TestRawCommand : RawCommand( - ConsoleCommandOwner, + owner, "testRaw" ) { override suspend fun CommandSender.onCommand(args: MessageChain) { @@ -65,7 +65,9 @@ object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") { } internal val sender by lazy { ConsoleCommandSender } -internal val owner by lazy { ConsoleCommandOwner } + +internal object TestUnitCommandOwner : CommandOwner by ConsoleCommandOwner +internal val owner by lazy { TestUnitCommandOwner } @OptIn(ExperimentalCommandDescriptors::class) @@ -88,12 +90,13 @@ internal class TestCommand { fun testRegister() { try { unregisterAllCommands(ConsoleCommandOwner) // builtins + unregisterAllCommands(owner) // testing unit unregisterCommand(TestSimpleCommand) assertTrue(TestCompositeCommand.register()) assertFalse(TestCompositeCommand.register()) - assertEquals(1, getRegisteredCommands(ConsoleCommandOwner).size) + assertEquals(1, getRegisteredCommands(owner).size) assertEquals(1, CommandManagerImpl._registeredCommands.size) assertEquals(2, @@ -198,7 +201,7 @@ internal class TestCommand { fun `composite sub command resolution conflict`() { runBlocking { val composite = object : CompositeCommand( - ConsoleCommandOwner, + owner, "tr" ) { @Suppress("UNUSED_PARAMETER") @@ -233,7 +236,7 @@ internal class TestCommand { ) val composite = object : CompositeCommand( - ConsoleCommandOwner, + owner, "test22", overrideContext = buildCommandArgumentContext { add(object : CommandValueArgumentParser { @@ -291,7 +294,7 @@ internal class TestCommand { fun `test optional argument command`() { runBlocking { val optionCommand = object : CompositeCommand( - ConsoleCommandOwner, + owner, "testOptional" ) { @SubCommand @@ -315,7 +318,7 @@ internal class TestCommand { fun `test vararg`() { runBlocking { val optionCommand = object : CompositeCommand( - ConsoleCommandOwner, + owner, "test" ) { @SubCommand diff --git a/backend/mirai-console/test/logging/TestALC_PathBased.kt b/backend/mirai-console/test/logging/TestALC_PathBased.kt new file mode 100644 index 000000000..72c45beb2 --- /dev/null +++ b/backend/mirai-console/test/logging/TestALC_PathBased.kt @@ -0,0 +1,55 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.logging + +import org.junit.jupiter.api.Test + +@Suppress("ClassName") +internal class TestALC_PathBased { + @Test + fun `test AbstractLoggerController$PathBased`() { + val config = mapOf( + "test" to "ALL", + "test.test" to "VERBOSE", + "test.test.test" to "NONE", + ).mapValues { AbstractLoggerController.LogPriority.valueOf(it.value) } + + val c = object : AbstractLoggerController.PathBased() { + override val defaultPriority: LogPriority + get() = LogPriority.NONE + + override fun findPriority(identity: String?): LogPriority? { + if (identity == null) return defaultPriority + return config[identity] + } + + fun priority(i: String?): LogPriority = getPriority(i) + } + + fun assertSame(path: String?, p: String) { + kotlin.test.assertSame(c.priority(path), AbstractLoggerController.LogPriority.valueOf(p)) + } + + assertSame("test.test.test", "NONE") + assertSame("test.test.test.more.test", "NONE") + + assertSame("test.test.t1", "VERBOSE") + assertSame("test.test.t15w", "VERBOSE") + assertSame("test.test", "VERBOSE") + + assertSame("test", "ALL") + assertSame("test.tes1ww", "ALL") + assertSame("test.asldjawe.awej2oi3", "ALL") + + assertSame("AWawex", "NONE") + assertSame("awpejaszx.aljewkz", "NONE") + assertSame("test0.awekjo23xxxxx", "NONE") + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 29fe2bea4..f2faf5936 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,6 +53,7 @@ val experimentalAnnotations = arrayOf( "kotlin.experimental.ExperimentalTypeInference", "kotlinx.coroutines.ExperimentalCoroutinesApi", "kotlinx.serialization.ExperimentalSerializationApi", + "kotlin.io.path.ExperimentalPathApi", "io.ktor.util.KtorExperimentalAPI", "net.mamoe.mirai.utils.MiraiInternalAPI", diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 81dc13016..19f1befac 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -11,15 +11,15 @@ object Versions { const val core = "1.3.3" - const val console = "1.0.1-dev-1" + const val console = "1.1.0-dev-32" const val consoleGraphical = "0.0.7" const val consoleTerminal = console const val kotlinCompiler = "1.4.20" const val kotlinStdlib = "1.4.20" - const val kotlinIntellijPlugin = "1.4.20-RC-IJ2020.2-1" // -release - const val intellij = "2020.2.1" + const val kotlinIntellijPlugin = "1.4.20-release-IJ2020.2-1" // keep to newest as kotlinCompiler + const val intellij = "2020.2.1" // don't update easily unless you want your disk space -= 500MB const val coroutines = "1.4.0" @@ -34,7 +34,7 @@ object Versions { const val blockingBridge = "1.4.1" @Suppress("SpellCheckingInspection") - const val yamlkt = "0.7.3" + const val yamlkt = "0.7.4" const val intellijGradlePlugin = "0.4.16" } @@ -42,7 +42,7 @@ object Versions { const val `kotlin-compiler` = "org.jetbrains.kotlin:kotlin-compiler:${Versions.kotlinCompiler}" const val `kotlin-stdlib` = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlinStdlib}" -const val `kotlin-stdlib-jdk8` = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlinStdlib}" +const val `kotlin-stdlib-jdk8` = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlinStdlib}" const val `kotlin-reflect` = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinStdlib}" const val `kotlin-test` = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlinStdlib}" const val `kotlin-test-junit5` = "org.jetbrains.kotlin:kotlin-test-junit5:${Versions.kotlinStdlib}" diff --git a/docs/ConfiguringProjects.md b/docs/ConfiguringProjects.md index 9f876c25a..17ee1530b 100644 --- a/docs/ConfiguringProjects.md +++ b/docs/ConfiguringProjects.md @@ -20,8 +20,8 @@ console 由后端和前端一起工作. 使用时必须选择一个前端. | 版本类型 | 版本号 | |:------:|:------------------------------:| -| 稳定 | 1.0.0 | -| 预览 | - | +| 稳定 | 1.0.1 | +| 预览 | - | | 开发 | [![Version]][Bintray Download] | ## 配置项目 diff --git a/docs/Contributing.md b/docs/Contributing.md index f4e4befbb..a9d0bf32d 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -26,7 +26,41 @@ Mirai Console 项目由四个模块组成:后端,前端,Gradle 插件,In ### 构建 ```shell script -gradlew build +./gradlew build ``` -首次加载和构建 mirai-console 项目可能要花费数小时时间。 \ No newline at end of file +首次加载和构建 mirai-console 项目可能要花费数小时时间。 + +## 贡献代码 + +### 代码风格 +- 请优先使用 Kotlin +- 请遵守 [Kotlin 官方代码风格](https://www.kotlincn.net/docs/reference/coding-conventions.html) + +## 发布版本 + +(以下内容针对拥有 Mirai Console write 权限的项目成员) + +若你要发布一个 Mirai Console dev release: + +1. 更新 buildSrc/Versions.kt 中 `project` 版本号为目标版本; +2. 本地执行 `./gradlew fillBuildConstants`; +3. Push 第 1,2 步的修改为同一个 commit,commit 备注为版本号名称,如 `1.0.1-dev-1`; +4. 添加 Git 版本号 tag,格式为 `1.0.1-dev-1`(不带 `v`); +5. `git push --tags` 推送 tag 更新,GitHub Actions 将会检测到 tag 更新并执行 JCenter 发布。 + + +若你要发布一个 Mirai Console 稳定版 release,请按顺序进行如下检查: + + +1. 在 GitHub [milestones](https://github.com/mamoe/mirai-console/milestones) 确认目标版本的工作已经处理完毕; +2. Close milestone; +3. 更新 buildSrc/Versions.kt 中 `project` 版本号为目标版本; +4. 在 [ConfiguringProjects](ConfiguringProjects.md#选择版本) 更新稳定版本号; +5. 本地执行 `./gradlew fillBuildConstants`; +6. Push 前几步的修改为同一个 commit,commit 备注为版本号名称,如 `1.1.0`; +7. 在 GitHub release 根据 Git commit 记录编写更新记录: + - 描述所有来自社区的 PR 记录; + - 完整列举公开 API 的任何变动,简要描述或省略内部变动; + - 为更改按 “后端”,“前端”,“IDE 插件”,“Gradle 插件” 分类; +8. 点击 `Publish`。GitHub Actions 将会检测到 tag 更新并执行 JCenter 发布。 diff --git a/docs/Run.md b/docs/Run.md index d7c7edbdd..f48365e1e 100644 --- a/docs/Run.md +++ b/docs/Run.md @@ -9,8 +9,10 @@ Mirai Console 可以独立启动,也可以被嵌入到某个应用中。 ## 手动配置独立启动 +强烈建议使用自动启动工具,若无法使用,可以参考如下手动启动方式。 + ### 环境 -- JRE 11+ / JDK 11+ +- JRE 8+ / JDK 8+ ### 准备文件 @@ -22,7 +24,7 @@ Mirai Console 可以独立启动,也可以被嵌入到某个应用中。 只有 mirai-console 前端才有入口点 `main` 方法。目前只有一个 terminal 前端可用。 -#### 从JCenter下载模块 +#### 从 JCenter 下载模块 mirai 在版本发布时会将发布的构建存放与 [mirai-bintray-repo]。 @@ -32,15 +34,15 @@ mirai 在版本发布时会将发布的构建存放与 [mirai-bintray-repo]。 ```shell script # 注: 自行更换对应版本号 -# Download Mirai Core All +# Download mirai-core-all curl -L https://maven.aliyun.com/repository/public/net/mamoe/mirai-core-all/1.3.3/mirai-core-all-1.3.3-all.jar -o mirai-core-all-1.3.3.jar -# Download Mirai Console All +# Download mirai-console curl -L https://maven.aliyun.com/repository/public/net/mamoe/mirai-console/1.0.0/mirai-console-1.0.0-all.jar -o mirai-console-1.0.0.jar -# Download Mirai Console Terminal +# Download mirai-console-terminal curl -L https://maven.aliyun.com/repository/public/net/mamoe/mirai-console-terminal/1.0.0/mirai-console-terminal-1.0.0-all.jar -o mirai-console-terminal-1.0.0.jar @@ -48,7 +50,7 @@ curl -L https://maven.aliyun.com/repository/public/net/mamoe/mirai-console-termi ### 启动 mirai-console-terminal 前端 -1. 下载如下三个模块的最新版本文件并放到一个文件夹内 (如 `libs`)(详见 [下载模块](#从JCenter下载模块)): +1. 下载如下三个模块的最新版本文件并放到一个文件夹内 (如 `libs`)(详见 [下载模块](#从-jcenter-下载模块)): - mirai-core-all - mirai-console - mirai-console-terminal @@ -77,19 +79,66 @@ echo -e '\033]2;Mirai Console\007' java -cp "./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader $* ``` -然后就可以开始使用 mirai-console 了 +然后就可以开始使用 mirai-console 了。 #### mirai-console-terminal 前端参数 -使用 `./start-mirai-console --help` 查看 mirai-console-terminal 支持的启动参数 - - -### 启动 mirai-console-pure 前端 - -与启动 `mirai-console-terminal` 前端大体相同 -- 下载 `mirai-console-terminal` 改成下载 `mirai-console-pure` -- 启动入口从 `net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader` 改成 `net.mamoe.mirai.console.pure.MiraiConsolePureLoader` - +使用 `./start-mirai-console --help` 查看 mirai-console-terminal 支持的启动参数。 [mirai-repo]: https://github.com/project-mirai/mirai-repo/tree/master/shadow [mirai-bintray-repo]: https://bintray.com/him188moe/mirai [mirai-core-all]: https://bintray.com/him188moe/mirai/mirai-core-all + + +## 嵌入应用启动(实验性) + +Mirai Console 可以嵌入一个 JVM 应用启动。 + +### 环境 + +- JDK 1.8+ / Android SDK 26+ (Android 8+) +- Kotlin 1.4+ + +### 添加依赖 + +[选择版本](ConfiguringProjects.md#选择版本) + +`build.gradle.kts`: +```kotlin +repositories { + jcenter() +} +dependencies { + implementation("net.mamoe:mirai-console:1.0.1") + implementation("net.mamoe:mirai-console-terminal:1.0.1") + implementation("net.mamoe:mirai-core:1.3.3") +} +``` + +### 启动 Terminal 前端 + +一行启动: +```kotlin +MiraiConsoleTerminalLoader.startAsDaemon() +``` + +注意, Mirai Console 将会以 '守护进程' 形式启动,不会阻止主线程退出。 + +### 从内存加载 JVM 插件(实验性) + +在嵌入使用时,插件可以直接加载: + +```kotlin +```kotlin +MiraiConsoleTerminalLoader.startAsDaemon() +// 先启动 Mirai Console + +// Kotlin +Plugin.load() // 扩展函数 +Plugin.enable() // 扩展函数 + +// Java +PluginManager.INSTANCE.loadPlugin(Plugin) +PluginManager.INSTANCE.enablePlugin(Plugin) +``` + +但注意:这种方法目前是实验性的——一些特定的功能如注册扩展可能不会正常工作。 \ No newline at end of file diff --git a/frontend/mirai-console-terminal/src/ConsoleThread.kt b/frontend/mirai-console-terminal/src/ConsoleThread.kt index b6cdcaa28..2756b7da9 100644 --- a/frontend/mirai-console-terminal/src/ConsoleThread.kt +++ b/frontend/mirai-console-terminal/src/ConsoleThread.kt @@ -55,8 +55,8 @@ internal fun startupConsoleThread() { } MiraiConsole.launch(CoroutineName("Console Command")) { while (true) { - try { - val next = MiraiConsole.requestInput("").let { + val next = try { + MiraiConsole.requestInput("").let { when { it.isBlank() -> it it.startsWith(CommandManager.commandPrefix) -> it @@ -64,9 +64,25 @@ internal fun startupConsoleThread() { else -> CommandManager.commandPrefix + it } } - if (next.isBlank()) { - continue - } + } catch (e: InterruptedException) { + return@launch + } catch (e: CancellationException) { + return@launch + } catch (e: UserInterruptException) { + BuiltInCommands.StopCommand.run { ConsoleCommandSender.handle() } + return@launch + } catch (eof: EndOfFileException) { + consoleLogger.warning("Closing input service...") + return@launch + } catch (e: Throwable) { + consoleLogger.error("Error in reading next command", e) + consoleLogger.warning("Closing input service...") + return@launch + } + if (next.isBlank()) { + continue + } + try { // consoleLogger.debug("INPUT> $next") when (val result = ConsoleCommandSender.executeCommand(next)) { is Success -> { @@ -97,12 +113,6 @@ internal fun startupConsoleThread() { return@launch } catch (e: CancellationException) { return@launch - } catch (e: UserInterruptException) { - BuiltInCommands.StopCommand.run { ConsoleCommandSender.handle() } - return@launch - } catch (eof: EndOfFileException) { - consoleLogger.warning("Closing input service...") - return@launch } catch (e: Throwable) { consoleLogger.error("Unhandled exception", e) } diff --git a/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt b/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt index 581728676..0488d58ef 100644 --- a/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt +++ b/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt @@ -75,6 +75,7 @@ class MiraiConsoleImplementationTerminal MiraiConsole.mainLogger.error("Exception in coroutine $coroutineName", throwable) }) { override val consoleInput: ConsoleInput get() = ConsoleInputImpl + override val isAnsiSupported: Boolean get() = true override fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver { return DefaultLoginSolver(input = { requestInput("LOGIN> ") }) @@ -146,7 +147,7 @@ private object ConsoleFrontEndDescImpl : MiraiConsoleFrontEndDescription { override val version: SemVersion = SemVersion(net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.versionConst) } -private val ANSI_RESET = Ansi().reset().toString() +internal val ANSI_RESET = Ansi().reset().toString() internal val LoggerCreator: (identity: String?) -> MiraiLogger = { PlatformLogger(identity = it, output = { line -> diff --git a/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt b/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt index aead7f1d8..e54b68b2f 100644 --- a/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt +++ b/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt @@ -191,7 +191,7 @@ internal fun overrideSTD() { internal object ConsoleCommandSenderImplTerminal : MiraiConsoleImplementation.ConsoleCommandSenderImpl { override suspend fun sendMessage(message: String) { kotlin.runCatching { - lineReader.printAbove(message) + lineReader.printAbove(message + ANSI_RESET) }.onFailure { exception -> // If failed. It means JLine Terminal not working... PrintStream(FileOutputStream(FileDescriptor.err)).use { diff --git a/gradle.properties b/gradle.properties index 57e095583..829238342 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,5 +2,4 @@ kotlin.code.style=official org.gradle.vfs.watch=true kotlin.parallel.tasks.in.project=true -org.gradle.parallel=true -org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m -Dfile.encoding=UTF-8 +org.gradle.parallel=true \ No newline at end of file diff --git a/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt b/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt index 7f2544a39..95acfc18b 100644 --- a/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt +++ b/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt @@ -10,14 +10,19 @@ package net.mamoe.mirai.console.compiler.common.diagnostics import com.intellij.psi.PsiElement import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0.create import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1.create import org.jetbrains.kotlin.diagnostics.DiagnosticFactory2.create import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.diagnostics.Severity.ERROR -import org.jetbrains.kotlin.psi.KtCallExpression -import org.jetbrains.kotlin.psi.KtNamedDeclaration -import org.jetbrains.kotlin.psi.KtTypeProjection +import org.jetbrains.kotlin.diagnostics.Severity.WARNING +import org.jetbrains.kotlin.psi.* +/** + * 如何增加一个错误: + * 1. 在 [MiraiConsoleErrors] 添加 + * 2. 在 [MiraiConsoleErrorsRendering] 添加对应的 render + */ object MiraiConsoleErrors { @JvmField val ILLEGAL_PLUGIN_DESCRIPTION = create(ERROR) @@ -46,6 +51,18 @@ object MiraiConsoleErrors { @JvmField val ILLEGAL_PERMISSION_REGISTER_USE = create(ERROR) + @JvmField + val ILLEGAL_VERSION_REQUIREMENT = create(ERROR) + +// @JvmField +// val INAPPLICABLE_COMMAND_ANNOTATION = create(ERROR) + + @JvmField + val RESTRICTED_CONSOLE_COMMAND_OWNER = create(WARNING) + + @JvmField + val ILLEGAL_COMMAND_DECLARATION_RECEIVER = create(ERROR) + @Suppress("ObjectPropertyName", "unused") @JvmField @Deprecated("", level = DeprecationLevel.ERROR) diff --git a/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt b/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt index effb040ff..b1974df60 100644 --- a/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt +++ b/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.console.compiler.common.diagnostics +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_COMMAND_DECLARATION_RECEIVER import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_COMMAND_NAME import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_COMMAND_REGISTER_USE import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_ID @@ -16,12 +17,17 @@ import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.IL import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_NAMESPACE import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_REGISTER_USE import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_VERSION_REQUIREMENT import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.NOT_CONSTRUCTABLE_TYPE +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.UNSERIALIZABLE_TYPE import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap import org.jetbrains.kotlin.diagnostics.rendering.Renderers +/** + * @see MiraiConsoleErrors + */ object MiraiConsoleErrorsRendering : DefaultErrorMessages.Extension { private val MAP = DiagnosticFactoryToRendererMap("MiraiConsole").apply { put( @@ -84,6 +90,29 @@ object MiraiConsoleErrorsRendering : DefaultErrorMessages.Extension { Renderers.DECLARATION_NAME, Renderers.STRING ) + + put( + ILLEGAL_VERSION_REQUIREMENT, + "{1}", + Renderers.STRING, + Renderers.STRING + ) + + put( + ILLEGAL_COMMAND_DECLARATION_RECEIVER, + "指令函数的接收者参数必须为 CommandSender 及其子类或无接收者.", + ) + + put( + RESTRICTED_CONSOLE_COMMAND_OWNER, + "插件不允许使用 ConsoleCommandOwner 构造指令, 请使用插件主类作为 CommandOwner", + ) + +// put( +// INAPPLICABLE_COMMAND_ANNOTATION, +// "''{0}'' 无法在顶层函数使用.", +// Renderers.STRING, +// ) } override fun getMap() = MAP diff --git a/tools/compiler-common/src/resolve/resolveTypes.kt b/tools/compiler-common/src/resolve/resolveTypes.kt index c8d5bce00..9f3ce2a51 100644 --- a/tools/compiler-common/src/resolve/resolveTypes.kt +++ b/tools/compiler-common/src/resolve/resolveTypes.kt @@ -32,6 +32,9 @@ val AUTO_SERVICE = FqName("com.google.auto.service.AutoService") val COMPOSITE_COMMAND_SUB_COMMAND_FQ_NAME = FqName("net.mamoe.mirai.console.command.CompositeCommand.SubCommand") val SIMPLE_COMMAND_HANDLER_COMMAND_FQ_NAME = FqName("net.mamoe.mirai.console.command.SimpleCommand.Handler") +val COMMAND_SENDER_FQ_NAME = FqName("net.mamoe.mirai.console.command.CommandSender") +val CONSOLE_COMMAND_SENDER_FQ_NAME = FqName("net.mamoe.mirai.console.command.ConsoleCommandSender") +val CONSOLE_COMMAND_OWNER_FQ_NAME = FqName("net.mamoe.mirai.console.command.ConsoleCommandOwner") /////////////////////////////////////////////////////////////////////////// // Plugin @@ -69,7 +72,8 @@ enum class ResolveContextKind { PERMISSION_NAME, PERMISSION_ID, - RESTRICTED_NO_ARG_CONSTRUCTOR + RESTRICTED_NO_ARG_CONSTRUCTOR, + RESTRICTED_CONSOLE_COMMAND_OWNER, ; companion object { diff --git a/tools/gradle-plugin/README.md b/tools/gradle-plugin/README.md index 493d793dd..d1d1c79d0 100644 --- a/tools/gradle-plugin/README.md +++ b/tools/gradle-plugin/README.md @@ -9,10 +9,14 @@ Mirai Console Gradle 插件。 ## 功能 - 为 `main` 源集配置 `mirai-core`,`mirai-console` 依赖 -- 为 `test` 源集配置 `mirai-core-qqandroid`, `mirai-console-terminal` 的依赖 (用于启动测试) -- 添加 mirai 依赖仓库链接 -- 配置插件 JAR 打包构建任务 `buildPlugin` (带依赖) +- 为 `test` 源集配置 `mirai-core-qqandroid`, `mirai-console-terminal` 的依赖 (用于启动测试) +- 配置 Kotlin 编译目标为 Java 1.8 +- 配置 Kotlin 编译器 jvm-default 设置为 `all`, 即为所有接口中的默认实现生成 Java 1.8 起支持的 `default` 方法 +- 配置 Java 编译目标为 Java 1.8 +- 配置 Java 编译编码为 UTF-8 +- 配置插件 JAR 打包构建任务 `buildPlugin`(带依赖, 成品 JAR 可以被 Mirai Console 加载) +支持 Kotlin 多平台项目(Multiplatform Projects)。每个 JVM 或 Android 目标平台都会被如上配置,对应打包任务带有编译目标的名称,如 `buildPluginJvm` ### `buildPlugin` @@ -20,7 +24,7 @@ Mirai Console Gradle 插件。 #### 执行 `buildPlugin` ```shell script -$ gradlew buildPlugin +./gradlew buildPlugin ``` 打包结果存放在 `build/mirai/` 目录下。 @@ -45,3 +49,5 @@ mirai { excludeDependency("com.google.code.gson", "gson") } ``` + +插件一般不需要手动排除依赖。Mirai Console 已经包含的依赖都会自动在打包过程中被排除。 \ No newline at end of file diff --git a/tools/gradle-plugin/build.gradle.kts b/tools/gradle-plugin/build.gradle.kts index 4f50923f1..9c5b6a004 100644 --- a/tools/gradle-plugin/build.gradle.kts +++ b/tools/gradle-plugin/build.gradle.kts @@ -15,6 +15,7 @@ plugins { dependencies { compileOnly(gradleApi()) + compileOnly(gradleKotlinDsl()) compileOnly(kotlin("gradle-plugin-api").toString()) { exclude("org.jetbrains.kotlin", "kotlin-stdlib") } @@ -26,11 +27,16 @@ dependencies { api("com.github.jengelman.gradle.plugins:shadow:6.0.0") api(`jetbrains-annotations`) + api("com.jfrog.bintray.gradle:gradle-bintray-plugin:${Versions.bintray}") } version = Versions.console description = "Gradle plugin for Mirai Console" +kotlin { + explicitApi() +} + pluginBundle { website = "https://github.com/mamoe/mirai-console" vcsUrl = "https://github.com/mamoe/mirai-console" diff --git a/tools/gradle-plugin/src/BuildMiraiPluginTask.kt b/tools/gradle-plugin/src/BuildMiraiPluginTask.kt new file mode 100644 index 000000000..53227973f --- /dev/null +++ b/tools/gradle-plugin/src/BuildMiraiPluginTask.kt @@ -0,0 +1,25 @@ +package net.mamoe.mirai.console.gradle + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import java.io.File + +@CacheableTask +public open class BuildMiraiPluginTask : ShadowJar() { + @Internal + public var targetField: KotlinTarget? = null + + @get:Internal + public val target: KotlinTarget + get() = targetField!! + + /** + * ShadowJar 打包结果 + */ + @get:OutputFile + public val output: File + get() = outputs.files.singleFile +} \ No newline at end of file diff --git a/tools/gradle-plugin/src/MiraiConsoleExtension.kt b/tools/gradle-plugin/src/MiraiConsoleExtension.kt index 572a74a76..c1c868b6f 100644 --- a/tools/gradle-plugin/src/MiraiConsoleExtension.kt +++ b/tools/gradle-plugin/src/MiraiConsoleExtension.kt @@ -12,7 +12,12 @@ package net.mamoe.mirai.console.gradle import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.jfrog.bintray.gradle.BintrayExtension +import com.jfrog.bintray.gradle.BintrayPlugin import org.gradle.api.JavaVersion +import org.gradle.api.XmlProvider +import org.gradle.api.plugins.PluginContainer +import org.gradle.api.publish.maven.MavenPublication /** * ``` @@ -22,41 +27,41 @@ import org.gradle.api.JavaVersion * ``` */ // must be open -open class MiraiConsoleExtension { +public open class MiraiConsoleExtension { /** * 为 `true` 时不自动添加 mirai-core 的依赖 * * 默认: `false` */ - var noCore: Boolean = false + public var noCore: Boolean = false /** * 为 `true` 时不自动为 test 模块添加 mirai-core-qqandroid 的依赖. * * 默认: `false` */ - var noTestCoreQQAndroid: Boolean = false + public var noTestCoreQQAndroid: Boolean = false /** * 为 `true` 时不自动添加 mirai-console 的依赖. * * 默认: `false` */ - var noConsole: Boolean = false + public var noConsole: Boolean = false /** * 自动添加的 mirai-core 和 mirai-core-qqandroid 的版本. * * 默认: 与本 Gradle 插件编译时的 mirai-core 版本相同. [VersionConstants.CORE_VERSION] */ - var coreVersion: String = VersionConstants.CORE_VERSION + public var coreVersion: String = VersionConstants.CORE_VERSION /** * 自动添加的 mirai-console 后端和前端的版本. * * 默认: 与本 Gradle 插件版本相同. [VersionConstants.CONSOLE_VERSION] */ - var consoleVersion: String = VersionConstants.CONSOLE_VERSION + public var consoleVersion: String = VersionConstants.CONSOLE_VERSION /** * 自动为 test 模块添加的前端依赖名称 @@ -65,7 +70,7 @@ open class MiraiConsoleExtension { * * 默认: [MiraiConsoleFrontEndKind.TERMINAL] */ - var useTestConsoleFrontEnd: MiraiConsoleFrontEndKind? = MiraiConsoleFrontEndKind.TERMINAL + public var useTestConsoleFrontEnd: MiraiConsoleFrontEndKind? = MiraiConsoleFrontEndKind.TERMINAL /** * Java 和 Kotlin 编译目标. 至少为 [JavaVersion.VERSION_1_8]. @@ -74,7 +79,7 @@ open class MiraiConsoleExtension { * * 默认: [JavaVersion.VERSION_1_8] */ - var jvmTarget: JavaVersion = JavaVersion.VERSION_1_8 + public var jvmTarget: JavaVersion = JavaVersion.VERSION_1_8 /** * 默认会配置 Kotlin 编译器参数 "-Xjvm-default=all". 将此项设置为 `false` 可避免配置. @@ -83,7 +88,7 @@ open class MiraiConsoleExtension { * * 默认: `false` */ - var dontConfigureKotlinJvmDefault: Boolean = false + public var dontConfigureKotlinJvmDefault: Boolean = false internal val shadowConfigurations: MutableList Unit> = mutableListOf() internal val excludedDependencies: MutableSet = mutableSetOf() @@ -96,7 +101,7 @@ open class MiraiConsoleExtension { /** * 配置 [ShadowJar] (即打包插件) */ - fun configureShadow(configure: ShadowJar.() -> Unit) { + public fun configureShadow(configure: ShadowJar.() -> Unit) { shadowConfigurations.add(configure) } @@ -105,7 +110,7 @@ open class MiraiConsoleExtension { * * @param notation 格式为 "groupId:name". 如 "org.jetbrains.kotlin:kotlin-stdlib" */ - fun excludeDependency(notation: String) { + public fun excludeDependency(notation: String) { requireNotNull(notation.count { it == ':' } == 1) { "Invalid dependency notation $notation." } excludedDependencies.add(ExcludedDependency(notation.substringBefore(':'), notation.substringAfter(':'))) } @@ -116,11 +121,192 @@ open class MiraiConsoleExtension { * @param group 如 "org.jetbrains.kotlin" * @param name 如 "kotlin-stdlib" */ - fun excludeDependency(group: String, name: String) { + public fun excludeDependency(group: String, name: String) { excludedDependencies.add(ExcludedDependency(group, name)) } + + /** + * Bintray 插件成品 JAR 发布 配置. + * + * @see PluginPublishing + * @since 1.1.0 + */ + public val publishing: PluginPublishing = PluginPublishing() + + /** + * 控制自动配置 Bintray 发布. 默认为 `false`, 表示不自动配置发布. + * + * 开启后将会: + * - 创建名为 "mavenJava" 的 [MavenPublication] + * - [应用][PluginContainer.apply] [BintrayPlugin], 配置 Bintray 相关参数 + * - 创建 task "publishPlugin" + * + * @since 1.1.0 + */ + public var publishingEnabled: Boolean = false + + /** + * 开启自动配置 Bintray 插件成品 JAR 发布, 并以 [configure] 配置 [PluginPublishing]. + * + * @see [PluginPublishing] + * @see publishingEnabled + * @since 1.1.0 + */ + public inline fun publishing(crossinline configure: PluginPublishing.() -> Unit) { + publishingEnabled = true + publishing.run(configure) + } + + /** + * 开启自动配置 Bintray 插件成品 JAR 发布. + * + * @see [PluginPublishing] + * @see publishingEnabled + * @since 1.1.0 + */ + public fun publishing() { + publishingEnabled = true + } + + /** + * Bintray 插件成品 JAR 发布 配置. + * + * 对于一个属性 PROP, 会按以下顺序依次尝试读取: + * 1. Gradle 参数 + * - "gradle.properties" + * - ext + * - Gradle -P 启动参数 + * 2. [System.getProperty] "PROP" + * 3. 当前和所有父 project 根目录下 "PROP" 文件的内容 + * 4. [System.getenv] "PROP" + * + * @see publishing + * @see publishingEnabled + * @since 1.1.0 + */ + public class PluginPublishing internal constructor() { + /////////////////////////////////////////////////////////////////////////// + // Required arguments + /////////////////////////////////////////////////////////////////////////// + + /** + * Bintray 账户名. 必须. + * 若为 `null`, 将会以 [PluginPublishing] 中描述的步骤获取 "bintray.user" + * + * @see [PluginPublishing] + */ + public var user: String? = null + + /** + * Bintray 账户 key. 必须. + * 若为 `null`, 将会以 [PluginPublishing] 中描述的步骤获取 "bintray.key" + */ + public var key: String? = null + + /** + * 目标仓库名称. 必须. + * 若为 `null`, 将会以 [PluginPublishing] 中描述的步骤获取 "bintray.repo" + */ + public var repo: String? = null + + /** + * 目标仓库名称. 必须. + * 若为 `null`, 将会以 [PluginPublishing] 中描述的步骤获取 "bintray.package" + */ + public var packageName: String? = null + + + /////////////////////////////////////////////////////////////////////////// + // Optional arguments + /////////////////////////////////////////////////////////////////////////// + + // Artifact + + /** + * 发布的 artifact id. 默认为 `project.name`. + * + * artifact id 是类似于 "net.mamoe:mirai-console:1.1.0" 中的 "mirai-console" + */ + public var artifactId: String? = null + + /** + * 发布的 group id. 默认为 `project.group`. + * + * group id 是类似于 "net.mamoe:mirai-console:1.1.0" 中的 "net.mamoe" + */ + public var groupId: String? = null + + /** + * 发布的版本号, 默认为 `project.version` + * + * 版本号是类似于 "net.mamoe:mirai-console:1.1.0" 中的 "1.1.0" + */ + public var version: String? = null + + /** + * 发布的描述, 默认为 `project.description` + */ + public var description: String? = null + + // Bintray + + /** + * Bintray organization 名. 可选. + * 若为 `null`, 将会以 [PluginPublishing] 中描述的步骤获取 "bintray.org". + * 仍然无法获取时发布到 [user] 账号下的仓库 [repo], 否则发布到指定 [org] 下的仓库 [repo]. + */ + public var org: String? = null + + /** + * 上传后自动发布. 默认 `true`. + */ + public var publish: Boolean = true + + /** + * 当文件冲突时覆盖. 默认 `false`. + */ + public var override: Boolean = false + + // Custom configurations + + internal val bintrayConfigs = mutableListOf Unit>() + internal val bintrayPackageConfigConfigs = mutableListOf Unit>() + internal val mavenPomConfigs = mutableListOf Unit>() + internal val mavenPublicationConfigs = mutableListOf Unit>() + + /** + * 自定义配置 [BintrayExtension],覆盖 + */ + public fun bintray(configure: BintrayExtension.() -> Unit) { + bintrayConfigs.add(configure) + } + + /** + * 自定义配置 [BintrayExtension.PackageConfig] + */ + public fun packageConfig(configure: BintrayExtension.PackageConfig.() -> Unit) { + bintrayPackageConfigConfigs.add(configure) + } + + /** + * 自定义配置 maven pom.xml [XmlProvider] + */ + public fun mavenPom(configure: XmlProvider.() -> Unit) { + mavenPomConfigs.add(configure) + } + + /** + * 自定义配置 [MavenPublication] + */ + public fun mavenPublication(configure: MavenPublication.() -> Unit) { + mavenPublicationConfigs.add(configure) + } + } } -enum class MiraiConsoleFrontEndKind { +/** + * @see MiraiConsoleExtension.useTestConsoleFrontEnd + */ +public enum class MiraiConsoleFrontEndKind { TERMINAL, } \ No newline at end of file diff --git a/tools/gradle-plugin/src/MiraiConsoleGradlePlugin.kt b/tools/gradle-plugin/src/MiraiConsoleGradlePlugin.kt index 06ea0c733..7fa6ad695 100644 --- a/tools/gradle-plugin/src/MiraiConsoleGradlePlugin.kt +++ b/tools/gradle-plugin/src/MiraiConsoleGradlePlugin.kt @@ -13,7 +13,7 @@ package net.mamoe.mirai.console.gradle import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.jfrog.bintray.gradle.BintrayPlugin import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPlugin @@ -25,17 +25,18 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.KotlinTarget -class MiraiConsoleGradlePlugin : Plugin { - companion object { +public class MiraiConsoleGradlePlugin : Plugin { + public companion object { internal const val BINTRAY_REPOSITORY_URL = "https://dl.bintray.com/him188moe/mirai" } - private fun KotlinSourceSet.configureSourceSet(project: Project) { + private fun KotlinSourceSet.configureSourceSet(project: Project, target: KotlinTarget) { languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") - dependencies { configureDependencies(project, this@configureSourceSet) } + dependencies { configureDependencies(project, this@configureSourceSet, target) } } private fun Project.configureTarget(target: KotlinTarget) { @@ -48,24 +49,37 @@ class MiraiConsoleGradlePlugin : Plugin { if (!miraiExtension.dontConfigureKotlinJvmDefault) freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" } } - target.compilations.flatMap { it.allKotlinSourceSets }.forEach { sourceSet -> - sourceSet.configureSourceSet(project) + when (target.platformType) { + KotlinPlatformType.jvm, + KotlinPlatformType.androidJvm, + KotlinPlatformType.common + -> { + target.compilations.flatMap { it.allKotlinSourceSets }.forEach { sourceSet -> + sourceSet.configureSourceSet(project, target) + } + } + else -> { + } } } @Suppress("SpellCheckingInspection") - private fun KotlinDependencyHandler.configureDependencies(project: Project, sourceSet: KotlinSourceSet) { + private fun KotlinDependencyHandler.configureDependencies(project: Project, sourceSet: KotlinSourceSet, target: KotlinTarget) { val miraiExtension = project.miraiExtension + val isJvm = target.platformType == KotlinPlatformType.jvm || target.platformType == KotlinPlatformType.androidJvm + if (!miraiExtension.noCore) compileOnly("net.mamoe:mirai-core:${miraiExtension.coreVersion}") - if (!miraiExtension.noConsole) compileOnly("net.mamoe:mirai-console:${miraiExtension.consoleVersion}") + if (!miraiExtension.noConsole && isJvm) compileOnly("net.mamoe:mirai-console:${miraiExtension.consoleVersion}") if (sourceSet.name.endsWith("test", ignoreCase = true)) { if (!miraiExtension.noCore) api("net.mamoe:mirai-core:${miraiExtension.coreVersion}") - if (!miraiExtension.noConsole) api("net.mamoe:mirai-console:${miraiExtension.consoleVersion}") + if (!miraiExtension.noConsole && isJvm) api("net.mamoe:mirai-console:${miraiExtension.consoleVersion}") if (!miraiExtension.noTestCoreQQAndroid) api("net.mamoe:mirai-core-qqandroid:${miraiExtension.coreVersion}") - when (miraiExtension.useTestConsoleFrontEnd) { - MiraiConsoleFrontEndKind.TERMINAL -> api("net.mamoe:mirai-console-terminal:${miraiExtension.consoleVersion}") + if (isJvm) { + when (miraiExtension.useTestConsoleFrontEnd) { + MiraiConsoleFrontEndKind.TERMINAL -> api("net.mamoe:mirai-console-terminal:${miraiExtension.consoleVersion}") + } } } } @@ -87,18 +101,18 @@ class MiraiConsoleGradlePlugin : Plugin { tasks.findByName("shadowJar")?.enabled = false - fun registerBuildPluginTask(target: KotlinTarget, isSinglePlatform: Boolean) { - tasks.create(if (isSinglePlatform) "buildPlugin" else "buildPlugin${target.name.capitalize()}", ShadowJar::class.java).apply shadow@{ + fun registerBuildPluginTask(target: KotlinTarget, isSingleTarget: Boolean) { + tasks.create("buildPlugin".wrapNameWithPlatform(target, isSingleTarget), BuildMiraiPluginTask::class.java).apply shadow@{ group = "mirai" + targetField = target + + archiveExtension.set("mirai.jar") val compilations = target.compilations.filter { it.name == MAIN_COMPILATION_NAME } compilations.forEach { dependsOn(it.compileKotlinTask) - from(it.output) - for (allKotlinSourceSet in it.allKotlinSourceSets) { - from(allKotlinSourceSet.resources) - } + from(it.output.allOutputs) } from(project.configurations.getByName("runtimeClasspath").copyRecursive { dependency -> @@ -128,17 +142,20 @@ class MiraiConsoleGradlePlugin : Plugin { } override fun apply(target: Project): Unit = with(target) { - target.extensions.create("mirai", MiraiConsoleExtension::class.java) + extensions.create("mirai", MiraiConsoleExtension::class.java) - target.plugins.apply(JavaPlugin::class.java) - target.plugins.apply(ShadowPlugin::class.java) - - target.repositories.maven { it.setUrl(BINTRAY_REPOSITORY_URL) } + plugins.apply(JavaPlugin::class.java) + plugins.apply("org.gradle.maven-publish") + plugins.apply("org.gradle.maven") + plugins.apply(ShadowPlugin::class.java) + plugins.apply(BintrayPlugin::class.java) + repositories.maven { it.setUrl(BINTRAY_REPOSITORY_URL) } afterEvaluate { configureCompileTarget() - registerBuildPluginTasks() kotlinTargets.forEach { configureTarget(it) } + registerBuildPluginTasks() + configurePublishing() } } } @@ -156,4 +173,7 @@ internal val Project.kotlinTargets: Collection is KotlinSingleTargetExtension -> listOf(kotlinExtension.target) else -> error("[MiraiConsole] Internal error: kotlinExtension is neither KotlinMultiplatformExtension nor KotlinSingleTargetExtension") } - } \ No newline at end of file + } + +internal val Project.kotlinJvmOrAndroidTargets: Collection + get() = kotlinTargets.filter { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm } diff --git a/tools/gradle-plugin/src/VersionConstants.kt b/tools/gradle-plugin/src/VersionConstants.kt index 919c42906..a7c77b185 100644 --- a/tools/gradle-plugin/src/VersionConstants.kt +++ b/tools/gradle-plugin/src/VersionConstants.kt @@ -10,6 +10,6 @@ package net.mamoe.mirai.console.gradle internal object VersionConstants { - const val CONSOLE_VERSION = "1.0.1-dev-1" // value is written here automatically during build + const val CONSOLE_VERSION = "1.1.0-dev-32" // value is written here automatically during build const val CORE_VERSION = "1.3.3" // value is written here automatically during build } \ No newline at end of file diff --git a/tools/gradle-plugin/src/publishing.kt b/tools/gradle-plugin/src/publishing.kt new file mode 100644 index 000000000..db01b23de --- /dev/null +++ b/tools/gradle-plugin/src/publishing.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.gradle + +import com.google.gson.Gson +import com.jfrog.bintray.gradle.tasks.BintrayUploadTask +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.getValue +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.registering +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import java.io.File + + +private val Project.selfAndParentProjects: Sequence + get() = generateSequence(this) { it.parent } + +private fun Project.findPropertySmart(propName: String): String? { + return findProperty(propName)?.toString() + ?: System.getProperty(propName) + ?: selfAndParentProjects.map { it.projectDir.resolve(propName) }.find { it.exists() }?.readText() + ?: System.getenv(propName) +} + +private fun Project.findPropertySmartOrFail(propName: String): String { + return findPropertySmart(propName) + ?: error("[Mirai Console] Cannot find property for publication: '$propName'. Please check your 'mirai' configuration.") +} + +internal fun Project.configurePublishing() { + if (!miraiExtension.publishingEnabled) return + val isSingleTarget = kotlinJvmOrAndroidTargets.size == 1 + + kotlinJvmOrAndroidTargets.forEach { + registerPublishPluginTasks(it, isSingleTarget) + registerMavenPublications(it, isSingleTarget) + } + + registerBintrayPublish() +} + +private inline fun TaskContainer.getSingleTask(): T = filterIsInstance().single() + +private fun Project.registerPublishPluginTasks() { + val isSingleTarget = kotlinJvmOrAndroidTargets.size == 1 + kotlinJvmOrAndroidTargets.forEach { registerPublishPluginTasks(it, isSingleTarget) } +} + +// effectively public +internal data class PluginMetadata( + val groupId: String, + val artifactId: String, + val version: String, + val description: String?, + val dependencies: List +) + +internal fun String.wrapNameWithPlatform(target: KotlinTarget, isSingleTarget: Boolean): String { + return if (isSingleTarget) this else "$this${target.name.capitalize()}" +} + +private fun Project.registerPublishPluginTasks(target: KotlinTarget, isSingleTarget: Boolean) { + val generateMetadataTask = + tasks.register("generatePluginMetadata".wrapNameWithPlatform(target, isSingleTarget)).get().apply { + group = "mirai" + + val metadataFile = + project.buildDir.resolve("mirai").resolve(if (isSingleTarget) "mirai-plugin.metadata" else "mirai-plugin-${target.name}.metadata") + outputs.file(metadataFile) + + + + doLast { + val mirai = miraiExtension + + val output = outputs.files.singleFile + output.parentFile.mkdir() + + val dependencies = configurations[target.compilations["main"].apiConfigurationName].allDependencies.map { + "${it.group}:${it.name}:${it.version}" + } + + val json = Gson().toJson(PluginMetadata( + groupId = mirai.publishing.groupId ?: project.group.toString(), + artifactId = mirai.publishing.artifactId ?: project.name, + version = mirai.publishing.version ?: project.version.toString(), + description = mirai.publishing.description ?: project.description, + dependencies = dependencies + )) + + logger.info("Generated mirai plugin metadata json: $json") + + output.writeText(json) + } + } + + val bintrayUpload = tasks.getByName(BintrayUploadTask.getTASK_NAME()).dependsOn( + "buildPlugin".wrapNameWithPlatform(target, isSingleTarget), + generateMetadataTask, + // "shadowJar", + tasks.filterIsInstance().single { it.target == target } + ) + tasks.register("publishPlugin".wrapNameWithPlatform(target, isSingleTarget)).get().apply { + group = "mirai" + dependsOn(bintrayUpload) + } +} + +internal inline fun File.renamed(block: File.(nameWithoutExtension: String) -> String): File = this.resolveSibling(block(this, nameWithoutExtension)) + +private fun Project.registerBintrayPublish() { + val mirai = miraiExtension + + bintray { + user = mirai.publishing.user ?: findPropertySmartOrFail("bintray.user") + key = mirai.publishing.key ?: findPropertySmartOrFail("bintray.key") + + val targets = kotlinJvmOrAndroidTargets + if (targets.size == 1) { + setPublications("mavenJava") + } else { + setPublications(*targets.map { "mavenJava".wrapNameWithPlatform(it, false) }.toTypedArray()) + } + + setConfigurations("archives") + + publish = mirai.publishing.publish + override = mirai.publishing.override + + pkg.apply { + repo = mirai.publishing.repo ?: findPropertySmartOrFail("bintray.repo") + name = mirai.publishing.packageName ?: findPropertySmartOrFail("bintray.package") + userOrg = mirai.publishing.org ?: findPropertySmart("bintray.org") + desc = mirai.publishing.description ?: project.description + + mirai.publishing.bintrayPackageConfigConfigs.forEach { it.invoke(this) } + } + + mirai.publishing.bintrayConfigs.forEach { it.invoke(this) } + } +} + +private fun Project.registerMavenPublications(target: KotlinTarget, isSingleTarget: Boolean) { + val mirai = miraiExtension + + @Suppress("DEPRECATION") + val sourcesJar by tasks.registering(Jar::class) { + classifier = "sources" + from(sourceSets["main"].allSource) + } + + publishing { + /* + repositories { + maven { + // change to point to your repo, e.g. http://my.org/repo + url = uri("$buildDir/repo") + } + }*/ + publications.register("mavenJava".wrapNameWithPlatform(target, isSingleTarget), MavenPublication::class.java) { publication -> + with(publication) { + from(components["java"]) + + this.groupId = mirai.publishing.groupId ?: project.group.toString() + this.artifactId = mirai.publishing.artifactId ?: project.name + this.version = mirai.publishing.version ?: project.version.toString() + + pom.withXml { xml -> + val root = xml.asNode() + root.appendNode("description", project.description) + root.appendNode("name", project.name) + // root.appendNode("url", vcs) + root.children().last() + + mirai.publishing.mavenPomConfigs.forEach { it.invoke(xml) } + } + + artifact(sourcesJar.get()) + artifact(tasks.filterIsInstance().single { it.target == target }) + artifact(mapOf( + "source" to tasks.getByName("generatePluginMetadata".wrapNameWithPlatform(target, isSingleTarget)).outputs.files.singleFile, + "extension" to "metadata" + )) + + mirai.publishing.mavenPublicationConfigs.forEach { it.invoke(this) } + } + } + } +} + + +/** + * Configures the [bintray][com.jfrog.bintray.gradle.BintrayExtension] extension. + */ +@PublishedApi +internal fun Project.bintray(configure: com.jfrog.bintray.gradle.BintrayExtension.() -> Unit): Unit = + (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("bintray", configure) + +@PublishedApi +internal val Project.sourceSets: org.gradle.api.tasks.SourceSetContainer + get() = (this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("sourceSets") as org.gradle.api.tasks.SourceSetContainer + +/** + * Configures the [publishing][org.gradle.api.publish.PublishingExtension] extension. + */ +@PublishedApi +internal fun Project.publishing(configure: org.gradle.api.publish.PublishingExtension.() -> Unit): Unit = + (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("publishing", configure) diff --git a/tools/intellij-plugin/run/projects/test-project/build.gradle.kts b/tools/intellij-plugin/run/projects/test-project/build.gradle.kts index 3938bea1d..dacfba69d 100644 --- a/tools/intellij-plugin/run/projects/test-project/build.gradle.kts +++ b/tools/intellij-plugin/run/projects/test-project/build.gradle.kts @@ -1,8 +1,7 @@ plugins { - kotlin("jvm") version "1.4.10" - kotlin("plugin.serialization") version "1.4.10" - kotlin("kapt") version "1.4.10" - id("com.github.johnrengelman.shadow") version "5.2.0" + kotlin("jvm") version "1.4.20" + kotlin("plugin.serialization") version "1.4.20" + id("net.mamoe.mirai-console") version "1.1.0-dev-32" } group = "org.example" @@ -12,32 +11,4 @@ repositories { mavenLocal() jcenter() mavenCentral() -} - -kotlin.sourceSets.all { - languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") -} - -dependencies { - compileOnly(kotlin("stdlib-jdk8")) - - val core = "1.3.2" - val console = "1.0-RC-1" - - compileOnly("net.mamoe:mirai-console:$console") - compileOnly("net.mamoe:mirai-core:$core") - - val autoService = "1.0-rc7" - kapt("com.google.auto.service", "auto-service", autoService) - compileOnly("com.google.auto.service", "auto-service-annotations", autoService) - - testImplementation("net.mamoe:mirai-console:$console") - testImplementation("net.mamoe:mirai-core:$core") - testImplementation("net.mamoe:mirai-console-terminal:$console") - testImplementation(kotlin("stdlib-jdk8")) -} - -kotlin.target.compilations.all { - kotlinOptions.freeCompilerArgs += "-Xjvm-default=enable" - kotlinOptions.jvmTarget = "1.8" } \ No newline at end of file diff --git a/tools/intellij-plugin/run/projects/test-project/settings.gradle.kts b/tools/intellij-plugin/run/projects/test-project/settings.gradle.kts index 0e9d380ad..fcee558c6 100644 --- a/tools/intellij-plugin/run/projects/test-project/settings.gradle.kts +++ b/tools/intellij-plugin/run/projects/test-project/settings.gradle.kts @@ -1,2 +1,9 @@ rootProject.name = "test-project" +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + jcenter() + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt index 34a50f190..64c9095ad 100644 --- a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt +++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt @@ -8,6 +8,7 @@ import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin +import net.mamoe.mirai.console.util.SemVersion const val T = "org.example" // 编译期常量 @@ -24,6 +25,18 @@ object MyPluginMain : KotlinPlugin( PermissionService.INSTANCE.register(permissionId("dvs"), "ok") PermissionService.INSTANCE.register(permissionId("perm with space"), "error") PermissionId("Namespace with space", "Name with space") + SemVersion.parseRangeRequirement("") + SemVersion.parseRangeRequirement("
") + SemVersion.parseRangeRequirement("SB YELLOW") + SemVersion.parseRangeRequirement("1.0.0 || 2.0.0 || ") + SemVersion.parseRangeRequirement("1.0.0 || 2.0.0") + SemVersion.parseRangeRequirement("1.0.0 || 2.0.0 && 3.0.0") + SemVersion.parseRangeRequirement("{}") + SemVersion.parseRangeRequirement("||") + SemVersion.parseRangeRequirement(">= 114.514 || = 1919.810 || (1.1, 1.2)") + SemVersion.parseRangeRequirement("0.0.0 || {90.48}") + SemVersion.parseRangeRequirement("{114514.1919810}") + SemVersion.parseRangeRequirement("}") } fun test() { @@ -32,6 +45,9 @@ object MyPluginMain : KotlinPlugin( } +val x = "弱智黄色" + + object MyData : AutoSavePluginData("") { val value by value("") val value2 by value>() diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MySimpleCommand.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MySimpleCommand.kt index 85199308a..9ac1aaf83 100644 --- a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MySimpleCommand.kt +++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MySimpleCommand.kt @@ -2,10 +2,16 @@ package org.example.myplugin import kotlinx.serialization.Serializable import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.ConsoleCommandOwner import net.mamoe.mirai.console.command.SimpleCommand import net.mamoe.mirai.console.data.AutoSavePluginConfig import net.mamoe.mirai.console.data.value +object MySimpleCommand0001 : SimpleCommand( + ConsoleCommandOwner, "foo", + description = "示例指令" +) {} + object MySimpleCommand000 : SimpleCommand( MyPluginMain, "foo", description = "示例指令" @@ -17,7 +23,7 @@ object MySimpleCommand000 : SimpleCommand( } object DataTest : AutoSavePluginConfig("data") { - val pp by value(NoDefaultValue(1)) + val pp by value(NoDefaultValue(1)) } @Serializable diff --git a/tools/intellij-plugin/src/IDEContainerContributor.kt b/tools/intellij-plugin/src/IDEContainerContributor.kt index 00b331fc7..c6b8d1f59 100644 --- a/tools/intellij-plugin/src/IDEContainerContributor.kt +++ b/tools/intellij-plugin/src/IDEContainerContributor.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.console.intellij +import net.mamoe.mirai.console.intellij.diagnostics.CommandDeclarationChecker import net.mamoe.mirai.console.intellij.diagnostics.ContextualParametersChecker import net.mamoe.mirai.console.intellij.diagnostics.PluginDataValuesChecker import org.jetbrains.kotlin.container.StorageComponentContainer @@ -24,5 +25,6 @@ class IDEContainerContributor : StorageComponentContainerContributor { ) { container.useInstance(ContextualParametersChecker()) container.useInstance(PluginDataValuesChecker()) + container.useInstance(CommandDeclarationChecker()) } } \ No newline at end of file diff --git a/tools/intellij-plugin/src/diagnostics/CommandDeclarationChecker.kt b/tools/intellij-plugin/src/diagnostics/CommandDeclarationChecker.kt new file mode 100644 index 000000000..f840fc5c4 --- /dev/null +++ b/tools/intellij-plugin/src/diagnostics/CommandDeclarationChecker.kt @@ -0,0 +1,44 @@ +package net.mamoe.mirai.console.intellij.diagnostics + +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_COMMAND_DECLARATION_RECEIVER +import net.mamoe.mirai.console.compiler.common.resolve.COMMAND_SENDER_FQ_NAME +import net.mamoe.mirai.console.intellij.resolve.hasSuperType +import net.mamoe.mirai.console.intellij.resolve.isCompositeCommandSubCommand +import net.mamoe.mirai.console.intellij.resolve.isSimpleCommandHandler +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker +import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext + +class CommandDeclarationChecker : DeclarationChecker { + override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) { + if (declaration !is KtNamedFunction) return + + // exclusive checks + when { + declaration.isSimpleCommandHandler() -> { + } + + declaration.isCompositeCommandSubCommand() -> { + } + else -> return + } + + // common checks + checkCommandReceiverParameter(declaration)?.let { context.report(it) } + } + + companion object { + fun checkCommandReceiverParameter(declaration: KtNamedFunction): Diagnostic? { + val receiverTypeRef = declaration.receiverTypeReference ?: return null // no receiver, accept. + val receiver = receiverTypeRef.resolveReferencedType() ?: return null // unresolved type + if (!receiver.hasSuperType(COMMAND_SENDER_FQ_NAME)) { + return ILLEGAL_COMMAND_DECLARATION_RECEIVER.on(receiverTypeRef) + } + + return null + } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt b/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt index 67d62adc1..7f6b9d10b 100644 --- a/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt +++ b/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt @@ -9,23 +9,31 @@ package net.mamoe.mirai.console.intellij.diagnostics -import com.intellij.psi.PsiElement import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_COMMAND_NAME import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_ID import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_NAME import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_NAMESPACE import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_VERSION_REQUIREMENT +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.RESTRICTED_CONSOLE_COMMAND_OWNER +import net.mamoe.mirai.console.compiler.common.resolve.CONSOLE_COMMAND_OWNER_FQ_NAME import net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind import net.mamoe.mirai.console.compiler.common.resolve.resolveContextKinds +import net.mamoe.mirai.console.intellij.resolve.findChild import net.mamoe.mirai.console.intellij.resolve.resolveAllCalls import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValues import net.mamoe.mirai.console.intellij.resolve.valueParametersWithArguments +import net.mamoe.mirai.console.intellij.util.RequirementHelper +import net.mamoe.mirai.console.intellij.util.RequirementParser import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.diagnostics.Diagnostic -import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.idea.inspections.collections.isCalling +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext import java.util.* +import kotlin.reflect.KFunction2 /** * Checks parameters with [ResolveContextKind] @@ -43,10 +51,12 @@ class ContextualParametersChecker : DeclarationChecker { private val SEMANTIC_VERSIONING_REGEX = Regex("""^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?${'$'}""") - fun checkPluginId(inspectionTarget: PsiElement, value: String): Diagnostic? { + fun checkPluginId(inspectionTarget: KtElement, value: String): Diagnostic? { if (value.isBlank()) return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件 Id 不能为空. \n插件 Id$syntax") - if (value.none { it == '.' }) return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, - "插件 Id '$value' 无效. 插件 Id 必须同时包含 groupId 和插件名称. $syntax") + if (value.none { it == '.' }) return ILLEGAL_PLUGIN_DESCRIPTION.on( + inspectionTarget, + "插件 Id '$value' 无效. 插件 Id 必须同时包含 groupId 和插件名称. $syntax" + ) val lowercaseId = value.toLowerCase() @@ -60,7 +70,7 @@ class ContextualParametersChecker : DeclarationChecker { return null } - fun checkPluginName(inspectionTarget: PsiElement, value: String): Diagnostic? { + fun checkPluginName(inspectionTarget: KtElement, value: String): Diagnostic? { if (value.isBlank()) return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件名不能为空") val lowercaseName = value.toLowerCase() FORBIDDEN_ID_NAMES.firstOrNull { it == lowercaseName }?.let { illegal -> @@ -69,14 +79,14 @@ class ContextualParametersChecker : DeclarationChecker { return null } - fun checkPluginVersion(inspectionTarget: PsiElement, value: String): Diagnostic? { + fun checkPluginVersion(inspectionTarget: KtElement, value: String): Diagnostic? { if (!SEMANTIC_VERSIONING_REGEX.matches(value)) { return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "版本号无效: '$value'. \nhttps://semver.org/lang/zh-CN/") } return null } - fun checkCommandName(inspectionTarget: PsiElement, value: String): Diagnostic? { + fun checkCommandName(inspectionTarget: KtElement, value: String): Diagnostic? { return when { value.isBlank() -> ILLEGAL_COMMAND_NAME.on(inspectionTarget, value, "指令名不能为空") value.any { it.isWhitespace() } -> ILLEGAL_COMMAND_NAME.on(inspectionTarget, value, "暂时不允许指令名中存在空格") @@ -86,7 +96,7 @@ class ContextualParametersChecker : DeclarationChecker { } } - fun checkPermissionNamespace(inspectionTarget: PsiElement, value: String): Diagnostic? { + fun checkPermissionNamespace(inspectionTarget: KtElement, value: String): Diagnostic? { return when { value.isBlank() -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "权限命名空间不能为空") value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "不允许权限命名空间中存在空格") @@ -95,7 +105,7 @@ class ContextualParametersChecker : DeclarationChecker { } } - fun checkPermissionName(inspectionTarget: PsiElement, value: String): Diagnostic? { + fun checkPermissionName(inspectionTarget: KtElement, value: String): Diagnostic? { return when { value.isBlank() -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "权限名称不能为空") value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "不允许权限名称中存在空格") @@ -104,7 +114,7 @@ class ContextualParametersChecker : DeclarationChecker { } } - fun checkPermissionId(inspectionTarget: PsiElement, value: String): Diagnostic? { + fun checkPermissionId(inspectionTarget: KtElement, value: String): Diagnostic? { return when { value.isBlank() -> ILLEGAL_PERMISSION_ID.on(inspectionTarget, value, "权限 Id 不能为空") value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_ID.on(inspectionTarget, value, "暂时不允许权限 Id 中存在空格") @@ -114,14 +124,54 @@ class ContextualParametersChecker : DeclarationChecker { } @Suppress("UNUSED_PARAMETER") - fun checkVersionRequirement(inspectionTarget: PsiElement, value: String): Diagnostic? { - // TODO: 2020/10/23 checkVersionRequirement + fun checkVersionRequirement(inspectionTarget: KtElement, value: String): Diagnostic? { + return try { + RequirementHelper.RequirementChecker.processLine(RequirementParser.TokenReader(value)) + null + } catch (err: Throwable) { + ILLEGAL_VERSION_REQUIREMENT.on(inspectionTarget, value, err.message ?: err.toString()) + } + } + + fun checkConsoleCommandOwner(context: DeclarationCheckerContext, inspectionTarget: KtElement, argument: ValueArgument): Diagnostic? { + val expr = argument.getArgumentExpression() ?: return null + + if (expr is KtReferenceExpression) { + if (expr.getResolvedCall(context)?.isCalling(CONSOLE_COMMAND_OWNER_FQ_NAME) == true) { + return RESTRICTED_CONSOLE_COMMAND_OWNER.on(inspectionTarget) + } + } + return null } } - private val checkersMap: EnumMap Diagnostic?> = - EnumMap Diagnostic?>(ResolveContextKind::class.java).apply { + fun interface ElementChecker { + operator fun invoke(context: DeclarationCheckerContext, declaration: KtElement, valueArgument: ValueArgument, value: String?): Diagnostic? + } + + private val checkersMap: EnumMap = + EnumMap(ResolveContextKind::class.java).apply { + + fun put(key: ResolveContextKind, value: KFunction2): ElementChecker? { + return put(key) { _, d, _, v -> + if (v != null) value(d, v) + else null + } + } + + fun put(key: ResolveContextKind, value: KFunction2): ElementChecker? { + return put(key) { _, d, v, _ -> + value(d, v) + } + } + + fun put(key: ResolveContextKind, value: (DeclarationCheckerContext, KtElement, ValueArgument) -> Diagnostic?): ElementChecker? { + return put(key) { c, d, v, _ -> + value(c, d, v) + } + } + put(ResolveContextKind.PLUGIN_NAME, ::checkPluginName) put(ResolveContextKind.PLUGIN_ID, ::checkPluginId) put(ResolveContextKind.SEMANTIC_VERSION, ::checkPluginVersion) @@ -130,6 +180,7 @@ class ContextualParametersChecker : DeclarationChecker { put(ResolveContextKind.PERMISSION_NAMESPACE, ::checkPermissionNamespace) put(ResolveContextKind.PERMISSION_ID, ::checkPermissionId) put(ResolveContextKind.VERSION_REQUIREMENT, ::checkVersionRequirement) + put(ResolveContextKind.RESTRICTED_CONSOLE_COMMAND_OWNER, ::checkConsoleCommandOwner) } override fun check( @@ -137,8 +188,20 @@ class ContextualParametersChecker : DeclarationChecker { descriptor: DeclarationDescriptor, context: DeclarationCheckerContext, ) { - declaration.resolveAllCalls(context.bindingContext) - .asSequence() + val calls: Sequence> = when (declaration) { + is KtClassOrObject -> { + declaration.findChild()?.resolveAllCalls(context.bindingContext) + // ignore class body, which will be [check]ed + } + +// is KtNamedFunction -> { +// +// } + else -> declaration.resolveAllCalls(context.bindingContext) + + } ?: return + + calls .flatMap { call -> call.valueParametersWithArguments().asSequence() } @@ -151,13 +214,14 @@ class ContextualParametersChecker : DeclarationChecker { } .flatMap { it.asSequence() } .mapNotNull { (kind, argument) -> - argument.resolveStringConstantValues()?.let { const -> - Triple(kind, argument, const) - } + Triple(kind, argument, argument.resolveStringConstantValues()) } - .forEach { (fn, argument, resolvedConstants) -> - for (resolvedConstant in resolvedConstants) { - fn(argument.asElement(), resolvedConstant)?.let { context.report(it) } + .forEach { (fn, argument, resolvedConstantsSequence) -> + val resolvedConstants = resolvedConstantsSequence?.toList().orEmpty() + if (resolvedConstants.isEmpty()) { + fn(context, argument.asElement(), argument, null)?.let { context.report(it) } + } else for (resolvedConstant in resolvedConstants) { + fn(context, argument.asElement(), argument, resolvedConstant)?.let { context.report(it) } } } return diff --git a/tools/intellij-plugin/src/resolve/resolveIdea.kt b/tools/intellij-plugin/src/resolve/resolveIdea.kt index 3cb91405f..2117a2ef7 100644 --- a/tools/intellij-plugin/src/resolve/resolveIdea.kt +++ b/tools/intellij-plugin/src/resolve/resolveIdea.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.console.intellij.resolve +import com.intellij.psi.PsiClass import com.intellij.psi.PsiDeclarationStatement import com.intellij.psi.PsiElement import com.intellij.psi.util.parentsWithSelf @@ -60,6 +61,19 @@ val KtPureClassOrObject.allSuperTypes: Sequence } } +val PsiClass.allSuperTypes: Sequence + get() = sequence { + interfaces.forEach { + yield(it) + yieldAll(it.allSuperTypes) + } + val superClass = superClass + if (superClass != null) { + yield(superClass) + yieldAll(superClass.allSuperTypes) + } + } + fun KtConstructorCalleeExpression.getTypeAsUserType(): KtUserType? { val reference = typeReference if (reference != null) { @@ -71,7 +85,26 @@ fun KtConstructorCalleeExpression.getTypeAsUserType(): KtUserType? { return null } +fun KtClassOrObject.hasSuperType(fqName: FqName): Boolean = allSuperNames.contains(fqName) +fun KtClass.hasSuperType(fqName: FqName): Boolean = allSuperNames.contains(fqName) + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@kotlin.internal.LowPriorityInOverloadResolution +fun PsiElement.hasSuperType(fqName: FqName): Boolean = allSuperNames.contains(fqName) + val KtClassOrObject.allSuperNames: Sequence get() = allSuperTypes.mapNotNull { it.getKotlinFqName() } +val PsiClass.allSuperNames: Sequence get() = allSuperTypes.mapNotNull { clazz -> clazz.qualifiedName?.let { FqName(it) } } + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@kotlin.internal.LowPriorityInOverloadResolution +val PsiElement.allSuperNames: Sequence + get() { + return when (this) { + is KtClassOrObject -> allSuperNames + is PsiClass -> allSuperNames + else -> emptySequence() + } + } fun getElementForLineMark(callElement: PsiElement): PsiElement = when (callElement) { @@ -88,10 +121,10 @@ val KtAnnotationEntry.annotationClass: KtClass? fun KtAnnotated.hasAnnotation(fqName: FqName): Boolean = this.annotationEntries.any { it.annotationClass?.getKotlinFqName() == fqName } -fun KtDeclaration.resolveAllCalls(bindingContext: BindingContext): Sequence> { +fun KtElement.resolveAllCalls(bindingContext: BindingContext): Sequence> { return allChildrenWithSelf - .filterIsInstance() - .mapNotNull { it.calleeExpression?.getResolvedCall(bindingContext) } + .filterIsInstance() + .mapNotNull { it.getResolvedCall(bindingContext) } } fun KtDeclaration.resolveAllCallsWithElement(bindingContext: BindingContext): Sequence, KtCallExpression>> { diff --git a/tools/intellij-plugin/src/util/RequirementHelper.kt b/tools/intellij-plugin/src/util/RequirementHelper.kt new file mode 100644 index 000000000..f3e99c56c --- /dev/null +++ b/tools/intellij-plugin/src/util/RequirementHelper.kt @@ -0,0 +1,43 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.util + +@Suppress("RegExpRedundantEscape") +object RequirementHelper { + private val directVersion = """^[0-9]+(\.[0-9]+)+(|[\-+].+)$""".toRegex() + private val versionSelect = """^[0-9]+(\.[0-9]+)*\.x$""".toRegex() + private val versionMathRange = + """([\[\(])([0-9]+(\.[0-9]+)+(|[\-+].+))\s*\,\s*([0-9]+(\.[0-9]+)+(|[\-+].+))([\]\)])""".toRegex() + private val versionRule = """^((\>\=)|(\<\=)|(\=)|(\!\=)|(\>)|(\<))\s*([0-9]+(\.[0-9]+)+(|[\-+].+))$""".toRegex() + + private val SEM_VERSION_REGEX = + """^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$""".toRegex() + + fun isValid(rule: String): Boolean { + return rule.trim().let { + directVersion.matches(it) || + versionSelect.matches(it) || + versionMathRange.matches(it) || + versionRule.matches(it) + } + } + + internal object RequirementChecker : RequirementParser.ProcessorBase() { + override fun processLogic(isAnd: Boolean, chunks: Iterable) { + } + + override fun processString(reader: RequirementParser.TokenReader, token: RequirementParser.Token.Content) { + if (!isValid(token.content)) { + token.ia(reader, "`${token.content}` 无效.") + } + } + + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/util/RequirementParser.kt b/tools/intellij-plugin/src/util/RequirementParser.kt new file mode 100644 index 000000000..892cd75cc --- /dev/null +++ b/tools/intellij-plugin/src/util/RequirementParser.kt @@ -0,0 +1,335 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.util + +import kotlin.math.max +import kotlin.math.min + +internal class RequirementParser { + sealed class Token { + open var line: Int = -1 + open var pos: Int = -1 + open var sourcePos: Int = -1 + open lateinit var content: String + + sealed class GroupBod : Token() { + class Left : GroupBod() { + override var content: String + get() = "{" + set(_) {} + } + + class Right : GroupBod() { + override var content: String + get() = "}" + set(_) {} + } + } + + sealed class Logic : Token() { + class And : Logic() { + override var content: String + get() = "&&" + set(_) {} + } + + class Or : Logic() { + override var content: String + get() = "||" + set(_) {} + } + } + + class Content : Token() + class Ending : Token() { + override var content: String + get() = "" + set(_) {} + } + + object Begin : Token() { + override var content: String + get() = "" + set(_) {} + override var line: Int + get() = 0 + set(_) {} + override var pos: Int + get() = 0 + set(_) {} + override var sourcePos: Int + get() = 0 + set(_) {} + } + + override fun toString(): String { + return javaClass.canonicalName.substringAfterLast('.') + " - $content [$line, $pos]" + } + } + + companion object { + const val END = '\u0000' + } + + class TokenReader( + @JvmField val content: String + ) { + @JvmField + var pos: Int = 0 + + @JvmField + var line: Int = 0 + + @JvmField + var posi: Int = 0 + + @JvmField + var latestToken: Token = Token.Begin + + @JvmField + var insertToken: Token? = Token.Begin + fun peekChar(): Char { + if (pos < content.length) + return content[pos] + return END + } + + fun peekNextChar(): Char { + if (pos + 1 < content.length) + return content[pos + 1] + return END + } + + fun nextChar(): Char { + val char = peekChar() + pos++ + if (char == '\n') { + line++ + posi = 0 + } else { + posi++ + } + return char + } + + fun nextToken(): Token { + insertToken?.let { insertToken = null; return it } + return nextToken0().also { latestToken = it } + } + + private fun nextToken0(): Token { + if (pos < content.length) { + while (peekChar().isWhitespace()) { + nextChar() + } + val startIndex = pos + if (startIndex >= content.length) { + return Token.Ending().also { + it.line = line + it.pos = posi + it.sourcePos = content.length + } + } + val pline = line + val ppos = posi + nextChar() + when (content[startIndex]) { + '&' -> { + if (peekChar() == '&') { + return Token.Logic.And().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + nextChar() + } + } + } + '|' -> { + if (peekChar() == '|') { + return Token.Logic.Or().also { + nextChar() + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + } + '{' -> { + return Token.GroupBod.Left().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + '}' -> { + return Token.GroupBod.Right().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + } + while (true) { + when (val c = peekChar()) { + '&', '|' -> { + if (c == peekNextChar()) { + break + } + nextChar() + } + '{', '}' -> { + break + } + END -> break + else -> nextChar() + } + } + val endIndex = pos + return Token.Content().also { + it.content = content.substring(startIndex, endIndex) + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + return Token.Ending().also { + it.line = line + it.pos = posi + it.sourcePos = content.length + } + } + } + + interface TokensProcessor { + fun process(reader: TokenReader): R + fun processLine(reader: TokenReader): R + fun processLogic(isAnd: Boolean, chunks: Iterable): R + } + + abstract class ProcessorBase : TokensProcessor { + fun Token.ia(reader: TokenReader, msg: String, cause: Throwable? = null): Nothing { + throw IllegalArgumentException("$msg (at [$line, $pos], ${cutSource(reader, sourcePos)})", cause) + } + + fun cutSource(reader: TokenReader, index: Int): String { + val content = reader.content + val s = max(0, index - 10) + val e = min(content.length, index + 10) + return content.substring(s, e) + } + + override fun process(reader: TokenReader): R { + return when (val nextToken = reader.nextToken()) { + is Token.Begin, + is Token.GroupBod.Left -> { + val first = when (val next = reader.nextToken()) { + is Token.Content -> { + processString(reader, next) + } + is Token.GroupBod.Right -> { + nextToken.ia( + reader, if (nextToken is Token.Begin) + "无效的关键字 `}`" + else "空规则组" + ) + } + is Token.Logic -> { + nextToken.ia(reader, "规则不允许以逻辑操作符开始") + } + is Token.Ending -> { + nextToken.ia( + reader, if (nextToken is Token.Begin) + "规则为空" + else "需要更多内容" + ) + } + is Token.GroupBod.Left -> { + reader.insertToken = next + process(reader) + } + else -> { + next.ia(reader, "Bad token $next") + } + } + // null -> not set + // true -> AND mode + // false-> OR mode + var mode: Boolean? = null + val chunks = arrayListOf(first) + while (true) { + when (val next = reader.nextToken()) { + is Token.Ending, + is Token.GroupBod.Right -> { + val isEndingOfGroup = next is Token.GroupBod.Right + val isStartingOfGroup = nextToken is Token.GroupBod.Left + if (isStartingOfGroup != isEndingOfGroup) { + fun getType(type: Boolean) = if (type) "`}`" else "<结束>" + next.ia(reader, "需要 ${getType(isStartingOfGroup)}, 但是找到了 ${getType(isEndingOfGroup)}") + } else { + // reader.insertToken = next + break + } + } + is Token.Logic -> { + val stx = next is Token.Logic.And + if (mode == null) mode = stx + else if (mode != stx) { + fun getMode(type: Boolean) = if (type) "`&&`" else "`||`" + next.ia( + reader, "为了避免语义混乱, 不允许在一层规则组混合使用 `&&` 和 `||`, 请显式使用 `{}` 分离. " + + "需要 ${getMode(mode)}, 但是找到了 ${getMode(stx)}" + ) + } + chunks.add(process(reader)) + } + else -> { + next.ia( + reader, "Except ${ + when (mode) { + null -> "`&&` or `||`" + true -> "`&&`" + false -> "`||`" + } + } but get `${next.content}`" + ) + } + } + } + if (mode == null) { + first + } else { + processLogic(mode, chunks) + } + } + is Token.Content -> { + processString(reader, nextToken) + } + is Token.Ending -> { + nextToken.ia(reader, "需要更多值.") + } + else -> { + nextToken.ia(reader, "Assert Error: $nextToken") + } + } + } + + abstract fun processString(reader: TokenReader, token: Token.Content): R + + + override fun processLine(reader: TokenReader): R { + return process(reader).also { + val tok = reader.nextToken() + if (reader.nextToken() !is Token.Ending) { + tok.ia(reader, "Token Reader 未完成解析") + } + } + } + } +}