From 203636d309921ddd93bcb6539d78e05f749b5e7e Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 29 Dec 2020 17:23:56 +0800 Subject: [PATCH] Support UsingStringPlusMessageInspection and ConvertToPlainTextFix --- .../resources/META-INF/plugin.xml | 6 + .../PluginMainServiceNotConfigured.html | 8 + .../UsingStringPlusMessage.html | 14 ++ .../InspectionGadgetsBundle.properties | 3 +- .../messages/InspectionsBundle.properties | 3 +- .../UsingStringPlusMessageInspection.kt | 78 +++++++++ .../diagnostics/fix/ConvertToPlainTextFix.kt | 154 ++++++++++++++++++ .../src/resolve/resolveIdea.kt | 8 + 8 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 tools/intellij-plugin/resources/inspectionDescriptions/PluginMainServiceNotConfigured.html create mode 100644 tools/intellij-plugin/resources/inspectionDescriptions/UsingStringPlusMessage.html create mode 100644 tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt create mode 100644 tools/intellij-plugin/src/diagnostics/fix/ConvertToPlainTextFix.kt diff --git a/tools/intellij-plugin/resources/META-INF/plugin.xml b/tools/intellij-plugin/resources/META-INF/plugin.xml index 4e7a1187d..05f8eb827 100644 --- a/tools/intellij-plugin/resources/META-INF/plugin.xml +++ b/tools/intellij-plugin/resources/META-INF/plugin.xml @@ -38,6 +38,12 @@ groupKey="group.names.plugin.service.issues" enabledByDefault="true" level="WARNING" implementationClass="net.mamoe.mirai.console.intellij.diagnostics.PluginMainServiceNotConfiguredInspection"/> + + + + + \ No newline at end of file diff --git a/tools/intellij-plugin/resources/inspectionDescriptions/UsingStringPlusMessage.html b/tools/intellij-plugin/resources/inspectionDescriptions/UsingStringPlusMessage.html new file mode 100644 index 000000000..c2473ef5d --- /dev/null +++ b/tools/intellij-plugin/resources/inspectionDescriptions/UsingStringPlusMessage.html @@ -0,0 +1,14 @@ + + +

Write your description here. + Start the description with a verb in 3rd person singular, like reports, detects, highlights. + In the first sentence, briefly explain what exactly the inspection helps you detect. + Make sure the sentence is not very long and complicated. + The first sentence must be in a dedicated paragraph separated from the rest of the text. This will make the + description easier to read. + Make sure the description doesn’t just repeat the inspection title. +

+ +

Text after this comment will only be shown in the settings of the inspection.

+ + \ No newline at end of file diff --git a/tools/intellij-plugin/resources/messages/InspectionGadgetsBundle.properties b/tools/intellij-plugin/resources/messages/InspectionGadgetsBundle.properties index d400c9a4c..ae2906e9a 100644 --- a/tools/intellij-plugin/resources/messages/InspectionGadgetsBundle.properties +++ b/tools/intellij-plugin/resources/messages/InspectionGadgetsBundle.properties @@ -6,4 +6,5 @@ # # https://github.com/mamoe/mirai/blob/master/LICENSE # -plugin.service.not.configured.display.name=Plugin main not configured \ No newline at end of file +plugin.service.not.configured.display.name=Plugin main not configured +using.string.plus.message.display.name=Using string plus message \ No newline at end of file diff --git a/tools/intellij-plugin/resources/messages/InspectionsBundle.properties b/tools/intellij-plugin/resources/messages/InspectionsBundle.properties index 9fe590aa0..6a4f1e3a4 100644 --- a/tools/intellij-plugin/resources/messages/InspectionsBundle.properties +++ b/tools/intellij-plugin/resources/messages/InspectionsBundle.properties @@ -6,4 +6,5 @@ # # https://github.com/mamoe/mirai/blob/master/LICENSE # -group.names.plugin.service.issues=Plugin main class issues \ No newline at end of file +group.names.plugin.service.issues=Plugin service issues +group.names.message.issues=Message issues \ No newline at end of file diff --git a/tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt b/tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt new file mode 100644 index 000000000..bf686bbd7 --- /dev/null +++ b/tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.diagnostics + +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElementVisitor +import net.mamoe.mirai.console.compiler.common.castOrNull +import net.mamoe.mirai.console.intellij.diagnostics.fix.ConvertToPlainTextFix +import net.mamoe.mirai.console.intellij.resolve.findChild +import net.mamoe.mirai.console.intellij.resolve.hasSuperType +import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection +import org.jetbrains.kotlin.idea.search.declarationsSearch.findDeepestSuperMethodsKotlinAware +import org.jetbrains.kotlin.nj2k.postProcessing.resolve +import org.jetbrains.kotlin.nj2k.postProcessing.type +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.psi.psiUtil.referenceExpression +import java.util.* + +/* +private val bundle by lazy { + BundleUtil.loadLanguageBundle(PluginMainServiceNotConfiguredInspection::class.java.classLoader, "messages.InspectionGadgetsBundle")!! +}*/ + +class UsingStringPlusMessageInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return referenceExpressionVisitor visitor@{ expression -> + val originalCallee = expression.resolve() ?: return@visitor + if (originalCallee !is KtNamedFunction) return@visitor + + val callee = findDeepestSuperMethodsKotlinAware(originalCallee).lastOrNull() as? KtNamedFunction ?: originalCallee + + val className = callee.containingClassOrObject?.fqName?.asString() + if (className != "kotlin.String") return@visitor + if (callee.name != "plus") return@visitor + + val parent = expression.parent + + val inspectionTarget = when (parent) { + is KtBinaryExpression -> { + val right = parent.right?.referenceExpression()?.resolve() as? KtDeclaration ?: return@visitor + val rightType = right.type() ?: return@visitor + if (!rightType.hasSuperType("net.mamoe.mirai.message.data.Message")) return@visitor + parent.left + } + is KtCallExpression -> { + val argumentType = parent + .valueArguments.singleOrNull() + ?.findChild() + ?.resolve()?.castOrNull()?.type() + ?: return@visitor + if (!argumentType.hasSuperType("net.mamoe.mirai.message.data.Message")) return@visitor + + parent.parent?.castOrNull()?.receiverExpression // explicit receiver, inspection on it. + ?: parent.findChild() // implicit receiver, inspection on 'plus' + } + else -> null + } ?: return@visitor + + println(expression::class.qualifiedName + " " + callee::class.qualifiedName + " " + callee.text) + + holder.registerProblem( + inspectionTarget, + "使用 String + Message 会导致 Message 被转换为 String 再相加", + ProblemHighlightType.WARNING, + ConvertToPlainTextFix(inspectionTarget) + ) + } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/diagnostics/fix/ConvertToPlainTextFix.kt b/tools/intellij-plugin/src/diagnostics/fix/ConvertToPlainTextFix.kt new file mode 100644 index 000000000..bd88076c1 --- /dev/null +++ b/tools/intellij-plugin/src/diagnostics/fix/ConvertToPlainTextFix.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.diagnostics.fix + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import org.jetbrains.annotations.NonNls +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.imports.canBeAddedToImport +import org.jetbrains.kotlin.idea.imports.importableFqName +import org.jetbrains.kotlin.idea.inspections.KotlinUniversalQuickFix +import org.jetbrains.kotlin.idea.quickfix.KotlinCrossLanguageQuickFixAction +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.idea.references.resolveMainReferenceToDescriptors +import org.jetbrains.kotlin.idea.references.resolveToDescriptors +import org.jetbrains.kotlin.idea.util.ImportDescriptorResult +import org.jetbrains.kotlin.idea.util.ImportInsertHelper +import org.jetbrains.kotlin.idea.util.getFactoryForImplicitReceiverWithSubtypeOf +import org.jetbrains.kotlin.idea.util.getResolutionScope +import org.jetbrains.kotlin.nj2k.postProcessing.resolve +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector +import org.jetbrains.kotlin.psi.psiUtil.getReceiverExpression +import org.jetbrains.kotlin.psi.psiUtil.referenceExpression +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.utils.checkWithAttachment + +class ConvertToPlainTextFix( + /** + * Maybe: + * + * - [KtNameReferenceExpression]: if implicit receiver + * - [KtExpression] + */ + element: KtExpression, +) : KotlinCrossLanguageQuickFixAction(element), KotlinUniversalQuickFix { + + override fun getFamilyName(): String = "Mirai Console" + override fun getText(): String = "将 String 转换为 PlainText" + + fun KtPsiFactory.createAnalyzableDeclaration(@NonNls text: String, context: PsiElement): TDeclaration { + val file = createAnalyzableFile("Dummy.kt", text, context) + val declarations = file.declarations + checkWithAttachment(declarations.size == 1, { "unexpected ${declarations.size} declarations" }) { + it.withAttachment("text.kt", text) + for (d in declarations.withIndex()) { + it.withAttachment("declaration${d.index}.kt", d.value.text) + } + } + @Suppress("UNCHECKED_CAST") + return declarations.first() as TDeclaration + } + + override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) { + if (editor == null) return + if (file !is KtFile) return + val element = element ?: return + + val psiFactory = KtPsiFactory(project) + + if (element.parent is KtBinaryExpression) { + // 'str + msg' + + val replaced = element.replace(psiFactory.createExpression("net.mamoe.mirai.message.data.PlainText(${element.text})")) + as? KtElement ?: return + ShortenReferences.DEFAULT.process(replaced) + return + } + + val resolved = element.referenceExpression()?.resolve() ?: return + if (resolved !is KtDeclaration) return + // 'plus' function + // perform fix on receiver + val dotQualifiedExpr = element.parent + if (dotQualifiedExpr is KtDotQualifiedExpression) { + // got explicit receiver + val replaced = dotQualifiedExpr.receiverExpression + .replace(psiFactory.createExpression("net.mamoe.mirai.message.data.PlainText(${dotQualifiedExpr.receiverExpression.text})")) + as? KtElement ?: return + + ShortenReferences.DEFAULT.process(replaced) + } else { + // implicit receiver + val context = element.analyze() + val scope = element.getResolutionScope(context) ?: return + + val descriptor = element.resolveToCall()?.resultingDescriptor ?: return + val receiverDescriptor = descriptor.extensionReceiverParameter + ?: descriptor.dispatchReceiverParameter + ?: return + val receiverType = receiverDescriptor.type + + val expressionFactory = scope.getFactoryForImplicitReceiverWithSubtypeOf(receiverType) ?: return + val receiverText = if (expressionFactory.isImmediate) "this" else expressionFactory.expressionText + + // element.parent is 'plus(msg)' + // replace it with a dot qualified expr + val replaced = + element.parent.replace(psiFactory.createExpression("net.mamoe.mirai.message.data.PlainText($receiverText).${element.parent.text}")) + as? KtElement ?: return + ShortenReferences.DEFAULT.process(replaced) + + } + } + + fun applyImport(targetElement: KtElement) { + val targets = targetElement.resolveMainReferenceToDescriptors() + if (targets.isEmpty()) return + + val fqName = targets.map { it.importableFqName!! }.single() + + val file = targetElement.containingKtFile + val helper = ImportInsertHelper.getInstance(targetElement.project) + if (helper.importDescriptor(file, targets.first()) == ImportDescriptorResult.FAIL) return + + val qualifiedExpressions = file.collectDescendantsOfType { qualifiedExpression -> + val selector = qualifiedExpression.getQualifiedElementSelector() as? KtNameReferenceExpression + selector?.getReferencedNameAsName() == fqName.shortName() && target(qualifiedExpression)?.importableFqName == fqName + } + val userTypes = file.collectDescendantsOfType { userType -> + val selector = userType.getQualifiedElementSelector() as? KtNameReferenceExpression + selector?.getReferencedNameAsName() == fqName.shortName() && target(userType)?.importableFqName == fqName + } + + //TODO: not deep + ShortenReferences.DEFAULT.process(qualifiedExpressions + userTypes) + } + + private fun target(qualifiedElement: KtElement): DeclarationDescriptor? { + val nameExpression = qualifiedElement.getQualifiedElementSelector() as? KtNameReferenceExpression ?: return null + val receiver = nameExpression.getReceiverExpression() ?: return null + val bindingContext = qualifiedElement.analyze(BodyResolveMode.PARTIAL) + if (bindingContext[BindingContext.QUALIFIER, receiver] == null) return null + + val targets = nameExpression.mainReference.resolveToDescriptors(bindingContext) + if (targets.isEmpty()) return null + if (!targets.all { it.canBeAddedToImport() }) return null + return targets.singleOrNull() + } +} diff --git a/tools/intellij-plugin/src/resolve/resolveIdea.kt b/tools/intellij-plugin/src/resolve/resolveIdea.kt index 2117a2ef7..af3a6684f 100644 --- a/tools/intellij-plugin/src/resolve/resolveIdea.kt +++ b/tools/intellij-plugin/src/resolve/resolveIdea.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.descriptors.CallableDescriptor import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor import org.jetbrains.kotlin.descriptors.VariableDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.idea.refactoring.fqName.fqName import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName import org.jetbrains.kotlin.idea.references.KtSimpleNameReference import org.jetbrains.kotlin.name.FqName @@ -35,6 +36,8 @@ 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.types.KotlinType +import org.jetbrains.kotlin.types.typeUtil.supertypes import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance @@ -85,6 +88,11 @@ fun KtConstructorCalleeExpression.getTypeAsUserType(): KtUserType? { return null } +fun KotlinType.hasSuperType(fqName: String, includeSelf: Boolean = true): Boolean { + if (this.fqName?.asString() == fqName) return true + return this.supertypes().any { it.hasSuperType("net.mamoe.mirai.message.data.Message", false) } +} + fun KtClassOrObject.hasSuperType(fqName: FqName): Boolean = allSuperNames.contains(fqName) fun KtClass.hasSuperType(fqName: FqName): Boolean = allSuperNames.contains(fqName)