Support UsingStringPlusMessageInspection and ConvertToPlainTextFix

This commit is contained in:
Him188 2020-12-29 17:23:56 +08:00
parent d2965c87f5
commit 203636d309
8 changed files with 272 additions and 2 deletions

View File

@ -38,6 +38,12 @@
groupKey="group.names.plugin.service.issues" enabledByDefault="true" level="WARNING"
implementationClass="net.mamoe.mirai.console.intellij.diagnostics.PluginMainServiceNotConfiguredInspection"/>
<localInspection groupPath="Mirai console" language="kotlin" shortName="UsingStringPlusMessage"
bundle="messages.InspectionGadgetsBundle"
key="using.string.plus.message.display.name" groupBundle="messages.InspectionsBundle"
groupKey="group.names.message.issues" enabledByDefault="true" level="WARNING"
implementationClass="net.mamoe.mirai.console.intellij.diagnostics.UsingStringPlusMessageInspection"/>
<!--
<intentionAction>
<className>net.mamoe.mirai.console.intellij.diagnostics.fix.AbuseYellowIntention</className>

View File

@ -0,0 +1,8 @@
<html>
<body>
<p>检查插件主类服务配置情况。
</p>
<!-- tooltip end -->
<!--<p>Text after this comment will only be shown in the settings of the inspection.</p>-->
</body>
</html>

View File

@ -0,0 +1,14 @@
<html>
<body>
<p>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 doesnt just repeat the inspection title.
</p>
<!-- tooltip end -->
<p>Text after this comment will only be shown in the settings of the inspection.</p>
</body>
</html>

View File

@ -7,3 +7,4 @@
# https://github.com/mamoe/mirai/blob/master/LICENSE
#
plugin.service.not.configured.display.name=Plugin main not configured
using.string.plus.message.display.name=Using string plus message

View File

@ -6,4 +6,5 @@
#
# https://github.com/mamoe/mirai/blob/master/LICENSE
#
group.names.plugin.service.issues=Plugin main class issues
group.names.plugin.service.issues=Plugin service issues
group.names.message.issues=Message issues

View File

@ -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<KtReferenceExpression>()
?.resolve()?.castOrNull<KtDeclaration>()?.type()
?: return@visitor
if (!argumentType.hasSuperType("net.mamoe.mirai.message.data.Message")) return@visitor
parent.parent?.castOrNull<KtDotQualifiedExpression>()?.receiverExpression // explicit receiver, inspection on it.
?: parent.findChild<KtNameReferenceExpression>() // 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)
)
}
}
}

View File

@ -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<KtExpression>(element), KotlinUniversalQuickFix {
override fun getFamilyName(): String = "Mirai Console"
override fun getText(): String = "将 String 转换为 PlainText"
fun <TDeclaration : KtDeclaration> 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<KtDotQualifiedExpression> { qualifiedExpression ->
val selector = qualifiedExpression.getQualifiedElementSelector() as? KtNameReferenceExpression
selector?.getReferencedNameAsName() == fqName.shortName() && target(qualifiedExpression)?.importableFqName == fqName
}
val userTypes = file.collectDescendantsOfType<KtUserType> { 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()
}
}

View File

@ -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)