mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-10 18:40:15 +08:00
Support UsingStringPlusMessageInspection and ConvertToPlainTextFix
This commit is contained in:
parent
d2965c87f5
commit
203636d309
@ -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>
|
||||
|
@ -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>
|
@ -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 doesn’t 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>
|
@ -6,4 +6,5 @@
|
||||
#
|
||||
# https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
#
|
||||
plugin.service.not.configured.display.name=Plugin main not configured
|
||||
plugin.service.not.configured.display.name=Plugin main not configured
|
||||
using.string.plus.message.display.name=Using string plus message
|
@ -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
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user