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 index da5b520e0..1d79320c2 100644 --- a/tools/intellij-plugin/README.md +++ b/tools/intellij-plugin/README.md @@ -4,4 +4,14 @@ IntelliJ 平台的 Mirai Console 开发插件 ## 功能 -### 诊断 \ No newline at end of file +### 诊断 + +#### 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/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt index 7abb6a55c..9f1bdcbd5 100644 --- a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt +++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt @@ -3,9 +3,25 @@ package org.example.myplugin import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin +val T = "scas" + "pp" // 编译期常量 + object MyPluginMain : KotlinPlugin( JvmPluginDescription( - "net.mamoe.main", + T, + "0.1.0", + ) { + name(".") + id("") + } +) { + fun test() { + + } +} + +object MyPluginMain2 : KotlinPlugin( + JvmPluginDescription( + "", "0.1.0", ) { name(".") diff --git a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDescriptionChecker.kt b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDescriptionChecker.kt index 5f8c0b64a..74f68de7b 100644 --- a/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDescriptionChecker.kt +++ b/tools/intellij-plugin/src/main/kotlin/net/mamoe/mirai/console/intellij/diagnostics/PluginDescriptionChecker.kt @@ -13,7 +13,7 @@ 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.findChildren +import net.mamoe.mirai.console.intellij.resolve.findChild import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValue import net.mamoe.mirai.console.intellij.resolve.valueParameters import org.jetbrains.kotlin.descriptors.DeclarationDescriptor @@ -36,28 +36,30 @@ class PluginDescriptionChecker : DeclarationChecker { 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 = arrayOf("main", "console", "plugin", "config", "data") + private const val syntax = """类似于 "net.mamoe.mirai.example-plugin", 其中 "net.mamoe.mirai" 为 groupId, "example-plugin" 为插件名. """ + fun checkPluginId(inspectionTarget: PsiElement, value: String): Diagnostic? { - if (value.isBlank()) return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "Plugin id cannot be blank") + if (value.isBlank()) return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件 Id 不能为空. \n插件 Id$syntax") if (value.none { it == '.' }) return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, - "'$value' is illegal. Plugin id must consist of both domain and name. ") + "插件 Id '$value' 无效. 插件 Id 必须同时包含 groupId 和插件名称. $syntax") val lowercaseId = value.toLowerCase() if (ID_REGEX.matchEntire(value) == null) { - return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "Plugin does not match regex '${ID_REGEX.pattern}'.") + return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件 Id 无效. 正确的插件 Id 应该满足正则表达式 '${ID_REGEX.pattern}', \n$syntax") } FORBIDDEN_ID_NAMES.firstOrNull { it == lowercaseId }?.let { illegal -> - return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "Plugin id contains illegal word: '$illegal'.") + return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "'$illegal' 不允许作为插件 Id. 确保插件 Id 不完全是这个名称.") } return null } fun checkPluginName(inspectionTarget: PsiElement, value: String): Diagnostic? { - if (value.isBlank()) return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "Plugin name cannot be blank") + if (value.isBlank()) return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件名不能为空.") val lowercaseName = value.toLowerCase() FORBIDDEN_ID_NAMES.firstOrNull { it == lowercaseName }?.let { illegal -> - return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "Plugin name is illegal: '$illegal'.") + return MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "'$illegal' 不允许作为插件名. 确保插件名不完全是这个名称.") } return null } @@ -90,15 +92,13 @@ class PluginDescriptionChecker : DeclarationChecker { context: DeclarationCheckerContext, ) { val call = expression.calleeExpression.getResolvedCallOrResolveToCall(context) ?: return // unresolved - call.valueArgumentsByIndex?.forEach { resolvedValueArgument -> - for ((parameter, argument) in call.valueParameters.zip(resolvedValueArgument.arguments)) { - val parameterContextKind = parameter.resolveContextKind - if (checkersMap.containsKey(parameterContextKind)) { - val value = argument.getArgumentExpression() - ?.resolveStringConstantValue(context.bindingContext) ?: continue - for ((kind, fn) in checkersMap) { - if (parameterContextKind == kind) fn(argument.asElement(), value)?.let { context.report(it) } - } + for ((parameter, argument) in call.valueParameters.zip(call.valueArgumentsByIndex?.mapNotNull { it.arguments.firstOrNull() }.orEmpty())) { + val parameterContextKind = parameter.resolveContextKind + if (checkersMap.containsKey(parameterContextKind)) { + val value = argument.getArgumentExpression() + ?.resolveStringConstantValue(context.bindingContext) ?: continue + for ((kind, fn) in checkersMap) { + if (parameterContextKind == kind) fn(argument.asElement(), value)?.let { context.report(it) } } } } @@ -113,9 +113,9 @@ class PluginDescriptionChecker : DeclarationChecker { when (declaration) { is KtObjectDeclaration -> { // check super type constructor - val superTypeCallEntry = declaration.findChildren()?.findChildren() ?: return + val superTypeCallEntry = declaration.findChild()?.findChild() ?: return // val constructorCall = superTypeCallEntry.findChildren()?.resolveToCall() ?: return - val valueArgumentList = superTypeCallEntry.findChildren() ?: return + val valueArgumentList = superTypeCallEntry.findChild() ?: return valueArgumentList.arguments.asSequence().mapNotNull(KtValueArgument::getArgumentExpression).forEach { if (it.shouldPerformCheck()) { check(it as KtCallExpression, context) @@ -126,10 +126,10 @@ class PluginDescriptionChecker : DeclarationChecker { is KtClassOrObject -> { // check constructor - val superTypeCallEntry = declaration.findChildren()?.findChildren() ?: return + val superTypeCallEntry = declaration.findChild()?.findChild() ?: return - val constructorCall = superTypeCallEntry.findChildren()?.resolveToCall() ?: return - val valueArgumentList = superTypeCallEntry.findChildren() ?: return + val constructorCall = superTypeCallEntry.findChild()?.resolveToCall() ?: return + val valueArgumentList = superTypeCallEntry.findChild() ?: return } 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 index ee7acee78..b0c261942 100644 --- 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 @@ -9,6 +9,7 @@ package net.mamoe.mirai.console.intellij.resolve +import com.intellij.psi.PsiDeclarationStatement import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import net.mamoe.mirai.console.compiler.common.castOrNull @@ -19,6 +20,8 @@ import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor import org.jetbrains.kotlin.descriptors.VariableDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall 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.* @@ -28,6 +31,7 @@ import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.constants.StringValue import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance /** @@ -98,7 +102,7 @@ val PsiElement.allChildrenFlat: Sequence } } -inline fun PsiElement.findChildren(): E? = this.children.find { it is E } as E? +inline fun PsiElement.findChild(): E? = this.children.find { it is E } as E? fun KtElement?.getResolvedCallOrResolveToCall( context: BindingContext, @@ -111,10 +115,23 @@ val ResolvedCall.valueParameters: List { + when (val reference = references.firstIsInstance().resolve()) { + is KtDeclaration -> { + val descriptor = reference.descriptor.castOrNull() ?: return null + val compileTimeConstant = descriptor.compileTimeInitializer ?: return null + return compileTimeConstant.castOrNull()?.value + } + is PsiDeclarationStatement -> { + + } + } + } is KtStringTemplateExpression -> { if (hasInterpolation()) return null return entries.joinToString("") { it.text } } + /* is KtCallExpression -> { val callee = this.calleeExpression?.getResolvedCallOrResolveToCall(bindingContext)?.resultingDescriptor if (callee is VariableDescriptor) { @@ -122,7 +139,7 @@ fun KtExpression.resolveStringConstantValue(bindingContext: BindingContext): Str return compileTimeConstant.castOrNull()?.value } return null - } + }*/ is KtConstantExpression -> { // TODO: 2020/9/18 KtExpression.resolveStringConstantValue: KtConstantExpression }