diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2255d9e4d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +[*.{kt, kts}] +max_line_length = 160 +tab_width = 4 +ij_continuation_indent_size = 4 +indent_size = 4 \ No newline at end of file diff --git a/.github/workflows/Publishing.yml b/.github/workflows/Publishing.yml new file mode 100644 index 000000000..12a8e5450 --- /dev/null +++ b/.github/workflows/Publishing.yml @@ -0,0 +1,63 @@ +# This is a basic workflow to help you get started with Actions + +name: Bintray Publish + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + release: + types: + - created + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Gradle clean + run: ./gradlew clean + - name: Gradle build + run: ./gradlew build # if test's failed, don't publish + - name: Check keys + run: ./gradlew + :mirai-console:ensureBintrayAvailable + -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} + -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} + - name: Gradle :mirai-console:fillBuildConstants + run: ./gradlew + :mirai-console:fillBuildConstants + -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} + -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} + - name: Gradle :mirai-console:bintrayUpload + run: ./gradlew + :mirai-console:bintrayUpload + -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} + -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} + - name: Gradle :mirai-console-terminal:bintrayUpload + run: ./gradlew + :mirai-console-terminal:bintrayUpload + -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} + -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} + - name: Gradle :mirai-console-compiler-common:bintrayUpload + run: ./gradlew + :mirai-console-compiler-common:bintrayUpload + -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} + -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} + - name: Gradle :mirai-console-intellij:bintrayUpload + run: ./gradlew + :mirai-console-intellij:bintrayUpload + -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} + -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} + - name: Publish Gradle plugin + run: ./gradlew + :mirai-console-gradle:publishPlugins + -Dgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} + -Dgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} diff --git a/.github/workflows/bintray.yml b/.github/workflows/bintray.yml deleted file mode 100644 index c4abe1dba..000000000 --- a/.github/workflows/bintray.yml +++ /dev/null @@ -1,37 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Bintray Publish - -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch -on: - release: - types: - - created - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Gradle clean - run: ./gradlew clean - - name: Gradle build - run: ./gradlew build # if test's failed, don't publish - - name: Check keys - run: ./gradlew :mirai-console:ensureBintrayAvailable -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} - - name: Gradle :mirai-console:fillBuildConstants - run: ./gradlew :mirai-console:fillBuildConstants -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} - - name: Gradle :mirai-console:bintrayUpload - run: ./gradlew :mirai-console:bintrayUpload -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} - - name: Gradle :mirai-console-pure:bintrayUpload - run: ./gradlew :mirai-console-pure:bintrayUpload -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }} - diff --git a/.github/workflows/shadow.yml b/.github/workflows/shadow.yml index da3bccf38..4b52408e7 100644 --- a/.github/workflows/shadow.yml +++ b/.github/workflows/shadow.yml @@ -28,8 +28,8 @@ jobs: run: ./gradlew build # if test's failed, don't publish - name: Gradle :mirai-console:githubUpload run: ./gradlew :mirai-console:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }} - - name: Gradle :mirai-console-pure:githubUpload - run: ./gradlew :mirai-console-pure:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }} + - name: Gradle :mirai-console-terminal:githubUpload + run: ./gradlew :mirai-console-terminal:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }} # - name: Upload artifact diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 000000000..c872c7a5d Binary files /dev/null and b/.idea/icon.png differ diff --git a/README.md b/README.md index 84f369e09..9bc33196e 100644 --- a/README.md +++ b/README.md @@ -14,51 +14,33 @@ Mirai 是一个在全平台下运行,提供 QQ 协议支持的高效率机器 # mirai-console -[ ![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-console/images/download.svg?) ](https://bintray.com/him188moe/mirai/mirai-console/) -高效率插件支持 QQ 机器人框架, 机器人核心来自 [mirai](https://github.com/mamoe/mirai) +高效率 QQ 机器人框架,机器人核心来自 [mirai](https://github.com/mamoe/mirai) -## 模块说明 +![Gradle CI](https://github.com/mamoe/mirai-console/workflows/Gradle%20CI/badge.svg?branch=master) +[![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -console 由后端和前端一起工作. 使用时必须选择一个前端. +## 开发 -- `mirai-console`: console 的后端, 包含插件管理, 指令系统, 配置系统. +- **[准备工作 - 环境和前置知识](docs/Preparations.md)** +- **[配置项目](docs/ConfiguringProjects.md)** +- **[启动 Console](docs/Run.md)** + +### 后端插件开发基础 + +- 插件 - [Plugin 模块](docs/Plugins.md) +- 指令 - [Command 模块](docs/Commands.md) +- 存储 - [PluginData 模块](docs/PluginData.md) +- 权限 - [Permission 模块](docs/Permissions.md) +**示例插件**: +- [mirai-console-example-plugin (Kotlin DSL)](https://github.com/Him188/mirai-console-example-plugin) +- [mirai-console-example-plugin (Groovy DSL)](https://github.com/Karlatemp/mirai-console-example-plugin) -前端: +### 后端插件开发进阶 -- `mirai-console-pure`: console 的轻量命令行前端. -- `mirai-console-graphical`: console 的 JavaFX 图形化界面前端. (开发中) -- `mirai-console-terminal`: console 的 Unix 终端界面前端. (开发中) +- 扩展 - [Extension 模块和扩展点](docs/Extensions.md) - -**注意:`mirai-console` 后端和 pure 前端正在进行完全的重构, 所有 API 都不具有稳定性** - -### 使用 - -**查看示例插件**: [mirai-console-example-plugin](https://github.com/Him188/mirai-console-example-plugin) - -正在更新中的文档:[参考文档](docs/README.md) - -#### Gradle -`CORE_VERSION`: [ ![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg?) ](https://bintray.com/him188moe/mirai/mirai-core/) -`CONSOLE_VERSION`: [ ![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-console/images/download.svg?) ](https://bintray.com/him188moe/mirai/mirai-console/) - - -build.gradle.kts -```kotlin -repositories { - jcenter() -} - -dependencies { - implementation("net.mamoe:mirai-core:$CORE_VERSION") // mirai-core 的 API - implementation("net.mamoe:mirai-console:$CONSOLE_VERSION") // 后端 - - testImplementation("net.mamoe:mirai-console-pure:$CONSOLE_VERSION") // 前端, 用于启动测试 -} -``` - -#### Maven -同理 Gradle +### 实现前端 +- [FrontEnd](docs/FrontEnd.md) \ No newline at end of file diff --git a/backend/codegen/README.md b/backend/codegen/README.md new file mode 100644 index 000000000..013c05c7b --- /dev/null +++ b/backend/codegen/README.md @@ -0,0 +1,6 @@ +# Mirai Console - Backend.codegen + +后端代码生成模块,用于最小化重复代码的人工成本。 + +- `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) diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/ValueSettingCodegen.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/ValuePluginDataCodegen.kt similarity index 100% rename from backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/ValueSettingCodegen.kt rename to backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/ValuePluginDataCodegen.kt diff --git a/backend/mirai-console/README.md b/backend/mirai-console/README.md new file mode 100644 index 000000000..451b5aaee --- /dev/null +++ b/backend/mirai-console/README.md @@ -0,0 +1,3 @@ +# Mirai Console - Backend + +Mirai Console 后端模块. 发布为 `net.mamoe:mirai-console`. \ No newline at end of file diff --git a/backend/mirai-console/build.gradle.kts b/backend/mirai-console/build.gradle.kts index 9d927a467..4795a996c 100644 --- a/backend/mirai-console/build.gradle.kts +++ b/backend/mirai-console/build.gradle.kts @@ -56,50 +56,26 @@ kotlin { } dependencies { - implementation("net.mamoe:mirai-core:${Versions.core}") + compileAndTestRuntime("net.mamoe:mirai-core:${Versions.core}") + compileAndTestRuntime(kotlin("stdlib", Versions.kotlinStdlib)) + compileAndTestRuntime(kotlin("stdlib-jdk8", Versions.kotlinStdlib)) - implementation(kotlinx("serialization-core", Versions.serialization)) - implementation(kotlin("reflect")) + compileAndTestRuntime("org.jetbrains.kotlinx:atomicfu:${Versions.atomicFU}") + compileAndTestRuntime(kotlinx("coroutines-core", Versions.coroutines)) + compileAndTestRuntime(kotlinx("serialization-core", Versions.serialization)) + compileAndTestRuntime(kotlin("reflect")) - api("net.mamoe.yamlkt:yamlkt:${Versions.yamlkt}") - implementation("org.jetbrains.kotlinx:atomicfu:${Versions.atomicFU}") - api("org.jetbrains:annotations:19.0.0") - api(kotlinx("coroutines-jdk8", Versions.coroutines)) + smartImplementation("net.mamoe.yamlkt:yamlkt:${Versions.yamlkt}") + smartImplementation("org.jetbrains:annotations:19.0.0") + smartApi(kotlinx("coroutines-jdk8", Versions.coroutines)) - api("com.vdurmont:semver4j:3.1.0") - - //api(kotlinx("collections-immutable", Versions.collectionsImmutable)) - - testApi(kotlinx("serialization-core", Versions.serialization)) testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}") testApi(kotlin("stdlib-jdk8")) testApi(kotlin("test")) testApi(kotlin("test-junit5")) - testImplementation("org.junit.jupiter:junit-jupiter-api:5.2.0") + testApi("org.junit.jupiter:junit-jupiter-api:5.2.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0") - - -// val autoService = "1.0-rc7" -// kapt("com.google.auto.service", "auto-service", autoService) -// compileOnly("com.google.auto.service", "auto-service-annotations", autoService) -} - -ext.apply { - // 傻逼 compileAndRuntime 没 exclude 掉 - // 傻逼 gradle 第二次配置 task 会覆盖掉第一次的配置 - val x: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.() -> Unit = { - dependencyFilter.exclude { - when ("${it.moduleGroup}:${it.moduleName}") { - "net.mamoe:mirai-core" -> true - "org.jetbrains.kotlin:kotlin-stdlib" -> true - "org.jetbrains.kotlin:kotlin-stdlib-jdk8" -> true - "net.mamoe:mirai-core-qqandroid" -> true - else -> false - } - } - } - set("shadowJar", x) } tasks { @@ -120,14 +96,14 @@ tasks { Regex("""val buildDate: Instant = Instant.ofEpochSecond\(.*\)""") ) { """val buildDate: Instant = Instant.ofEpochSecond(${ - Instant.now().getEpochSecond() + Instant.now().epochSecond })""" } .replace( - Regex("""val version: Semver = Semver\(".*", Semver.SemverType.LOOSE\)""") - ) { """val version: Semver = Semver("${project.version}", Semver.SemverType.LOOSE)""" } + Regex("""const val versionConst:\s+String\s+=\s+".*"""") + ) { """const val versionConst: String = "${project.version}"""" } ) - } + } } } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt index 8bed464da..42106685e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt @@ -12,12 +12,12 @@ package net.mamoe.mirai.console -import com.vdurmont.semver4j.Semver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import net.mamoe.mirai.Bot import net.mamoe.mirai.console.MiraiConsole.INSTANCE import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start +import net.mamoe.mirai.console.command.BuiltInCommands import net.mamoe.mirai.console.extensions.BotConfigurationAlterer import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage @@ -28,6 +28,7 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoader 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.MiraiLogger import java.io.File @@ -78,7 +79,7 @@ public interface MiraiConsole : CoroutineScope { /** * 此 Console 后端版本号 */ - public val version: Semver + public val version: SemVersion @ConsoleExperimentalApi @@ -146,6 +147,10 @@ public interface MiraiConsole : CoroutineScope { else -> null!! } } + + @ConsoleExperimentalApi("This is a low-level API and might be removed in the future.") + public val isActive: Boolean + get() = job.isActive } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEndDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEndDescription.kt index 21c1f8103..f8155a9d1 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEndDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEndDescription.kt @@ -9,7 +9,8 @@ package net.mamoe.mirai.console -import com.vdurmont.semver4j.Semver +import net.mamoe.mirai.console.util.SemVersion + /** * 有关前端实现的信息 @@ -28,7 +29,7 @@ public interface MiraiConsoleFrontEndDescription { /** * 此前端实现的名称 */ - public val version: Semver + public val version: SemVersion /** * 兼容的 [MiraiConsole] 后端版本号 @@ -37,11 +38,10 @@ public interface MiraiConsoleFrontEndDescription { * * 返回 `null` 表示禁止 [MiraiConsole] 后端检查版本兼容性. */ - public val compatibleBackendVersion: Semver? get() = null + public val compatibleBackendVersion: SemVersion? get() = null /** * 返回显示在 [MiraiConsole] 启动时的信息 */ - @JvmDefault public fun render(): String = "Frontend ${name}: version ${version}, provided by $vendor" } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt index 8e53e4f9c..a1fd6347a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt @@ -108,12 +108,10 @@ public interface MiraiConsoleImplementation : CoroutineScope { @JvmSynthetic - @JvmDefault public override suspend fun sendMessage(message: Message): Unit = withContext(Dispatchers.IO) { sendMessageJ(message) } @JvmSynthetic - @JvmDefault public override suspend fun sendMessage(message: String): Unit = withContext(Dispatchers.IO) { sendMessageJ(message) } } @@ -175,11 +173,21 @@ public interface MiraiConsoleImplementation : CoroutineScope { internal lateinit var instance: MiraiConsoleImplementation private val initLock = ReentrantLock() + /** + * 可由前端调用, 获取当前的 [MiraiConsoleImplementation] 实例 + * + * 必须在 [start] 之后才能使用. + */ + @JvmStatic + @ConsoleFrontEndImplementation + public fun getInstance(): MiraiConsoleImplementation = instance + /** 由前端调用, 初始化 [MiraiConsole] 实例并启动 */ @JvmStatic @ConsoleFrontEndImplementation @Throws(MalformedMiraiConsoleImplementationError::class) public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock { + if (::instance.isInitialized) error("Mirai Console is already initialized.") this@Companion.instance = this MiraiConsoleImplementationBridge.doStart() } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/AbstractCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/AbstractCommand.kt new file mode 100644 index 000000000..7de3bd955 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/AbstractCommand.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission +import net.mamoe.mirai.console.permission.Permission + +/** + * [Command] 的基础实现 + * + * @see SimpleCommand + * @see CompositeCommand + * @see RawCommand + */ +public abstract class AbstractCommand +@JvmOverloads constructor( + public final override val owner: CommandOwner, + public final override val primaryName: String, + public final override val secondaryNames: Array, + public override val description: String = "", + parentPermission: Permission = owner.parentPermission, + /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ + public override val prefixOptional: Boolean = false, +) : Command { + init { + Command.checkCommandName(primaryName) + secondaryNames.forEach(Command.Companion::checkCommandName) + } + + public override val usage: String get() = description + public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt index 919818b8f..1b24675be 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt @@ -132,12 +132,12 @@ public object BuiltInCommands { onFailure = { throwable -> sendMessage( "Login failed: ${throwable.localizedMessage ?: throwable.message ?: throwable.toString()}" + - if (this is CommandSenderOnMessage<*>) { - CommandManagerImpl.launch(CoroutineName("stacktrace delayer from Login")) { - fromEvent.nextMessageOrNull(60.secondsToMillis) { it.message.contentEquals("stacktrace") } - } - "\n 1 分钟内发送 stacktrace 以获取堆栈信息" - } else "" + if (this is CommandSenderOnMessage<*>) { + CommandManagerImpl.launch(CoroutineName("stacktrace delayer from Login")) { + fromEvent.nextMessageOrNull(60.secondsToMillis) { it.message.contentEquals("stacktrace") } + } + "\n 1 分钟内发送 stacktrace 以获取堆栈信息" + } else "" ) throw throwable @@ -148,7 +148,7 @@ public object BuiltInCommands { public object PermissionCommand : CompositeCommand( ConsoleCommandOwner, "permission", "权限", "perm", - description = "Manage permissions", + description = "管理权限", overrideContext = buildCommandArgumentContext { PermitteeId::class with PermitteeIdArgumentParser Permission::class with PermissionIdArgumentParser.map { id -> @@ -159,30 +159,47 @@ public object BuiltInCommands { }, ), BuiltInCommandInternal { // TODO: 2020/9/10 improve Permission command + + @Description("授权一个权限") @SubCommand("permit", "grant", "add") - public suspend fun CommandSender.permit(target: PermitteeId, permission: Permission) { + public suspend fun CommandSender.permit( + @Name("被许可人 ID") target: PermitteeId, + @Name("权限 ID") permission: Permission, + ) { target.grantPermission(permission) sendMessage("OK") } + @Description("取消授权一个权限") @SubCommand("cancel", "deny", "remove") - public suspend fun CommandSender.cancel(target: PermitteeId, permission: Permission) { + public suspend fun CommandSender.cancel( + @Name("被许可人 ID") target: PermitteeId, + @Name("权限 ID") permission: Permission, + ) { target.denyPermission(permission, false) sendMessage("OK") } + @Description("取消授权一个权限及其所有子权限") @SubCommand("cancelAll", "denyAll", "removeAll") - public suspend fun CommandSender.cancelAll(target: PermitteeId, permission: Permission) { + public suspend fun CommandSender.cancelAll( + @Name("被许可人 ID") target: PermitteeId, + @Name("权限 ID") permission: Permission, + ) { target.denyPermission(permission, true) sendMessage("OK") } + @Description("查看被授权权限列表") @SubCommand("permittedPermissions", "pp", "grantedPermissions", "gp") - public suspend fun CommandSender.permittedPermissions(target: PermitteeId) { + public suspend fun CommandSender.permittedPermissions( + @Name("被许可人 ID") target: PermitteeId, + ) { val grantedPermissions = target.getPermittedPermissions() sendMessage(grantedPermissions.joinToString("\n") { it.id.toString() }) } + @Description("查看所有权限列表") @SubCommand("listPermissions", "lp") public suspend fun CommandSender.listPermissions() { sendMessage(PermissionService.INSTANCE.getRegisteredPermissions().joinToString("\n") { it.id.toString() }) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt index a746b50e8..43346dcb9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt @@ -15,11 +15,11 @@ import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.java.JCommand -import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission -import net.mamoe.mirai.console.internal.command.isValidSubName +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.SingleMessage /** * 指令 @@ -34,11 +34,19 @@ import net.mamoe.mirai.message.data.SingleMessage */ public interface Command { /** - * 指令名. 需要至少有一个元素. 所有元素都不能带有空格 + * 主指令名. 将会参与构成 [Permission.id]. * - * @see Command.primaryName 获取主要指令名 + * 不允许包含 [空格][Char.isWhitespace], '.', ':'. */ - public val names: Array + @ResolveContext(COMMAND_NAME) + public val primaryName: String + + /** + * 次要指令名 + * @see Command.primaryName 获取主指令名 + */ + @ResolveContext(COMMAND_NAME) + public val secondaryNames: Array /** * 用法说明, 用于发送给用户. [usage] 一般包含 [description]. @@ -51,12 +59,18 @@ public interface Command { public val description: String /** - * 指令权限 + * 此指令所分配的权限. + * + * ### 实现约束 + * - [Permission.id] 应由 [CommandOwner.permissionId] 创建. 因此保证相同的 [PermissionId.namespace] + * - [PermissionId.name] 应为 [主指令名][primaryName] */ public val permission: Permission /** - * 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 + * 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选. + * + * 会影响聊天语境中的解析. */ public val prefixOptional: Boolean @@ -69,7 +83,7 @@ public interface Command { /** * 在指令被执行时调用. * - * @param args 指令参数. 数组元素类型可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. + * @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数. * * @see CommandManager.executeCommand 查看更多信息 */ @@ -77,41 +91,37 @@ public interface Command { public suspend fun CommandSender.onCommand(args: MessageChain) public companion object { + /** - * 主要指令名. 为 [Command.names] 的第一个元素. + * 获取所有指令名称 (包含 [primaryName] 和 [secondaryNames]). + * + * @return 数组大小至少为 1. 第一个元素总是 [primaryName]. 随后是保持原顺序的 [secondaryNames] */ @JvmStatic - public val Command.primaryName: String - get() = names[0] + public val Command.allNames: Array + get() = arrayOf(primaryName, *secondaryNames) + + /** + * 检查指令名的合法性. 在非法时抛出 [IllegalArgumentException] + */ + @JvmStatic + @Throws(IllegalArgumentException::class) + public fun checkCommandName(@ResolveContext(COMMAND_NAME) name: String) { + when { + name.isBlank() -> throw IllegalArgumentException("Command name should not be blank.") + name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in command name.") + name.contains(':') -> throw IllegalArgumentException("':' is forbidden in command name.") + name.contains('.') -> throw IllegalArgumentException("'.' is forbidden in command name.") + } + } } } +/** + * 调用 [Command.onCommand] + * @see Command.onCommand + */ @JvmSynthetic public suspend inline fun Command.onCommand(sender: CommandSender, args: MessageChain): Unit = sender.onCommand(args) -/** - * [Command] 的基础实现 - * - * @see SimpleCommand - * @see CompositeCommand - * @see RawCommand - */ -public abstract class AbstractCommand -@JvmOverloads constructor( - /** 指令拥有者. */ - public override val owner: CommandOwner, - vararg names: String, - description: String = "", - parentPermission: Permission = owner.parentPermission, - /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ - public override val prefixOptional: Boolean = false, -) : Command { - public override val description: String = description.trimIndent() - public override val names: Array = - names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> - list.firstOrNull { !it.isValidSubName() }?.let { error("Invalid name: $it") } - }.toTypedArray() - - public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) } -} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt index a2cef3687..1998b71b0 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt @@ -11,7 +11,6 @@ package net.mamoe.mirai.console.command -import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt index a14c70c39..b2c058d78 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt @@ -26,11 +26,15 @@ import net.mamoe.mirai.message.data.* public interface CommandManager { /** * 获取已经注册了的属于这个 [CommandOwner] 的指令列表. + * + * @return 这一时刻的浅拷贝. */ public val CommandOwner.registeredCommands: List /** * 获取所有已经注册了指令列表. + * + * @return 这一时刻的浅拷贝. */ public val allRegisteredCommands: List @@ -49,8 +53,8 @@ public interface CommandManager { * * @param override 是否覆盖重名指令. * - * 若原有指令 P, 其 [Command.names] 为 'a', 'b', 'c'. - * 新指令 Q, 其 [Command.names] 为 'b', 将会覆盖原指令 A 注册的 'b'. + * 若原有指令 P, 其 [Command.secondaryNames] 为 'a', 'b', 'c'. + * 新指令 Q, 其 [Command.secondaryNames] 为 'b', 将会覆盖原指令 A 注册的 'b'. * * 即注册完成后, 'a' 和 'c' 将会解析到指令 P, 而 'b' 会解析到指令 Q. * @@ -71,19 +75,23 @@ public interface CommandManager { public fun Command.findDuplicate(): Command? /** - * 取消注册这个指令. 若指令未注册, 返回 `false`. + * 取消注册这个指令. + * + * 若指令未注册, 返回 `false`. */ @JvmName("unregisterCommand") public fun Command.unregister(): Boolean /** - * 当 [this] 已经 [注册][register] 后返回 `true` + * 当 [this] 已经 [注册][register] 时返回 `true` */ @JvmName("isCommandRegistered") public fun Command.isRegistered(): Boolean /** - * 解析并执行一个指令 + * 解析并执行一个指令. + * + * 如要避免参数解析, 请使用 [Command.onCommand] * * ### 指令解析流程 * 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理) @@ -116,7 +124,6 @@ public interface CommandManager { * @return 执行结果 * @see executeCommand */ - @JvmDefault @JvmBlockingBridge public suspend fun CommandSender.executeCommand( message: String, @@ -139,7 +146,6 @@ public interface CommandManager { * 执行一个确切的指令 * @see executeCommand 获取更多信息 */ - @JvmDefault @JvmBlockingBridge @JvmName("executeCommand") public suspend fun Command.execute( @@ -151,7 +157,7 @@ public interface CommandManager { public companion object INSTANCE : CommandManager by CommandManagerImpl { // TODO: 2020/8/20 https://youtrack.jetbrains.com/issue/KT-41191 - override val CommandOwner.registeredCommands: List get() = CommandManagerImpl.run { registeredCommands } + override val CommandOwner.registeredCommands: List get() = CommandManagerImpl.run { this@registeredCommands.registeredCommands } override fun CommandOwner.unregisterAllCommands(): Unit = CommandManagerImpl.run { unregisterAllCommands() } override fun Command.register(override: Boolean): Boolean = CommandManagerImpl.run { register(override) } override fun Command.findDuplicate(): Command? = CommandManagerImpl.run { findDuplicate() } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt index 2515d05b8..f59033c95 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt @@ -10,6 +10,8 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PERMISSION_NAME import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermissionIdNamespace @@ -36,5 +38,7 @@ public interface CommandOwner : PermissionIdNamespace { internal object ConsoleCommandOwner : CommandOwner { override val parentPermission: Permission get() = BuiltInCommands.parentPermission - override fun permissionId(name: String): PermissionId = PermissionId("console", "command.$name") + override fun permissionId( + @ResolveContext(PERMISSION_NAME) name: String, + ): PermissionId = PermissionId("console", name) } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt index d0d976a30..f966ce96d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt @@ -9,7 +9,6 @@ package net.mamoe.mirai.console.command -import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt index 9d3b76f60..08aaccac0 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt @@ -171,7 +171,6 @@ public interface CommandSender : CoroutineScope, Permittee { * * 对于 [MemberCommandSender], 这个函数总是发送给所在群 */ - @JvmDefault @JvmBlockingBridge public suspend fun sendMessage(message: String): MessageReceipt? @@ -501,7 +500,6 @@ public fun CommandSender.getBotOrNull(): Bot? { * * 控制台拥有一切指令的执行权限. */ -// 前端实现 public object ConsoleCommandSender : AbstractCommandSender() { public const val NAME: String = "ConsoleCommandSender" @@ -514,12 +512,15 @@ public object ConsoleCommandSender : AbstractCommandSender() { public override val permitteeId: AbstractPermitteeId.Console = AbstractPermitteeId.Console public override val coroutineContext: CoroutineContext by lazy { MiraiConsole.childScopeContext(NAME) } + + @JvmBlockingBridge public override suspend fun sendMessage(message: Message): Nothing? { MiraiConsoleImplementationBridge.consoleCommandSender.sendMessage(message) return null } - public override suspend fun sendMessage(message: String): MessageReceipt? { + @JvmBlockingBridge + public override suspend fun sendMessage(message: String): Nothing? { MiraiConsoleImplementationBridge.consoleCommandSender.sendMessage(message) return null } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt index 5cd7bdb2b..926fcd19a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt @@ -18,9 +18,12 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.description.* +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.message.data.MessageChain import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION @@ -81,12 +84,13 @@ import kotlin.annotation.AnnotationTarget.FUNCTION */ public abstract class CompositeCommand( owner: CommandOwner, - vararg names: String, + @ResolveContext(COMMAND_NAME) primaryName: String, + @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, description: String = "no description available", parentPermission: Permission = owner.parentPermission, prefixOptional: Boolean = false, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, -) : Command, AbstractReflectionCommand(owner, names, description, parentPermission, prefixOptional), +) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), CommandArgumentContextAware { /** @@ -105,7 +109,9 @@ public abstract class CompositeCommand( */ @Retention(RUNTIME) @Target(FUNCTION) - protected annotation class SubCommand(vararg val value: String) + protected annotation class SubCommand( + @ResolveContext(COMMAND_NAME) vararg val value: String, + ) /** 指令描述 */ @Retention(RUNTIME) @@ -113,6 +119,7 @@ public abstract class CompositeCommand( protected annotation class Description(val value: String) /** 参数名, 将参与构成 [usage] */ + @ConsoleExperimentalApi("Classname might change") @Retention(RUNTIME) @Target(AnnotationTarget.VALUE_PARAMETER) protected annotation class Name(val value: String) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt index 621172e97..a924e6982 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt @@ -14,6 +14,8 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand 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.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.message.data.MessageChain @@ -35,11 +37,13 @@ public abstract class RawCommand( * @see CommandOwner */ public override val owner: CommandOwner, - /** 指令名. 需要至少有一个元素. 所有元素都不能带有空格 */ - public override vararg val names: String, + /** 主指令名. */ + @ResolveContext(COMMAND_NAME) public override val primaryName: String, + /** 次要指令名. */ + @ResolveContext(COMMAND_NAME) public override vararg val secondaryNames: String, /** 用法说明, 用于发送给用户 */ public override val usage: String = "", - /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ + /** 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] */ public override val description: String = "", /** 指令父权限 */ parentPermission: Permission = owner.parentPermission, diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt index 55e5ef438..27bb0f6e3 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt @@ -20,6 +20,8 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.description.* 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.internal.command.AbstractReflectionCommand import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.permission.Permission @@ -50,12 +52,13 @@ import net.mamoe.mirai.message.data.MessageChain */ public abstract class SimpleCommand( owner: CommandOwner, - vararg names: String, + @ResolveContext(COMMAND_NAME) primaryName: String, + @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, description: String = "no description available", parentPermission: Permission = owner.parentPermission, prefixOptional: Boolean = false, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, -) : Command, AbstractReflectionCommand(owner, names, description, parentPermission, prefixOptional), +) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), CommandArgumentContextAware { /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt index 8cf2456b5..cacea64e0 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt @@ -20,6 +20,8 @@ import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.contact.* +import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.PlainText import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf @@ -75,6 +77,9 @@ public interface CommandArgumentContext { Double::class with DoubleArgumentParser Float::class with FloatArgumentParser + Image::class with ImageArgumentParser + PlainText::class with PlainTextArgumentParser + Contact::class with ExistingContactArgumentParser User::class with ExistingUserArgumentParser Member::class with ExistingMemberArgumentParser diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt index 3d717cbb6..08f7eee05 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt @@ -77,7 +77,6 @@ public interface CommandArgumentParser { * @see CommandArgumentParserException */ @Throws(CommandArgumentParserException::class) - @JvmDefault public fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt index 9e5e22fe4..28d49cd11 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt @@ -19,10 +19,7 @@ import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.contact.* import net.mamoe.mirai.getFriendOrNull import net.mamoe.mirai.getGroupOrNull -import net.mamoe.mirai.message.data.At -import net.mamoe.mirai.message.data.MessageContent -import net.mamoe.mirai.message.data.SingleMessage -import net.mamoe.mirai.message.data.content +import net.mamoe.mirai.message.data.* /** @@ -80,15 +77,44 @@ public object StringArgumentParser : InternalCommandArgumentParserExtensions { + public override fun parse(raw: String, sender: CommandSender): Image { + return kotlin.runCatching { + Image(raw) + }.getOrElse { + illegalArgument("无法解析 $raw 为图片.") + } + } + + override fun parse(raw: MessageContent, sender: CommandSender): Image { + if (raw is Image) return raw + return super.parse(raw, sender) + } +} + +public object PlainTextArgumentParser : InternalCommandArgumentParserExtensions { + public override fun parse(raw: String, sender: CommandSender): PlainText { + return PlainText(raw) + } + + override fun parse(raw: MessageContent, sender: CommandSender): PlainText { + if (raw is PlainText) return raw + return super.parse(raw, sender) + } +} + /** * 当字符串内容为(不区分大小写) "true", "yes", "enabled" */ public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> { public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str -> str.equals("true", ignoreCase = true) - || str.equals("yes", ignoreCase = true) - || str.equals("enabled", ignoreCase = true) - || str.equals("on", ignoreCase = true) + || str.equals("yes", ignoreCase = true) + || str.equals("enabled", ignoreCase = true) + || str.equals("on", ignoreCase = true) } } @@ -365,10 +391,10 @@ internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArg } else { var index = 1 illegalArgument("无法找到成员 $idOrCard。 多个成员满足搜索结果或匹配度不足: \n\n" + - candidates.joinToString("\n", limit = 6) { - val percentage = (it.second * 100).toDecimalPlace(0) - "#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4% - } + candidates.joinToString("\n", limit = 6) { + val percentage = (it.second * 100).toDecimalPlace(0) + "#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4% + } ) } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt index 38d3d2c0f..1fbf748c0 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCommand.kt @@ -16,7 +16,6 @@ import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.SingleMessage /** * 为 Java 用户添加协程帮助的 [Command]. @@ -33,9 +32,9 @@ public interface JCommand : Command { /** * 在指令被执行时调用. * - * @param args 指令参数. 数组元素类型可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. + * @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数. * * @see CommandManager.executeCommand 查看更多信息 */ - public fun onCommand(sender: CommandSender, args: MessageChain) // overrides bridge + public fun onCommand(sender: CommandSender, args: MessageChain) // overrides blocking bridge } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt index 6dacd29ac..d09d2275c 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt @@ -14,6 +14,8 @@ import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.CompositeCommand import net.mamoe.mirai.console.command.description.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.permission.Permission /** @@ -24,7 +26,7 @@ import net.mamoe.mirai.console.permission.Permission * public final class MyCompositeCommand extends CompositeCommand { * public static final MyCompositeCommand INSTANCE = new MyCompositeCommand(); * - * public MyCompositeCommand() { + * private MyCompositeCommand() { * super(MyPluginMain.INSTANCE, "manage") // "manage" 是主指令名 * } * @@ -69,11 +71,12 @@ import net.mamoe.mirai.console.permission.Permission public abstract class JCompositeCommand @JvmOverloads constructor( owner: CommandOwner, - vararg names: String, + @ResolveContext(COMMAND_NAME) primaryName: String, + @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, parentPermission: Permission = owner.parentPermission, -) : CompositeCommand(owner, *names, parentPermission = parentPermission) { - /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ - public final override var description: String = "<no descriptions given>" +) : CompositeCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = parentPermission) { + /** 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] */ + public final override var description: String = "<no descriptions available>" protected set public final override var permission: Permission = super.permission diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt index 07b03c641..84b06ea5c 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.message.data.MessageChain @@ -51,15 +53,15 @@ public abstract class JRawCommand * @see CommandOwner */ public override val owner: CommandOwner, - /** 指令名. 需要至少有一个元素. 所有元素都不能带有空格 */ - public override vararg val names: String, + @ResolveContext(COMMAND_NAME) public override val primaryName: String, + @ResolveContext(COMMAND_NAME) public override vararg val secondaryNames: String, parentPermission: Permission = owner.parentPermission, ) : Command { /** 用法说明, 用于发送给用户 */ public override var usage: String = "<no usages given>" protected set - /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ + /** 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] */ public final override var description: String = "<no descriptions given>" protected set diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt index 37f7022b9..98e650ebd 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt @@ -14,12 +14,15 @@ import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.SimpleCommand import net.mamoe.mirai.console.command.description.CommandArgumentContext +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.permission.Permission /** * Java 实现: * ```java * public final class MySimpleCommand extends JSimpleCommand { + * public static final MySimpleCommand INSTANCE = new MySimpleCommand(); * private MySimpleCommand() { * super(MyPlugin.INSTANCE, "tell") * // 可选设置如下属性 @@ -41,9 +44,10 @@ import net.mamoe.mirai.console.permission.Permission */ public abstract class JSimpleCommand( owner: CommandOwner, - vararg names: String, + @ResolveContext(COMMAND_NAME) primaryName: String, + @ResolveContext(COMMAND_NAME) vararg secondaryNames: String, basePermission: Permission, -) : SimpleCommand(owner, *names, parentPermission = basePermission) { +) : SimpleCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = basePermission) { public override var description: String = super.description protected set diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/ResolveContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/ResolveContext.kt new file mode 100644 index 000000000..9dff987e2 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/ResolveContext.kt @@ -0,0 +1,51 @@ +/* + * 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 + */ + +@file:Suppress("unused") + +package net.mamoe.mirai.console.compiler.common + +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.annotation.AnnotationTarget.* + +/** + * 标记一个参数的语境类型, 用于帮助编译器和 IntelliJ 插件进行语境推断. + */ +@ConsoleExperimentalApi +@Target( + VALUE_PARAMETER, + PROPERTY, FIELD, + FUNCTION, + TYPE, TYPE_PARAMETER +) +@Retention(AnnotationRetention.BINARY) +public annotation class ResolveContext( + val kind: Kind, +) { + /** + * 元素数量可能在任意时间被改动 + */ + public enum class Kind { + /////////////////////////////////////////////////////////////////////////// + // ConstantKind + /////////////////////////////////////////////////////////////////////////// + + PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION + PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION + PLUGIN_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION + + COMMAND_NAME, // ILLEGAL_COMMAND_NAME + + PERMISSION_NAMESPACE, // ILLEGAL_COMMAND_NAMESPACE + PERMISSION_NAME, // ILLEGAL_COMMAND_NAME + PERMISSION_ID, // ILLEGAL_COMMAND_ID + + RESTRICTED_NO_ARG_CONSTRUCTOR, // NOT_CONSTRUCTABLE_TYPE + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/RestrictedScope.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/RestrictedScope.kt new file mode 100644 index 000000000..640016cc7 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/RestrictedScope.kt @@ -0,0 +1,28 @@ +/* + * 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.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/main/kotlin/net/mamoe/mirai/console/data/AbstractPluginData.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AbstractPluginData.kt index 882e6bb02..90b22bd06 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AbstractPluginData.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AbstractPluginData.kt @@ -7,13 +7,18 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE") +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE", "unused") package net.mamoe.mirai.console.data import kotlinx.serialization.KSerializer -import net.mamoe.mirai.console.data.PluginData.ValueNode +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule import net.mamoe.mirai.console.internal.data.PluginDataImpl +import net.mamoe.mirai.console.internal.data.getAnnotationListForValueSerialization +import net.mamoe.mirai.console.internal.data.valueName +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import kotlin.reflect.KProperty /** * [PluginData] 的默认实现. 支持使用 `by value()` 等委托方法创建 [Value] 并跟踪其改动. @@ -21,6 +26,11 @@ import net.mamoe.mirai.console.internal.data.PluginDataImpl * @see PluginData */ public abstract class AbstractPluginData : PluginData, PluginDataImpl() { + /** + * 这个 [PluginData] 保存时使用的名称. + */ + public abstract override val saveName: String + /** * 添加了追踪的 [ValueNode] 列表, 即通过 `by value` 初始化的属性列表. * @@ -28,22 +38,145 @@ public abstract class AbstractPluginData : PluginData, PluginDataImpl() { * * @see provideDelegate */ - public override val valueNodes: MutableList<ValueNode<*>> = mutableListOf() + @ConsoleExperimentalApi + public val valueNodes: MutableList<ValueNode<*>> = mutableListOf() /** * 供手动实现时值跟踪使用 (如 Java 用户). 一般 Kotlin 用户需使用 [provideDelegate] */ - public override fun <T : SerializerAwareValue<*>> T.track(valueName: String, annotations: List<Annotation>): T = - apply { valueNodes.add(ValueNode(valueName, this, annotations, this.serializer)) } + @ConsoleExperimentalApi + public fun <T : SerializerAwareValue<*>> track( + value: T, + /** + * 值名称. + * + * 如果属性带有 [ValueName], 则使用 [ValueName.value], + * 否则使用 [属性名称][KProperty.name] + * + * @see [ValueNode.value] + */ + valueName: String, + annotations: List<Annotation>, + ): T = value.apply { this@AbstractPluginData.valueNodes.add(ValueNode(valueName, this, annotations, this.serializer)) } /** - * 所有 [valueNodes] 更新和保存序列化器. 仅供内部使用 + * 使用 `by value()` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪, 并创建 [ValueNode] 加入 [valueNodes] */ + public operator fun <T : SerializerAwareValue<*>> T.provideDelegate( + thisRef: Any?, + property: KProperty<*>, + ): T = track(this, property.valueName, property.getAnnotationListForValueSerialization()) + + /** + * 所有 [valueNodes] 更新和保存序列化器. + */ + @ConsoleExperimentalApi public final override val updaterSerializer: KSerializer<Unit> get() = super.updaterSerializer + @ConsoleExperimentalApi + public override val serializersModule: SerializersModule = EmptySerializersModule + /** * 当所属于这个 [PluginData] 的 [Value] 的 [值][Value.value] 被修改时被调用. */ - public abstract override fun onValueChanged(value: Value<*>) + @ConsoleExperimentalApi + public override fun onValueChanged(value: Value<*>) { + // no-op by default + } + + /** + * 当这个 [PluginData] 被放入一个 [PluginDataStorage] 时调用 + */ + @ConsoleExperimentalApi + public override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) { + // no-op by default + } + + /** + * 由 [track] 创建, 来自一个通过 `by value` 初始化的属性节点. + */ + @ConsoleExperimentalApi + public data class ValueNode<T>( + /** + * 节点名称. + * + * 如果属性带有 [ValueName], 则使用 [ValueName.value], + * 否则使用 [属性名称][KProperty.name] + */ + val valueName: String, + /** + * 属性值代理 + */ + val value: Value<out T>, + /** + * 注解列表 + */ + val annotations: List<Annotation>, + /** + * 属性值更新器 + */ + val updaterSerializer: KSerializer<Unit>, + ) +} + +/** + * 获取这个 [KProperty] 委托的 [Value] + * + * 示例: + * ``` + * object MyData : AutoSavePluginData(PluginMain) { + * val list: List<String> by value() + * } + * + * val value: Value<List<String>> = MyData.findBackingFieldValue(MyData::list) + * ``` + * + * @see PluginData + */ +@ConsoleExperimentalApi +public fun <T> AbstractPluginData.findBackingFieldValue(property: KProperty<T>): Value<out T>? = + findBackingFieldValue(property.valueName) + +/** + * 获取这个 [KProperty] 委托的 [Value] + * + * 示例: + * ``` + * object MyData : AutoSavePluginData(PluginMain) { + * @ValueName("theList") + * val list: List<String> by value() + * val int: Int by value() + * } + * + * val value: Value<List<String>> = MyData.findBackingFieldValue("theList") // 需使用 @ValueName 标注的名称 + * val intValue: Value<Int> = MyData.findBackingFieldValue("int") + * ``` + * + * @see PluginData + */ +@ConsoleExperimentalApi +public fun <T> AbstractPluginData.findBackingFieldValue(propertyValueName: String): Value<out T>? { + @Suppress("UNCHECKED_CAST") + return this.valueNodes.find { it.valueName == propertyValueName }?.value as Value<out T> +} + +/** + * 获取这个 [KProperty] 委托的 [Value] + * + * 示例: + * ``` + * object MyData : AutoSavePluginData(PluginMain) { + * val list: List<String> by value() + * } + * + * val value: PluginData.ValueNode<List<String>> = MyData.findBackingFieldValueNode(MyData::list) + * ``` + * + * @see PluginData + */ +@ConsoleExperimentalApi +public fun <T> AbstractPluginData.findBackingFieldValueNode(property: KProperty<T>): AbstractPluginData.ValueNode<out T>? { + @Suppress("UNCHECKED_CAST") + return this.valueNodes.find { it == property } as AbstractPluginData.ValueNode<out T>? } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginConfig.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginConfig.kt index f6aa40dfa..1e3793e5a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginConfig.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginConfig.kt @@ -23,4 +23,10 @@ import kotlinx.coroutines.Job * @see PluginConfig * @see AutoSavePluginData */ -public open class AutoSavePluginConfig : AutoSavePluginData(), PluginConfig \ No newline at end of file +public open class AutoSavePluginConfig : AutoSavePluginData, PluginConfig { + @Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("AutoSavePluginConfig(\"把我改成保存名称\")")) + @Suppress("DEPRECATION_ERROR") + public constructor() : super() + + public constructor(saveName: String) : super(saveName) +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt index 85e45bc10..0ab74fc19 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/AutoSavePluginData.kt @@ -18,6 +18,7 @@ import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.internal.plugin.updateWhen import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.* +import kotlin.reflect.full.findAnnotation /** * 链接自动保存的 [PluginData]. @@ -29,14 +30,29 @@ import net.mamoe.mirai.utils.* * @see PluginData */ public open class AutoSavePluginData private constructor( - @Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any? + // KEEP THIS PRIMARY CONSTRUCTOR FOR FUTURE USE: WE'LL SUPPORT SERIALIZERS_MODULE FOR POLYMORPHISM + @Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any?, ) : AbstractPluginData() { private lateinit var owner_: AutoSavePluginDataHolder private val autoSaveIntervalMillis_: LongRange get() = owner_.autoSaveIntervalMillis private lateinit var storage_: PluginDataStorage - public constructor() : this(null) + public final override val saveName: String + get() = _saveName + private lateinit var _saveName: String + + public constructor(saveName: String) : this(null) { + _saveName = saveName + } + + @Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("AutoSavePluginData(\"把我改成保存名称\")")) + public constructor() : this(null) { + val clazz = this::class + _saveName = clazz.findAnnotation<ValueName>()?.value + ?: clazz.qualifiedName + ?: throw IllegalArgumentException("Cannot find a serial name for ${this::class}") + } @ConsoleExperimentalApi override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) { @@ -57,7 +73,7 @@ public open class AutoSavePluginData private constructor( ?.let { return@invokeOnCompletion } MiraiConsole.mainLogger.error( "An exception occurred when saving config ${this@AutoSavePluginData::class.qualifiedNameOrTip} " + - "but CoroutineExceptionHandler not found in PluginDataHolder.coroutineContext for ${owner::class.qualifiedNameOrTip}", + "but CoroutineExceptionHandler not found in PluginDataHolder.coroutineContext for ${owner::class.qualifiedNameOrTip}", e ) } @@ -121,6 +137,7 @@ public open class AutoSavePluginData private constructor( } } + @ConsoleExperimentalApi public final override fun onValueChanged(value: Value<*>) { debuggingLogger1.error { "onValueChanged: $value" } if (::owner_.isInitialized) { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt index df2381c75..88f74e033 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt @@ -37,4 +37,10 @@ import net.mamoe.mirai.console.data.java.JAutoSavePluginConfig * * @see PluginData */ -public interface PluginConfig : PluginData \ No newline at end of file +public interface PluginConfig : PluginData { + /** + * 警告: [PluginConfig] 的实现处于实验性阶段. + * + * 自主实现 [PluginConfig] 将得不到兼容性保障. 请仅考虑使用 [AutoSavePluginConfig] + */ +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt index 7288eaa3d..174170e1b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt @@ -18,20 +18,27 @@ package net.mamoe.mirai.console.data import kotlinx.serialization.KSerializer +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.serializersModuleOf +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_NO_ARG_CONSTRUCTOR import net.mamoe.mirai.console.data.java.JAutoSavePluginData -import net.mamoe.mirai.console.internal.data.* +import net.mamoe.mirai.console.internal.data.createInstanceSmart +import net.mamoe.mirai.console.internal.data.typeOf0 +import net.mamoe.mirai.console.internal.data.valueFromKTypeImpl +import net.mamoe.mirai.console.internal.data.valueImpl +import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.plugin.jvm.reloadPluginData import net.mamoe.mirai.console.util.ConsoleExperimentalApi import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KClass -import kotlin.reflect.KProperty import kotlin.reflect.KType -import kotlin.reflect.full.findAnnotation /** - * 一个插件内部的, 对用户隐藏的数据对象. 可包含对多个 [Value] 的值变更的跟踪. + * 一个插件内部的, 对用户隐藏的数据对象. 可包含对多个 [Value] 的值变更的跟踪. 典型的实现为 [AbstractPluginData]. * - * [PluginData] 不涉及有关数据的存储, 而是只维护数据结构: [属性节点列表][valueNodes]. + * [AbstractPluginData] 不涉及有关数据的存储, 而是只维护数据结构: [属性节点列表][AbstractPluginData.valueNodes]. * * 有关存储方案, 请查看 [PluginDataStorage]. * @@ -68,7 +75,7 @@ import kotlin.reflect.full.findAnnotation * val theList: MutableList<String> = AccountPluginData.list * ``` * - * 但也注意, 不要存储 `AccountPluginData.list`. 它可能受不到值跟踪. 若必要存储, 请使用 [PluginData.findBackingFieldValue] + * 但也注意, 不要存储 `AccountPluginData.list`. 它可能受不到值跟踪. 若必要存储, 请使用 [AbstractPluginData.findBackingFieldValue] * * ### 使用 Java * @@ -98,93 +105,36 @@ import kotlin.reflect.full.findAnnotation * * 要查看详细的解释,请查看 [docs/PluginData.md](https://github.com/mamoe/mirai-console/blob/master/docs/PluginData.md) * - * @see JvmPlugin.reloadPluginData 通过 [JvmPlugin] 获取指定 [PluginData] 实例. + * @see AbstractJvmPlugin.reloadPluginData 通过 [JvmPlugin] 获取指定 [PluginData] 实例. * @see PluginDataStorage [PluginData] 存储仓库 * @see PluginDataExtensions 相关 [SerializerAwareValue] 映射函数 */ public interface PluginData { /** - * 添加了追踪的 [ValueNode] 列表 (即使用 `by value()` 委托的属性), 即通过 `by value` 初始化的属性列表. - * - * 他们的修改会被跟踪, 并触发 [onValueChanged]. - * - * @see provideDelegate - * @see track - */ - public val valueNodes: MutableList<ValueNode<*>> - - /** - * 这个 [PluginData] 保存时使用的名称. 默认通过 [ValueName] 获取, 否则使用 [类全名][KClass.qualifiedName] (即 [Class.getCanonicalName]) + * 这个 [PluginData] 保存时使用的名称. */ @ConsoleExperimentalApi public val saveName: String - get() { - val clazz = this::class - return clazz.findAnnotation<ValueName>()?.value - ?: clazz.qualifiedName - ?: throw IllegalArgumentException("Cannot find a serial name for ${this::class}") - } - /** - * 由 [provideDelegate] 创建, 来自一个通过 `by value` 初始化的属性节点. - */ @ConsoleExperimentalApi - public data class ValueNode<T>( - /** - * 节点名称. - * - * 如果属性带有 [ValueName], 则使用 [ValueName.value], - * 否则使用 [属性名称][KProperty.name] - */ - val valueName: String, - /** - * 属性值代理 - */ - val value: Value<out T>, - /** - * 注解列表 - */ - val annotations: List<Annotation>, - /** - * 属性值更新器 - */ - val updaterSerializer: KSerializer<Unit> - ) - - /** - * 使用 `by value()` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪, 并创建 [ValueNode] 加入 [valueNodes] - */ - public operator fun <T : SerializerAwareValue<*>> T.provideDelegate( - thisRef: Any?, - property: KProperty<*> - ): T = track(property.valueName, property.getAnnotationListForValueSerialization()) - - /** - * 供手动实现时值跟踪使用 (如 Java 用户). 一般 Kotlin 用户需使用 [provideDelegate] - */ - public fun <T : SerializerAwareValue<*>> T.track( - /** - * 值名称. - * - * 如果属性带有 [ValueName], 则使用 [ValueName.value], - * 否则使用 [属性名称][KProperty.name] - * - * @see [ValueNode.value] - */ - valueName: String, - annotations: List<Annotation> - ): T - - /** - * 所有 [valueNodes] 更新和保存序列化器. 仅供内部使用 - */ public val updaterSerializer: KSerializer<Unit> /** * 当所属于这个 [PluginData] 的 [Value] 的 [值][Value.value] 被修改时被调用. + * 调用者为 [Value] 的实现. */ + @ConsoleExperimentalApi public fun onValueChanged(value: Value<*>) + /** + * 用于支持多态序列化. + * + * @see SerializersModule + * @see serializersModuleOf + */ + @ConsoleExperimentalApi + public val serializersModule: SerializersModule + /** * 当这个 [PluginData] 被放入一个 [PluginDataStorage] 时调用 */ @@ -192,62 +142,6 @@ public interface PluginData { public fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) } -/** - * 获取这个 [KProperty] 委托的 [Value] - * - * 如, 对于 - * ``` - * object MyData : AutoSavePluginData(PluginMain) { - * val list: List<String> by value() - * } - * - * val value: Value<List<String>> = MyData.findBackingFieldValue(MyData::list) - * ``` - * - * @see PluginData - */ -public fun <T> PluginData.findBackingFieldValue(property: KProperty<T>): Value<out T>? = - findBackingFieldValue(property.valueName) - -/** - * 获取这个 [KProperty] 委托的 [Value] - * - * 如, 对于 - * ``` - * object MyData : AutoSavePluginData(PluginMain) { - * @ValueName("theList") - * val list: List<String> by value() - * val int: Int by value() - * } - * - * val value: Value<List<String>> = MyData.findBackingFieldValue("theList") // 需使用 @ValueName 标注的名称 - * val intValue: Value<Int> = MyData.findBackingFieldValue("int") - * ``` - * - * @see PluginData - */ -public fun <T> PluginData.findBackingFieldValue(propertyValueName: String): Value<out T>? { - return this.valueNodes.find { it.valueName == propertyValueName }?.value as Value<T> -} - - -/** - * 获取这个 [KProperty] 委托的 [Value] - * - * 如, 对于 - * ``` - * object MyData : AutoSavePluginData(PluginMain) { - * val list: List<String> by value() - * } - * - * val value: PluginData.ValueNode<List<String>> = MyData.findBackingFieldValueNode(MyData::list) - * ``` - * - * @see PluginData - */ -public fun <T> PluginData.findBackingFieldValueNode(property: KProperty<T>): PluginData.ValueNode<out T>? { - return this.valueNodes.find { it == property } as PluginData.ValueNode<out T>? -} // don't default = 0, cause ambiguity //// region PluginData_value_primitives CODEGEN //// @@ -313,7 +207,7 @@ public fun PluginData.value(default: String): SerializerAwareValue<String> = val @LowPriorityInOverloadResolution public inline fun <reified T> PluginData.value( default: T, - crossinline apply: T.() -> Unit = {} + crossinline apply: T.() -> Unit = {}, ): SerializerAwareValue<T> = valueFromKType(typeOf0<T>(), default).also { it.value.apply() } @@ -321,9 +215,10 @@ public inline fun <reified T> PluginData.value( * 通过具体化类型创建一个 [SerializerAwareValue]. * @see valueFromKType 查看更多实现信息 */ +@ResolveContext(RESTRICTED_NO_ARG_CONSTRUCTOR) @LowPriorityInOverloadResolution -public inline fun <reified T> PluginData.value(apply: T.() -> Unit = {}): SerializerAwareValue<T> = - valueImpl<T>(typeOf0<T>(), T::class).also { it.value.apply() } +public inline fun <@ResolveContext(RESTRICTED_NO_ARG_CONSTRUCTOR) reified T> + PluginData.value(apply: T.() -> Unit = {}): SerializerAwareValue<T> = valueImpl<T>(typeOf0<T>(), T::class).also { it.value.apply() } @Suppress("UNCHECKED_CAST") @PublishedApi diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataHolder.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataHolder.kt index 06d655913..af2bae5db 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataHolder.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataHolder.kt @@ -41,7 +41,6 @@ public interface PluginDataHolder { * @see Companion.newPluginDataInstance * @see KClass.createType */ - @JvmDefault public fun <T : PluginData> newPluginDataInstance(type: KType): T = newPluginDataInstanceUsingReflection<PluginData>(type) as T @@ -64,7 +63,6 @@ public interface AutoSavePluginDataHolder : PluginDataHolder, CoroutineScope { /** * 仅支持确切的 [PluginData] 类型 */ - @JvmDefault public override fun <T : PluginData> newPluginDataInstance(type: KType): T { val classifier = type.classifier?.cast<KClass<PluginData>>() require(classifier != null && classifier.java == PluginData::class.java) { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt index 2f09d6f09..8e4e667ac 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt @@ -135,6 +135,7 @@ public interface MultiFilePluginDataStorage : PluginDataStorage { } } +@ConsoleExperimentalApi @get:JvmSynthetic public inline val MultiFilePluginDataStorage.directory: File get() = this.directoryPath.toFile() \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/ValueName.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/ValueName.kt index 9737bf73a..e9de3a500 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/ValueName.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/ValueName.kt @@ -14,7 +14,7 @@ package net.mamoe.mirai.console.data * * 例: * ``` - * object AccountPluginData : PluginData by ... { + * object AccountPluginData : AutoSavePluginData() { * @ValueName("info") * val map: Map<String, String> by value("a" to "b") * } @@ -26,6 +26,9 @@ package net.mamoe.mirai.console.data * map: * a: b * ``` + * + * @see PluginData + * @see Value */ @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginConfig.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginConfig.kt index 16a7764eb..1ca820466 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginConfig.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginConfig.kt @@ -37,4 +37,10 @@ import net.mamoe.mirai.console.data.PluginData * @see JAutoSavePluginData * @see PluginConfig */ -public abstract class JAutoSavePluginConfig : AutoSavePluginConfig(), PluginConfig +public abstract class JAutoSavePluginConfig : AutoSavePluginConfig, PluginConfig { + @Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("JAutoSavePluginConfig(\"把我改成保存名称\")")) + @Suppress("DEPRECATION_ERROR") + public constructor() : super() + + public constructor(saveName: String) : super(saveName) +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginData.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginData.kt index 835522027..442f19cc9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginData.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/java/JAutoSavePluginData.kt @@ -66,7 +66,13 @@ import kotlin.reflect.full.createType * * @see PluginData */ -public abstract class JAutoSavePluginData : AutoSavePluginData(), PluginConfig { +public abstract class JAutoSavePluginData : AutoSavePluginData, PluginConfig { + @Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("JAutoSavePluginData(\"把我改成保存名称\")")) + @Suppress("DEPRECATION_ERROR") + public constructor() : super() + + public constructor(saveName: String) : super(saveName) + //// region JPluginData_value_primitives CODEGEN //// /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleBuildConstants.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleBuildConstants.kt index 8ee34200d..b3abe5cbd 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleBuildConstants.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleBuildConstants.kt @@ -9,13 +9,14 @@ package net.mamoe.mirai.console.internal -import com.vdurmont.semver4j.Semver +import net.mamoe.mirai.console.util.SemVersion import java.time.Instant internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants) @JvmStatic - val buildDate: Instant = Instant.ofEpochSecond(1599934775) + val buildDate: Instant = Instant.ofEpochSecond(1601134282) + const val versionConst: String = "1.0-RC-dev-29" @JvmStatic - val version: Semver = Semver("1.0-M4", Semver.SemverType.LOOSE) + val version: SemVersion = SemVersion(versionConst) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt index a971e6e3a..175af2331 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt @@ -11,7 +11,6 @@ package net.mamoe.mirai.console.internal -import com.vdurmont.semver4j.Semver import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -23,7 +22,6 @@ import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.command.BuiltInCommands -import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.ConsoleCommandSender import net.mamoe.mirai.console.data.PluginDataStorage @@ -48,6 +46,7 @@ import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleInput +import net.mamoe.mirai.console.util.SemVersion import net.mamoe.mirai.utils.* import java.nio.file.Path import java.time.Instant @@ -67,7 +66,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI private val instance: MiraiConsoleImplementation by MiraiConsoleImplementation.Companion::instance override val buildDate: Instant by MiraiConsoleBuildConstants::buildDate - override val version: Semver by MiraiConsoleBuildConstants::version + override val version: SemVersion by MiraiConsoleBuildConstants::version override val rootPath: Path by instance::rootPath override val frontEndDescription: MiraiConsoleFrontEndDescription by instance::frontEndDescription @@ -191,6 +190,10 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI PluginManagerImpl.enableAllLoadedPlugins() + for (registeredCommand in CommandManager.allRegisteredCommands) { + registeredCommand.permission // init + } + mainLogger.info { "${PluginManagerImpl.plugins.size} plugin(s) enabled." } } @@ -221,9 +224,9 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI } @Suppress("SpellCheckingInspection") - @Retention(AnnotationRetention.SOURCE) + @Retention(AnnotationRetention.BINARY) @DslMarker - private annotation class ILoveOmaeKumikoForever + internal annotation class ILoveOmaeKumikoForever @ILoveOmaeKumikoForever private inline fun phase(block: () -> Unit) { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt index 6178ef36f..eb34fb34a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.command.* -import net.mamoe.mirai.console.command.Command.Companion.primaryName +import net.mamoe.mirai.console.command.Command.Companion.allNames import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.subscribeAlways @@ -31,8 +31,9 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine MiraiConsole.createLogger("command") } + @Suppress("ObjectPropertyName") @JvmField - internal val registeredCommands: MutableList<Command> = mutableListOf() + internal val _registeredCommands: MutableList<Command> = mutableListOf() @JvmField internal val requiredPrefixCommandMap: MutableMap<String, Command> = mutableMapOf() @@ -89,8 +90,8 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine ///// IMPL - override val CommandOwner.registeredCommands: List<Command> get() = CommandManagerImpl.registeredCommands.filter { it.owner == this } - override val allRegisteredCommands: List<Command> get() = registeredCommands.toList() // copy + override val CommandOwner.registeredCommands: List<Command> get() = _registeredCommands.filter { it.owner == this } + override val allRegisteredCommands: List<Command> get() = _registeredCommands.toList() // copy override val commandPrefix: String get() = "/" override fun CommandOwner.unregisterAllCommands() { for (registeredCommand in registeredCommands) { @@ -100,24 +101,28 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine override fun Command.register(override: Boolean): Boolean { if (this is CompositeCommand) this.subCommands // init lazy - this.permission // init lazy - this.names // init lazy - this.description // init lazy - this.usage // init lazy + kotlin.runCatching { + this.permission // init lazy + this.secondaryNames // init lazy + this.description // init lazy + this.usage // init lazy + }.onFailure { + throw IllegalStateException("Failed to init command ${this@register}.", it) + } modifyLock.withLock { if (!override) { if (findDuplicate() != null) return false } - registeredCommands.add(this@register) + _registeredCommands.add(this@register) if (this.prefixOptional) { - for (name in this.names) { + for (name in this.allNames) { val lowerCaseName = name.toLowerCase() optionalPrefixCommandMap[lowerCaseName] = this requiredPrefixCommandMap[lowerCaseName] = this } } else { - for (name in this.names) { + for (name in this.allNames) { val lowerCaseName = name.toLowerCase() optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency requiredPrefixCommandMap[lowerCaseName] = this @@ -128,21 +133,21 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine } override fun Command.findDuplicate(): Command? = - registeredCommands.firstOrNull { it.names intersectsIgnoringCase this.names } + _registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase this.allNames } override fun Command.unregister(): Boolean = modifyLock.withLock { if (this.prefixOptional) { - this.names.forEach { + this.allNames.forEach { optionalPrefixCommandMap.remove(it) } } - this.names.forEach { + this.allNames.forEach { requiredPrefixCommandMap.remove(it) } - registeredCommands.remove(this) + _registeredCommands.remove(this) } - override fun Command.isRegistered(): Boolean = this in registeredCommands + override fun Command.isRegistered(): Boolean = this in _registeredCommands override suspend fun Command.execute( sender: CommandSender, diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt index 0a3948277..f922229d4 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt @@ -12,7 +12,6 @@ package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.* -import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.description.CommandArgumentContext import net.mamoe.mirai.console.command.description.CommandArgumentContextAware import net.mamoe.mirai.console.internal.data.kClassQualifiedNameOrTip @@ -42,19 +41,21 @@ internal object SimpleCommandSubCommandAnnotationResolver : function.hasAnnotation<SimpleCommand.Handler>() override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> = - baseCommand.names + baseCommand.secondaryNames } internal abstract class AbstractReflectionCommand @JvmOverloads constructor( owner: CommandOwner, - names: Array<out String>, + primaryName: String, + secondaryNames: Array<out String>, description: String = "<no description available>", parentPermission: Permission = owner.parentPermission, prefixOptional: Boolean = false, ) : Command, AbstractCommand( owner, - names = names, + primaryName = primaryName, + secondaryNames = secondaryNames, description = description, parentPermission = parentPermission, prefixOptional = prefixOptional @@ -251,7 +252,7 @@ internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseComm internal fun AbstractReflectionCommand.createSubCommand( function: KFunction<*>, - context: CommandArgumentContext + context: CommandArgumentContext, ): AbstractReflectionCommand.SubCommandDescriptor { val notStatic = !function.hasAnnotation<JvmStatic>() //val overridePermission = null//function.findAnnotation<CompositeCommand.PermissionId>()//optional diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt index b57516237..ac83357ac 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt @@ -10,7 +10,6 @@ package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.Command -import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.contact.Group diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt index a753a52cd..45fa24f88 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt @@ -9,10 +9,9 @@ package net.mamoe.mirai.console.internal.data -import net.mamoe.mirai.console.data.MultiFilePluginDataStorage -import net.mamoe.mirai.console.data.PluginData -import net.mamoe.mirai.console.data.PluginDataHolder -import net.mamoe.mirai.console.data.PluginDataStorage +import kotlinx.serialization.json.Json +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.data.* import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.MiraiLogger @@ -42,7 +41,7 @@ internal open class MultiFilePluginDataStorageImpl( } else { this.store(holder, instance) // save an initial copy } - logger.debug { "Successfully loaded PluginData: ${instance.saveName} (containing ${instance.valueNodes.size} properties)" } + logger.debug { "Successfully loaded PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" } } protected open fun getPluginDataFile(holder: PluginDataHolder, instance: PluginData): File { @@ -62,28 +61,33 @@ internal open class MultiFilePluginDataStorageImpl( return file.toFile().also { it.createNewFile() } } + private val json = Json { + prettyPrint = true + ignoreUnknownKeys = true + isLenient = true + allowStructuredMapKeys = true + } + + private val yaml = Yaml.default + @ConsoleExperimentalApi public override fun store(holder: PluginDataHolder, instance: PluginData) { - val yaml =/* if (instance.saveName == "PermissionService") Json { - prettyPrint = true - ignoreUnknownKeys = true - isLenient = true - allowStructuredMapKeys = true - } /*Yaml( - configuration = YamlConfiguration( - mapSerialization = YamlConfiguration.MapSerialization.FLOW_MAP, - listSerialization = YamlConfiguration.ListSerialization.FLOW_SEQUENCE, - classSerialization = YamlConfiguration.MapSerialization.FLOW_MAP - ) - )*/ else */Yaml.default getPluginDataFile(holder, instance).writeText( kotlin.runCatching { yaml.encodeToString(instance.updaterSerializer, Unit) + }.recoverCatching { + // Just use mainLogger for convenience. + MiraiConsole.mainLogger.warning( + "Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. " + + "Please report this exception and relevant configurations to https://github.com/mamoe/mirai-console/issues/new", + it + ) + json.encodeToString(instance.updaterSerializer, Unit) }.getOrElse { throw IllegalStateException("Exception while saving $instance, saveName=${instance.saveName}", it) } ) - logger.debug { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.valueNodes.size} properties)" } + logger.debug { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" } } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt index 4dc70fa25..e52d4d79a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt @@ -18,9 +18,9 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import net.mamoe.mirai.console.data.AbstractPluginData +import net.mamoe.mirai.console.data.AbstractPluginData.ValueNode import net.mamoe.mirai.console.data.PluginData -import net.mamoe.mirai.console.data.PluginData.ValueNode -import net.mamoe.mirai.console.data.Value import net.mamoe.mirai.console.data.ValueDescription import net.mamoe.mirai.console.data.ValueName import net.mamoe.yamlkt.Comment @@ -34,12 +34,25 @@ import kotlin.reflect.KAnnotatedElement * - Auto-saving */ internal abstract class PluginDataImpl { - internal fun findNodeInstance(name: String): ValueNode<*>? = valueNodes.firstOrNull { it.valueName == name } + init { + @Suppress("LeakingThis") + check(this is AbstractPluginData) + } - internal abstract val valueNodes: MutableList<ValueNode<*>> + private fun findNodeInstance(name: String): ValueNode<*>? { + check(this is AbstractPluginData) + return valueNodes.firstOrNull { it.valueName == name } + } internal open val updaterSerializer: KSerializer<Unit> = object : KSerializer<Unit> { - override val descriptor: SerialDescriptor get() = dataUpdaterSerializerDescriptor + override val descriptor: SerialDescriptor by lazy { + check(this@PluginDataImpl is AbstractPluginData) + kotlinx.serialization.descriptors.buildClassSerialDescriptor((this@PluginDataImpl as PluginData).saveName) { + for (valueNode in valueNodes) valueNode.run { + element(valueName, updaterSerializer.descriptor, annotations = annotations, isOptional = true) + } + } + } @Suppress("UNCHECKED_CAST") override fun deserialize(decoder: Decoder) { @@ -84,6 +97,8 @@ internal abstract class PluginDataImpl { @Suppress("UNCHECKED_CAST") override fun serialize(encoder: Encoder, value: Unit) { + check(this@PluginDataImpl is AbstractPluginData) + val descriptor = descriptor with(encoder.beginStructure(descriptor)) { repeat(descriptor.elementsCount) { index -> @@ -100,18 +115,6 @@ internal abstract class PluginDataImpl { } } - - /** - * flatten - */ - abstract fun onValueChanged(value: Value<*>) - private val dataUpdaterSerializerDescriptor by lazy { - kotlinx.serialization.descriptors.buildClassSerialDescriptor((this as PluginData).saveName) { - for (valueNode in valueNodes) valueNode.run { - element(valueName, updaterSerializer.descriptor, annotations = annotations, isOptional = true) - } - } - } } internal fun KAnnotatedElement.getAnnotationListForValueSerialization(): List<Annotation> { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/AutoLoginConfig.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/AutoLoginConfig.kt index 3fee86f5b..5cf269874 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/AutoLoginConfig.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/AutoLoginConfig.kt @@ -6,10 +6,7 @@ import net.mamoe.mirai.console.data.value import net.mamoe.mirai.console.internal.util.md5 import net.mamoe.mirai.console.internal.util.toUHexString -internal object AutoLoginConfig : AutoSavePluginConfig() { - override val saveName: String - get() = "AutoLogin" - +internal object AutoLoginConfig : AutoSavePluginConfig("AutoLogin") { @ValueDescription( """ 账号和明文密码列表 diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt index feec64e53..037c65ba5 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/valueFromKTypeImpl.kt @@ -143,7 +143,7 @@ internal fun KClass<*>.isPrimitiveOrBuiltInSerializableValue(): Boolean { Byte::class, Short::class, Int::class, Long::class, Boolean::class, Char::class, String::class, - Pair::class, Triple::class // TODO: 2020/6/24 支持 PairValue, TripleValue + //Pair::class, Triple::class // TODO: 2020/6/24 支持 PairValue, TripleValue -> return true } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt index 5b50e64ca..60bb09cd8 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extension/BuiltInSingletonExtensionSelector.kt @@ -16,9 +16,7 @@ internal object BuiltInSingletonExtensionSelector : SingletonExtensionSelector { internal val config: SaveData = SaveData() - internal class SaveData : AutoSavePluginConfig() { - override val saveName: String get() = "ExtensionSelector" - + internal class SaveData : AutoSavePluginConfig("ExtensionSelector") { val value: MutableMap<String, String> by value() } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/permission/BuiltInPermissionServices.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/permission/BuiltInPermissionServices.kt index 2d2271312..d787e3358 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/permission/BuiltInPermissionServices.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/permission/BuiltInPermissionServices.kt @@ -117,12 +117,12 @@ internal object BuiltInPermissionService : AbstractConcurrentPermissionService<P @Suppress("RedundantVisibilityModifier") internal class ConcurrentSaveData private constructor( - public override val saveName: String, + saveName: String, @Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any?, - ) : AutoSavePluginConfig() { + ) : AutoSavePluginConfig(saveName) { public val grantedPermissionMap: PluginDataExtensions.NotNullMutableMap<PermissionId, MutableSet<AbstractPermitteeId>> - by value<MutableMap<PermissionId, MutableSet<AbstractPermitteeId>>>(ConcurrentHashMap()) - .withDefault { CopyOnWriteArraySet() } + by value<MutableMap<PermissionId, MutableSet<AbstractPermitteeId>>>(ConcurrentHashMap()) + .withDefault { CopyOnWriteArraySet() } public companion object { @JvmStatic diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/BuiltInJvmPluginLoaderImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/BuiltInJvmPluginLoaderImpl.kt index 2fc247161..6a97d2b15 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/BuiltInJvmPluginLoaderImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/BuiltInJvmPluginLoaderImpl.kt @@ -68,12 +68,12 @@ internal object BuiltInJvmPluginLoaderImpl : pluginClassLoader.declaredFilter = exportManagers[0] } }.map { (f, pluginClassLoader) -> - f to (pluginClassLoader.findServices( + f to pluginClassLoader.findServices( JvmPlugin::class, KotlinPlugin::class, AbstractJvmPlugin::class, JavaPlugin::class - ).loadAllServices()) + ).loadAllServices() }.flatMap { (f, list) -> list.associateBy { f }.asSequence() @@ -121,7 +121,9 @@ internal object BuiltInJvmPluginLoaderImpl : override fun disable(plugin: JvmPlugin) { if (!plugin.isEnabled) return - ensureActive() + + if (MiraiConsole.isActive) + ensureActive() if (plugin is JvmPluginInternal) { plugin.internalOnDisable() diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginClassLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginClassLoader.kt index 9154ba714..0e68e3951 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginClassLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginClassLoader.kt @@ -7,24 +7,28 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE * */ - @file:Suppress("MemberVisibilityCanBePrivate") package net.mamoe.mirai.console.internal.plugin import net.mamoe.mirai.console.plugin.jvm.ExportManager +import java.io.File import java.net.URL import java.net.URLClassLoader +import java.util.* import java.util.concurrent.ConcurrentHashMap -internal class LoadingDeniedException(name: String) : ClassNotFoundException(name) - internal class JvmPluginClassLoader( - val source: Any, - urls: Array<URL>, + val file: File, parent: ClassLoader?, - val classLoaders: Collection<JvmPluginClassLoader> -) : URLClassLoader(urls, parent) { + val classLoaders: Collection<JvmPluginClassLoader>, +) : URLClassLoader(arrayOf(file.toURI().toURL()), parent) { + //// 只允许插件 getResource 时获取插件自身资源, #205 + override fun getResources(name: String?): Enumeration<URL> = findResources(name) + override fun getResource(name: String?): URL? = findResource(name) + // getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源 + // 因此无需 override getResourceAsStream + override fun toString(): String { return "JvmPluginClassLoader{source=$source}" } @@ -118,3 +122,5 @@ internal class JvmPluginClassLoader( } } } + +internal class LoadingDeniedException(name: String) : ClassNotFoundException(name) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt index 8dda88fa7..9284baaa1 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt @@ -18,7 +18,6 @@ import net.mamoe.mirai.console.extension.PluginComponentStorage import net.mamoe.mirai.console.internal.data.mkdir import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.PermissionService -import net.mamoe.mirai.console.permission.PermissionService.Companion.allocatePermissionIdForPlugin import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader @@ -26,7 +25,6 @@ import net.mamoe.mirai.console.plugin.ResourceContainer.Companion.asResourceCont import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin.Companion.onLoad -import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.util.NamedSupervisorJob import net.mamoe.mirai.utils.MiraiLogger import java.io.File @@ -50,7 +48,7 @@ internal abstract class JvmPluginInternal( final override val parentPermission: Permission by lazy { PermissionService.INSTANCE.register( - PermissionService.INSTANCE.allocatePermissionIdForPlugin(name, "*"), + PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.ROOT_PERMISSION), "The base permission" ) } @@ -151,6 +149,7 @@ internal abstract class JvmPluginInternal( ) ) .also { + if (!MiraiConsole.isActive) return@also BuiltInJvmPluginLoaderImpl.coroutineContext[Job]!!.invokeOnCompletion { this.cancel() } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt index f53ebac6f..2f15ba42a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt @@ -27,6 +27,7 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoadException import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope +import net.mamoe.mirai.console.util.SemVersion.Companion.contains import net.mamoe.mirai.utils.info import java.io.File import java.nio.file.Path diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/util/semver/RangeTokenReader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/util/semver/RangeTokenReader.kt new file mode 100644 index 000000000..00a0524b2 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/util/semver/RangeTokenReader.kt @@ -0,0 +1,251 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + * + */ + +@file:Suppress("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<Token>, 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<Token> = ArrayList<Token>( + 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<Token>, root: Boolean): List<Token> = ArrayList<Token>().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<Token>, 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): SemVersion.Requirement { + 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 : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean { + rules.forEach { if (it.test(version)) return true } + return false + } + } + } + TokenType.AND -> { + return object : SemVersion.Requirement { + 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/main/kotlin/net/mamoe/mirai/console/internal/util/semver/SemVersionInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/util/semver/SemVersionInternal.kt new file mode 100644 index 000000000..d57c226e9 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/util/semver/SemVersionInternal.kt @@ -0,0 +1,290 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + * + */ + +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 + +@Suppress("RegExpRedundantEscape") +internal object SemVersionInternal { + 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() + + /** 解析核心版本号, eg: `1.0.0` -> IntArray[1, 0, 0] */ + @JvmStatic + private fun String.parseMainVersion(): IntArray = + split('.').map { it.toInt() }.toIntArray() + + + fun parse(version: String): SemVersion { + if (!SEM_VERSION_REGEX.matches(version)) { + throw IllegalArgumentException("`$version` not a valid version") + } + var mainVersionEnd = 0 + kotlin.run { + val iterator = version.iterator() + while (iterator.hasNext()) { + val next = iterator.next() + if (next == '-' || next == '+') { + break + } + mainVersionEnd++ + } + } + var identifier: String? = null + var metadata: String? = null + if (mainVersionEnd != version.length) { + when (version[mainVersionEnd]) { + '-' -> { + val metadataSplitter = version.indexOf('+', startIndex = mainVersionEnd) + if (metadataSplitter == -1) { + identifier = version.substring(mainVersionEnd + 1) + } else { + identifier = version.substring(mainVersionEnd + 1, metadataSplitter) + metadata = version.substring(metadataSplitter + 1) + } + } + '+' -> { + metadata = version.substring(mainVersionEnd + 1) + } + } + } + val mainVersion = version.substring(0, mainVersionEnd).parseMainVersion() + return SemVersion( + major = mainVersion[0], + minor = mainVersion[1], + patch = mainVersion.getOrNull(2), + identifier = identifier, + metadata = metadata + ) + } + + @JvmStatic + internal fun parseRule(rule: String): SemVersion.Requirement { + val trimmed = rule.trim() + if (directVersion.matches(trimmed)) { + val parsed = SemVersion.invoke(trimmed) + return object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = version.compareTo(parsed) == 0 + } + } + if (versionSelect.matches(trimmed)) { + val regex = ("^" + + trimmed.replace(".", "\\.") + .replace("x", ".+") + + "$" + ).toRegex() + return object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = regex.matches(version.toString()) + } + } + versionMathRange.matchEntire(trimmed)?.let { range -> + // 1 mode + // 2 first + // 5 sec + // 8 type + var typeStart = range.groupValues[1][0] + var typeEnd = range.groupValues[8][0] + var start = SemVersion.invoke(range.groupValues[2]) + var end = SemVersion.invoke(range.groupValues[5]) + if (start > end) { + val c = end + end = start + start = c + val x = typeEnd + typeEnd = typeStart + typeStart = x + } + val a: (SemVersion) -> Boolean = when (typeStart) { + '[', ']' -> ({ start <= it }) + '(', ')' -> ({ start < it }) + else -> throw AssertionError() + } + val b: (SemVersion) -> Boolean = when (typeEnd) { + '[', ']' -> ({ it <= end }) + '(', ')' -> ({ it < end }) + else -> throw AssertionError() + } + return object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = a(version) && b(version) + } + } + versionRule.matchEntire(trimmed)?.let { result -> + val operator = result.groupValues[1] + val version1 = SemVersion.invoke(result.groupValues[8]) + return when (operator) { + ">=" -> { + object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = version >= version1 + } + } + ">" -> { + object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = version > version1 + } + } + "<=" -> { + object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = version <= version1 + } + } + "<" -> { + object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = version < version1 + } + } + "=" -> { + object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = version.compareTo(version1) == 0 + } + } + "!=" -> { + object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = version.compareTo(version1) != 0 + } + } + else -> error("operator=$operator, version=$version1") + } + } + throw IllegalArgumentException("Cannot parse $rule") + } + + private fun SemVersion.Requirement.withRule(rule: String): SemVersion.Requirement { + return object : SemVersion.Requirement { + override fun test(version: SemVersion): Boolean = this@withRule.test(version) + override fun toString(): String = rule + } + } + + @JvmStatic + fun parseRangeRequirement(requirement: String): SemVersion.Requirement { + 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)).withRule(requirement) + }.onFailure { error -> + throw IllegalArgumentException("Exception in parsing $requirement\n\n" + buildString { + collected.forEach { dump("", it) } + }, error) + }.getOrThrow() + } + + @JvmStatic + fun compareInternal(source: SemVersion, other: SemVersion): Int { + // ignored metadata in comparing + + // If $this equals $other (without metadata), + // return same. + // Compare main-version + + source.major.compareTo(other.major).takeUnless { it == 0 }?.let { return it } + source.minor.compareTo(other.minor).takeUnless { it == 0 }?.let { return it } + (source.patch ?: 0).compareTo(other.patch ?: 0).takeUnless { it == 0 }?.let { return it } + + // If main-versions are same. + var identifier0 = source.identifier + var identifier1 = other.identifier + // If anyone doesn't have the identifier... + if (identifier0 == null || identifier1 == null) { + return when (identifier0) { + identifier1 -> { // null == null + // Nobody has identifier + 0 + } + null -> { + // $other has identifier, but $this don't have identifier + // E.g: + // this = 1.0.0 + // other = 1.0.0-dev + 1 + } + // It is the opposite of the above. + else -> -1 + } + } + fun String.getSafe(index: Int) = getOrElse(index) { ' ' } + + // ignored same prefix + fun getSameSize(s1: String, s2: String): Int { + val size = min(s1.length, s2.length) + // 1.0-RC19 -> 19 + // 1.0-RC107 -> 107 + var realSameSize = 0 + for (index in 0 until size) { + if (s1[index] != s2[index]) { + return realSameSize + } else { + if (!s1[index].isDigit()) { + realSameSize = index + 1 + } + } + } + return realSameSize + } + + // We ignore the same parts. Because we only care about the differences. + // E.g: + // 1.0-RC1 -> 1 + // 1.0-RC2 -> 2 + val ignoredSize = getSameSize(identifier0, identifier1) + identifier0 = identifier0.substring(ignoredSize) + identifier1 = identifier1.substring(ignoredSize) + // Multi-chunk comparing + val chunks0 = identifier0.split('-', '.') + val chunks1 = identifier1.split('-', '.') + chunkLoop@ for (index in 0 until (max(chunks0.size, chunks1.size))) { + val value0 = chunks0.getOrNull(index) + val value1 = chunks1.getOrNull(index) + // Any chunk is null + if (value0 == null || value1 == null) { + // value0 == null && value1 == null is impossible + return if (value0 == null) { + // E.g: + // value0 = 1.0-RC-dev + // value1 = 1.0-RC-dev-1 + -1 + } else { + // E.g: + // value0 = 1.0-RC-dev-1 + // value1 = 1.0-RC-dev + 1 + } + } + try { + val result = value0.toInt().compareTo(value1.toInt()) + if (result != 0) { + return result + } + continue@chunkLoop + } catch (ignored: NumberFormatException) { + } + // compare chars + for (index0 in 0 until (max(value0.length, value1.length))) { + val result = value0.getSafe(index0).compareTo(value1.getSafe(index0)) + if (result != 0) + return result + } + } + return 0 + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt index 289b4b214..b7c7cd80f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt @@ -13,9 +13,9 @@ import net.mamoe.mirai.console.command.BuiltInCommands import net.mamoe.mirai.console.command.Command /** - * 一个权限. + * 一个抽象的「权限」. 由 [PermissionService] 实现不同, [Permission] 可能会有多种实例. 但一个权限总是拥有确定的 [id]. * - * 由 [PermissionService] 实现不同, [Permission] 可能会有多种实例. 但一个权限总是拥有确定的 [id]. + * 在匹配权限时, 应使用唯一的 [id] 作为依据. 而不应该使用 [Permission] 实例. 同时, [Permission] 也不适合存储. * * **注意**: 请不要手动实现这个接口. 总是从 [PermissionService.register] 获得实例. * @@ -32,6 +32,7 @@ import net.mamoe.mirai.console.command.Command * #### 手动申请权限 * [PermissionService.register] */ +@PermissionImplementation public interface Permission { /** * 唯一识别 ID. 所有权限的 [id] 都互不相同. @@ -49,6 +50,8 @@ public interface Permission { /** * 父权限. * + * 在检查权限时, 若一个 [Permittee] 拥有父 + * * [RootPermission] 的 parent 为自身 */ public val parent: Permission diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt index 2d527d1e2..9a92e8be2 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt @@ -12,6 +12,8 @@ package net.mamoe.mirai.console.permission import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.* import net.mamoe.mirai.console.internal.data.map @@ -25,10 +27,17 @@ import net.mamoe.mirai.console.internal.data.map */ @Serializable(with = PermissionId.PermissionIdAsStringSerializer::class) public data class PermissionId( - public val namespace: String, - public val name: String, + @ResolveContext(PERMISSION_NAMESPACE) public val namespace: String, + @ResolveContext(PERMISSION_NAME) public val name: String, ) { init { + require(!namespace.contains(' ')) { + "' ' is not allowed in namespace" + } + require(!name.contains(' ')) { + "' ' is not allowed in id" + } + require(!namespace.contains(':')) { "':' is not allowed in namespace" } @@ -54,13 +63,39 @@ public data class PermissionId( * @throws IllegalArgumentException 在解析失败时抛出. */ @JvmStatic - public fun parseFromString(string: String): PermissionId { + public fun parseFromString(@ResolveContext(PERMISSION_ID) string: String): PermissionId { return kotlin.runCatching { string.split(':').let { (namespace, id) -> PermissionId(namespace, id) } }.getOrElse { throw IllegalArgumentException("Could not parse PermissionId from '$string'", it) } } + + /** + * 检查 [PermissionId.name] 的合法性. 在非法时抛出 [IllegalArgumentException] + */ + @JvmStatic + @Throws(IllegalArgumentException::class) + public fun checkPermissionIdName(@ResolveContext(PERMISSION_NAME) value: String) { + when { + value.isBlank() -> throw IllegalArgumentException("PermissionId.name should not be blank.") + value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.name.") + value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.name.") + } + } + + /** + * 检查 [PermissionId.namespace] 的合法性. 在非法时抛出 [IllegalArgumentException] + */ + @JvmStatic + @Throws(IllegalArgumentException::class) + public fun checkPermissionIdNamespace(@ResolveContext(PERMISSION_NAME) value: String) { + when { + value.isBlank() -> throw IllegalArgumentException("PermissionId.namespace should not be blank.") + value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.namespace.") + value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.namespace.") + } + } } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionIdNamespace.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionIdNamespace.kt index 6c8cb4b4c..90a043608 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionIdNamespace.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionIdNamespace.kt @@ -9,12 +9,18 @@ package net.mamoe.mirai.console.permission +import net.mamoe.mirai.console.command.Command +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PERMISSION_NAME + /** * [PermissionId] 的命名空间. 用于提供 [PermissionId.namespace]. */ public interface PermissionIdNamespace { /** - * 创建一个此命名空间下的 [PermitteeId] + * 创建一个此命名空间下的 [PermitteeId]. + * + * 在指令初始化时, 会申请对应权限. 此时 [name] 为 "command.$primaryName` 其中 [primaryName][Command.primaryName]. */ - public fun permissionId(name: String): PermissionId + public fun permissionId(@ResolveContext(PERMISSION_NAME) name: String): PermissionId } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionImplementation.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionImplementation.kt index c215e224d..2e79f262e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionImplementation.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionImplementation.kt @@ -12,10 +12,12 @@ package net.mamoe.mirai.console.permission import kotlin.annotation.AnnotationTarget.* /** - * 表示一个应该由权限插件实现的类. + * 表示一个应该由专有的权限插件 (提供 [PermissionService] 的插件) 实现的类. * - * 这样的类不能被用户手动实现或者继承, 也不能使用属性委托或者类委托, 或者其他任意改变实现类的手段. - * 用户仅应该使用从 [PermissionService] 或其他途径获取这些对象, 而不能自行实现它们. + * + * 这样的类不能被用户手动实现或者继承, 也不能使用属性委托或者类委托, 或者其他任意直接或间接实现他们的手段 (否则会导致 [PermissionService] 处理异常). + * + * 普通插件仅应该使用从 [PermissionService] 或其他途径获取这些对象. */ @Retention(AnnotationRetention.BINARY) @Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt index ed014fa99..1a12b8af1 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt @@ -11,9 +11,13 @@ package net.mamoe.mirai.console.permission +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.extensions.PermissionServiceProvider import net.mamoe.mirai.console.internal.permission.checkType import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf +import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.console.plugin.name import kotlin.reflect.KClass /** @@ -77,6 +81,8 @@ public interface PermissionService<P : Permission> { * 申请并注册一个权限 [Permission]. * * @throws PermissionRegistryConflictException 当已存在一个 [PermissionId] 时抛出. + * + * @return 申请到的 [Permission] 实例 */ @Throws(PermissionRegistryConflictException::class) public fun register( @@ -85,21 +91,29 @@ public interface PermissionService<P : Permission> { parent: Permission = RootPermission, ): P + /** 为 [Plugin] 分配一个 [PermissionId] */ + public fun allocatePermissionIdForPlugin( + plugin: Plugin, + @ResolveContext(COMMAND_NAME) permissionName: String, + reason: PluginPermissionIdRequestType + ): PermissionId = allocatePermissionIdForPluginDefaultImplement(plugin, permissionName, reason) + /////////////////////////////////////////////////////////////////////////// /** * 授予 [permitteeId] 以 [permission] 权限 * - * Console 内建的权限服务支持授予操作. 但插件扩展的权限服务可能不支持. + * Console 内建的权限服务支持此操作. 但插件扩展的权限服务可能不支持. * * @throws UnsupportedOperationException 当插件扩展的 [PermissionService] 不支持这样的操作时抛出. */ + @Throws(UnsupportedOperationException::class) public fun permit(permitteeId: PermitteeId, permission: P) /** * 撤销 [permitteeId] 的 [permission] 授权 * - * Console 内建的权限服务支持授予操作. 但插件扩展的权限服务可能不支持. + * Console 内建的权限服务支持此操作. 但插件扩展的权限服务可能不支持. * * @param recursive `true` 时递归撤销所有子权限. * 例如, 若 [permission] 为 "*:*", @@ -108,8 +122,18 @@ public interface PermissionService<P : Permission> { * * @throws UnsupportedOperationException 当插件扩展的 [PermissionService] 不支持这样的操作时抛出. */ + @Throws(UnsupportedOperationException::class) public fun cancel(permitteeId: PermitteeId, permission: P, recursive: Boolean) + /** [Plugin] 尝试分配的 [PermissionId] 来源 */ + public enum class PluginPermissionIdRequestType { + /** For [Plugin.parentPermission] */ + ROOT_PERMISSION, + + /** For [Plugin.permissionId] */ + PERMISSION_ID + } + public companion object { internal var instanceField: PermissionService<*>? = null @@ -118,11 +142,21 @@ public interface PermissionService<P : Permission> { public val INSTANCE: PermissionService<out Permission> get() = instanceField ?: error("PermissionService is not yet initialized therefore cannot be used.") + /** + * 获取一个权限, 失败时抛出 [NoSuchElementException] + */ + @Throws(NoSuchElementException::class) public fun <P : Permission> PermissionService<P>.getOrFail(id: PermissionId): P = get(id) ?: throw NoSuchElementException("Permission not found: $id") - internal fun PermissionService<*>.allocatePermissionIdForPlugin(name: String, id: String) = - PermissionId("plugin.${name.toLowerCase()}", id.toLowerCase()) + internal fun PermissionService<*>.allocatePermissionIdForPluginDefaultImplement( + plugin: Plugin, + @ResolveContext(COMMAND_NAME) permissionName: String, + reason: PluginPermissionIdRequestType + ) = PermissionId( + plugin.name.toLowerCase().replace(' ', '.'), + permissionName.toLowerCase().replace(' ', '.') + ) public fun PermissionId.findCorrespondingPermission(): Permission? = INSTANCE[this] diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermitteeId.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermitteeId.kt index b1178870f..ec46d5da7 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermitteeId.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermitteeId.kt @@ -14,6 +14,7 @@ package net.mamoe.mirai.console.permission import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.command.BuiltInCommands import net.mamoe.mirai.console.internal.data.map import net.mamoe.mirai.console.internal.permission.parseFromStringImpl import net.mamoe.mirai.console.permission.AbstractPermitteeId.* @@ -107,6 +108,33 @@ public interface PermitteeId { * * - 若指令 A 的权限被授予给 [AnyMember], 那么一个 [ExactMember] 可以执行这个指令. * + * #### 字符串表示 + * + * 当使用 [PermitteeId.asString] 时, 不同的类型的返回值如下表所示. 这些格式也适用于 [BuiltInCommands.PermissionCommand]. + * + * (不区分大小写. 不区分 Bot). + * + * + * | 被许可人类型 | 字符串表示示例 | 备注 | + * |:----------------:|:-----------:|:-------------------------------------| + * | 控制台 | console | | + * | 精确群 | g123456 | 表示群, 而不表示群成员 | + * | 精确好友 | f123456 | 必须通过好友消息 | + * | 精确临时会话 | t123456.789 | 群 123456 内的成员 789. 必须通过临时会话 | + * | 精确群成员 | m123456.789 | 群 123456 内的成员 789. 同时包含临时会话. | + * | 精确用户 | u123456 | 同时包含群成员, 好友, 临时会话 | + * | 任意群 | g* | | + * | 任意群的任意群员 | m* | | + * | 精确群的任意群员 | m123456.* | 群 123456 内的任意成员. 同时包含临时会话. | + * | 任意群的任意临时会话 | t* | 必须通过临时会话 | + * | 精确群的任意临时会话 | t123456.* | 群 123456 内的任意成员. 必须通过临时会话 | + * | 任意好友 | f* | | + * | 任意用户 | u* | 任何人在任何环境 | + * | 任意对象 | * | 即任何人, 任何群, 控制台 | + * + * + * #### 关系图 + * * ``` * Console AnyContact * ↑ @@ -141,7 +169,7 @@ public interface PermitteeId { * ExactTemp * ``` */ -@Serializable(with = AbstractPermitteeId.AsStringSerializer::class) +@Serializable(with = AsStringSerializer::class) public sealed class AbstractPermitteeId( public final override vararg val directParents: PermitteeId, ) : PermitteeId { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt index 55a00f667..8db84c890 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt @@ -11,7 +11,6 @@ package net.mamoe.mirai.console.plugin -import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable @@ -20,6 +19,7 @@ import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.loader.PluginLoader +import net.mamoe.mirai.console.util.SemVersion /** * 表示一个 mirai-console 插件. @@ -62,7 +62,7 @@ public inline val Plugin.name: String get() = this.description.name /** * 获取 [PluginDescription.version] */ -public inline val Plugin.version: Semver get() = this.description.version +public inline val Plugin.version: SemVersion get() = this.description.version /** * 获取 [PluginDescription.info] diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginFileExtensions.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginFileExtensions.kt index 276113880..459d768b4 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginFileExtensions.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginFileExtensions.kt @@ -41,28 +41,24 @@ public interface PluginFileExtensions { * 从数据目录获取一个文件. * @see dataFolderPath */ - @JvmDefault public fun resolveDataFile(relativePath: String): File = dataFolderPath.resolve(relativePath).toFile() /** * 从数据目录获取一个文件. * @see dataFolderPath */ - @JvmDefault public fun resolveDataPath(relativePath: String): Path = dataFolderPath.resolve(relativePath) /** * 从数据目录获取一个文件. * @see dataFolderPath */ - @JvmDefault public fun resolveDataFile(relativePath: Path): File = dataFolderPath.resolve(relativePath).toFile() /** * 从数据目录获取一个文件路径. * @see dataFolderPath */ - @JvmDefault public fun resolveDataPath(relativePath: Path): Path = dataFolderPath.resolve(relativePath) @@ -83,27 +79,23 @@ public interface PluginFileExtensions { * 从配置目录获取一个文件. * @see configFolderPath */ - @JvmDefault public fun resolveConfigFile(relativePath: String): File = configFolderPath.resolve(relativePath).toFile() /** * 从配置目录获取一个文件. * @see configFolderPath */ - @JvmDefault public fun resolveConfigPath(relativePath: String): Path = configFolderPath.resolve(relativePath) /** * 从配置目录获取一个文件. * @see configFolderPath */ - @JvmDefault public fun resolveConfigFile(relativePath: Path): File = configFolderPath.resolve(relativePath).toFile() /** * 从配置目录获取一个文件路径. * @see configFolderPath */ - @JvmDefault public fun resolveConfigPath(relativePath: Path): Path = configFolderPath.resolve(relativePath) } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/ResourceContainer.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/ResourceContainer.kt index e2e26cc0c..8937f4146 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/ResourceContainer.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/ResourceContainer.kt @@ -37,7 +37,6 @@ public interface ResourceContainer { * * @return 资源文件内容. 在未找到文件时返回 `null`. */ - @JvmDefault public fun getResource(path: String): String? = getResource(path, Charsets.UTF_8) /** @@ -45,7 +44,6 @@ public interface ResourceContainer { * * @return 资源文件内容. 在未找到文件时返回 `null`. */ - @JvmDefault public fun getResource(path: String, charset: Charset): String? = this.getResourceAsStream(path)?.use(InputStream::readBytes)?.let(::String) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt index 49bfad6ff..d73ac9cec 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt @@ -11,6 +11,10 @@ package net.mamoe.mirai.console.plugin.description +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_ID +import net.mamoe.mirai.console.util.SemVersion + /** * 插件的一个依赖的信息. * @@ -20,16 +24,15 @@ public data class PluginDependency @JvmOverloads constructor( /** * 依赖插件 ID, [PluginDescription.id] */ - public val id: String, + @ResolveContext(PLUGIN_ID) public val id: String, /** * 依赖版本号. 为 null 时则为不限制版本. * * 版本遵循 [语义化版本 2.0 规范](https://semver.org/lang/zh-CN/), * - * ### 示例 - * `Requirement.buildIvy("[1.0, 2.0)")` + * @see SemVersion.Requirement */ - public val versionRequirement: VersionRequirement? = null, + public val versionRequirement: SemVersion.Requirement? = null, /** * 若为 `false`, 插件在找不到此依赖时也能正常加载. */ @@ -46,7 +49,10 @@ public data class PluginDependency @JvmOverloads constructor( /** * @see PluginDependency */ - public constructor(name: String, isOptional: Boolean = false) : this( - name, null, isOptional + public constructor( + @ResolveContext(PLUGIN_ID) id: String, + isOptional: Boolean = false, + ) : this( + id, null, isOptional ) } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt index e9af5a3b1..ca9267495 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt @@ -9,8 +9,10 @@ package net.mamoe.mirai.console.plugin.description -import com.vdurmont.semver4j.Semver +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.* import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.console.util.SemVersion /** @@ -46,6 +48,7 @@ public interface PluginDescription { * @see ID_REGEX * @see FORBIDDEN_ID_NAMES */ + @ResolveContext(PLUGIN_ID) public val id: String /** @@ -60,6 +63,7 @@ public interface PluginDescription { * * @see FORBIDDEN_ID_NAMES */ + @ResolveContext(PLUGIN_NAME) public val name: String /** @@ -88,7 +92,8 @@ public interface PluginDescription { * * @see Semver 语义化版本. 允许 [宽松][Semver.SemverType.LOOSE] 类型版本. */ - public val version: Semver + @ResolveContext(PLUGIN_VERSION) + public val version: SemVersion /** * 插件信息, 允许为空 diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/VersionRequirement.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/VersionRequirement.kt deleted file mode 100644 index b8ec88690..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/VersionRequirement.kt +++ /dev/null @@ -1,242 +0,0 @@ -package net.mamoe.mirai.console.plugin.description - -import com.vdurmont.semver4j.Requirement -import com.vdurmont.semver4j.Semver - -public sealed class VersionRequirement { - public abstract operator fun contains(version: Semver): Boolean - public fun contains(version: String): Boolean = contains(Semver(version, Semver.SemverType.LOOSE)) - - public class Exact - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - constructor( - version: Semver, - ) : VersionRequirement() { - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - public val version: Semver = version.toStrict() - - @Suppress("DEPRECATION_ERROR") - public constructor(version: String) : this(Semver(version, Semver.SemverType.LOOSE)) - - @Suppress("DEPRECATION_ERROR") - override fun contains(version: Semver): Boolean = this.version.isEquivalentTo(version.toStrict()) - } - - public data class MatchesNpmPattern( - val pattern: String, - ) : VersionRequirement() { - private val requirement = Requirement.buildNPM(pattern) - override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version.toStrict()) - } - - public data class MatchesIvyPattern( - val pattern: String, - ) : VersionRequirement() { - private val requirement = Requirement.buildIvy(pattern) - override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version.toStrict()) - } - - - public data class MatchesCocoapodsPattern( - val pattern: String, - ) : VersionRequirement() { - private val requirement = Requirement.buildCocoapods(pattern) - override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version.toStrict()) - } - - public abstract class Custom : VersionRequirement() - - @Suppress("MemberVisibilityCanBePrivate") - public class InRange( - begin: Semver, - public val beginInclusive: Boolean, - end: Semver, - public val endInclusive: Boolean, - ) : VersionRequirement() { - public val end: Semver = end.toStrict() - public val begin: Semver = begin.toStrict() - - public constructor( - begin: String, - beginInclusive: Boolean, - end: Semver, - endInclusive: Boolean, - ) : this(Semver(begin, Semver.SemverType.LOOSE), beginInclusive, end, endInclusive) - - public constructor( - begin: String, - beginInclusive: Boolean, - end: String, - endInclusive: Boolean, - ) : this(Semver(begin, Semver.SemverType.LOOSE), - beginInclusive, - Semver(end, Semver.SemverType.LOOSE), - endInclusive) - - public constructor( - begin: Semver, - beginInclusive: Boolean, - end: String, - endInclusive: Boolean, - ) : this(begin, beginInclusive, Semver(end, Semver.SemverType.LOOSE), endInclusive) - - override fun contains(version: Semver): Boolean { - val strict = version.toStrict() - return if (beginInclusive) { - strict.isGreaterThanOrEqualTo(begin) - } else { - strict.isGreaterThan(begin) - } && if (endInclusive) { - strict.isLowerThanOrEqualTo(end) - } else { - strict.isLowerThan(end) - } - } - - override fun toString(): String { - return buildString { - append(if (beginInclusive) "[" else "(") - append(begin) - append(",") - append(end) - append(if (endInclusive) "]" else ")") - } - } - } - - - @Suppress("unused", "DeprecatedCallableAddReplaceWith") - public class Builder { - @Suppress("DEPRECATION_ERROR") - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public fun exact(version: Semver): VersionRequirement = Exact(version) - - @ILoveKafuuChinoForever - public fun exact(version: String): VersionRequirement = Exact(version) - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public fun custom(checker: (version: Semver) -> Boolean): VersionRequirement { - return object : Custom() { - override fun contains(version: Semver): Boolean = checker(version) - } - } - - /** - * @see Semver.SemverType.NPM - */ - @ILoveKafuuChinoForever - public fun npmPattern(versionPattern: String): VersionRequirement { - return MatchesNpmPattern(versionPattern) - } - - /** - * @see Semver.SemverType.IVY - */ - @ILoveKafuuChinoForever - public fun ivyPattern(versionPattern: String): VersionRequirement { - return MatchesIvyPattern(versionPattern) - } - - /** - * @see Semver.SemverType.COCOAPODS - */ - @ILoveKafuuChinoForever - public fun cocoapodsPattern(versionPattern: String): VersionRequirement { - return MatchesCocoapodsPattern(versionPattern) - } - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public fun range( - begin: Semver, - beginInclusive: Boolean, - end: Semver, - endInclusive: Boolean, - ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public fun range( - begin: String, - beginInclusive: Boolean, - end: Semver, - endInclusive: Boolean, - ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public fun range( - begin: Semver, - beginInclusive: Boolean, - end: String, - endInclusive: Boolean, - ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) - - @ILoveKafuuChinoForever - public fun range( - begin: String, - beginInclusive: Boolean, - end: String, - endInclusive: Boolean, - ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) - - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public operator fun Semver.rangeTo(endInclusive: Semver): VersionRequirement { - return InRange(this, true, endInclusive, true) - } - - @ILoveKafuuChinoForever - public operator fun Semver.rangeTo(endInclusive: String): VersionRequirement { - return InRange(this, true, Semver(endInclusive, Semver.SemverType.LOOSE), true) - } - - @ILoveKafuuChinoForever - public operator fun String.rangeTo(endInclusive: String): VersionRequirement { - return InRange(Semver(this, Semver.SemverType.LOOSE), - true, - Semver(endInclusive, Semver.SemverType.LOOSE), - true) - } - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public operator fun String.rangeTo(endInclusive: Semver): VersionRequirement { - return InRange(Semver(this, Semver.SemverType.LOOSE), true, endInclusive, true) - } - - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public infix fun Semver.until(endExclusive: Semver): VersionRequirement { - return InRange(this, true, endExclusive, false) - } - - @ILoveKafuuChinoForever - public infix fun Semver.until(endExclusive: String): VersionRequirement { - return InRange(this, true, Semver(endExclusive, Semver.SemverType.LOOSE), false) - } - - @ILoveKafuuChinoForever - public infix fun String.until(endExclusive: String): VersionRequirement { - return InRange(Semver(this, Semver.SemverType.LOOSE), - true, - Semver(endExclusive, Semver.SemverType.LOOSE), - false) - } - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKafuuChinoForever - public infix fun String.until(endExclusive: Semver): VersionRequirement { - return InRange(Semver(this, Semver.SemverType.LOOSE), true, endExclusive, false) - } - - @Suppress("SpellCheckingInspection") - @Retention(AnnotationRetention.SOURCE) - @DslMarker - private annotation class ILoveKafuuChinoForever - } -} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt index 95dcfb1d0..9b64d84f9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.console.data.PluginConfig import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.internal.plugin.JvmPluginInternal import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.minutesToMillis import net.mamoe.mirai.utils.secondsToMillis @@ -37,7 +38,8 @@ public abstract class AbstractJvmPlugin @JvmOverloads constructor( public final override val loader: JvmPluginLoader get() = super<JvmPluginInternal>.loader - public final override fun permissionId(name: String): PermissionId = PermissionId(description.id, "command.$name") + public final override fun permissionId(name: String): PermissionId = + PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.PERMISSION_ID) /** * 重载 [PluginData] diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt index c76c3ee6a..d3aa7e7cb 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt @@ -18,6 +18,9 @@ package net.mamoe.mirai.console.plugin.jvm import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.console.compiler.common.RestrictedScope +import net.mamoe.mirai.console.compiler.common.RestrictedScope.Kind.COMMAND_REGISTER +import net.mamoe.mirai.console.compiler.common.RestrictedScope.Kind.PERMISSION_REGISTER import net.mamoe.mirai.console.extension.PluginComponentStorage import net.mamoe.mirai.console.permission.PermissionIdNamespace import net.mamoe.mirai.console.plugin.Plugin @@ -59,6 +62,7 @@ public interface JvmPlugin : Plugin, CoroutineScope, * * @receiver 组件容器 */ + @RestrictedScope(COMMAND_REGISTER, PERMISSION_REGISTER) public fun PluginComponentStorage.onLoad() {} /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt index 5517ace42..954d19919 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt @@ -11,10 +11,11 @@ package net.mamoe.mirai.console.plugin.jvm -import com.vdurmont.semver4j.Semver +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.* import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDescription -import net.mamoe.mirai.console.plugin.description.VersionRequirement +import net.mamoe.mirai.console.util.SemVersion /** * JVM 插件的描述. 通常作为 `plugin.yml` @@ -32,20 +33,21 @@ public interface JvmPluginDescription : PluginDescription { * 构建 [JvmPluginDescription] * @see JvmPluginDescriptionBuilder */ + @JvmName("create") @JvmSynthetic public operator fun invoke( /** * @see [PluginDescription.id] */ - id: String, + @ResolveContext(PLUGIN_ID) id: String, /** * @see [PluginDescription.version] */ - version: String, + @ResolveContext(PLUGIN_VERSION) version: String, /** * @see [PluginDescription.name] */ - name: String = id, + @ResolveContext(PLUGIN_NAME) name: String = id, block: JvmPluginDescriptionBuilder.() -> Unit = {}, ): JvmPluginDescription = JvmPluginDescriptionBuilder(id, version).apply { name(name) }.apply(block).build() @@ -53,22 +55,21 @@ public interface JvmPluginDescription : PluginDescription { * 构建 [JvmPluginDescription] * @see JvmPluginDescriptionBuilder */ - @Suppress("DEPRECATION_ERROR") - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) + @JvmName("create") @JvmSynthetic public operator fun invoke( /** * @see [PluginDescription.id] */ - id: String, + @ResolveContext(PLUGIN_ID) id: String, /** * @see [PluginDescription.version] */ - version: Semver, + @ResolveContext(PLUGIN_VERSION) version: SemVersion, /** * @see [PluginDescription.name] */ - name: String = id, + @ResolveContext(PLUGIN_NAME) name: String = id, block: JvmPluginDescriptionBuilder.() -> Unit = {}, ): JvmPluginDescription = JvmPluginDescriptionBuilder(id, version).apply { name(name) }.apply(block).build() } @@ -95,14 +96,14 @@ public interface JvmPluginDescription : PluginDescription { * * @see [JvmPluginDescription.invoke] */ -public class JvmPluginDescriptionBuilder -@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) -constructor( +public class JvmPluginDescriptionBuilder( private var id: String, - private var version: Semver, + private var version: SemVersion, ) { - @Suppress("DEPRECATION_ERROR") - public constructor(name: String, version: String) : this(name, Semver(version, Semver.SemverType.LOOSE)) + public constructor( + @ResolveContext(PLUGIN_ID) id: String, + @ResolveContext(PLUGIN_VERSION) version: String, + ) : this(id, SemVersion(version)) private var name: String = id private var author: String = "" @@ -110,19 +111,20 @@ constructor( private var dependencies: MutableSet<PluginDependency> = mutableSetOf() @ILoveKuriyamaMiraiForever - public fun name(value: String): JvmPluginDescriptionBuilder = apply { this.name = value.trim() } - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKuriyamaMiraiForever - public fun version(value: String): JvmPluginDescriptionBuilder = - apply { this.version = Semver(value, Semver.SemverType.LOOSE) } - - @Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR) - @ILoveKuriyamaMiraiForever - public fun version(value: Semver): JvmPluginDescriptionBuilder = apply { this.version = value } + public fun name(@ResolveContext(PLUGIN_NAME) value: String): JvmPluginDescriptionBuilder = + apply { this.name = value.trim() } @ILoveKuriyamaMiraiForever - public fun id(value: String): JvmPluginDescriptionBuilder = apply { this.id = value.trim() } + public fun version(@ResolveContext(PLUGIN_VERSION) value: String): JvmPluginDescriptionBuilder = + apply { this.version = SemVersion(value) } + + @ILoveKuriyamaMiraiForever + public fun version(@ResolveContext(PLUGIN_VERSION) value: SemVersion): JvmPluginDescriptionBuilder = + apply { this.version = value } + + @ILoveKuriyamaMiraiForever + public fun id(@ResolveContext(PLUGIN_ID) value: String): JvmPluginDescriptionBuilder = + apply { this.id = value.trim() } @ILoveKuriyamaMiraiForever public fun author(value: String): JvmPluginDescriptionBuilder = apply { this.author = value.trim() } @@ -151,9 +153,9 @@ constructor( */ @ILoveKuriyamaMiraiForever public fun dependsOn( - pluginId: String, + @ResolveContext(PLUGIN_ID) pluginId: String, isOptional: Boolean = false, - versionRequirement: VersionRequirement, + versionRequirement: SemVersion.Requirement, ): JvmPluginDescriptionBuilder = apply { this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional)) } @@ -165,8 +167,8 @@ constructor( */ @ILoveKuriyamaMiraiForever public fun dependsOn( - pluginId: String, - versionRequirement: VersionRequirement, + @ResolveContext(PLUGIN_ID) pluginId: String, + versionRequirement: SemVersion.Requirement, ): JvmPluginDescriptionBuilder = apply { this.dependencies.add(PluginDependency(pluginId, versionRequirement, false)) } @@ -178,46 +180,24 @@ constructor( */ @ILoveKuriyamaMiraiForever public fun dependsOn( - pluginId: String, + @ResolveContext(PLUGIN_ID) pluginId: String, isOptional: Boolean = false, ): JvmPluginDescriptionBuilder = apply { this.dependencies.add(PluginDependency(pluginId, null, isOptional)) } - /** - * 示例: - * - * ``` - * dependsOn("org.example.test-plugin") { "1.0.0".."1.2.0" } - * dependsOn("org.example.test-plugin") { npmPattern("1.x || >=2.5.0 || 5.0.0 - 7.2.3") } - * dependsOn("org.example.test-plugin") { ivyPattern("[1.0,2.0[") } - * dependsOn("org.example.test-plugin") { custom { it.toString() == "1.0.0" } } - * ``` - * - * @see PluginDependency - * @see VersionRequirement.Builder - */ - @ILoveKuriyamaMiraiForever - public fun dependsOn( - pluginId: String, - isOptional: Boolean = false, - versionRequirement: VersionRequirement.Builder.() -> VersionRequirement, - ): JvmPluginDescriptionBuilder = - apply { - this.dependencies.add(PluginDependency(pluginId, - VersionRequirement.Builder().run(versionRequirement), - isOptional)) - } - @Suppress("DEPRECATION_ERROR") public fun build(): JvmPluginDescription = SimpleJvmPluginDescription(name, version, id, author, info, dependencies) + /** + * 标注一个 [JvmPluginDescription] DSL + */ @Suppress("SpellCheckingInspection") - @Retention(AnnotationRetention.SOURCE) + @Retention(AnnotationRetention.BINARY) @DslMarker - private annotation class ILoveKuriyamaMiraiForever // https://zh.moegirl.org.cn/zh-cn/%E6%A0%97%E5%B1%B1%E6%9C%AA%E6%9D%A5 + internal annotation class ILoveKuriyamaMiraiForever // https://zh.moegirl.org.cn/zh-cn/%E6%A0%97%E5%B1%B1%E6%9C%AA%E6%9D%A5 } /** @@ -225,56 +205,26 @@ constructor( * * @see JvmPluginDescription */ -@Deprecated( - """ - 将在 1.0-RC 删除. 请使用 JvmPluginDescription. -""", - replaceWith = ReplaceWith( - "JvmPluginDescription", - "net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription" - ), - level = DeprecationLevel.ERROR -) -public data class SimpleJvmPluginDescription -@Deprecated( - """ - 构造器不稳定, 将在 1.0-RC 删除. 请使用 JvmPluginDescriptionBuilder. -""", - replaceWith = ReplaceWith( - "JvmPluginDescription(name, version) {}", - "net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription.Companion.invoke" - ), - level = DeprecationLevel.ERROR -) -@JvmOverloads public constructor( - public override val name: String, - public override val version: Semver, - public override val id: String = name, - public override val author: String = "", - public override val info: String = "", - public override val dependencies: Set<PluginDependency> = setOf(), +internal data class SimpleJvmPluginDescription +@JvmOverloads constructor( + override val name: String, + override val version: SemVersion, + override val id: String = name, + override val author: String = "", + override val info: String = "", + override val dependencies: Set<PluginDependency> = setOf(), ) : JvmPluginDescription { - @Deprecated( - """ - 构造器不稳定, 将在 1.0-RC 删除. 请使用 JvmPluginDescriptionBuilder. -""", - replaceWith = ReplaceWith( - "JvmPluginDescription.invoke(name, version) {}", - "net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription.Companion.invoke" - ), - level = DeprecationLevel.ERROR - ) @Suppress("DEPRECATION_ERROR") @JvmOverloads - public constructor( + constructor( name: String, version: String, id: String = name, author: String = "", info: String = "", dependencies: Set<PluginDependency> = setOf(), - ) : this(name, Semver(version, Semver.SemverType.LOOSE), id, author, info, dependencies) + ) : this(name, SemVersion(version), id, author, info, dependencies) init { require(!name.contains(':')) { "':' is forbidden in plugin name" } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginLoader.kt index 66986ee9e..30b4e7f34 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginLoader.kt @@ -15,7 +15,7 @@ import net.mamoe.mirai.console.internal.plugin.BuiltInJvmPluginLoaderImpl import net.mamoe.mirai.console.plugin.loader.FilePluginLoader /** - * 内建的 Jar (JVM) 插件加载器 + * JVM 插件加载器 */ public interface JvmPluginLoader : CoroutineScope, FilePluginLoader<JvmPlugin, JvmPluginDescription> { /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt index 04cae8515..280ce144b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt @@ -61,10 +61,9 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> { * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如无法读取插件信息等). * * @see PluginDescription 插件描述 - * @see getPluginDescription 无 receiver, 接受参数的版本. */ @Throws(PluginLoadException::class) - public fun getPluginDescription(plugin: P): D // Java signature: `public D getDescription(P)` + public fun getPluginDescription(plugin: P): D /** * 主动加载一个插件 (实例), 但不 [启用][enable] 它. 返回加载成功的主类实例 diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt index d10324141..92a82a616 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt @@ -96,7 +96,7 @@ import kotlin.internal.LowPriorityInOverloadResolution */ public interface MessageScope { /** - * 如果此 [MessageScope], 仅包含一个消息对象, 则 [realTarget] 指向这个对象. + * 如果此 [MessageScope] 仅包含一个消息对象, 则 [realTarget] 指向这个对象. 否则 [realTarget] 为 `null`. * * 对于 [CommandSender] 作为 [MessageScope], [realTarget] 总是指令执行者 [User], 即 [CommandSender.user] * @@ -116,7 +116,6 @@ public interface MessageScope { /** * 立刻以此发送消息给所有在此 [MessageScope] 下的消息对象 */ - @JvmDefault @JvmBlockingBridge public suspend fun sendMessage(message: String) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/SemVersion.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/SemVersion.kt new file mode 100644 index 000000000..a80c467a2 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/SemVersion.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + * + */ + +/* + * @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp> + */ + +@file:Suppress("unused") + +package net.mamoe.mirai.console.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.compiler.common.ResolveContext +import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_VERSION +import net.mamoe.mirai.console.internal.data.map +import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal +import net.mamoe.mirai.console.util.SemVersion.Companion.equals +import net.mamoe.mirai.console.util.SemVersion.Requirement + +/** + * [语义化版本](https://semver.org/lang/zh-CN/) 支持 + * + * 解析示例: + * + * `1.0.0-M4+c25733b8` 将会解析出下面的内容, + * [major] (主本号), [minor] (次版本号), [patch] (修订号), [identifier] (先行版本号) 和 [metadata] (元数据). + * ``` + * SemVersion( + * major = 1, + * minor = 0, + * patch = 0, + * identifier = "M4" + * metadata = "c25733b8" + * ) + * ``` + * 其中 identifier 和 metadata 都是可选的. + * + * 对于核心版本号, 此实现稍微比 semver 宽松一些, 允许 x.y 的存在. + * + * @see Requirement + * @see SemVersion.invoke + */ +@Serializable(with = SemVersion.SemVersionAsStringSerializer::class) +public data class SemVersion +/** + * @see SemVersion.invoke 字符串解析 + */ +internal constructor( + /** 主版本号 */ + public val major: Int, + /** 次版本号 */ + public val minor: Int, + /** 修订号 */ + public val patch: Int?, + /** 先行版本号识别符 */ + public val identifier: String? = null, + /** 版本号元数据, 不参与版本号对比([compareTo]), 但是参与版本号严格对比([equals]) */ + public val metadata: String? = null, +) : Comparable<SemVersion> { + /** + * 一条依赖规则 + * @see [parseRangeRequirement] + */ + public interface Requirement { + /** 在 [version] 满足此要求时返回 true */ + public fun test(version: SemVersion): Boolean + } + + public object SemVersionAsStringSerializer : KSerializer<SemVersion> by String.serializer().map( + serializer = { it.toString() }, + deserializer = { SemVersion(it) } + ) + + public companion object { + /** + * 解析一个版本号, 将会返回一个 [SemVersion], + * 如果发生解析错误将会抛出一个 [IllegalArgumentException] 或者 [NumberFormatException] + * + * 对于版本号的组成, 有以下规定: + * - 必须包含主版本号和次版本号 + * - 存在 先行版本号 的时候 先行版本号 不能为空 + * - 存在 元数据 的时候 元数据 不能为空 + * - 核心版本号只允许 `x.y` 和 `x.y.z` 的存在 + * - `1.0-RC` 是合法的 + * - `1.0.0-RC` 也是合法的, 与 `1.0-RC` 一样 + * - `1.0.0.0-RC` 是不合法的, 将会抛出一个 [IllegalArgumentException] + * + * 注意情况: + * - 第一个 `+` 之后的所有内容全部识别为元数据 + * - `1.0+METADATA-M4`, metadata="METADATA-M4" + * - 如果不确定版本号是否合法, 可以使用 [regex101.com](https://regex101.com/r/vkijKf/1/) 进行检查 + * - 此实现使用的正则表达式为 `^(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-]+)*))?$` + */ + @Throws(IllegalArgumentException::class, NumberFormatException::class) + @JvmStatic + @JvmName("parse") + public operator fun invoke(@ResolveContext(PLUGIN_VERSION) version: String): SemVersion = SemVersionInternal.parse(version) + + /** + * 解析一条依赖需求描述, 在无法解析的时候抛出 [IllegalArgumentException] + * + * 对于一条规则, 有以下方式可选 + * + * - `1.0.0-M4` 要求 1.0.0-M4 版本, 且只能是 1.0.0-M4 版本 + * - `1.x` 要求 1.x 版本 + * - `> 1.0.0-RC` 要求 1.0.0-RC 之后的版本, 不能是 1.0.0-RC + * - `>= 1.0.0-RC` 要求 1.0.0-RC 或之后的版本, 可以是 1.0.0-RC + * - `< 1.0.0-RC` 要求 1.0.0-RC 之前的版本, 不能是 1.0.0-RC + * - `<= 1.0.0-RC` 要求 1.0.0-RC 或之前的版本, 可以是 1.0.0-RC + * - `!= 1.0.0-RC` 要求 除了1.0.0-RC 的任何版本 + * - `[1.0.0, 1.2.0]` + * - `(1.0.0, 1.2.0]` + * - `[1.0.0, 1.2.0)` + * - `(1.0.0, 1.2.0)` [数学区间](https://baike.baidu.com/item/%E5%8C%BA%E9%97%B4/1273117) + * + * 对于多个规则, 允许使用逻辑符号 `{}`, `||`, `&&` + * 例如: + * - `1.x || 2.x || 3.0.0` + * - `<= 0.5.3 || >= 1.0.0` + * - `{> 1.0 && < 1.5} || {> 1.8}` + * - `{> 1.0 && < 1.5} || {> 1.8}` + * - `> 1.0.0 && != 1.2.0` + * + * 特别注意: + * - 依赖规则版本号不需要携带版本号元数据, 元数据不参与依赖需求的检查 + * - 如果目标版本号携带有先行版本号, 请不要忘记先行版本号 + * - 因为 `()` 已经用于数学区间, 使用 `{}` 替代 `()` + */ + @Throws(IllegalArgumentException::class) + @JvmStatic + public fun parseRangeRequirement(requirement: String): Requirement = + SemVersionInternal.parseRangeRequirement(requirement) + + /** @see [Requirement.test] */ + @JvmStatic + public fun Requirement.test(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(invoke(version)) + + /** + * 当满足 [requirement] 时返回 true, 否则返回 false + */ + @JvmStatic + public fun SemVersion.satisfies(requirement: Requirement): Boolean = requirement.test(this) + + /** for Kotlin only */ + @JvmStatic + @JvmSynthetic + public operator fun Requirement.contains(version: SemVersion): Boolean = test(version) + + /** for Kotlin only */ + @JvmStatic + @JvmSynthetic + public operator fun Requirement.contains(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(version) + } + + @Transient + private val toString: String by lazy(LazyThreadSafetyMode.NONE) { + buildString { + append(major) + append('.').append(minor) + patch?.let { append('.').append(it) } + identifier?.let { identifier -> + append('-').append(identifier) + } + metadata?.let { metadata -> + append('+').append(metadata) + } + } + } + + override fun toString(): String = toString + + /** + * 将 [SemVersion] 转为 Kotlin data class 风格的 [String] + */ + public fun toStructuredString(): String { + return "SemVersion(major=$major, minor=$minor, patch=$patch, identifier=$identifier, metadata=$metadata)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SemVersion + + return compareTo(other) == 0 && other.identifier == identifier && other.metadata == metadata + } + + override fun hashCode(): Int { + var result = major shl minor + result *= (patch ?: 1) + result = 31 * result + (identifier?.hashCode() ?: 0) + result = 31 * result + (metadata?.hashCode() ?: 0) + return result + } + + /** + * Compares this object with the specified object for order. Returns zero if this object is equal + * to the specified [other] object, a negative number if it's less than [other], or a positive number + * if it's greater than [other]. + */ + public override operator fun compareTo(other: SemVersion): Int { + return SemVersionInternal.run { compareInternal(this@SemVersion, other) } + } +} diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt index 8afa90d9a..969f03425 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt @@ -9,7 +9,6 @@ package net.mamoe.mirai.console -import com.vdurmont.semver4j.Semver import kotlinx.coroutines.* import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.command.CommandManager @@ -20,6 +19,7 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleInput import net.mamoe.mirai.console.util.ConsoleInternalApi +import net.mamoe.mirai.console.util.SemVersion import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.LoginSolver @@ -43,8 +43,8 @@ fun initTestEnvironment() { get() = "Test" override val vendor: String get() = "Test" - override val version: Semver - get() = Semver("1.0.0") + override val version: SemVersion + get() = SemVersion("1.0.0") } override val builtInPluginLoaders: List<Lazy<PluginLoader<*, *>>> = listOf(lazy { JvmPluginLoader }) diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt index 3db1b0a52..122475bed 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt @@ -79,7 +79,7 @@ internal class TestCommand { assertEquals(1, ConsoleCommandOwner.registeredCommands.size) - assertEquals(1, CommandManagerImpl.registeredCommands.size) + assertEquals(1, CommandManagerImpl._registeredCommands.size) assertEquals(2, CommandManagerImpl.requiredPrefixCommandMap.size) } finally { TestCompositeCommand.unregister() diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt index 22c052765..e50cb7e11 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt @@ -17,7 +17,7 @@ import kotlin.test.assertSame @OptIn(ConsoleInternalApi::class) internal class PluginDataTest { - class MyPluginData : AutoSavePluginData() { + class MyPluginData : AutoSavePluginData("test") { var int by value(1) val map: MutableMap<String, String> by value() val map2: MutableMap<String, MutableMap<String, String>> by value() diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/util/TestSemVersion.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/util/TestSemVersion.kt new file mode 100644 index 000000000..3f74dd191 --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/util/TestSemVersion.kt @@ -0,0 +1,225 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + * + */ + +/* + * @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp> + */ + +package net.mamoe.mirai.console.util + +import net.mamoe.mirai.console.util.SemVersion.Companion.test +import org.junit.jupiter.api.Test + +internal class TestSemVersion { + @Test + internal fun testCompare() { + fun String.sem(): SemVersion = SemVersion.invoke(this) + assert("1.0".sem() < "1.0.1".sem()) + assert("1.0.0".sem() == "1.0".sem()) + assert("1.1".sem() > "1.0.0".sem()) + assert("1.0-M4".sem() < "1.0-M5".sem()) + assert("1.0-M5-dev-7".sem() < "1.0-M5-dev-15".sem()) + assert("1.0-M5-dev-79".sem() < "1.0-M5-dev-7001".sem()) + assert("1.0-M6".sem() > "1.0-M5-dev-15".sem()) + assert("1.0-RC".sem() > "1.0-M5-dev-15".sem()) + assert("1.0-RC2".sem() > "1.0-RC".sem()) + // example on semver + // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 + assert("1.0.0-alpha".sem() < "1.0.0-alpha.1".sem()) + assert("1.0.0-alpha.1".sem() < "1.0.0-alpha.beta".sem()) + assert("1.0.0-alpha.beta".sem() < "1.0.0-beta".sem()) + assert("1.0.0-beta".sem() < "1.0.0-beta.2".sem()) + assert("1.0.0-beta.2".sem() < "1.0.0-beta.11".sem()) + assert("1.0.0-beta.11".sem() < "1.0.0-rc.1".sem()) + assert("1.0.0-rc.1".sem() < "1.0.0".sem()) + } + + @Test + internal fun testRequirement() { + fun SemVersion.Requirement.assert(version: String): SemVersion.Requirement { + assert(test(version)) { version } + return this + } + + fun assertInvalid(requirement: String) { + kotlin.runCatching { + SemVersion.parseRangeRequirement(requirement) + }.onSuccess { assert(false) { requirement } } + } + + fun SemVersion.Requirement.assertFalse(version: String): SemVersion.Requirement { + assert(!test(version)) { version } + return this + } + SemVersion.parseRangeRequirement("1.0") + .assert("1.0").assert("1.0.0") + .assertFalse("1.1.0").assertFalse("2.0.0") + SemVersion.parseRangeRequirement("1.x") + .assert("1.0").assert("1.1") + .assert("1.5").assert("1.14514") + .assertFalse("2.33") + SemVersion.parseRangeRequirement("2.0||1.2.x") + SemVersion.parseRangeRequirement("{2.0||1.2.x} && 1.1.0 &&1.2.3") + SemVersion.parseRangeRequirement("2.0 || 1.2.x") + .assert("2.0").assert("2.0.0") + .assertFalse("2.1") + .assert("1.2.5").assert("1.2.0").assertFalse("1.2") + .assertFalse("1.0.0") + SemVersion.parseRangeRequirement("[1.0.0, 19190.0]") + .assert("1.0.0").assertFalse("0.1.0") + .assert("19190.0").assertFalse("19198.10") + SemVersion.parseRangeRequirement("[1.0.0, 2.0.0)") + .assert("1.0.0").assert("1.2.3").assertFalse("2.0.0") + SemVersion.parseRangeRequirement("(2.0.0, 1.0.0]") + .assert("1.0.0").assert("1.2.3").assertFalse("2.0.0") + SemVersion.parseRangeRequirement("(2.0.0, 1.0.0)") + .assertFalse("1.0.0").assert("1.2.3").assertFalse("2.0.0") + SemVersion.parseRangeRequirement("(1.0.0, 2.0.0)") + .assertFalse("1.0.0").assert("1.2.3").assertFalse("2.0.0") + SemVersion.parseRangeRequirement(" >= 1.0.0") + .assert("1.0.0") + .assert("114.514.1919") + .assertFalse("0.0.0") + .assertFalse("0.98774587") + SemVersion.parseRangeRequirement("> 1.0.0") + .assertFalse("1.0.0") + SemVersion.parseRangeRequirement("!= 1.0.0 && != 2.0.0") + .assert("1.2.3").assert("2.1.1") + .assertFalse("1.0").assertFalse("1.0.0") + .assertFalse("2.0").assertFalse("2.0.0") + .assert("2.0.1").assert("1.0.1") + + SemVersion.parseRangeRequirement("> 1.0.0 || < 0.9.0") + .assertFalse("1.0.0") + .assert("0.8.0") + .assertFalse("0.9.0") + SemVersion.parseRangeRequirement("{>= 1.0.0 && <= 1.2.3} || {>= 2.0.0 && <= 2.2.3}") + .assertFalse("1.3.0") + .assert("1.0.0").assert("1.2.3") + .assertFalse("0.9.0") + .assert("2.0.0").assert("2.2.3").assertFalse("2.3.4") + + assertInvalid("WPOXAXW") + assertInvalid("1.0.0 || 1.0.0 && 1.0.0") + assertInvalid("{") + assertInvalid("}") + assertInvalid("") + assertInvalid("1.2.3 - 3.2.1") + assertInvalid("1.5.78 &&") + assertInvalid("|| 1.0.0") + } + + private fun String.check() { + val sem = SemVersion.invoke(this) + assert(this == sem.toString()) { "$this != $sem" } + } + + private fun String.checkInvalid() { + kotlin.runCatching { SemVersion.invoke(this) } + .onSuccess { assert(false) { "$this not a invalid sem-version" } } + } + + @Test + internal fun testSemVersionParsing() { + "0.0".check() + "1.0.0".check() + "1.2.3.4.5.6.7.8".checkInvalid() + "5555.0-A".check() + "5555.0-A+METADATA".check() + "5555.0+METADATA".check() + "987.0+wwwxx-wk".check() + "NOT.NUMBER".checkInvalid() + "0".checkInvalid() + "".checkInvalid() + "1.".checkInvalid() + "0.1-".checkInvalid() + "1.9+".checkInvalid() + "5.1+68-7".check() + "5.1+68-".check() + } + + @Test + internal fun testSemVersionOfficial() { + """ + 1.0-RC + 0.0.4 + 1.2.3 + 10.20.30 + 1.1.2-prerelease+meta + 1.1.2+meta + 1.1.2+meta-valid + 1.0.0-alpha + 1.0.0-beta + 1.0.0-alpha.beta + 1.0.0-alpha.beta.1 + 1.0.0-alpha.1 + 1.0.0-alpha0.valid + 1.0.0-alpha.0valid + 1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay + 1.0.0-rc.1+build.1 + 2.0.0-rc.1+build.123 + 1.2.3-beta + 10.2.3-DEV-SNAPSHOT + 1.2.3-SNAPSHOT-123 + 1.0.0 + 2.0.0 + 1.1.7 + 2.0.0+build.1848 + 2.0.1-alpha.1227 + 1.0.0-alpha+beta + 1.2.3----RC-SNAPSHOT.12.9.1--.12+788 + 1.2.3----R-S.12.9.1--.12+meta + 1.2.3----RC-SNAPSHOT.12.9.1--.12 + 1.0.0+0.build.1-rc.10000aaa-kk-0.1 + 1.0.0-0A.is.legal + """.trimIndent().split('\n').asSequence() + .filter { it.isNotBlank() }.map { it.trim() }.forEach { it.check() } + """ + 1 + 1.2.3-0123 + 1.2.3-0123.0123 + 1.1.2+.123 + +invalid + -invalid + -invalid+invalid + -invalid.01 + alpha + alpha.beta + alpha.beta.1 + alpha.1 + alpha+beta + alpha_beta + alpha. + alpha.. + beta + 1.0.0-alpha_beta + -alpha. + 1.0.0-alpha.. + 1.0.0-alpha..1 + 1.0.0-alpha...1 + 1.0.0-alpha....1 + 1.0.0-alpha.....1 + 1.0.0-alpha......1 + 1.0.0-alpha.......1 + 01.1.1 + 1.01.1 + 1.1.01 + 1.2.3.DEV + 1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788 + 1.2.31.2.3-RC + -1.0.3-gamma+b7718 + +justmeta + 9.8.7+meta+meta + 9.8.7-whatever+meta+meta + 99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12 + """.trimIndent().split('\n').asSequence() + .filter { it.isNotBlank() }.map { it.trim() }.forEach { it.checkInvalid() } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 4d51ad6de..ca24e980d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,12 @@ @file:Suppress("UnstableApiUsage") plugins { - id("com.jfrog.bintray") version Versions.bintray apply false - id("net.mamoe.kotlin-jvm-blocking-bridge") version Versions.blockingBridge apply false kotlin("jvm") version Versions.kotlinCompiler kotlin("plugin.serialization") version Versions.kotlinCompiler + id("com.jfrog.bintray") version Versions.bintray apply false + id("net.mamoe.kotlin-jvm-blocking-bridge") version Versions.blockingBridge apply false + id("com.gradle.plugin-publish") version "0.12.0" apply false + //id("com.bmuschko.nexus") version "2.3.1" apply false + //id("io.codearte.nexus-staging") version "0.11.0" apply false } tasks.withType(JavaCompile::class.java) { diff --git a/buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt b/buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt index 58ef99ac1..8dbd47d8a 100644 --- a/buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt +++ b/buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt @@ -36,8 +36,8 @@ class MiraiConsoleBuildPlugin : Plugin<Project> { attributes( "Manifest-Version" to "1", "Implementation-Vendor" to "Mamoe Technologies", - "Implementation-Title" to target.name.toString(), - "Implementation-Version" to target.version.toString() + "-" + gitVersion + "Implementation-Title" to target.name, + "Implementation-Version" to target.version.toString() //+ "+" + gitVersion ) } @Suppress("UNCHECKED_CAST") @@ -120,15 +120,16 @@ fun Project.findLatestFile(): Pair<String, File> { } ?: error("cannot find any file to upload")*/ } +/* val gitVersion: String by lazy { runCatching { val exec = Runtime.getRuntime().exec("git rev-parse HEAD") exec.waitFor() exec.inputStream.readBytes().toString(Charsets.UTF_8).trim().also { println("Git commit id: $it") - } - }.onFailure { + } }.onFailure { it.printStackTrace() return@lazy "UNKNOWN" }.getOrThrow() } +*/ \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/PublishingHelpers.kt b/buildSrc/src/main/kotlin/PublishingHelpers.kt index a6af10e47..95e16283a 100644 --- a/buildSrc/src/main/kotlin/PublishingHelpers.kt +++ b/buildSrc/src/main/kotlin/PublishingHelpers.kt @@ -7,6 +7,9 @@ import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.bundling.Jar import org.gradle.kotlin.dsl.* import upload.Bintray +import java.io.InputStream +import java.io.OutputStream +import java.security.MessageDigest import java.util.* import kotlin.reflect.KProperty @@ -51,10 +54,48 @@ internal fun org.gradle.api.Project.`publishing`(configure: org.gradle.api.publi (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("publishing", configure) +fun InputStream.md5(): ByteArray { + val digest = MessageDigest.getInstance("md5") + digest.reset() + use { input -> + object : OutputStream() { + override fun write(b: Int) { + digest.update(b.toByte()) + } + }.use { output -> + input.copyTo(output) + } + } + return digest.digest() +} + +@OptIn(ExperimentalUnsignedTypes::class) +@JvmOverloads +fun ByteArray.toUHexString( + separator: String = " ", + offset: Int = 0, + length: Int = this.size - offset +): String { + if (length == 0) { + return "" + } + val lastIndex = offset + length + return buildString(length * 2) { + this@toUHexString.forEachIndexed { index, it -> + if (index in offset until lastIndex) { + var ret = it.toUByte().toString(16).toUpperCase() + if (ret.length == 1) ret = "0$ret" + append(ret) + if (index < lastIndex - 1) append(separator) + } + } + } +} + inline fun Project.setupPublishing( artifactId: String, bintrayRepo: String = "mirai", - bintrayPkgName: String = "mirai-console", + bintrayPkgName: String = artifactId, vcs: String = "https://github.com/mamoe/mirai-console" ) { diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 859bb16b2..d5f4b7ac6 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -8,11 +8,10 @@ */ object Versions { - const val core = "1.2.3" - const val console = "1.0-M4" + const val core = "1.3.0" + const val console = "1.0-RC-dev-30" const val consoleGraphical = "0.0.7" - const val consoleTerminal = "0.1.0" - const val consolePure = console + const val consoleTerminal = console const val kotlinCompiler = "1.4.10" const val kotlinStdlib = kotlinCompiler @@ -27,6 +26,6 @@ object Versions { const val bintray = "1.8.5" - const val blockingBridge = "1.0.5" - const val yamlkt = "0.5.2" + const val blockingBridge = "1.1.0" + const val yamlkt = "0.5.3" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dependencyExtensions.kt b/buildSrc/src/main/kotlin/dependencyExtensions.kt index c7fc138a5..76b6fd9b5 100644 --- a/buildSrc/src/main/kotlin/dependencyExtensions.kt +++ b/buildSrc/src/main/kotlin/dependencyExtensions.kt @@ -7,8 +7,10 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.kotlin.dsl.DependencyHandlerScope +import org.gradle.kotlin.dsl.accessors.runtime.addDependencyTo @Suppress("unused") fun DependencyHandlerScope.kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version" @@ -17,7 +19,42 @@ fun DependencyHandlerScope.kotlinx(id: String, version: String) = "org.jetbrains fun DependencyHandlerScope.ktor(id: String, version: String = Versions.ktor) = "io.ktor:ktor-$id:$version" @Suppress("unused") -fun DependencyHandler.compileAndRuntime(any: Any) { +fun DependencyHandler.compileAndTestRuntime(any: Any) { add("compileOnly", any) - add("runtimeOnly", any) + add("testRuntimeOnly", any) +} + +fun DependencyHandler.smartApi( + dependencyNotation: String +): ExternalModuleDependency { + return smart("api", dependencyNotation) +} + +fun DependencyHandler.smartImplementation( + dependencyNotation: String +): ExternalModuleDependency { + return smart("implementation", dependencyNotation) +} + +private fun DependencyHandler.smart( + configuration: String, + dependencyNotation: String +): ExternalModuleDependency { + return addDependencyTo( + this, configuration, dependencyNotation + ) { + fun exclude(group: String, module: String) { + exclude(mapOf( + "group" to group, + "module" to module + )) + } + exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") + exclude("org.jetbrains.kotlin", "kotlin-stdlib") + exclude("org.jetbrains.kotlin", "kotlin-stdlib-common") + exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-common") + exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core") + exclude("org.jetbrains.kotlinx", "kotlinx-serialization-common") + exclude("org.jetbrains.kotlinx", "kotlinx-serialization-core") + } } diff --git a/docs/Appendix.md b/docs/Appendix.md new file mode 100644 index 000000000..b8fd0b047 --- /dev/null +++ b/docs/Appendix.md @@ -0,0 +1,49 @@ +# Mirai Console - Appendix + +### Mirai Console 演进 + +Mirai Console 是不断前进的框架,将来必定会发生 API 弃用和重构。 +维护者会严谨地推进每一项修改,并提供迁移周期(至少 2 个次版本)。 + +#### 版本规范 + +Mirai Console 的版本号遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/#spec-item-9) 规范。 + +在日常开发中, Mirai Console 会以 `-dev-1`,`-dev-2` 等版本后缀发布开发预览版本。这些版本仅用于兼容性测试等目的,无稳定性保证。 + +在大版本开发过程中,Mirai Console 会以 `-M1`, `-M2` 等版本后缀发布里程碑预览版本。代表一系列功能的完成,但还不稳定。 +这些版本里新增的 API 仍可能还会在下一个 Milestone 版本变化,因此请按需使用。 + +在大版本即将发布前,Mirai Console 会以 `-RC` 版本后缀发布最终的预览版本。 +`RC` 表示新版本 API 已经确定,离稳定版发布只差最后的一些内部优化或 bug 修复。 + +#### 版本选择 + +**稳定性**:稳定 (`x.y.z`) > 发布预览 (`-RC`) > 里程碑预览 (`-M`) > 开发 (`-dev`)。 + +| 目的 | 推荐至少更新到版本 | +|:--------------------------:|:--------------:| +| 生产环境 | `x.y.z` | +| 希望尽早体验稳定新特性的插件作者 | `-RC` | +| 无论如何都想体验新特性的插件作者 | `-M` | +| 前端实现者, 底层插件作者 | `-M` | +| 为 Mirai Console 提交 PR | `-dev` | + +其中,‘底层插件’ 表示提供扩展等的插件。如权限系统,其他语言插件加载器等。 + +#### 更新兼容性 + +对于 `x.y.z` 版本号: +- 当 `z` 增加时,只会有 bug 修复,和必要的新函数添加(为了解决某一个问题),不会有破坏性变化。 +- 当 `y` 增加时,可能有新 API 的引入,和旧 API 的弃用。但这些弃用会经过一个弃用周期后才被删除(隐藏)。向下兼容得到保证。 +- 当 `x` 增加时,任何 API 都可能会有变化。无兼容性保证。 + +#### 弃用周期 + +一个计划被删除的 API,将会在下一个次版本开始经历弃用周期。 + +如一个 API 在 `1.1.0` 起被弃用,它首先会是 `WARNING` (使用时会得到一个编译警告)弃用级别。 +在 `1.2.0` 上升为 `ERROR`(使用时会得到一个编译错误); +在 `1.3.0` 上升为 `HIDDEN`(使用者无法看到这些 API)。 + +`HIDDEN` 的 API 仍然会保留在代码中并正常编译,以提供二进制兼容性,直到下一个主版本更新。 diff --git a/docs/ConfiguringProjects.md b/docs/ConfiguringProjects.md new file mode 100644 index 000000000..3fa2db93f --- /dev/null +++ b/docs/ConfiguringProjects.md @@ -0,0 +1,80 @@ +# Mirai Console - Configuring Projects + +配置 Mirai Console 项目。 + +## 模块说明 + +console 由后端和前端一起工作. 使用时必须选择一个前端. + +- `mirai-console`: Mirai Console 后端。 + +- `mirai-console-terminal`: 终端前端,适用于 JVM。 +- [`MiraiAndroid`](https://github.com/mzdluo123/MiraiAndroid): Android 应用前端。 + +**注意:`mirai-console` 1.0-RC 发布之前, 前端请使用 `mirai-console-pure` 而不是 `mirai-console-terminal`** + +## 选择版本 + +有关各类版本的区别,参考 [版本规范](Appendix.md#版本规范) + +[Version]: https://api.bintray.com/packages/him188moe/mirai/mirai-console/images/download.svg? +[Bintray Download]: https://bintray.com/him188moe/mirai/mirai-console/ + +| 版本类型 | 版本号 | +|:------:|:------------:| +| 稳定 | - | +| 预览 | 1.0-M4 | +| 开发 | [![Version]][Bintray Download] | + +## 配置项目 + +### 使用模板项目 + +Mirai 鼓励插件开发者将自己的作品开源,并为此提供了模板项目。 + +注意,模板项目依赖的 Mirai Console 不一定是最新的。请检查 + +1. 访问 [mirai-console-plugin-template](https://github.com/project-mirai/mirai-console-plugin-template) +2. 点击绿色按钮 "Use this template",创建项目 +3. 克隆项目,检查并修改生成的属性 + +### 使用 Gradle 插件配置项目 + +`VERSION`: [选择版本](#选择版本) + +若使用 `build.gradle.kts`: +```kotlin +plugins { + id("net.mamoe.mirai-console") version "VERSION" +} +``` + +若使用 `build.gradle`: +```groovy +plugins { + id 'net.mamoe.mirai-console' version 'VERSION' +} +``` + +完成。Mirai Console Gradle 插件会为你配置依赖等所有编译环境。 + +### 手动配置项目 + +添加依赖: +`build.gradle.kts`: +```kotlin +repositories { + jcenter() +} + +dependencies { + compileOnly("net.mamoe:mirai-core:$CORE_VERSION") // mirai-core 的 API + compileOnly("net.mamoe:mirai-console:$CONSOLE_VERSION") // 后端 + + testImplementation("net.mamoe:mirai-console-terminal:$CONSOLE_VERSION") // 前端, 用于启动测试 + testImplementation("net.mamoe:mirai-console-terminal:$CONSOLE_VERSION") // 前端, 用于启动测试 +} +``` + +之后还需要配置 Kotlin `jvm-default` 编译参数,Kotlin 和 Java 的编译目标等。 +在打包插件时必须将依赖一并打包进插件 JAR,且排除 `mirai-core`,`mirai-console` 和它们的间接依赖,否则插件不会被加载。 \ No newline at end of file diff --git a/docs/Contributing.md b/docs/Contributing.md new file mode 100644 index 000000000..f4e4befbb --- /dev/null +++ b/docs/Contributing.md @@ -0,0 +1,32 @@ +# Mirai Console - Contributing + +感谢你来到这里,感谢你对 Mirai Console 做的一切贡献。 + +## 开发 Mirai Console + +### 模块 + +Mirai Console 项目由四个模块组成:后端,前端,Gradle 插件,Intellij 插件。 + +``` +/ +|--- backend 后端 +| |--- codegen 后端代码生成工具 +| `--- mirai-console 后端主模块, 发布为 net.mamoe:mirai-console +|--- buildSrc 项目构建 +|--- frontend 前端 +| `--- mirai-console-terminal 终端前端,发布为 net.mamoe:mirai-console-terminal +`--- tools 开发工具 + |--- compiler-common 编译器通用模块 + |--- gradle-plugin Gradle 插件,发布为 net.mamoe.mirai-console + `--- intellij-plugin IntelliJ 平台 IDE 插件,发布为 Mirai Console +``` + +请前往各模块内的 README.md 查看详细说明。 + +### 构建 +```shell script +gradlew build +``` + +首次加载和构建 mirai-console 项目可能要花费数小时时间。 \ No newline at end of file diff --git a/docs/Permissions.md b/docs/Permissions.md index 3dfb66204..6ba743f00 100644 --- a/docs/Permissions.md +++ b/docs/Permissions.md @@ -108,6 +108,28 @@ interface PermitteeId { 在 [`AbstractPermitteeId`] 查看其子类。 +#### 字符串表示 + +当使用 `PermitteeId.asString` 时, 不同的类型的返回值如下表所示. 这些格式也适用于 [权限指令](#使用内置权限服务指令). +(不区分大小写. 不区分 Bot). + +| 被许可人类型 | 字符串表示示例 | 备注 | +|:----------------:|:-----------:|:-------------------------------------| +| 控制台 | console | | +| 精确群 | g123456 | 表示群, 而不表示群成员 | +| 精确好友 | f123456 | 必须通过好友消息 | +| 精确临时会话 | t123456.789 | 群 123456 内的成员 789. 必须通过临时会话 | +| 精确群成员 | m123456.789 | 群 123456 内的成员 789. 同时包含临时会话. | +| 精确用户 | u123456 | 同时包含群成员, 好友, 临时会话 | +| 任意群 | g* | | +| 任意群的任意群员 | m* | | +| 精确群的任意群员 | m123456.* | 群 123456 内的任意成员. 同时包含临时会话. | +| 任意群的任意临时会话 | t* | 必须通过临时会话 | +| 精确群的任意临时会话 | t123456.* | 群 123456 内的任意成员. 必须通过临时会话 | +| 任意好友 | f* | | +| 任意用户 | u* | 任何人在任何环境 | +| 任意对象 | * | 即任何人, 任何群, 控制台 | + ### 获取被许可人 通常使用 `CommandSender.permitteeId`。 @@ -138,4 +160,18 @@ fun Permission.testPermission(PermitteeId): Boolean 如果希望手动注册一个其他用途的权限,使用 `PermissionService.register`。 -**注意**:权限只能在插件 [启用](Plugins.md#启用) 之后才能注册。否则会得到一个异常。 \ No newline at end of file +**注意**:权限只能在插件 [启用](Plugins.md#启用) 之后才能注册。否则会得到一个异常。 + +### 使用内置权限服务指令 + +**根指令**: "/permission", "/perm", "/权限" + +``` +/permission cancel <被许可人 ID> <权限 ID> 取消授权一个权限 +/permission cancelall <被许可人 ID> <权限 ID> 取消授权一个权限及其所有子权限 +/permission listpermissions 查看所有权限列表 +/permission permit <被许可人 ID> <权限 ID> 授权一个权限 +/permission permittedpermissions <被许可人 ID> 查看被授权权限列表 +``` + +其中, 被许可人 ID 使用 [字符串表示](#字符串表示), 权限 ID 参见 [权限 ID](#权限-id) diff --git a/docs/Preparations.md b/docs/Preparations.md new file mode 100644 index 000000000..24dd2d120 --- /dev/null +++ b/docs/Preparations.md @@ -0,0 +1,98 @@ +# Mirai Console - Preparations + +***如果跳过本节内容,你很可能遇到无法解决的问题。*** + +***此文档假设你是 JVM 平台的开发者。若不是,请参考[其他语言 SDK](https://github.com/mamoe/mirai#%E5%BC%80%E5%8F%91%E8%80%85)*** + +### JVM 环境要求 + +- 桌面 JVM:最低 Java 8,但推荐 Java 11 +- Android:Android SDK 26+ (Android 8.0) + +### 开发插件的准备工作 + +#### 安装 IDE 插件 + +推荐使用 [IntelliJ IDEA](https://www.jetbrains.com/idea/) 或 [Android Studio](https://developer.android.com/studio)。Mirai Console 提供 IntelliJ 插件来提升开发体验。 + +- [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge) ([JetBrains 插件仓库](https://plugins.jetbrains.com/plugin/14816-kotlin-jvm-blocking-bridge), [一键安装](https://plugins.jetbrains.com/embeddable/install/14816)):帮助 Java 用户调用 Kotlin suspend 函数 +- [Mirai Console IntelliJ](../tools/intellij-plugin/) ([JetBrains 插件仓库](https://plugins.jetbrains.com/plugin/15094-mirai-console), [一键安装](https://plugins.jetbrains.com/embeddable/install/15094)):提供错误检查等功能 + +## 前置知识 + +要学习为 mirai-console 开发原生支持的插件, 需要: + +- 掌握 Java 基础 +- 至少粗略了解 Kotlin 基础语法(30 分钟): + - [基本类型](https://www.kotlincn.net/docs/reference/basic-types.html) + - [类与继承](https://www.kotlincn.net/docs/reference/classes.html) + - [属性与字段](https://www.kotlincn.net/docs/reference/properties.html) + - [接口](https://www.kotlincn.net/docs/reference/interfaces.html) + - [扩展](https://www.kotlincn.net/docs/reference/extensions.html) + - [数据类](https://www.kotlincn.net/docs/reference/data-classes.html) + - [对象](https://www.kotlincn.net/docs/reference/object-declarations.html) + - [密封类](https://www.kotlincn.net/docs/reference/sealed-classes.html) + - **[Java 中调用 Kotlin](https://www.kotlincn.net/docs/reference/java-to-kotlin-interop.html)** +- 对于 Java 使用者,请阅读: + - [Java 用户的使用指南](#kotlin-源码阅读帮助) + - [在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数](#在-java-使用-mirai-console-中的-kotlin-suspend-函数) +- 对于 Kotlin 使用者,请熟知 [Kotlin `1.4` 版本带来的新特性](#mirai-console-使用的-kotlin-14-版本的新特性) + + +### Kotlin 源码阅读帮助 + +- Java 中的「方法」在 Kotlin 中均被称为「函数」。 +- Kotlin 默认的访问权限是 `public`。如 Kotlin `class Test` 相当于 Java 的 `public class Test {}` +- Kotlin 的函数定义 `fun test(int: Int): String` 相当于 Java 的方法定义 `public String test(int int)` + +### 在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数 + +#### 什么是 `suspend` 函数 + +`suspend` 函数中文是「挂起函数」,是 Kotlin 「[协程](https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html)」的一部分。 + +Kotlin 协程是语言级特性,函数的修饰符 `suspend` 会在编译阶段被处理。 + +对于一个挂起函数: +```kotlin +suspend fun test(): String +``` + +它会被编译为 `public Object test(Continuation<String> $completion)`。 + +这是因为 Kotlin 对所有挂起函数都有这样的内部变化,并在编译时实现了协程的一些特性。 + +Java 用户无法调用这样的方法,因为 `Continuation` 的实现很复杂。 + +Mamoe 为此开发了 Kotlin 编译器插件 [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge),通过 `@JvmBlockingBridge` 注解,在编译期额外生成一个供 Java 使用的方法,让 Java 用户可以使用拥有源码内相同的函数签名的方法。 + +要获取详细信息,参考 [Kotlin Jvm Blocking Bridge 编译器插件](https://github.com/mamoe/kotlin-jvm-blocking-bridge/blob/master/README-chs.md#%E7%BC%96%E8%AF%91%E5%99%A8%E6%8F%92%E4%BB%B6) + +### Mirai Console 使用的 Kotlin `1.4` 版本的新特性 + +在官方文档的 [语言特性与改进](https://www.kotlincn.net/docs/reference/whatsnew14.html#%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7%E4%B8%8E%E6%94%B9%E8%BF%9B) 基础上,Mirai Console 的一些设计基于 Kotlin 1.4 的更多新特性。 + +#### `object` 内的扩展函数的自动引用 +对于如下定义: +```kotlin +package org.example +object Obj { + fun String.foo() +} +``` +在 Kotlin `1.3`,要调用 `foo`,必须使用: +```kotlin +Obj.run { + "str".foo() +} +``` +因为 IDE 不会自动为 `String.foo` 添加 `import`。 + +Kotlin `1.4` 解决了这个问题。在使用 `"str".foo` 时 Kotlin 会自动添加 `org.example.Obj.foo` 的引用。 + +Mirai Console 很多单例对象都设计为 `interface + companion object INSTANCE` 的接口与实现模式,需要这样的新特性。例如: +```kotlin +interface MiraiConsole { + companion object INSTANCE : MiraiConsole by MiraiConsoleImpl // MiraiConsoleImpl 是内部实现,不公开 +} +``` diff --git a/docs/README.md b/docs/README.md index 3125e4e4e..95de05428 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,167 +1,3 @@ # Mirai Console 欢迎来到 mirai-console 开发文档! - -## 目录 - -- **[准备工作](#准备工作)** -- **[启动 Console](Run.md)** - -### 后端插件开发基础 - -- 插件 - [Plugin 模块](Plugins.md) -- 指令 - [Command 模块](Commands.md) -- 存储 - [PluginData 模块](PluginData.md) -- 权限 - [Permission 模块](Permissions.md) - -### 后端插件开发进阶 - -- 扩展 - [Extension 模块和扩展点](Extensions.md) - -### 实现前端 -- [FrontEnd](FrontEnd.md) - -[`Plugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt -[`Annotations`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/Annotations.kt -[`PluginData`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt -[`JavaPluginScheduler`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPluginScheduler.kt -[`JvmPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt -[`PluginConfig`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt -[`PluginLoader`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt -[`ConsoleInput`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ConsoleInput.kt -[`PluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt -[`Command`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt - -## 准备工作 -***如果跳过本节内容,你很可能遇到无法解决的问题。*** - -### 环境要求 - -*不接受降低最低版本要求的建议* - -- JDK 11 -- Android:Android SDK 26+ (Android 8.0) -- Kotlin: 1.4 - -*Mirai Console 需要的 Kotlin 版本会与 Kotlin 最新稳定版本同步。* - -### 开发插件的准备工作 - -- 安装并配置 JDK 11 - -若使用 Java,或要修改 Mirai Console: - -- 使用 [IntelliJ IDEA](https://www.jetbrains.com/idea/) (或 `Android Studio`)。 -- IDE 需装有 [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge) 插件 (先启动你的 IDE,再点击 [一键安装](https://plugins.jetbrains.com/embeddable/install/14816)) - -若使用 Kotlin,无特别要求。 - -### 前置知识 -要学习为 mirai-console 开发原生支持的插件, 需要: - -- 掌握 Java 基础 -- 至少粗略了解 Kotlin 基础语法(30 分钟): - - [基本类型](https://www.kotlincn.net/docs/reference/basic-types.html) - - [类与继承](https://www.kotlincn.net/docs/reference/classes.html) - - [属性与字段](https://www.kotlincn.net/docs/reference/properties.html) - - [接口](https://www.kotlincn.net/docs/reference/interfaces.html) - - [扩展](https://www.kotlincn.net/docs/reference/extensions.html) - - [数据类](https://www.kotlincn.net/docs/reference/data-classes.html) - - [对象](https://www.kotlincn.net/docs/reference/object-declarations.html) - - [密封类](https://www.kotlincn.net/docs/reference/sealed-classes.html) - - **[Java 中调用 Kotlin](https://www.kotlincn.net/docs/reference/java-to-kotlin-interop.html)** -- 对于 Java 使用者,请阅读 [Java 用户的使用指南](#java-用户的使用指南),[在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数](#在-java-使用-mirai-console-中的-kotlin-suspend-函数) -- 对于 Kotlin 使用者,请熟知 [Kotlin `1.4` 版本带来的新特性](#mirai-console-使用的-kotlin-14-版本的新特性) - - -## 附录 - -### Java 用户的使用指南 - -- Java 中的「方法」在 Kotlin 中均被称为「函数」。 -- Kotlin 默认的访问权限是 `public`。如 Kotlin `class Test` 相当于 Java 的 `public class Test {}` -- Kotlin 的函数定义 `fun test(int: Int): String` 相当于 Java 的方法定义 `public String test(int int)` - -### 在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数 - -#### 什么是 `suspend` 函数 - -`suspend` 函数中文是「挂起函数」,是 Kotlin 「[协程](https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html)」的一部分。 - -Kotlin 协程是语言级特性,函数的修饰符 `suspend` 会在编译阶段被处理。 - -对于一个挂起函数: -```kotlin -suspend fun test(): String -``` - -它会被编译为 `public Object test(Continuation<String> $completion)`。 - -这是因为 Kotlin 对所有挂起函数都有这样的内部变化,并在编译时实现了协程的一些特性。 - -Java 用户无法调用这样的方法,因为 `Continuation` 的实现很复杂。 - -Mamoe 为此开发了 Kotlin 编译器插件 [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge),通过 `@JvmBlockingBridge` 注解,在编译期额外生成一个供 Java 使用的方法,让 Java 用户可以使用拥有源码内相同的函数签名的方法。 - -要获取详细信息,参考 [Kotlin Jvm Blocking Bridge 编译器插件](https://github.com/mamoe/kotlin-jvm-blocking-bridge/blob/master/README-chs.md#%E7%BC%96%E8%AF%91%E5%99%A8%E6%8F%92%E4%BB%B6) - -### Mirai Console 使用的 Kotlin `1.4` 版本的新特性 - -在官方文档的 [语言特性与改进](https://www.kotlincn.net/docs/reference/whatsnew14.html#%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7%E4%B8%8E%E6%94%B9%E8%BF%9B) 基础上,Mirai Console 的一些设计基于 Kotlin 1.4 的更多新特性。 - -#### `object` 内的扩展函数的自动引用 -对于如下定义: -```kotlin -package org.example -object Obj { - fun String.foo() -} -``` -在 Kotlin `1.3`,要调用 `foo`,必须使用: -```kotlin -Obj.run { - "str".foo() -} -``` -因为 IDE 不会自动为 `String.foo` 添加 `import`。 - -Kotlin `1.4` 解决了这个问题。在使用 `"str".foo` 时 Kotlin 会自动添加 `org.example.Obj.foo` 的引用。 - -Mirai Console 很多单例对象都设计为 `interface + companion object INSTANCE` 的接口与实现模式,需要这样的新特性。例如: -```kotlin -interface MiraiConsole { - companion object INSTANCE : MiraiConsole by MiraiConsoleImpl // MiraiConsoleImpl 是内部实现,不公开 -} -``` - -#### Mirai Console 演进 - -Mirai Console 是不断前进的框架,将来必定会发生 API 弃用和重构。 -维护者会严谨地推进每一项修改,并提供迁移周期(至少 2 个次版本)。 - -##### 版本规范 - -Mirai Console 的版本号遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/#spec-item-9) 规范。 - -在大版本开发过程中,Mirai Console 会以 `-M1`, `-M2` 等版本后缀发布里程碑预览版本。代表一些功能基本完成,但还不稳定。 -但这些版本里新增的 API 可能还会在下一个 Milestone 版本变化,因此请按需使用。 - -在大版本即将发布前,Mirai Console 会以 `-RC` 版本后缀发布最终的预览版本。 -`RC` 表示新版本 API 已经确定,离稳定版发布只差最后的一些内部优化或 bug 修复。 - -##### 更新兼容性 - -对于 `x.y.z` 版本号: -- 当 `z` 增加时,只会有 bug 修复,和必要的新函数添加(为了解决某一个问题),不会有破坏性变化。 -- 当 `y` 增加时,可能有新 API 的引入,和旧 API 的弃用。但这些弃用会经过一个弃用周期后才被删除(隐藏)。向下兼容得到保证。 -- 当 `x` 增加时,任何 API 都可能会有变化。无兼容性保证。 - -##### 弃用周期 - -一个计划被删除的 API,将会在下一个次版本开始经历弃用周期。 - -如一个 API 在 `1.1.0` 起被弃用,它首先会是 `WARNING` (使用时会得到一个编译警告)弃用级别。 -在 `1.2.0` 上升为 `ERROR`(使用时会得到一个编译错误); -在 `1.3.0` 上升为 `HIDDEN`(使用者无法看到这些 API)。 - -`HIDDEN` 的 API 仍然会保留在代码中并正常编译,以提供二进制兼容性,直到下一个主版本更新。 diff --git a/docs/Run.md b/docs/Run.md index d5fdd2347..2e794c54f 100644 --- a/docs/Run.md +++ b/docs/Run.md @@ -2,9 +2,12 @@ Mirai Console 可以独立启动,也可以被嵌入到某个应用中。 -## 使用第三方工具自动启动 +## 使用工具自动独立启动 -## 独立启动 +官方: https://github.com/iTXTech/mirai-console-loader +第三方: https://github.com/LXY1226/MiraiOK + +## 手动配置独立启动 ### 环境 - JRE 11+ / JDK 11+ @@ -17,16 +20,16 @@ Mirai Console 可以独立启动,也可以被嵌入到某个应用中。 - mirai-console 任一前端 - 相关依赖 -只有 mirai-console 前端才有入口点 `main` 方法。目前只有一个 pure 前端可用。 +只有 mirai-console 前端才有入口点 `main` 方法。目前只有一个 terminal 前端可用。 -### 启动 mirai-console-pure 前端 +### 启动 mirai-console-terminal 前端 mirai 在版本发布时会同时发布打包依赖的 Shadow JAR,存放在 [mirai-repo]。 1. 在 [mirai-repo] 下载如下三个模块的最新版本文件并放到一个文件夹内 (如 `libs`): - mirai-core-qqandroid - mirai-console - - mirai-console-pure + - mirai-console-terminal 2. 创建一个新的文件, 名为 `start-mirai-console.bat`/`start-mirai-console.ps1`/`start-mirai-console.sh` @@ -34,26 +37,33 @@ Windows CMD: ```shell script @echo off title Mirai Console -java -cp "./libs/*" net.mamoe.mirai.console.pure.MiraiConsolePureLoader %* +java -cp "./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader %* pause ``` Windows PowerShell: ```shell script $Host.UI.RawUI.WindowTitle = "Mirai Console" -java -cp "./libs/*" net.mamoe.mirai.console.pure.MiraiConsolePureLoader $args +java -cp "./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader $args pause ``` Linux: ```shell script #!/usr/bin/env bash -java -cp "./libs/*" net.mamoe.mirai.console.pure.MiraiConsolePureLoader $* +java -cp "./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader $* ``` 然后就可以开始使用 mirai-console 了 -### mirai-console-pure 前端参数 -使用 `./start-mirai-console --help` 查看 mirai-console-pure 支持的启动参数 +#### mirai-console-terminal 前端参数 +使用 `./start-mirai-console --help` 查看 mirai-console-terminal 支持的启动参数 [mirai-repo]: https://github.com/project-mirai/mirai-repo/tree/master/shadow + + +### 启动 mirai-console-pure 前端 + +与启动 `mirai-console-terminal` 前端大体相同 +- 下载 `mirai-console-terminal` 改成下载 `mirai-console-pure` +- 启动入口从 `net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader` 改成 `net.mamoe.mirai.console.pure.MiraiConsolePureLoader` diff --git a/frontend/mirai-console-pure/build.gradle.kts b/frontend/mirai-console-terminal/build.gradle.kts similarity index 57% rename from frontend/mirai-console-pure/build.gradle.kts rename to frontend/mirai-console-terminal/build.gradle.kts index 57f6dd608..23d378113 100644 --- a/frontend/mirai-console-pure/build.gradle.kts +++ b/frontend/mirai-console-terminal/build.gradle.kts @@ -36,40 +36,18 @@ dependencies { implementation("org.jline:jline:3.15.0") implementation("org.fusesource.jansi:jansi:1.18") - compileAndRuntime(project(":mirai-console")) - compileAndRuntime("net.mamoe:mirai-core:${Versions.core}") - compileAndRuntime(kotlin("stdlib", Versions.kotlinStdlib)) // embedded by core + compileAndTestRuntime(project(":mirai-console")) + compileAndTestRuntime("net.mamoe:mirai-core:${Versions.core}") + compileAndTestRuntime(kotlin("stdlib-jdk8", Versions.kotlinStdlib)) // embedded by core - runtimeOnly("net.mamoe:mirai-core-qqandroid:${Versions.core}") testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}") testApi(project(":mirai-console")) - - -// val autoService = "1.0-rc7" -// kapt("com.google.auto.service", "auto-service", autoService) -// compileOnly("com.google.auto.service", "auto-service-annotations", autoService) -// testCompileOnly("com.google.auto.service", "auto-service-annotations", autoService) } -ext.apply { - // 傻逼 compileAndRuntime 没 exclude 掉 - // 傻逼 gradle 第二次配置 task 会覆盖掉第一次的配置 - val x: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.() -> Unit = { - dependencyFilter.include { - when ("${it.moduleGroup}:${it.moduleName}") { - "org.jline:jline" -> true - "org.fusesource.jansi:jansi" -> true - else -> false - } - } - } - this.set("shadowJar", x) -} +version = Versions.consoleTerminal -version = Versions.consolePure +description = "Console Terminal CLI frontend for mirai" -description = "Console Pure CLI frontend for mirai" - -setupPublishing("mirai-console-pure", bintrayPkgName = "mirai-console-pure") +setupPublishing("mirai-console-terminal", bintrayPkgName = "mirai-console-terminal") // endregion \ No newline at end of file diff --git a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt new file mode 100644 index 000000000..477d8dcdc --- /dev/null +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + * + */ + +package net.mamoe.mirai.console.pure + +import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader + +@Deprecated( + message = "Please use MiraiConsoleTerminalLoader", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith( + "MiraiConsoleTerminalLoader", + "net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader" + ) +) +object MiraiConsolePureLoader { + @Deprecated( + message = "for binary compatibility", + level = DeprecationLevel.ERROR + ) + @JvmStatic + fun main(args: Array<String>) { + System.err.println("WARNING: Mirai Console Pure已经更名为 Mirai Console Terminal") + System.err.println("请使用新的入口点 net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader") + MiraiConsoleTerminalLoader.main(args) + } +} diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/BufferedOutputStream.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/BufferedOutputStream.kt similarity index 98% rename from frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/BufferedOutputStream.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/BufferedOutputStream.kt index f5d0375cd..ad7f18160 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/BufferedOutputStream.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/BufferedOutputStream.kt @@ -5,9 +5,10 @@ * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. * * https://github.com/mamoe/mirai/blob/master/LICENSE + * */ -package net.mamoe.mirai.console.pure +package net.mamoe.mirai.console.terminal import java.io.ByteArrayOutputStream import java.io.OutputStream diff --git a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleInputImpl.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleInputImpl.kt new file mode 100644 index 000000000..c0a83f707 --- /dev/null +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleInputImpl.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + * + */ + +package net.mamoe.mirai.console.terminal + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import net.mamoe.mirai.console.util.ConsoleInput +import org.fusesource.jansi.Ansi +import org.jline.reader.EndOfFileException +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.concurrent.Executors +import kotlin.coroutines.resumeWithException + + +internal object ConsoleInputImpl : ConsoleInput { + private val format = DateTimeFormatter.ofPattern("HH:mm:ss") + internal val thread = Executors.newSingleThreadExecutor { task -> + Thread(task, "Mirai Console Input Thread").also { + it.isDaemon = false + } + } + internal var executingCoroutine: CancellableContinuation<String>? = null + + + override suspend fun requestInput(hint: String): String { + return suspendCancellableCoroutine { coroutine -> + if (thread.isShutdown || thread.isTerminated) { + coroutine.resumeWithException(EndOfFileException()) + return@suspendCancellableCoroutine + } + executingCoroutine = coroutine + kotlin.runCatching { + thread.submit { + kotlin.runCatching { + lineReader.readLine( + if (hint.isNotEmpty()) { + lineReader.printAbove( + Ansi.ansi() + .fgCyan() + .a( + LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()) + .format(format) + ) + .a(" ") + .fgMagenta().a(hint) + .reset() + .toString() + ) + "$hint > " + } else "> " + ) + }.let { result -> + executingCoroutine = null + coroutine.resumeWith(result) + } + } + }.onFailure { error -> + executingCoroutine = null + kotlin.runCatching { coroutine.resumeWithException(EndOfFileException(error)) } + } + } + } +} diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsolePureSettings.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleTerminalSettings.kt similarity index 87% rename from frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsolePureSettings.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleTerminalSettings.kt index 5a1fdec47..0c780724e 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsolePureSettings.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleTerminalSettings.kt @@ -5,12 +5,13 @@ * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. * * https://github.com/mamoe/mirai/blob/master/LICENSE + * */ /* * @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp> */ -package net.mamoe.mirai.console.pure +package net.mamoe.mirai.console.terminal @Retention(AnnotationRetention.BINARY) @RequiresOptIn(level = RequiresOptIn.Level.WARNING) @@ -23,10 +24,10 @@ package net.mamoe.mirai.console.pure AnnotationTarget.CONSTRUCTOR ) @MustBeDocumented -annotation class ConsolePureExperimentalApi +annotation class ConsoleTerminalExperimentalApi -@ConsolePureExperimentalApi -public object ConsolePureSettings { +@ConsoleTerminalExperimentalApi +public object ConsoleTerminalSettings { @JvmField var setupAnsi: Boolean = System.getProperty("os.name") .toLowerCase() diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt similarity index 75% rename from frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt index 276c6ed18..a21b63c1e 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/ConsoleThread.kt @@ -5,33 +5,48 @@ * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. * * https://github.com/mamoe/mirai/blob/master/LICENSE + * */ -package net.mamoe.mirai.console.pure +package net.mamoe.mirai.console.terminal import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.command.BuiltInCommands -import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.CommandExecuteStatus import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.terminal.noconsole.NoConsole import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.requestInput import net.mamoe.mirai.utils.DefaultLogger +import org.jline.reader.EndOfFileException import org.jline.reader.UserInterruptException val consoleLogger by lazy { DefaultLogger("console") } -@OptIn(ConsoleInternalApi::class, ConsolePureExperimentalApi::class) +@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class) internal fun startupConsoleThread() { - if (ConsolePureSettings.noConsole) return + if (terminal is NoConsole) return - MiraiConsole.launch(CoroutineName("Input")) { + MiraiConsole.launch(CoroutineName("Input Cancelling Daemon")) { + while (true) { + delay(2000) + } + }.invokeOnCompletion { + runCatching<Unit> { + terminal.close() + ConsoleInputImpl.thread.shutdownNow() + runCatching { + ConsoleInputImpl.executingCoroutine?.cancel(EndOfFileException()) + } + }.exceptionOrNull()?.printStackTrace() + } + MiraiConsole.launch(CoroutineName("Console Command")) { while (true) { try { val next = MiraiConsole.requestInput("").let { @@ -65,17 +80,14 @@ internal fun startupConsoleThread() { } catch (e: CancellationException) { return@launch } catch (e: UserInterruptException) { - MiraiConsole.cancel() + 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) } } } - - MiraiConsole.job.invokeOnCompletion { - runCatching { - terminal.close() - }.exceptionOrNull()?.printStackTrace() - } } diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/MiraiConsoleImplementationTerminal.kt similarity index 64% rename from frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/MiraiConsoleImplementationTerminal.kt index 471c02e7f..1cd89f853 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/MiraiConsoleImplementationTerminal.kt @@ -5,6 +5,7 @@ * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. * * https://github.com/mamoe/mirai/blob/master/LICENSE + * */ @file:Suppress( @@ -17,13 +18,15 @@ "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING", "EXPOSED_SUPER_CLASS" ) -@file:OptIn(ConsoleInternalApi::class, ConsoleFrontEndImplementation::class, ConsolePureExperimentalApi::class) +@file:OptIn(ConsoleInternalApi::class, ConsoleFrontEndImplementation::class, ConsoleTerminalExperimentalApi::class) -package net.mamoe.mirai.console.pure +package net.mamoe.mirai.console.terminal -import com.vdurmont.semver4j.Semver -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope import net.mamoe.mirai.console.ConsoleFrontEndImplementation import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription @@ -32,13 +35,10 @@ import net.mamoe.mirai.console.data.MultiFilePluginDataStorage import net.mamoe.mirai.console.data.PluginDataStorage import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader -import net.mamoe.mirai.console.pure.ConsoleInputImpl.requestInput -import net.mamoe.mirai.console.util.ConsoleExperimentalApi -import net.mamoe.mirai.console.pure.noconsole.AllEmptyLineReader -import net.mamoe.mirai.console.pure.noconsole.NoConsole -import net.mamoe.mirai.console.util.ConsoleInput -import net.mamoe.mirai.console.util.ConsoleInternalApi -import net.mamoe.mirai.console.util.NamedSupervisorJob +import net.mamoe.mirai.console.terminal.ConsoleInputImpl.requestInput +import net.mamoe.mirai.console.terminal.noconsole.AllEmptyLineReader +import net.mamoe.mirai.console.terminal.noconsole.NoConsole +import net.mamoe.mirai.console.util.* import net.mamoe.mirai.utils.* import org.fusesource.jansi.Ansi import org.jline.reader.LineReader @@ -46,31 +46,28 @@ import org.jline.reader.LineReaderBuilder import org.jline.reader.impl.completer.NullCompleter import org.jline.terminal.Terminal import org.jline.terminal.TerminalBuilder +import org.jline.terminal.impl.AbstractWindowsTerminal import java.nio.file.Path import java.nio.file.Paths -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter /** - * mirai-console-pure 后端实现 + * mirai-console-terminal 后端实现 * - * @see MiraiConsolePureLoader CLI 入口点 + * @see MiraiConsoleTerminalLoader CLI 入口点 */ @ConsoleExperimentalApi -class MiraiConsoleImplementationPure +class MiraiConsoleImplementationTerminal @JvmOverloads constructor( override val rootPath: Path = Paths.get(".").toAbsolutePath(), override val builtInPluginLoaders: List<Lazy<PluginLoader<*, *>>> = listOf(lazy { JvmPluginLoader }), override val frontEndDescription: MiraiConsoleFrontEndDescription = ConsoleFrontEndDescImpl, - override val consoleCommandSender: MiraiConsoleImplementation.ConsoleCommandSenderImpl = ConsoleCommandSenderImplPure, + override val consoleCommandSender: MiraiConsoleImplementation.ConsoleCommandSenderImpl = ConsoleCommandSenderImplTerminal, override val dataStorageForJvmPluginLoader: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("data")), override val dataStorageForBuiltIns: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("data")), override val configStorageForJvmPluginLoader: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("config")), override val configStorageForBuiltIns: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("config")), ) : MiraiConsoleImplementation, CoroutineScope by CoroutineScope( - NamedSupervisorJob("MiraiConsoleImplementationPure") + + NamedSupervisorJob("MiraiConsoleImplementationTerminal") + CoroutineExceptionHandler { coroutineContext, throwable -> if (throwable is CancellationException) { return@CoroutineExceptionHandler @@ -94,30 +91,9 @@ class MiraiConsoleImplementationPure } } -private object ConsoleInputImpl : ConsoleInput { - private val format = DateTimeFormatter.ofPattern("HH:mm:ss") - - override suspend fun requestInput(hint: String): String { - return withContext(Dispatchers.IO) { - lineReader.readLine( - if (hint.isNotEmpty()) { - lineReader.printAbove( - Ansi.ansi() - .fgCyan().a(LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).format(format)) - .a(" ") - .fgMagenta().a(hint) - .reset() - .toString() - ) - "$hint > " - } else "> " - ) - } - } -} - val lineReader: LineReader by lazy { - if (ConsolePureSettings.noConsole) return@lazy AllEmptyLineReader + val terminal = terminal + if (terminal is NoConsole) return@lazy AllEmptyLineReader LineReaderBuilder.builder() .terminal(terminal) @@ -126,7 +102,7 @@ val lineReader: LineReader by lazy { } val terminal: Terminal = run { - if (ConsolePureSettings.noConsole) return@run NoConsole + if (ConsoleTerminalSettings.noConsole) return@run NoConsole val dumb = System.getProperty("java.class.path") .contains("idea_rt.jar") || System.getProperty("mirai.idea") !== null || System.getenv("mirai.idea") !== null @@ -134,7 +110,33 @@ val terminal: Terminal = run { runCatching { TerminalBuilder.builder() .dumb(dumb) + .paused(true) .build() + .let { terminal -> + if (terminal is AbstractWindowsTerminal) { + val pumpField = runCatching { + AbstractWindowsTerminal::class.java.getDeclaredField("pump").also { + it.isAccessible = true + } + }.onFailure { err -> + err.printStackTrace() + return@let terminal.also { it.resume() } + }.getOrThrow() + var response = terminal + terminal.setOnClose { + response = NoConsole + } + terminal.resume() + val pumpThread = pumpField[terminal] as? Thread ?: return@let NoConsole + @Suppress("ControlFlowWithEmptyBody") + while (pumpThread.state == Thread.State.NEW); + Thread.sleep(1000) + terminal.setOnClose(null) + return@let response + } + terminal.resume() + terminal + } }.recoverCatching { TerminalBuilder.builder() .jansi(true) @@ -147,9 +149,11 @@ val terminal: Terminal = run { } private object ConsoleFrontEndDescImpl : MiraiConsoleFrontEndDescription { - override val name: String get() = "Pure" + override val name: String get() = "Terminal" override val vendor: String get() = "Mamoe Technologies" - override val version: Semver = net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version + // net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version + // is console's version not frontend's version + override val version: SemVersion = SemVersion(net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.versionConst) } private val ANSI_RESET = Ansi().reset().toString() diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/MiraiConsoleTerminalLoader.kt similarity index 76% rename from frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/MiraiConsoleTerminalLoader.kt index 0fa6abe1f..930e72b48 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/MiraiConsoleTerminalLoader.kt @@ -5,6 +5,7 @@ * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. * * https://github.com/mamoe/mirai/blob/master/LICENSE + * */ @file:Suppress( @@ -15,9 +16,9 @@ "INVISIBLE_GETTER", "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER", ) -@file:OptIn(ConsoleInternalApi::class, ConsolePureExperimentalApi::class) +@file:OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class) -package net.mamoe.mirai.console.pure +package net.mamoe.mirai.console.terminal import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -26,20 +27,22 @@ import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.data.AutoSavePluginDataHolder -import net.mamoe.mirai.console.pure.noconsole.SystemOutputPrintStream +import net.mamoe.mirai.console.terminal.noconsole.SystemOutputPrintStream import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.utils.DefaultLogger import net.mamoe.mirai.utils.minutesToMillis +import java.io.FileDescriptor +import java.io.FileOutputStream import java.io.PrintStream import kotlin.system.exitProcess /** - * mirai-console-pure CLI 入口点 + * mirai-console-terminal CLI 入口点 */ -object MiraiConsolePureLoader { +object MiraiConsoleTerminalLoader { @JvmStatic fun main(args: Array<String>) { parse(args, exitProcess = true) @@ -53,10 +56,11 @@ object MiraiConsolePureLoader { } } - @ConsolePureExperimentalApi + @ConsoleTerminalExperimentalApi fun printHelpMessage() { val help = listOf( - "" to "Mirai-Console[Pure FrontEnd] v" + kotlin.runCatching { + "" to "Mirai-Console[Terminal FrontEnd] v" + net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.versionConst, + "" to " [ BackEnd] v" + kotlin.runCatching { net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version }.getOrElse { "<unknown>" }, "" to "", @@ -96,7 +100,7 @@ object MiraiConsolePureLoader { } } - @ConsolePureExperimentalApi + @ConsoleTerminalExperimentalApi fun parse(args: Array<String>, exitProcess: Boolean = false) { val iterator = args.iterator() while (iterator.hasNext()) { @@ -107,19 +111,19 @@ object MiraiConsolePureLoader { return } "--no-console" -> { - ConsolePureSettings.noConsole = true + ConsoleTerminalSettings.noConsole = true } "--dont-setup-terminal-ansi" -> { - ConsolePureSettings.setupAnsi = false + ConsoleTerminalSettings.setupAnsi = false } "--no-ansi" -> { - ConsolePureSettings.noAnsi = true - ConsolePureSettings.setupAnsi = false + ConsoleTerminalSettings.noAnsi = true + ConsoleTerminalSettings.setupAnsi = false } "--reading-replacement" -> { - ConsolePureSettings.noConsoleSafeReading = true + ConsoleTerminalSettings.noConsoleSafeReading = true if (iterator.hasNext()) { - ConsolePureSettings.noConsoleReadingReplacement = iterator.next() + ConsoleTerminalSettings.noConsoleReadingReplacement = iterator.next() } else { println("Bad option `--reading-replacement`") println("Usage: --reading-replacement <string>") @@ -129,7 +133,7 @@ object MiraiConsolePureLoader { } } "--safe-reading" -> { - ConsolePureSettings.noConsoleSafeReading = true + ConsoleTerminalSettings.noConsoleSafeReading = true } else -> { println("Unknown option `$option`") @@ -140,13 +144,13 @@ object MiraiConsolePureLoader { } } } - if (ConsolePureSettings.noConsole) + if (ConsoleTerminalSettings.noConsole) SystemOutputPrintStream // Setup Output Channel } @Suppress("MemberVisibilityCanBePrivate") @ConsoleExperimentalApi - fun startAsDaemon(instance: MiraiConsoleImplementationPure = MiraiConsoleImplementationPure()) { + fun startAsDaemon(instance: MiraiConsoleImplementationTerminal = MiraiConsoleImplementationTerminal()) { instance.start() overrideSTD() startupConsoleThread() @@ -160,7 +164,7 @@ internal object ConsoleDataHolder : AutoSavePluginDataHolder, @ConsoleExperimentalApi override val dataHolderName: String - get() = "Pure" + get() = "Terminal" } internal fun overrideSTD() { @@ -168,25 +172,33 @@ internal fun overrideSTD() { PrintStream( BufferedOutputStream( logger = DefaultLogger("stdout").run { ({ line: String? -> info(line) }) } - ) + ), + false, + "UTF-8" ) ) System.setErr( PrintStream( BufferedOutputStream( logger = DefaultLogger("stderr").run { ({ line: String? -> warning(line) }) } - ) + ), + false, + "UTF-8" ) ) } -internal object ConsoleCommandSenderImplPure : MiraiConsoleImplementation.ConsoleCommandSenderImpl { +internal object ConsoleCommandSenderImplTerminal : MiraiConsoleImplementation.ConsoleCommandSenderImpl { override suspend fun sendMessage(message: String) { kotlin.runCatching { lineReader.printAbove(message) - }.onFailure { - consoleLogger.error("Exception while ConsoleCommandSenderImplPure.sendMessage", it) + }.onFailure { exception -> + // If failed. It means JLine Terminal not working... + PrintStream(FileOutputStream(FileDescriptor.err)).use { + it.println("Exception while ConsoleCommandSenderImplTerminal.sendMessage") + exception.printStackTrace(it) + } } } diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/noconsole/NoConsole.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/noconsole/NoConsole.kt similarity index 90% rename from frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/noconsole/NoConsole.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/noconsole/NoConsole.kt index d04a69470..45a8be66d 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/noconsole/NoConsole.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/terminal/noconsole/NoConsole.kt @@ -10,12 +10,12 @@ /* * @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp> */ -@file:OptIn(ConsolePureExperimentalApi::class) +@file:OptIn(ConsoleTerminalExperimentalApi::class) -package net.mamoe.mirai.console.pure.noconsole +package net.mamoe.mirai.console.terminal.noconsole -import net.mamoe.mirai.console.pure.ConsolePureExperimentalApi -import net.mamoe.mirai.console.pure.ConsolePureSettings +import net.mamoe.mirai.console.terminal.ConsoleTerminalExperimentalApi +import net.mamoe.mirai.console.terminal.ConsoleTerminalSettings import org.jline.keymap.KeyMap import org.jline.reader.* import org.jline.terminal.Attributes @@ -69,8 +69,8 @@ internal object AllIgnoredOutputStream : OutputStream() { } internal val SystemOutputPrintStream by lazy { - @OptIn(ConsolePureExperimentalApi::class) - if (ConsolePureSettings.setupAnsi) { + @OptIn(ConsoleTerminalExperimentalApi::class) + if (ConsoleTerminalSettings.setupAnsi) { org.fusesource.jansi.AnsiConsole.systemInstall() } System.out @@ -81,16 +81,16 @@ internal object AllEmptyLineReader : LineReader { override fun printAbove(str: String?) { if (str == null) return - @OptIn(ConsolePureExperimentalApi::class) - if (ConsolePureSettings.noAnsi) { + @OptIn(ConsoleTerminalExperimentalApi::class) + if (ConsoleTerminalSettings.noAnsi) { SystemOutputPrintStream.println(ANSI_REGEX.replace(str, "")) } else SystemOutputPrintStream.println(str) } - @OptIn(ConsolePureExperimentalApi::class) + @OptIn(ConsoleTerminalExperimentalApi::class) override fun readLine(): String = - if (ConsolePureSettings.noConsoleSafeReading) ConsolePureSettings.noConsoleReadingReplacement - else error("Unsupported Reading line when console front-end closed.") + if (ConsoleTerminalSettings.noConsoleSafeReading) ConsoleTerminalSettings.noConsoleReadingReplacement + else throw EndOfFileException("Unsupported Reading line when console front-end closed.") // region private fun <T> ignored(): T = error("Ignored") diff --git a/settings.gradle.kts b/settings.gradle.kts index 29aabb3f9..c69c29bf2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,7 +19,10 @@ fun includeProject(projectPath: String, path: String? = null) { includeProject(":mirai-console", "backend/mirai-console") includeProject(":mirai-console.codegen", "backend/codegen") -includeProject(":mirai-console-pure", "frontend/mirai-console-pure") +includeProject(":mirai-console-terminal", "frontend/mirai-console-terminal") +includeProject(":mirai-console-compiler-common", "tools/compiler-common") +includeProject(":mirai-console-intellij", "tools/intellij-plugin") +includeProject(":mirai-console-gradle", "tools/gradle-plugin") @Suppress("ConstantConditionIf") if (!disableOldFrontEnds) { diff --git a/tools/compiler-common/README.md b/tools/compiler-common/README.md new file mode 100644 index 000000000..a3e4973fb --- /dev/null +++ b/tools/compiler-common/README.md @@ -0,0 +1,5 @@ +# mirai-console-compiler-common + +Mirai Console 编译器后端通用模块. + +## \ No newline at end of file diff --git a/tools/compiler-common/build.gradle.kts b/tools/compiler-common/build.gradle.kts new file mode 100644 index 000000000..e733d5e55 --- /dev/null +++ b/tools/compiler-common/build.gradle.kts @@ -0,0 +1,74 @@ +@file:Suppress("UnusedImport") + +plugins { + kotlin("jvm") + id("java") + `maven-publish` + id("com.jfrog.bintray") +} + +repositories { + maven("http://maven.aliyun.com/nexus/content/groups/public/") +} + +version = Versions.console +description = "Mirai Console compiler common" + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(JavaCompile::class.java) { + options.encoding = "UTF8" +} + +kotlin { + sourceSets.all { + target.compilations.all { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" + //useIR = true + } + } + languageSettings.apply { + progressiveMode = true + + useExperimentalAnnotation("kotlin.Experimental") + useExperimentalAnnotation("kotlin.RequiresOptIn") + + useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") + useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI") + useExperimentalAnnotation("net.mamoe.mirai.console.ConsoleFrontEndImplementation") + useExperimentalAnnotation("net.mamoe.mirai.console.util.ConsoleExperimentalApi") + useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") + useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") + useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") + useExperimentalAnnotation("net.mamoe.mirai.console.util.ConsoleInternalApi") + } + } +} + +dependencies { + api("org.jetbrains:annotations:19.0.0") + api(kotlinx("coroutines-jdk8", Versions.coroutines)) + + compileOnly("org.jetbrains.kotlin:kotlin-compiler:${Versions.kotlinCompiler}") + compileOnly("org.jetbrains.kotlin:kotlin-compiler:${Versions.kotlinCompiler}") + + testApi(kotlin("test")) + testApi(kotlin("test-junit5")) + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.2.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0") +} + +tasks { + "test"(Test::class) { + useJUnitPlatform() + } +} + +setupPublishing("mirai-console-compiler-common") \ No newline at end of file diff --git a/tools/compiler-common/src/main/java/net/mamoe/mirai/console/compiler/common/diagnostics/MiraiConsoleErrors.java b/tools/compiler-common/src/main/java/net/mamoe/mirai/console/compiler/common/diagnostics/MiraiConsoleErrors.java new file mode 100644 index 000000000..a8181fb84 --- /dev/null +++ b/tools/compiler-common/src/main/java/net/mamoe/mirai/console/compiler/common/diagnostics/MiraiConsoleErrors.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.compiler.common.diagnostics; + +import com.intellij.psi.PsiElement; +import org.jetbrains.kotlin.descriptors.ClassDescriptor; +import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1; +import org.jetbrains.kotlin.diagnostics.DiagnosticFactory2; +import org.jetbrains.kotlin.diagnostics.Errors; +import org.jetbrains.kotlin.psi.KtNamedDeclaration; + +import static org.jetbrains.kotlin.diagnostics.Severity.ERROR; + +public interface MiraiConsoleErrors { + DiagnosticFactory1<PsiElement, String> ILLEGAL_PLUGIN_DESCRIPTION = DiagnosticFactory1.create(ERROR); + DiagnosticFactory1<PsiElement, String> NOT_CONSTRUCTABLE_TYPE = DiagnosticFactory1.create(ERROR); + DiagnosticFactory1<PsiElement, ClassDescriptor> UNSERIALIZABLE_TYPE = DiagnosticFactory1.create(ERROR); + DiagnosticFactory2<PsiElement, String, String> ILLEGAL_COMMAND_NAME = DiagnosticFactory2.create(ERROR); + DiagnosticFactory2<PsiElement, String, String> ILLEGAL_PERMISSION_NAME = DiagnosticFactory2.create(ERROR); + DiagnosticFactory2<PsiElement, String, String> ILLEGAL_PERMISSION_ID = DiagnosticFactory2.create(ERROR); + DiagnosticFactory2<PsiElement, String, String> ILLEGAL_PERMISSION_NAMESPACE = DiagnosticFactory2.create(ERROR); + DiagnosticFactory2<PsiElement, KtNamedDeclaration, String> ILLEGAL_COMMAND_REGISTER_USE = DiagnosticFactory2.create(ERROR); + DiagnosticFactory2<PsiElement, KtNamedDeclaration, String> ILLEGAL_PERMISSION_REGISTER_USE = DiagnosticFactory2.create(ERROR); + + @Deprecated + Object _init = new Object() { + { + Errors.Initializer.initializeFactoryNamesAndDefaultErrorMessages( + MiraiConsoleErrors.class, + MiraiConsoleErrorsRendering.INSTANCE + ); + } + }; +} diff --git a/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/diagnostics/MiraiConsoleErrorsRendering.kt b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/diagnostics/MiraiConsoleErrorsRendering.kt new file mode 100644 index 000000000..ca79ec5b3 --- /dev/null +++ b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/diagnostics/MiraiConsoleErrorsRendering.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.compiler.common.diagnostics + +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.* +import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages +import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap +import org.jetbrains.kotlin.diagnostics.rendering.Renderers + +object MiraiConsoleErrorsRendering : DefaultErrorMessages.Extension { + private val MAP = DiagnosticFactoryToRendererMap("MiraiConsole").apply { + put( + ILLEGAL_PLUGIN_DESCRIPTION, + "{0}", + Renderers.STRING, + ) + + put( + NOT_CONSTRUCTABLE_TYPE, + "类型 ''{0}'' 无法通过反射直接构造, 需要提供默认值.", + Renderers.STRING, + ) + + put( + UNSERIALIZABLE_TYPE, + "类型 ''{0}'' 无法被自动序列化, 需要添加序列化器", + Renderers.FQ_NAMES_IN_TYPES, + ) + + put( + ILLEGAL_COMMAND_NAME, + "指令名 ''{0}'' 无效: {1}", + Renderers.STRING, + Renderers.STRING, + ) + + put( + ILLEGAL_PERMISSION_NAME, + "权限名 ''{0}'' 无效: {1}", + Renderers.STRING, + Renderers.STRING, + ) + + put( + ILLEGAL_PERMISSION_ID, + "权限 Id ''{0}'' 无效: {1}", + Renderers.STRING, + Renderers.STRING, + ) + + put( + ILLEGAL_PERMISSION_NAMESPACE, + "权限命名空间 ''{0}'' 无效: {1}", + Renderers.STRING, + Renderers.STRING, + ) + + put( + ILLEGAL_COMMAND_REGISTER_USE, + "''{0}'' 无法使用在 ''{1}'' 环境下.", + Renderers.DECLARATION_NAME, + Renderers.STRING + ) + + put( + ILLEGAL_PERMISSION_REGISTER_USE, + "''{0}'' 无法使用在 ''{1}'' 环境下.", + Renderers.DECLARATION_NAME, + Renderers.STRING + ) + } + + override fun getMap() = MAP +} diff --git a/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveCommon.kt b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveCommon.kt new file mode 100644 index 000000000..670ccba04 --- /dev/null +++ b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveCommon.kt @@ -0,0 +1,49 @@ +/* + * 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.compiler.common.resolve + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.calls.components.hasDefaultValue + +fun Annotated.hasAnnotation(fqName: FqName) = this.annotations.hasAnnotation(fqName) +fun Annotated.findAnnotation(fqName: FqName) = this.annotations.findAnnotation(fqName) + + +val PsiElement.allChildrenWithSelf: Sequence<PsiElement> + get() = sequence { + yield(this@allChildrenWithSelf) + for (child in children) { + yieldAll(child.allChildrenWithSelf) + } + } + + +inline fun <reified E> PsiElement.findParent(): E? = this.parents.filterIsInstance<E>().firstOrNull() + + +val PsiElement.parents: Sequence<PsiElement> + get() { + val seed = if (this is PsiFile) null else parent + return generateSequence(seed) { if (it is PsiFile) null else it.parent } + } + + +fun ClassDescriptor.findNoArgConstructor(): ClassConstructorDescriptor? { + return constructors.find { desc -> + desc.valueParameters.all { it.hasDefaultValue() } + } +} + +fun ClassDescriptor.hasNoArgConstructor(): Boolean = this.findNoArgConstructor() != null \ No newline at end of file diff --git a/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveTypes.kt b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveTypes.kt new file mode 100644 index 000000000..463321b14 --- /dev/null +++ b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/resolve/resolveTypes.kt @@ -0,0 +1,81 @@ +/* + * 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.compiler.common.resolve + +import net.mamoe.mirai.console.compiler.common.castOrNull +import net.mamoe.mirai.console.compiler.common.firstValue +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.constants.EnumValue + +/////////////////////////////////////////////////////////////////////////// +// Serializer +/////////////////////////////////////////////////////////////////////////// + +val SERIALIZABLE_FQ_NAME = FqName("kotlinx.serialization.Serializable") + + +/////////////////////////////////////////////////////////////////////////// +// Command +/////////////////////////////////////////////////////////////////////////// + +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") + +/////////////////////////////////////////////////////////////////////////// +// Plugin +/////////////////////////////////////////////////////////////////////////// + +val PLUGIN_FQ_NAME = FqName("net.mamoe.mirai.console.plugin.Plugin") +val JVM_PLUGIN_DESCRIPTION_FQ_NAME = FqName("net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription") +val SIMPLE_JVM_PLUGIN_DESCRIPTION_FQ_NAME = FqName("net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription") + +/////////////////////////////////////////////////////////////////////////// +// PluginData +/////////////////////////////////////////////////////////////////////////// + +val PLUGIN_DATA_VALUE_FUNCTIONS_FQ_FQ_NAME = FqName("net.mamoe.mirai.console.data.value") + +/////////////////////////////////////////////////////////////////////////// +// Resolve +/////////////////////////////////////////////////////////////////////////// + +val RESOLVE_CONTEXT_FQ_NAME = FqName("net.mamoe.mirai.console.compiler.common.ResolveContext") + +/** + * net.mamoe.mirai.console.compiler.common.ResolveContext.Kind + */ +enum class ResolveContextKind { + PLUGIN_ID, + PLUGIN_NAME, + PLUGIN_VERSION, + + COMMAND_NAME, + + PERMISSION_NAMESPACE, + PERMISSION_NAME, + PERMISSION_ID, + + RESTRICTED_NO_ARG_CONSTRUCTOR + ; + + companion object { + fun valueOfOrNull(string: String): ResolveContextKind? = values().find { it.name == string } + } +} + +fun Annotated.isResolveContext(kind: ResolveContextKind) = this.resolveContextKind == kind + +val Annotated.resolveContextKind: ResolveContextKind? + get() { + val ann = this.findAnnotation(RESOLVE_CONTEXT_FQ_NAME) ?: return null + val (_, enumEntryName) = ann.allValueArguments.firstValue().castOrNull<EnumValue>()?.value ?: return null // undetermined kind + return ResolveContextKind.valueOf(enumEntryName.asString()) + } \ No newline at end of file diff --git a/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/utilCommon.kt b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/utilCommon.kt new file mode 100644 index 000000000..4726abb9f --- /dev/null +++ b/tools/compiler-common/src/main/kotlin/net/mamoe/mirai/console/compiler/common/utilCommon.kt @@ -0,0 +1,33 @@ +/* + * 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.compiler.common + +import org.jetbrains.kotlin.name.FqName +import kotlin.contracts.contract + +val SERIALIZABLE_FQ_NAME = FqName("kotlinx.serialization.Serializable") + +fun <K, V> Map<K, V>.firstValue(): V = this.entries.first().value +fun <K, V> Map<K, V>.firstKey(): K = this.entries.first().key + + +inline fun <reified T : Any> Any?.castOrNull(): T? { + contract { + returnsNotNull() implies (this@castOrNull is T) + } + return this as? T +} + +inline fun <reified T : Any> Any?.cast(): T { + contract { + returns() implies (this@cast is T) + } + return this as T +} \ No newline at end of file diff --git a/tools/gradle-plugin/README.md b/tools/gradle-plugin/README.md new file mode 100644 index 000000000..679bb73d7 --- /dev/null +++ b/tools/gradle-plugin/README.md @@ -0,0 +1,47 @@ +# Mirai Console Gradle Plugin + +Mirai Console Gradle 插件。 + +## 使用 + +参考 [ConfiguringProjects](../../docs/ConfiguringProjects.md#gradle)[ + +## 功能 + +- 为 `main` 源集配置 `mirai-core`,`mirai-console` 依赖 +- 为 `test` 源集配置 `mirai-core-qqandroid`, `mirai-console-terminal` 的依赖 (用于启动测试) +- 添加 mirai 依赖仓库链接 +- 配置插件 JAR 打包构建任务 `buildPlugin` (带依赖) + + +### `buildPlugin` + +用于打包插件和依赖为可以放入 Mirai Console `plugins` 目录加载的插件 JAR。 + +#### 执行 `buildPlugin` +```shell script +$ gradlew buildPlugin +``` + +打包结果存放在 `build/mirai/` 目录下。 + +## 配置 + +若要修改 Mirai Console Gradle 插件的默认配置,在 `build.gradle.kts` 或 `build.gradle` 内,使用 `mirai`: +```kotlin +mirai { // this: MiraiConsoleExtension + // 配置 +} +``` + +DSL 详见 [MiraiConsoleExtension](src/main/kotlin/net/mamoe/mirai/console/gradle/MiraiConsoleExtension.kt)。 + +#### 排除依赖 + +如果要在打包 JAR(`buildPlugin`)时排除一些依赖,请使用如下配置: + +```kotlin +mirai { + excludeDependency("com.google.code.gson", "gson") +} +``` diff --git a/tools/gradle-plugin/build.gradle.kts b/tools/gradle-plugin/build.gradle.kts new file mode 100644 index 000000000..d83c375a2 --- /dev/null +++ b/tools/gradle-plugin/build.gradle.kts @@ -0,0 +1,120 @@ +@file:Suppress("UnusedImport") + +plugins { + kotlin("jvm") + kotlin("kapt") + id("java-gradle-plugin") + id("com.gradle.plugin-publish") + id("java") + //signing + `maven-publish` + id("com.jfrog.bintray") + + id("com.github.johnrengelman.shadow") +} + +dependencies { + compileOnly(gradleApi()) + compileOnly(kotlin("gradle-plugin-api").toString()) { + exclude("org.jetbrains.kotlin", "kotlin-stdlib") + } + compileOnly(kotlin("gradle-plugin").toString()) { + exclude("org.jetbrains.kotlin", "kotlin-stdlib") + } + + compileOnly(kotlin("stdlib")) + + api("com.github.jengelman.gradle.plugins:shadow:6.0.0") + api("org.jetbrains:annotations:19.0.0") +} + +dependencies { + testApi(kotlin("test")) + testApi(kotlin("test-junit5")) + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.2.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0") +} + +version = Versions.console +description = "Gradle plugin for Mirai Console" + +pluginBundle { + website = "https://github.com/mamoe/mirai-console" + vcsUrl = "https://github.com/mamoe/mirai-console" + tags = listOf("framework", "kotlin", "mirai") +} + +gradlePlugin { + plugins { + create("miraiConsole") { + id = "net.mamoe.mirai-console" + displayName = "Mirai Console" + description = project.description + implementationClass = "net.mamoe.mirai.console.gradle.MiraiConsoleGradlePlugin" + } + } +} + +kotlin { + sourceSets.all { + target.compilations.all { + kotlinOptions { + apiVersion = "1.3" + languageVersion = "1.3" + jvmTarget = "1.8" + freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" + } + } + languageSettings.apply { + progressiveMode = true + + useExperimentalAnnotation("kotlin.RequiresOptIn") + useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") + useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") + } + } +} + +tasks { + "test"(Test::class) { + useJUnitPlatform() + } + + val compileKotlin by getting {} + + @Suppress("UNUSED_VARIABLE") + val fillBuildConstants by registering { + group = "mirai" + doLast { + (compileKotlin as org.jetbrains.kotlin.gradle.tasks.KotlinCompile).source.filter { it.name == "VersionConstants.kt" }.single() + .let { file -> + file.writeText( + file.readText() + .replace( + Regex("""const val CONSOLE_VERSION = ".*"""") + ) { + """const val CONSOLE_VERSION = "${Versions.console}"""" + } + .replace( + Regex("""const val CORE_VERSION = ".*"""") + ) { """const val CORE_VERSION = "${Versions.core}"""" } + ) + } + } + } + + compileKotlin.dependsOn(fillBuildConstants) +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(JavaCompile::class.java) { + options.encoding = "UTF8" +} + +setupPublishing("mirai-console-gradle") \ No newline at end of file diff --git a/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/IGNORED_DEPENDENCIES_IN_SHADOW.kt b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/IGNORED_DEPENDENCIES_IN_SHADOW.kt new file mode 100644 index 000000000..4b619f28f --- /dev/null +++ b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/IGNORED_DEPENDENCIES_IN_SHADOW.kt @@ -0,0 +1,132 @@ +/* + * 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 + */ + +@file:JvmMultifileClass +@file:JvmName("MiraiConsoleGradlePluginKt") + +package net.mamoe.mirai.console.gradle + +internal val IGNORED_DEPENDENCIES_IN_SHADOW = arrayOf( + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlin:kotlin-stdlib-common", + "org.jetbrains.kotlin:kotlin-stdlib-metadata", + "org.jetbrains.kotlin:kotlin-stdlib-jvm", + "org.jetbrains.kotlin:kotlin-stdlib-jdk7", + "org.jetbrains.kotlin:kotlin-stdlib-jdk8", + + "org.jetbrains.kotlin:kotlin-reflect", + "org.jetbrains.kotlin:kotlin-reflect-common", + "org.jetbrains.kotlin:kotlin-reflect-metadata", + "org.jetbrains.kotlin:kotlin-reflect-jvm", + + "org.jetbrains.kotlinx:kotlinx-serialization-core", + "org.jetbrains.kotlinx:kotlinx-serialization-core-common", + "org.jetbrains.kotlinx:kotlinx-serialization-core-metadata", + "org.jetbrains.kotlinx:kotlinx-serialization-core-jvm", + + "org.jetbrains.kotlinx:kotlinx-serialization-runtime", + "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common", + "org.jetbrains.kotlinx:kotlinx-serialization-runtime-metadata", + "org.jetbrains.kotlinx:kotlinx-serialization-runtime-jvm", + + "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", + "org.jetbrains.kotlinx:kotlinx-serialization-protobuf-common", + "org.jetbrains.kotlinx:kotlinx-serialization-protobuf-metadata", + "org.jetbrains.kotlinx:kotlinx-serialization-protobuf-jvm", + + "org.jetbrains.kotlinx:kotlinx-coroutines-core", + "org.jetbrains.kotlinx:kotlinx-coroutines-core-common", + "org.jetbrains.kotlinx:kotlinx-coroutines-core-metadata", + "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", + + "org.jetbrains.kotlinx:kotlinx-io", + "org.jetbrains.kotlinx:kotlinx-io-common", + "org.jetbrains.kotlinx:kotlinx-io-metadata", + "org.jetbrains.kotlinx:kotlinx-io-jvm", + + "org.jetbrains.kotlinx:kotlinx-coroutines-io", + "org.jetbrains.kotlinx:kotlinx-coroutines-io-common", + "org.jetbrains.kotlinx:kotlinx-coroutines-io-metadata", + "org.jetbrains.kotlinx:kotlinx-coroutines-io-jvm", + + "org.jetbrains.kotlinx:kotlinx-coroutines-core-jdk7", + "org.jetbrains.kotlinx:kotlinx-coroutines-core-jdk8", + + "org.jetbrains.kotlinx:kotlinx-coroutines-jdk7", + "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", + + "org.jetbrains.kotlinx:atomicFu", + "org.jetbrains.kotlinx:atomicFu-common", + "org.jetbrains.kotlinx:atomicFu-metadata", + "org.jetbrains.kotlinx:atomicFu-jvm", + + "org.jetbrains:annotations:19.0.0", + + "io.ktor:ktor-client", + "io.ktor:ktor-client-common", + "io.ktor:ktor-client-metadata", + "io.ktor:ktor-client-jvm", + + "io.ktor:ktor-client-cio", + "io.ktor:ktor-client-cio-common", + "io.ktor:ktor-client-cio-metadata", + "io.ktor:ktor-client-cio-jvm", + + "io.ktor:ktor-client-core", + "io.ktor:ktor-client-core-common", + "io.ktor:ktor-client-core-metadata", + "io.ktor:ktor-client-core-jvm", + + "io.ktor:ktor-client-network", + "io.ktor:ktor-client-network-common", + "io.ktor:ktor-client-network-metadata", + "io.ktor:ktor-client-network-jvm", + + "io.ktor:ktor-client-util", + "io.ktor:ktor-client-util-common", + "io.ktor:ktor-client-util-metadata", + "io.ktor:ktor-client-util-jvm", + + "io.ktor:ktor-client-http", + "io.ktor:ktor-client-http-common", + "io.ktor:ktor-client-http-metadata", + "io.ktor:ktor-client-http-jvm", + + "org.bouncyCastle:bcProv-jdk15on", + + "net.mamoe:mirai-core", + "net.mamoe:mirai-core-metadata", + "net.mamoe:mirai-core-common", + "net.mamoe:mirai-core-jvm", + + "net.mamoe:mirai-core-api", // for future + "net.mamoe:mirai-core-api-metadata", + "net.mamoe:mirai-core-api-common", + "net.mamoe:mirai-core-api-jvm", + + "net.mamoe:mirai-core-qqAndroid", + "net.mamoe:mirai-core-qqAndroid-metadata", + "net.mamoe:mirai-core-qqAndroid-common", + "net.mamoe:mirai-core-qqAndroid-jvm", + + "net.mamoe:mirai-console", + "net.mamoe:mirai-console-api", // for future + "net.mamoe:mirai-console-terminal", + "net.mamoe:mirai-console-graphical", + + "net.mamoe.yamlKt:yamlKt", + "net.mamoe.yamlKt:yamlKt-common", + "net.mamoe.yamlKt:yamlKt-metadata", + "net.mamoe.yamlKt:yamlKt-jvm", + + "net.mamoe:kotlin-jvm-blocking-bridge", + "net.mamoe:kotlin-jvm-blocking-bridge-common", + "net.mamoe:kotlin-jvm-blocking-bridge-metadata", + "net.mamoe:kotlin-jvm-blocking-bridge-jvm" +).map { it.toLowerCase() }.map { MiraiConsoleExtension.ExcludedDependency(it.substringBefore(':'), it.substringAfterLast(':')) }.toTypedArray() \ No newline at end of file diff --git a/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/MiraiConsoleExtension.kt b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/MiraiConsoleExtension.kt new file mode 100644 index 000000000..74dc3043a --- /dev/null +++ b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/MiraiConsoleExtension.kt @@ -0,0 +1,126 @@ +/* + * 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 + */ + +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.gradle + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.JavaVersion + +/** + * ``` + * mirai { + * // 配置 + * } + * ``` + */ +// must be open +open class MiraiConsoleExtension { + /** + * 为 `true` 时不自动添加 mirai-core 的依赖 + * + * 默认: `false` + */ + var noCore: Boolean = false + + /** + * 为 `true` 时不自动为 test 模块添加 mirai-core-qqandroid 的依赖. + * + * 默认: `false` + */ + var noTestCoreQQAndroid: Boolean = false + + /** + * 为 `true` 时不自动添加 mirai-console 的依赖. + * + * 默认: `false` + */ + var noConsole: Boolean = false + + /** + * 自动添加的 mirai-core 和 mirai-core-qqandroid 的版本. + * + * 默认: 与本 Gradle 插件编译时的 mirai-core 版本相同. [VersionConstants.CORE_VERSION] + */ + var coreVersion: String = VersionConstants.CORE_VERSION + + /** + * 自动添加的 mirai-console 后端和前端的版本. + * + * 默认: 与本 Gradle 插件版本相同. [VersionConstants.CONSOLE_VERSION] + */ + var consoleVersion: String = VersionConstants.CONSOLE_VERSION + + /** + * 自动为 test 模块添加的前端依赖名称 + * + * 为 `null` 时不自动为 test 模块添加前端依赖. + * + * 默认: [MiraiConsoleFrontEndKind.TERMINAL] + */ + var useTestConsoleFrontEnd: MiraiConsoleFrontEndKind? = MiraiConsoleFrontEndKind.TERMINAL + + /** + * Java 和 Kotlin 编译目标. 至少为 [JavaVersion.VERSION_1_8]. + * + * 一般人不需要修改此项. + * + * 默认: [JavaVersion.VERSION_1_8] + */ + var jvmTarget: JavaVersion = JavaVersion.VERSION_1_8 + + /** + * 默认会配置 Kotlin 编译器参数 "-Xjvm-default=all". 将此项设置为 `false` 可避免配置. + * + * 一般人不需要修改此项. + * + * 默认: `false` + */ + var dontConfigureKotlinJvmDefault: Boolean = false + + internal val shadowConfigurations: MutableList<ShadowJar.() -> Unit> = mutableListOf() + internal val excludedDependencies: MutableSet<ExcludedDependency> = mutableSetOf() + + internal data class ExcludedDependency( + val group: String, + val name: String + ) + + /** + * 配置 [ShadowJar] (即打包插件) + */ + fun configureShadow(configure: ShadowJar.() -> Unit) { + shadowConfigurations.add(configure) + } + + /** + * 在插件打包时忽略一个依赖 + * + * @param notation 格式为 "groupId:name". 如 "org.jetbrains.kotlin:kotlin-stdlib" + */ + fun excludeDependency(notation: String) { + requireNotNull(notation.count { it == ':' } == 1) { "Invalid dependency notation $notation." } + excludedDependencies.add(ExcludedDependency(notation.substringBefore(':'), notation.substringAfter(':'))) + } + + /** + * 在插件打包时忽略一个依赖 + * + * @param group 如 "org.jetbrains.kotlin" + * @param name 如 "kotlin-stdlib" + */ + fun excludeDependency(group: String, name: String) { + excludedDependencies.add(ExcludedDependency(group, name)) + } +} + +enum class MiraiConsoleFrontEndKind { + TERMINAL, +} \ No newline at end of file diff --git a/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/MiraiConsoleGradlePlugin.kt b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/MiraiConsoleGradlePlugin.kt new file mode 100644 index 000000000..051502390 --- /dev/null +++ b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/MiraiConsoleGradlePlugin.kt @@ -0,0 +1,156 @@ +/* + * 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 + */ + +@file:JvmMultifileClass +@file:JvmName("MiraiConsoleGradlePluginKt") + +package net.mamoe.mirai.console.gradle + +import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.compile.JavaCompile +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +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.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget + +class MiraiConsoleGradlePlugin : Plugin<Project> { + companion object { + internal const val BINTRAY_REPOSITORY_URL = "https://dl.bintray.com/him188moe/mirai" + } + + private fun KotlinSourceSet.configureSourceSet(project: Project) { + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + dependencies { configureDependencies(project, this@configureSourceSet) } + } + + private fun Project.configureTarget(target: KotlinTarget) { + val miraiExtension = project.miraiExtension + + for (compilation in target.compilations) with(compilation) { + kotlinOptions { + if (this !is KotlinJvmOptions) return@kotlinOptions + jvmTarget = miraiExtension.jvmTarget.toString() + if (!miraiExtension.dontConfigureKotlinJvmDefault) freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" + } + } + target.compilations.flatMap { it.allKotlinSourceSets }.forEach { sourceSet -> + sourceSet.configureSourceSet(project) + } + } + + @Suppress("SpellCheckingInspection") + private fun KotlinDependencyHandler.configureDependencies(project: Project, sourceSet: KotlinSourceSet) { + val miraiExtension = project.miraiExtension + + if (!miraiExtension.noCore) compileOnly("net.mamoe:mirai-core:${miraiExtension.coreVersion}") + if (!miraiExtension.noConsole) 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.noTestCoreQQAndroid) api("net.mamoe:mirai-core-qqandroid:${miraiExtension.coreVersion}") + when (miraiExtension.useTestConsoleFrontEnd) { + MiraiConsoleFrontEndKind.TERMINAL -> api("net.mamoe:mirai-console-terminal:${miraiExtension.consoleVersion}") + } + } + } + + private fun Project.configureCompileTarget() { + extensions.findByType(JavaPluginExtension::class.java)?.apply { + val miraiExtension = miraiExtension + sourceCompatibility = miraiExtension.jvmTarget + targetCompatibility = miraiExtension.jvmTarget + } + + tasks.withType(JavaCompile::class.java) { + it.options.encoding = "UTF8" + } + } + + private fun Project.registerBuildPluginTasks() { + val miraiExtension = this.miraiExtension + + 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@{ + group = "mirai" + + val compilations = target.compilations.filter { it.name == MAIN_COMPILATION_NAME } + + compilations.forEach { + dependsOn(it.compileKotlinTask) + from(it.output) + } + + from(project.configurations.getByName("runtimeClasspath").copyRecursive { dependency -> + for (excludedDependency in IGNORED_DEPENDENCIES_IN_SHADOW + miraiExtension.excludedDependencies) { + if (excludedDependency.group == dependency.group + && excludedDependency.name == dependency.name + ) return@copyRecursive false + } + true + }) + + exclude { file -> + file.name.endsWith(".sf", ignoreCase = true) + } + + destinationDirectory.value(project.layout.projectDirectory.dir(project.buildDir.name).dir("mirai")) + + miraiExtension.shadowConfigurations.forEach { it.invoke(this@shadow) } + } + } + + val targets = kotlinTargets + val isSingleTarget = targets.size == 1 + targets.forEach { target -> + registerBuildPluginTask(target, isSingleTarget) + } + } + + override fun apply(target: Project): Unit = with(target) { + target.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) } + + afterEvaluate { + configureCompileTarget() + registerBuildPluginTasks() + kotlinTargets.forEach { configureTarget(it) } + } + } +} + +internal val Project.miraiExtension: MiraiConsoleExtension + get() = extensions.findByType(MiraiConsoleExtension::class.java) ?: error("Cannot find MiraiConsoleExtension in project ${this.name}") + +internal val Project.kotlinTargets: Collection<KotlinTarget> + get() { + val kotlinExtension = extensions.findByType(KotlinProjectExtension::class.java) + ?: error("Kotlin plugin not applied. Please read https://www.kotlincn.net/docs/reference/using-gradle.html") + + return when (kotlinExtension) { + is KotlinMultiplatformExtension -> kotlinExtension.targets + is KotlinSingleTargetExtension -> listOf(kotlinExtension.target) + else -> error("[MiraiConsole] Internal error: kotlinExtension is neither KotlinMultiplatformExtension nor KotlinSingleTargetExtension") + } + } \ No newline at end of file diff --git a/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/VersionConstants.kt b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/VersionConstants.kt new file mode 100644 index 000000000..6c293ee10 --- /dev/null +++ b/tools/gradle-plugin/src/main/kotlin/net/mamoe/mirai/console/gradle/VersionConstants.kt @@ -0,0 +1,15 @@ +/* + * 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 + +internal object VersionConstants { + const val CONSOLE_VERSION = "1.0-RC-dev-30" // value is written here automatically during build + const val CORE_VERSION = "1.3.0" // value is written here automatically during build +} \ No newline at end of file diff --git a/tools/intellij-plugin/.images/ILLEGAL_PLUGIN_DESCRIPTION.png b/tools/intellij-plugin/.images/ILLEGAL_PLUGIN_DESCRIPTION.png new file mode 100644 index 000000000..830a907e4 Binary files /dev/null and b/tools/intellij-plugin/.images/ILLEGAL_PLUGIN_DESCRIPTION.png differ diff --git a/tools/intellij-plugin/README.md b/tools/intellij-plugin/README.md new file mode 100644 index 000000000..1d79320c2 --- /dev/null +++ b/tools/intellij-plugin/README.md @@ -0,0 +1,17 @@ +# mirai-console-intellij + +IntelliJ 平台的 Mirai Console 开发插件 + +## 功能 + +### 诊断 + +#### ILLEGAL_PLUGIN_DESCRIPTION + +[PluginDescriptionChecker.kt](src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDescriptionChecker.kt#L34) + +- 使用 [ResolveContext](../../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/compiler/common/ResolveContext.kt) +- 检测 Plugin Id, Plugin Name, Plugin Version 的合法性. 并在非法时提示正确的语法. +- 支持编译期常量 + +![ILLEGAL_PLUGIN_DESCRIPTION](.images/ILLEGAL_PLUGIN_DESCRIPTION.png) diff --git a/tools/intellij-plugin/build.gradle.kts b/tools/intellij-plugin/build.gradle.kts new file mode 100644 index 000000000..2c02d5f7c --- /dev/null +++ b/tools/intellij-plugin/build.gradle.kts @@ -0,0 +1,120 @@ +@file:Suppress("UnusedImport") + +plugins { + kotlin("jvm") + id("java") + `maven-publish` + id("com.jfrog.bintray") + + id("org.jetbrains.intellij") version "0.4.16" + +} + +repositories { + maven("http://maven.aliyun.com/nexus/content/groups/public/") +} + +version = Versions.console +description = "IntelliJ plugin for Mirai Console" + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(JavaCompile::class.java) { + options.encoding = "UTF8" +} + +// See https://github.com/JetBrains/gradle-intellij-plugin/ +intellij { + version = "2020.2.1" + isDownloadSources = true + updateSinceUntilBuild = false + + setPlugins( + "org.jetbrains.kotlin:1.4.10-release-IJ2020.2-1@staging", + "java" + ) +} + +tasks.getByName("publishPlugin", org.jetbrains.intellij.tasks.PublishTask::class) { + val pluginKey = project.findProperty("jetbrains.hub.key")?.toString() + if (pluginKey != null) { + logger.info("Found jetbrains.hub.key") + setToken(pluginKey) + } else { + logger.info("jetbrains.hub.key not found") + } +} + +tasks.withType<org.jetbrains.intellij.tasks.PatchPluginXmlTask> { + sinceBuild("193.*") + untilBuild("205.*") + pluginDescription(""" + Plugin development support for <a href='https://github.com/mamoe/mirai-console'>Mirai Console</a> + + <h3>Features</h3> + <ul> + <li>Inspections for plugin properties, for example, checking PluginDescription.</li> + <li>Inspections for illegal calls.</li> + <li>Intentions for resolving serialization problems.</li> + </ul> + """.trimIndent()) + changeNotes(""" + Initial release + """.trimIndent()) +} + +kotlin { + sourceSets.all { + target.compilations.all { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" + //useIR = true + } + } + languageSettings.apply { + progressiveMode = true + + useExperimentalAnnotation("kotlin.Experimental") + useExperimentalAnnotation("kotlin.RequiresOptIn") + + useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") + useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI") + useExperimentalAnnotation("net.mamoe.mirai.console.ConsoleFrontEndImplementation") + useExperimentalAnnotation("net.mamoe.mirai.console.util.ConsoleExperimentalApi") + useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") + useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") + useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") + useExperimentalAnnotation("net.mamoe.mirai.console.util.ConsoleInternalApi") + } + } +} + +dependencies { + api("org.jetbrains:annotations:19.0.0") + api(kotlinx("coroutines-jdk8", Versions.coroutines)) + + api(project(":mirai-console-compiler-common")) + + compileOnly("org.jetbrains.kotlin:kotlin-compiler:${Versions.kotlinCompiler}") + compileOnly("org.jetbrains.kotlin:kotlin-compiler:${Versions.kotlinCompiler}") + compileOnly(files("libs/ide-common.jar")) + + testApi(kotlin("test")) + testApi(kotlin("test-junit5")) + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.2.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0") +} + +tasks { + "test"(Test::class) { + useJUnitPlatform() + } +} + +setupPublishing("mirai-console-intellij") \ No newline at end of file diff --git a/tools/intellij-plugin/libs/ide-common.jar b/tools/intellij-plugin/libs/ide-common.jar new file mode 100644 index 000000000..724e639bd Binary files /dev/null and b/tools/intellij-plugin/libs/ide-common.jar differ diff --git a/tools/intellij-plugin/run/projects/.gitignore b/tools/intellij-plugin/run/projects/.gitignore new file mode 100644 index 000000000..3d1806f15 --- /dev/null +++ b/tools/intellij-plugin/run/projects/.gitignore @@ -0,0 +1,5 @@ +local.properties +build/ +build +.gradle +.idea diff --git a/tools/intellij-plugin/run/projects/test-project/build.gradle.kts b/tools/intellij-plugin/run/projects/test-project/build.gradle.kts new file mode 100644 index 000000000..e1c0ff8b7 --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + kotlin("jvm") version "1.4.0" + kotlin("plugin.serialization") version "1.4.0" + kotlin("kapt") version "1.4.0" + id("com.github.johnrengelman.shadow") version "5.2.0" +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenLocal() + jcenter() + mavenCentral() +} + +kotlin.sourceSets.all { + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") +} + +dependencies { + compileOnly(kotlin("stdlib-jdk8")) + + val core = "1.3.0" + val console = "1.0-RC-dev-28" + + 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-pure:$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/gradle.properties b/tools/intellij-plugin/run/projects/test-project/gradle.properties new file mode 100644 index 000000000..7fc6f1ff2 --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.jar b/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..490fda857 Binary files /dev/null and b/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.properties b/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..a4b442974 --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/tools/intellij-plugin/run/projects/test-project/gradlew b/tools/intellij-plugin/run/projects/test-project/gradlew new file mode 100644 index 000000000..2fe81a7d9 --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/tools/intellij-plugin/run/projects/test-project/gradlew.bat b/tools/intellij-plugin/run/projects/test-project/gradlew.bat new file mode 100644 index 000000000..62bd9b9cc --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/tools/intellij-plugin/run/projects/test-project/settings.gradle.kts b/tools/intellij-plugin/run/projects/test-project/settings.gradle.kts new file mode 100644 index 000000000..0e9d380ad --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "test-project" + 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 new file mode 100644 index 000000000..b010e4c9f --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt @@ -0,0 +1,45 @@ +package org.example.myplugin + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.console.data.AutoSavePluginConfig +import net.mamoe.mirai.console.data.value +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 + +val T = "org.example" // 编译期常量 + +object MyPluginMain : KotlinPlugin( + JvmPluginDescription( + T, + "0.1.0", + ) { + name(".") + } +) { + override fun onEnable() { + super.onEnable() + PermissionService.INSTANCE.register(permissionId("dvs"), "ok") + } + + fun test() { + + } +} + +object DataTest : AutoSavePluginConfig("data") { + val p by value<HasDefaultValue>() + val pp by value<NoDefaultValue>() +} + +@Serializable +data class HasDefaultValue( + val x: Int = 0, +) + +data class NoDefaultValue( + val y: Int, +) + +val y = "傻逼 yellow" \ No newline at end of file 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 new file mode 100644 index 000000000..30699c49a --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MySimpleCommand.kt @@ -0,0 +1,14 @@ +package org.example.myplugin + +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.SimpleCommand + +object MySimpleCommand000 : SimpleCommand( + MyPluginMain, "foo", + description = "示例指令" +) { + @Handler + suspend fun CommandSender.handle(int: Int, str: String) { + + } +} diff --git a/tools/intellij-plugin/run/projects/test-project/src/test/kotlin/RunConsole.kt b/tools/intellij-plugin/run/projects/test-project/src/test/kotlin/RunConsole.kt new file mode 100644 index 000000000..1e011b60a --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/src/test/kotlin/RunConsole.kt @@ -0,0 +1,17 @@ +import net.mamoe.mirai.alsoLogin +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable +import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.load +import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader +import org.example.myplugin.MyPluginMain + +suspend fun main() { + MiraiConsoleTerminalLoader.startAsDaemon() + + MyPluginMain.load() // 主动加载插件, Console 会调用 MyPluginMain.onLoad + MyPluginMain.enable() // 主动启用插件, Console 会调用 MyPluginMain.onEnable + + val bot = MiraiConsole.addBot(123456, "").alsoLogin() // 登录一个测试环境的 Bot + + MiraiConsole.job.join() +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/IDEContainerContributor.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/IDEContainerContributor.kt new file mode 100644 index 000000000..772e5acb4 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/IDEContainerContributor.kt @@ -0,0 +1,28 @@ +/* + * 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.intellij + +import net.mamoe.mirai.console.intellij.diagnostics.ContextualParametersChecker +import net.mamoe.mirai.console.intellij.diagnostics.PluginDataValuesChecker +import org.jetbrains.kotlin.container.StorageComponentContainer +import org.jetbrains.kotlin.container.useInstance +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor + +class IDEContainerContributor : StorageComponentContainerContributor { + override fun registerModuleComponents( + container: StorageComponentContainer, + platform: org.jetbrains.kotlin.platform.TargetPlatform, + moduleDescriptor: ModuleDescriptor, + ) { + container.useInstance(ContextualParametersChecker()) + container.useInstance(PluginDataValuesChecker()) + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/Icons.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/Icons.kt new file mode 100644 index 000000000..ae7217290 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/Icons.kt @@ -0,0 +1,18 @@ +/* + * 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.intellij + +import com.intellij.openapi.util.IconLoader +import javax.swing.Icon + +object Icons { + val CommandDeclaration: Icon = IconLoader.getIcon("/icons/commandDeclaration.svg") + val PluginMainDeclaration: Icon = IconLoader.getIcon("/icons/pluginMainDeclaration.png") +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/QuickFixRegistrar.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/QuickFixRegistrar.kt new file mode 100644 index 000000000..6e338736c --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/QuickFixRegistrar.kt @@ -0,0 +1,23 @@ +package net.mamoe.mirai.console.intellij + +import com.intellij.codeInsight.intention.IntentionAction +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors +import net.mamoe.mirai.console.intellij.diagnostics.fix.AddSerializerFix +import org.jetbrains.kotlin.diagnostics.DiagnosticFactory +import org.jetbrains.kotlin.idea.quickfix.KotlinIntentionActionsFactory +import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor +import org.jetbrains.kotlin.idea.quickfix.QuickFixes + +class QuickFixRegistrar : QuickFixContributor { + override fun registerQuickFixes(quickFixes: QuickFixes) { + fun DiagnosticFactory<*>.registerFactory(vararg factory: KotlinIntentionActionsFactory) { + quickFixes.register(this, *factory) + } + + fun DiagnosticFactory<*>.registerActions(vararg action: IntentionAction) { + quickFixes.register(this, *action) + } + + MiraiConsoleErrors.UNSERIALIZABLE_TYPE.registerFactory(AddSerializerFix) + } +} diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/ContextualParametersChecker.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/ContextualParametersChecker.kt new file mode 100644 index 000000000..790ba9596 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/ContextualParametersChecker.kt @@ -0,0 +1,148 @@ +/* + * 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.intellij.diagnostics + +import com.intellij.psi.PsiElement +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.* +import net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind +import net.mamoe.mirai.console.compiler.common.resolve.resolveContextKind +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 org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker +import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext +import java.util.* + +/** + * Checks paramters with [ResolveContextKind] + */ +class ContextualParametersChecker : DeclarationChecker { + companion object { + private val ID_REGEX: Regex = Regex("""([a-zA-Z]+(?:\.[a-zA-Z0-9]+)*)\.([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)""") + private val FORBIDDEN_ID_NAMES: Array<String> = arrayOf("main", "console", "plugin", "config", "data") + + private const val syntax = """类似于 "net.mamoe.mirai.example-plugin", 其中 "net.mamoe.mirai" 为 groupId, "example-plugin" 为插件名""" + + /** + * https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + */ + 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? { + 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") + + val lowercaseId = value.toLowerCase() + + if (ID_REGEX.matchEntire(value) == null) { + return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件 Id 无效. 正确的插件 Id 应该满足正则表达式 '${ID_REGEX.pattern}', \n$syntax") + } + + FORBIDDEN_ID_NAMES.firstOrNull { it == lowercaseId }?.let { illegal -> + return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "'$illegal' 不允许作为插件 Id. 确保插件 Id 不完全是这个名称") + } + return null + } + + fun checkPluginName(inspectionTarget: PsiElement, value: String): Diagnostic? { + if (value.isBlank()) return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件名不能为空") + val lowercaseName = value.toLowerCase() + FORBIDDEN_ID_NAMES.firstOrNull { it == lowercaseName }?.let { illegal -> + return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "'$illegal' 不允许作为插件名. 确保插件名不完全是这个名称") + } + return null + } + + fun checkPluginVersion(inspectionTarget: PsiElement, 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? { + return when { + value.isBlank() -> ILLEGAL_COMMAND_NAME.on(inspectionTarget, value, "指令名不能为空") + value.any { it.isWhitespace() } -> ILLEGAL_COMMAND_NAME.on(inspectionTarget, value, "暂时不允许指令名中存在空格") + value.contains(':') -> ILLEGAL_COMMAND_NAME.on(inspectionTarget, value, "指令名不允许包含 ':'") + value.contains('.') -> ILLEGAL_COMMAND_NAME.on(inspectionTarget, value, "指令名不允许包含 '.'") + else -> null + } + } + + fun checkPermissionNamespace(inspectionTarget: PsiElement, value: String): Diagnostic? { + return when { + value.isBlank() -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "权限命名空间不能为空") + value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "暂时不允许权限命名空间中存在空格") + value.contains(':') -> ILLEGAL_PERMISSION_NAMESPACE.on(inspectionTarget, value, "权限命名空间不允许包含 ':'") + else -> null + } + } + + fun checkPermissionName(inspectionTarget: PsiElement, value: String): Diagnostic? { + return when { + value.isBlank() -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "权限名称不能为空") + value.any { it.isWhitespace() } -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "暂时不允许权限名称中存在空格") + value.contains(':') -> ILLEGAL_PERMISSION_NAME.on(inspectionTarget, value, "权限名称不允许包含 ':'") + else -> null + } + } + + fun checkPermissionId(inspectionTarget: PsiElement, 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 中存在空格") + value.count { it == ':' } != 1 -> ILLEGAL_PERMISSION_ID.on(inspectionTarget, value, "权限 Id 必须为 \"命名空间:名称\". 且命名空间和名称均不能包含 ':'") + else -> null + } + } + } + + private val checkersMap: EnumMap<ResolveContextKind, (declaration: PsiElement, value: String) -> Diagnostic?> = + EnumMap<ResolveContextKind, (declaration: PsiElement, value: String) -> Diagnostic?>(ResolveContextKind::class.java).apply { + put(ResolveContextKind.PLUGIN_NAME, ::checkPluginName) + put(ResolveContextKind.PLUGIN_ID, ::checkPluginId) + put(ResolveContextKind.PLUGIN_VERSION, ::checkPluginVersion) + put(ResolveContextKind.COMMAND_NAME, ::checkCommandName) + put(ResolveContextKind.PERMISSION_NAME, ::checkPermissionName) + put(ResolveContextKind.PERMISSION_NAMESPACE, ::checkPermissionNamespace) + put(ResolveContextKind.PERMISSION_ID, ::checkPermissionId) + } + + override fun check( + declaration: KtDeclaration, + descriptor: DeclarationDescriptor, + context: DeclarationCheckerContext, + ) { + declaration.resolveAllCalls(context.bindingContext) + .flatMap { call -> + call.valueParametersWithArguments().asSequence() + } + .mapNotNull { (p, a) -> + p.resolveContextKind?.let(checkersMap::get)?.let { it to a } + } + .mapNotNull { (kind, argument) -> + argument.resolveStringConstantValues()?.let { const -> + Triple(kind, argument, const) + } + } + .forEach { (fn, argument, resolvedConstants) -> + for (resolvedConstant in resolvedConstants) { + fn(argument.asElement(), resolvedConstant)?.let { context.report(it) } + } + } + return + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDataValuesChecker.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDataValuesChecker.kt new file mode 100644 index 000000000..4130b6ce3 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDataValuesChecker.kt @@ -0,0 +1,63 @@ +/* + * 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.intellij.diagnostics + +import com.intellij.psi.PsiElement +import net.mamoe.mirai.console.compiler.common.SERIALIZABLE_FQ_NAME +import net.mamoe.mirai.console.compiler.common.castOrNull +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors +import net.mamoe.mirai.console.compiler.common.resolve.* +import net.mamoe.mirai.console.intellij.resolve.resolveAllCallsWithElement +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.inspections.collections.isCalling +import org.jetbrains.kotlin.idea.refactoring.fqName.fqName +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker +import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext +import org.jetbrains.kotlin.types.SimpleType + + +class PluginDataValuesChecker : DeclarationChecker { + override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) { + val bindingContext = context.bindingContext + declaration.resolveAllCallsWithElement(bindingContext) + .filter { (call) -> call.isCalling(PLUGIN_DATA_VALUE_FUNCTIONS_FQ_FQ_NAME) } + .filter { (call) -> + call.resultingDescriptor.resolveContextKind == ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR + }.flatMap { (call, element) -> + call.typeArguments.entries.associateWith { element }.asSequence() + }.filter { (e, _) -> + val (p, t) = e + (p.isReified || p.resolveContextKind == ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) + && t is SimpleType + }.forEach { (e, callExpr) -> + val (_, type) = e + val classDescriptor = type.constructor.declarationDescriptor?.castOrNull<ClassDescriptor>() + + val inspectionTarget: PsiElement by lazy { + callExpr.typeArguments.find { it.references.firstOrNull()?.canonicalText == type.fqName?.toString() } ?: callExpr + } + + if (classDescriptor == null + || !classDescriptor.hasNoArgConstructor() + ) return@forEach context.report(MiraiConsoleErrors.NOT_CONSTRUCTABLE_TYPE.on( + inspectionTarget, + type.fqName?.asString().toString()) + ) + + if (!classDescriptor.hasAnnotation(SERIALIZABLE_FQ_NAME)) // TODO: 2020/9/18 external serializers + return@forEach context.report(MiraiConsoleErrors.UNSERIALIZABLE_TYPE.on( + inspectionTarget, + classDescriptor + )) + } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/diagnosticsUtil.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/diagnosticsUtil.kt new file mode 100644 index 000000000..6fd2874b4 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/diagnosticsUtil.kt @@ -0,0 +1,31 @@ +/* + * 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.intellij.diagnostics + +import net.mamoe.mirai.console.intellij.resolve.getResolvedCallOrResolveToCall +import org.jetbrains.kotlin.descriptors.CallableDescriptor +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +fun DeclarationCheckerContext.report(diagnostic: Diagnostic) { + return this.trace.report(diagnostic) +} + +val DeclarationCheckerContext.bindingContext get() = this.trace.bindingContext + +fun KtElement?.getResolvedCallOrResolveToCall( + context: DeclarationCheckerContext, + bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL, +): ResolvedCall<out CallableDescriptor>? { + return this.getResolvedCallOrResolveToCall(context.bindingContext, bodyResolveMode) +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/fix/AbuseYellowIntention.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/fix/AbuseYellowIntention.kt new file mode 100644 index 000000000..fbb08fea4 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/fix/AbuseYellowIntention.kt @@ -0,0 +1,30 @@ +/* + * 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.intellij.diagnostics.fix +/* + +import com.intellij.openapi.editor.Editor +import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValues +import org.jetbrains.kotlin.idea.intentions.SelfTargetingIntention +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtStringTemplateExpression + +@Suppress("IntentionDescriptionNotFoundInspection") // +class AbuseYellowIntention : + SelfTargetingIntention<KtStringTemplateExpression>(KtStringTemplateExpression::class.java, { "Abuse yellow" }, { "Abuse yellow" }) { + override fun applyTo(element: KtStringTemplateExpression, editor: Editor?) { + element.replace(KtPsiFactory(element).createExpression("\"弱智黄色\"")) + } + + override fun isApplicableTo(element: KtStringTemplateExpression, caretOffset: Int): Boolean { + return element.resolveStringConstantValues().firstOrNull() == "黄色" + } + +}*/ \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/fix/AddSerializerFix.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/fix/AddSerializerFix.kt new file mode 100644 index 000000000..c276b3c8c --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/fix/AddSerializerFix.kt @@ -0,0 +1,44 @@ +package net.mamoe.mirai.console.intellij.diagnostics.fix + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import net.mamoe.mirai.console.compiler.common.SERIALIZABLE_FQ_NAME +import net.mamoe.mirai.console.compiler.common.castOrNull +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.diagnostics.DiagnosticWithParameters1 +import org.jetbrains.kotlin.idea.inspections.KotlinUniversalQuickFix +import org.jetbrains.kotlin.idea.quickfix.KotlinCrossLanguageQuickFixAction +import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory +import org.jetbrains.kotlin.idea.util.addAnnotation +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtModifierListOwner + +/** + * @see MiraiConsoleErrors.UNSERIALIZABLE_TYPE + */ +class AddSerializerFix( + element: KtClassOrObject, +) : KotlinCrossLanguageQuickFixAction<KtModifierListOwner>(element), KotlinUniversalQuickFix { + + override fun getFamilyName(): String = "添加注解" + override fun getText(): String = "添加 @Serializable" + + override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) { + element?.addAnnotation(SERIALIZABLE_FQ_NAME) ?: return + } + + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val classDescriptor = diagnostic.castOrNull<DiagnosticWithParameters1<*, *>>()?.a?.castOrNull<ClassDescriptor>() ?: return null + val ktClassOrObject = classDescriptor.findPsi()?.castOrNull<KtClassOrObject>() ?: return null + return AddSerializerFix(ktClassOrObject) + } + + override fun isApplicableForCodeFragment(): Boolean = false + } +} diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/line/marker/CommandDeclarationLineMarkerProvider.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/line/marker/CommandDeclarationLineMarkerProvider.kt new file mode 100644 index 000000000..43ba32e21 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/line/marker/CommandDeclarationLineMarkerProvider.kt @@ -0,0 +1,50 @@ +/* + * 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.intellij.line.marker + +import com.intellij.codeHighlighting.Pass +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.psi.PsiElement +import net.mamoe.mirai.console.intellij.Icons +import net.mamoe.mirai.console.intellij.resolve.getElementForLineMark +import net.mamoe.mirai.console.intellij.resolve.isSimpleCommandHandlerOrCompositeCommandSubCommand +import org.jetbrains.kotlin.psi.KtNamedFunction + +class CommandDeclarationLineMarkerProvider : LineMarkerProvider { + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + if (element !is KtNamedFunction) return null + if (!element.isSimpleCommandHandlerOrCompositeCommandSubCommand()) return null + return Info(getElementForLineMark(element.funKeyword ?: element.identifyingElement ?: element)) + } + + @Suppress("DEPRECATION") + class Info( + callElement: PsiElement, + ) : LineMarkerInfo<PsiElement>( + callElement, + callElement.textRange, + Icons.CommandDeclaration, + Pass.LINE_MARKERS, + { + "子指令定义" + }, + null, + GutterIconRenderer.Alignment.RIGHT + ) { + override fun createGutterRenderer(): GutterIconRenderer? { + return object : LineMarkerInfo.LineMarkerGutterIconRenderer<PsiElement>(this) { + override fun getClickAction(): AnAction? = null + } + } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/line/marker/PluginMainLineMarkerProvider.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/line/marker/PluginMainLineMarkerProvider.kt new file mode 100644 index 000000000..b69cd1f4d --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/line/marker/PluginMainLineMarkerProvider.kt @@ -0,0 +1,61 @@ +/* + * 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.intellij.line.marker + +import com.intellij.codeHighlighting.Pass +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.psi.PsiElement +import com.intellij.util.castSafelyTo +import net.mamoe.mirai.console.compiler.common.resolve.PLUGIN_FQ_NAME +import net.mamoe.mirai.console.compiler.common.resolve.parents +import net.mamoe.mirai.console.intellij.Icons +import net.mamoe.mirai.console.intellij.resolve.allSuperNames +import net.mamoe.mirai.console.intellij.resolve.getElementForLineMark +import org.jetbrains.kotlin.nj2k.postProcessing.resolve +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtConstructor +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.jetbrains.kotlin.psi.KtReferenceExpression + +class PluginMainLineMarkerProvider : LineMarkerProvider { + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + if (element !is KtReferenceExpression) return null + val objectDeclaration = + element.parents.filterIsInstance<KtObjectDeclaration>().firstOrNull() ?: return null + val kotlinPluginClass = + element.resolve().castSafelyTo<KtConstructor<*>>()?.parent?.castSafelyTo<KtClass>() ?: return null + if (kotlinPluginClass.allSuperNames.none { it == PLUGIN_FQ_NAME }) return null + return Info(getElementForLineMark(objectDeclaration)) + } + + @Suppress("DEPRECATION") + class Info( + callElement: PsiElement, + ) : LineMarkerInfo<PsiElement>( + callElement, + callElement.textRange, + Icons.PluginMainDeclaration, + Pass.LINE_MARKERS, + { + "Mirai Console Plugin" + }, + null, + GutterIconRenderer.Alignment.RIGHT + ) { + override fun createGutterRenderer(): GutterIconRenderer? { + return object : LineMarkerInfo.LineMarkerGutterIconRenderer<PsiElement>(this) { + override fun getClickAction(): AnAction? = null + } + } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/resolve/resolveIdea.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/resolve/resolveIdea.kt new file mode 100644 index 000000000..723da4408 --- /dev/null +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/resolve/resolveIdea.kt @@ -0,0 +1,177 @@ +/* + * 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.intellij.resolve + +import com.intellij.psi.PsiDeclarationStatement +import com.intellij.psi.PsiElement +import net.mamoe.mirai.console.compiler.common.castOrNull +import net.mamoe.mirai.console.compiler.common.resolve.COMPOSITE_COMMAND_SUB_COMMAND_FQ_NAME +import net.mamoe.mirai.console.compiler.common.resolve.SIMPLE_COMMAND_HANDLER_COMMAND_FQ_NAME +import net.mamoe.mirai.console.compiler.common.resolve.allChildrenWithSelf +import net.mamoe.mirai.console.compiler.common.resolve.findParent +import org.jetbrains.kotlin.descriptors.CallableDescriptor +import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.descriptors.VariableDescriptor +import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName +import org.jetbrains.kotlin.idea.references.KtSimpleNameReference +import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.nj2k.postProcessing.resolve +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.calls.callUtil.getCall +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.constants.ArrayValue +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.constants.StringValue +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance + + +/** + * For CompositeCommand.SubCommand + */ +fun KtNamedFunction.isCompositeCommandSubCommand(): Boolean = this.hasAnnotation(COMPOSITE_COMMAND_SUB_COMMAND_FQ_NAME) + +/** + * SimpleCommand.Handler + */ +fun KtNamedFunction.isSimpleCommandHandler(): Boolean = this.hasAnnotation(SIMPLE_COMMAND_HANDLER_COMMAND_FQ_NAME) + +fun KtNamedFunction.isSimpleCommandHandlerOrCompositeCommandSubCommand(): Boolean = + this.isSimpleCommandHandler() || this.isCompositeCommandSubCommand() + + +val KtPureClassOrObject.allSuperTypes: Sequence<KtSuperTypeListEntry> + get() = sequence { + yieldAll(superTypeListEntries) + for (list in superTypeListEntries.asSequence()) { + yieldAll((list.typeAsUserType?.referenceExpression?.resolve() as? KtClass)?.allSuperTypes.orEmpty()) + } + } + +fun KtConstructorCalleeExpression.getTypeAsUserType(): KtUserType? { + val reference = typeReference + if (reference != null) { + val element = reference.typeElement + if (element is KtUserType) { + return element + } + } + return null +} + +val KtClassOrObject.allSuperNames: Sequence<FqName> get() = allSuperTypes.mapNotNull { it.getKotlinFqName() } + +fun getElementForLineMark(callElement: PsiElement): PsiElement = + when (callElement) { + is KtSimpleNameExpression -> callElement.getReferencedNameElement() + else -> + // a fallback, + //but who knows what to reference in KtArrayAccessExpression ? + generateSequence(callElement, { it.firstChild }).last() + } + +val KtAnnotationEntry.annotationClass: KtClass? + get() = calleeExpression?.constructorReferenceExpression?.resolve()?.findParent<KtClass>() + +fun KtAnnotated.hasAnnotation(fqName: FqName): Boolean = + this.annotationEntries.any { it.annotationClass?.getKotlinFqName() == fqName } + +fun KtDeclaration.resolveAllCalls(bindingContext: BindingContext): Sequence<ResolvedCall<*>> { + return allChildrenWithSelf + .filterIsInstance<KtCallExpression>() + .mapNotNull { it.calleeExpression?.getResolvedCallOrResolveToCall(bindingContext) } +} + +fun KtDeclaration.resolveAllCallsWithElement(bindingContext: BindingContext): Sequence<Pair<ResolvedCall<out CallableDescriptor>, KtCallExpression>> { + return allChildrenWithSelf + .filterIsInstance<KtCallExpression>() + .mapNotNull { + val callee = it.calleeExpression ?: return@mapNotNull null + val resolved = callee.getResolvedCall(bindingContext) ?: return@mapNotNull null + + resolved to it + } +} + +fun ResolvedCall<*>.valueParametersWithArguments(): List<Pair<ValueParameterDescriptor, ValueArgument>> { + return this.valueParameters.zip(this.valueArgumentsByIndex?.mapNotNull { it.arguments.firstOrNull() }.orEmpty()) +} + +fun ValueArgument.resolveStringConstantValues(): Sequence<String>? { + return this.getArgumentExpression()?.resolveStringConstantValues() +} + +val PsiElement.allChildrenFlat: Sequence<PsiElement> + get() { + return sequence { + for (child in children) { + yield(child) + yieldAll(child.allChildrenFlat) + } + } + } + +inline fun <reified E> PsiElement.findChild(): E? = this.children.find { it is E } as E? + +fun KtElement?.getResolvedCallOrResolveToCall( + context: BindingContext, + bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL, +): ResolvedCall<out CallableDescriptor>? { + return this?.getCall(context)?.getResolvedCall(context)// ?: this?.resolveToCall(bodyResolveMode) +} + +val ResolvedCall<out CallableDescriptor>.valueParameters: List<ValueParameterDescriptor> get() = this.resultingDescriptor.valueParameters + +fun ConstantValue<*>.selfOrChildrenConstantStrings(): Sequence<String> { + return when (this) { + is StringValue -> sequenceOf(value) + is ArrayValue -> sequence { + yieldAll(this@selfOrChildrenConstantStrings.selfOrChildrenConstantStrings()) + } + else -> emptySequence() + } +} + +fun KtExpression.resolveStringConstantValues(): Sequence<String> { + when (this) { + is KtNameReferenceExpression -> { + when (val reference = references.firstIsInstance<KtSimpleNameReference>().resolve()) { + is KtDeclaration -> { + val descriptor = reference.descriptor.castOrNull<VariableDescriptor>() ?: return emptySequence() + val compileTimeConstant = descriptor.compileTimeInitializer ?: return emptySequence() + return compileTimeConstant.selfOrChildrenConstantStrings() + } + is PsiDeclarationStatement -> { + // TODO: 2020/9/18 compile-time constants from Java + } + } + } + is KtStringTemplateExpression -> { + if (hasInterpolation()) return emptySequence() + return sequenceOf(entries.joinToString("") { it.text }) + } + /* + is KtCallExpression -> { + val callee = this.calleeExpression?.getResolvedCallOrResolveToCall(bindingContext)?.resultingDescriptor + if (callee is VariableDescriptor) { + val compileTimeConstant = callee.compileTimeInitializer ?: return null + return compileTimeConstant.castOrNull<StringValue>()?.value + } + return null + }*/ + is KtConstantExpression -> { + // TODO: 2020/9/18 KtExpression.resolveStringConstantValue: KtConstantExpression + } + } + return emptySequence() +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/resources/META-INF/plugin.xml b/tools/intellij-plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 000000000..12267adbc --- /dev/null +++ b/tools/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,39 @@ +<idea-plugin> + <id>net.mamoe.mirai-console</id> + + <name>Mirai Console</name> + + <vendor + email="support@mamoe.net" + url="https://github.com/mamoe/"> + Mamoe Technologies + </vendor> + + <depends>com.intellij.modules.java</depends> + <depends>com.intellij.modules.platform</depends> + <depends>org.jetbrains.kotlin</depends> + + <extensions defaultExtensionNs="com.intellij"> + <codeInsight.lineMarkerProvider language="JAVA" + implementationClass="net.mamoe.mirai.console.intellij.line.marker.PluginMainLineMarkerProvider"/> + <codeInsight.lineMarkerProvider language="kotlin" + implementationClass="net.mamoe.mirai.console.intellij.line.marker.PluginMainLineMarkerProvider"/> + <codeInsight.lineMarkerProvider language="JAVA" + implementationClass="net.mamoe.mirai.console.intellij.line.marker.CommandDeclarationLineMarkerProvider"/> + <codeInsight.lineMarkerProvider language="kotlin" + implementationClass="net.mamoe.mirai.console.intellij.line.marker.CommandDeclarationLineMarkerProvider"/> + + <!-- + <intentionAction> + <className>net.mamoe.mirai.console.intellij.diagnostics.fix.AbuseYellowIntention</className> + <category>Mirai</category> + </intentionAction> + --> + </extensions> + + <extensions defaultExtensionNs="org.jetbrains.kotlin"> + <storageComponentContainerContributor + implementation="net.mamoe.mirai.console.intellij.IDEContainerContributor"/> + <quickFixContributor implementation="net.mamoe.mirai.console.intellij.QuickFixRegistrar"/> + </extensions> +</idea-plugin> \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/resources/icons/commandDeclaration.svg b/tools/intellij-plugin/src/main/resources/icons/commandDeclaration.svg new file mode 100644 index 000000000..1e1c7a10e --- /dev/null +++ b/tools/intellij-plugin/src/main/resources/icons/commandDeclaration.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> + <path fill="#9AA7B0" fill-opacity=".8" fill-rule="evenodd" + d=" + M7.06393077,7.93322956 + L4.76000023,10.0664986 + L6.03195752,11.2442368 + L9.59577548,7.94440533 + L9.58370576,7.93322956 + L9.59577548,7.92205378 + L11.89970602,5.79996049 + L10.62774873,4.62222227 + L7.06393077,7.93322956 + Z M2,3 + L14,3 + L14,13 + L2,13 + L2,3 Z"/> + <!-- center 6.329853125 --> +</svg> \ No newline at end of file diff --git a/tools/intellij-plugin/src/main/resources/icons/pluginIcon.svg b/tools/intellij-plugin/src/main/resources/icons/pluginIcon.svg new file mode 100644 index 000000000..72a9d1a7b --- /dev/null +++ b/tools/intellij-plugin/src/main/resources/icons/pluginIcon.svg @@ -0,0 +1,119 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" + width="1372.000000pt" height="1379.000000pt" viewBox="0 0 1372.000000 1379.000000" + preserveAspectRatio="xMidYMid meet"> + <metadata> + Created by potrace 1.15, written by Peter Selinger 2001-2017 + </metadata> + <g transform="translate(0.000000,1379.000000) scale(0.100000,-0.100000)" + fill="#000000" stroke="none"> + <path d="M6465 11443 c-189 -17 -441 -94 -483 -148 -16 -20 -26 -24 -45 -20 +-68 17 -348 -52 -502 -124 -120 -56 -264 -142 -370 -220 -52 -39 -91 -63 -88 +-53 6 15 5 16 -13 2 -77 -58 -150 -131 -271 -266 -233 -262 -338 -415 -500 +-730 l-93 -181 -47 46 c-27 25 -81 63 -120 84 -64 33 -80 38 -127 34 -63 -5 +-99 -29 -179 -120 -167 -191 -345 -491 -441 -742 -21 -55 -47 -118 -57 -140 +-37 -80 -90 -343 -165 -806 -92 -579 -96 -598 -138 -674 -85 -150 -276 -422 +-342 -486 -191 -185 -353 -257 -385 -172 -15 41 10 161 44 211 16 24 26 46 22 +50 -11 11 -68 -63 -110 -141 l-38 -71 10 -93 c5 -51 13 -96 17 -100 10 -10 +202 60 281 103 108 60 334 263 448 404 33 41 57 65 53 54 -17 -53 -57 -250 +-72 -349 -11 -76 -15 -150 -12 -239 l5 -128 -80 -137 c-113 -191 -255 -473 +-315 -628 l-52 -131 64 -155 c117 -280 265 -560 430 -812 78 -119 85 -127 83 +-93 -1 20 -5 34 -8 32 -5 -3 -31 65 -35 91 0 6 -17 44 -37 85 -19 41 -38 87 +-42 101 -6 26 -5 27 31 27 21 0 79 6 130 13 76 10 96 10 108 -1 34 -27 9 -130 +-49 -206 -26 -34 -70 -51 -90 -34 -9 7 -16 5 -26 -7 -7 -10 -9 -15 -4 -10 4 4 +16 0 26 -9 13 -12 28 -15 51 -10 18 4 42 8 53 8 11 1 25 9 32 17 11 14 12 12 +13 -12 0 -16 4 -26 10 -22 6 3 10 18 10 33 0 20 16 42 59 84 70 67 137 162 +158 224 23 67 13 141 -21 171 -53 45 -139 29 -338 -67 -69 -33 -135 -60 -146 +-60 -29 0 -80 46 -99 89 -24 55 -28 282 -9 471 48 452 167 922 304 1195 37 75 +112 198 118 193 1 -2 -3 -46 -11 -99 -32 -234 -28 -444 11 -522 9 -18 38 -54 +65 -82 l48 -50 10 -102 c12 -112 39 -199 74 -230 35 -32 100 -64 107 -52 4 5 +14 7 24 3 9 -4 43 -1 75 6 50 10 62 10 90 -4 61 -32 62 -102 2 -141 -85 -54 +-201 -59 -285 -12 -47 26 -77 19 -37 -9 12 -9 30 -33 42 -55 26 -51 71 -79 +129 -79 59 0 137 51 244 160 44 44 80 79 81 78 16 -19 55 -83 55 -92 0 -6 -40 +-52 -90 -103 -73 -76 -92 -102 -104 -144 -20 -67 -20 -138 0 -167 14 -21 52 +-33 39 -12 -3 5 15 1 41 -11 59 -25 194 -37 194 -17 0 7 -21 20 -47 28 -74 23 +-76 43 -25 198 22 70 44 150 48 177 4 29 16 61 30 77 32 39 47 89 35 120 -12 +31 -55 63 -98 72 l-31 7 1 84 2 83 30 -57 c41 -77 138 -173 191 -189 56 -16 +84 -15 84 3 0 8 -7 15 -16 15 -63 0 -214 223 -268 395 -42 133 -84 322 -106 +472 -19 132 -19 183 0 208 4 6 11 44 15 85 l8 75 33 -115 c68 -233 151 -402 +268 -552 54 -68 78 -64 28 5 -34 48 -85 147 -79 153 2 2 21 -22 43 -53 40 -60 +249 -272 268 -274 8 0 85 -143 124 -231 2 -5 -9 -13 -25 -19 -67 -25 -103 -85 +-103 -171 0 -84 24 -119 115 -174 15 -8 19 -27 22 -120 5 -170 23 -474 31 +-529 5 -36 3 -64 -9 -100 -10 -27 -23 -99 -30 -160 -7 -60 -16 -115 -20 -121 +-11 -14 -9 -144 1 -144 5 0 20 -23 34 -52 l25 -52 -34 -30 c-20 -19 -36 -43 +-38 -61 -3 -16 -24 -61 -48 -100 -23 -38 -56 -99 -73 -135 -30 -62 -31 -70 +-29 -178 3 -100 -2 -138 -42 -334 -25 -121 -45 -225 -45 -232 0 -11 165 -118 +317 -205 40 -23 152 -81 250 -129 1441 -710 3147 -654 4549 148 466 267 910 +635 1269 1052 90 105 295 369 295 381 0 11 -68 105 -73 101 -3 -3 -29 29 -58 +71 -30 42 -82 115 -117 162 -35 46 -88 125 -118 175 -30 50 -62 97 -70 105 -8 +8 -19 23 -24 33 -6 11 -28 24 -49 30 -36 9 -38 13 -50 65 -12 55 -115 274 +-165 354 -20 32 -56 152 -56 187 0 3 -8 3 -19 -1 -10 -3 -67 -7 -127 -9 -60 +-2 -140 -11 -179 -20 -67 -16 -70 -16 -95 3 -47 34 -178 97 -283 134 -10 4 +-15 12 -11 18 3 6 8 28 9 48 2 20 8 79 14 131 6 52 11 133 11 179 l0 84 30 +-42 c51 -69 82 -83 170 -76 41 4 103 8 138 8 61 2 65 1 97 -35 75 -81 71 -213 +-10 -366 -34 -63 -19 -75 18 -16 58 93 80 153 83 228 2 39 6 91 10 116 7 52 +-9 121 -33 146 -17 17 -38 17 -195 -1 -44 -5 -51 -2 -87 30 -47 42 -83 116 +-111 224 -26 101 -25 117 1 64 24 -49 44 -57 35 -15 -30 143 -34 297 -6 297 8 +0 24 -16 36 -35 25 -40 72 -90 85 -90 16 0 10 28 -7 34 -11 4 -14 13 -11 27 4 +13 -1 24 -14 32 -10 6 -26 32 -34 58 -15 44 -15 47 14 110 17 35 30 57 31 49 +0 -8 11 -37 24 -65 14 -27 32 -71 41 -97 10 -26 32 -64 50 -85 17 -21 40 -63 +50 -93 32 -99 122 -260 145 -260 5 0 -10 39 -34 88 -78 154 -129 337 -157 562 +-15 114 -16 370 -5 650 6 140 5 249 -3 345 -11 132 -45 396 -57 440 -8 28 60 +-84 106 -174 167 -335 318 -902 360 -1361 25 -264 1 -657 -46 -768 -17 -39 +-18 -56 -3 -47 14 9 52 97 77 180 92 301 34 949 -140 1570 -173 620 -508 1399 +-784 1823 -29 45 -51 82 -49 82 3 0 31 -15 63 -32 223 -126 549 -447 729 -718 +182 -274 349 -719 442 -1179 41 -201 60 -357 67 -551 11 -284 -18 -515 -90 +-714 -39 -106 -47 -151 -22 -126 30 31 126 329 126 389 0 12 4 21 8 21 5 0 15 +33 22 73 7 39 19 92 27 116 12 36 13 75 4 250 -6 113 -18 247 -26 296 -27 162 +-19 154 50 -50 27 -77 56 -151 66 -165 10 -14 37 -92 59 -175 63 -228 88 -298 +129 -362 56 -85 105 -138 120 -129 10 6 8 13 -7 30 -40 45 -34 49 12 8 26 -24 +70 -54 99 -68 98 -45 247 -25 321 44 17 16 52 40 78 54 48 27 83 27 151 -1 16 +-6 39 -11 53 -11 32 0 32 28 0 36 -16 4 -23 11 -19 20 4 10 -3 14 -26 14 -40 +0 -92 46 -94 83 -1 16 -7 25 -14 23 -44 -17 -51 298 -8 330 8 6 15 16 15 22 0 +6 20 34 44 61 115 129 143 307 77 481 -16 43 -17 50 -4 50 11 0 14 7 10 23 -3 +12 -13 68 -22 124 -20 112 -20 113 -101 113 l-41 0 -54 106 c-57 111 -151 238 +-222 299 -110 95 -278 186 -467 255 -108 38 -117 44 -142 83 -14 23 -112 190 +-217 370 -285 489 -449 743 -588 909 -250 298 -483 536 -604 617 -101 68 -209 +115 -302 131 -67 12 -157 6 -157 -9 0 -5 -16 -19 -35 -32 -49 -34 -101 -106 +-118 -164 -8 -27 -18 -48 -23 -46 -5 2 -72 45 -149 96 -499 328 -915 517 +-1255 569 -219 34 -540 52 -685 39z m4318 -2684 c93 -48 203 -130 248 -186 53 +-66 126 -177 159 -243 l23 -44 -39 29 c-132 99 -284 244 -318 304 -5 9 -30 41 +-55 71 -87 103 -87 103 -18 69z m-238 -321 c8 -18 14 -27 15 -20 0 8 24 -7 53 +-33 146 -132 346 -239 524 -281 65 -15 121 -32 125 -38 11 -16 10 -134 -3 +-202 -19 -107 -47 -188 -70 -203 -17 -10 -19 -15 -9 -21 11 -6 7 -19 -18 -56 +-17 -26 -28 -52 -25 -57 8 -13 51 31 89 90 33 50 65 78 77 67 18 -18 124 140 +154 230 l13 41 -6 -40 c-3 -21 -12 -52 -20 -67 -20 -39 -18 -55 6 -33 10 9 27 +40 36 67 9 27 18 47 20 45 13 -12 34 -171 34 -251 0 -111 -28 -234 -64 -276 +-37 -44 -86 -173 -98 -258 -14 -98 -1 -148 53 -207 21 -23 39 -45 39 -48 0 +-13 -87 -29 -150 -29 -188 2 -349 149 -502 457 -62 125 -75 169 -93 330 -37 +333 -94 575 -182 773 -12 28 -20 52 -17 52 3 0 11 -15 19 -32z m-7030 -1511 +c20 -102 25 -157 25 -280 0 -148 -1 -154 -30 -216 -17 -35 -54 -100 -84 -145 +-69 -105 -86 -139 -86 -173 l-1 -28 -28 31 c-35 38 -71 131 -71 182 0 60 39 +306 71 454 35 159 64 261 116 402 l37 101 13 -100 c7 -55 24 -157 38 -228z +m727 -262 c20 -38 48 -106 63 -150 15 -44 36 -96 46 -116 41 -82 101 -247 115 +-318 8 -42 17 -87 20 -101 5 -22 3 -21 -20 10 -95 132 -169 288 -250 532 -50 +152 -147 508 -146 539 1 8 32 -62 69 -156 37 -93 84 -201 103 -240z m5031 254 +l-2 -151 -55 -166 c-63 -194 -72 -238 -82 -387 -7 -124 -12 -121 -44 26 -40 +180 -35 392 12 501 11 27 18 51 15 54 -7 7 61 175 79 193 22 24 24 9 3 -49 +-21 -60 -23 -74 -10 -66 4 3 19 34 31 68 21 58 50 128 54 128 0 0 0 -68 -1 +-151z m-5559 -714 c30 -105 31 -113 13 -103 -24 13 -97 4 -137 -17 -49 -25 +-60 -20 -60 28 1 52 42 255 65 317 9 25 19 74 22 110 l7 65 28 -143 c16 -79 +44 -194 62 -257z m1439 126 c97 -145 227 -321 289 -392 l46 -54 -56 -22 c-30 +-13 -56 -22 -57 -20 -14 16 -133 232 -187 341 -68 135 -151 322 -125 282 6 +-11 47 -72 90 -135z m-583 -159 c47 -82 125 -197 187 -275 30 -38 53 -70 51 +-71 -1 -1 -28 -12 -58 -23 -30 -12 -62 -24 -70 -27 -12 -5 -15 2 -14 31 1 21 +5 46 9 56 11 25 3 28 -34 14 -22 -8 -34 -8 -39 -1 -4 5 -24 20 -44 33 -34 20 +-37 26 -30 49 9 33 9 205 -1 250 -9 45 5 34 43 -36z m-91 -344 c16 -24 43 -58 +61 -77 17 -18 30 -35 28 -36 -37 -32 -138 -102 -142 -97 -3 3 -6 -2 -6 -12 0 +-28 -39 0 -57 41 -25 60 11 185 59 209 13 7 24 12 25 13 1 0 15 -18 32 -41z"/> + <path d="M11666 7295 c-48 -120 -45 -233 8 -300 l23 -28 6 154 c3 85 2 174 -1 +199 l-7 45 -29 -70z"/> + <path d="M3194 4441 c4 -6 0 -11 -9 -11 -9 0 -28 -12 -42 -27 -15 -16 -23 -22 +-18 -14 5 10 4 12 -4 7 -6 -4 -8 -12 -5 -18 4 -6 -6 -23 -22 -37 -17 -14 -25 +-24 -19 -21 6 3 -1 -6 -15 -19 -14 -13 -19 -20 -13 -17 9 5 11 2 7 -8 -3 -9 +-10 -13 -15 -10 -5 3 -6 -3 -2 -12 5 -13 3 -15 -8 -9 -16 10 34 -54 141 -178 +l75 -88 3 198 c3 208 -3 244 -41 264 -14 8 -18 8 -13 0z"/> + </g> +</svg> diff --git a/tools/intellij-plugin/src/main/resources/icons/pluginMainDeclaration.png b/tools/intellij-plugin/src/main/resources/icons/pluginMainDeclaration.png new file mode 100644 index 000000000..7b107ff3d Binary files /dev/null and b/tools/intellij-plugin/src/main/resources/icons/pluginMainDeclaration.png differ