mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-10 18:40:15 +08:00
Resource not closed inspection (#286)
* Initial support of ResourceNotClosedInspection, #283 * Improve UsingStringPlusMessageInspection * Add fix for sendAsImageTo and uploadAsImage * Add Fix for ResourceNotClosedInspection * ResourceNotClosedInspection support for Java * Add WrapWithResourceUseCallIntention for Kotlin, #284
This commit is contained in:
parent
5cfd9d7aa9
commit
fada587297
@ -44,18 +44,34 @@
|
||||
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="ResourceNotClosed"
|
||||
bundle="messages.InspectionGadgetsBundle"
|
||||
key="resource.not.closed.display.name" groupBundle="messages.InspectionsBundle"
|
||||
groupKey="group.names.mirai.core.issues" enabledByDefault="true" level="WARNING"
|
||||
implementationClass="net.mamoe.mirai.console.intellij.diagnostics.ResourceNotClosedInspection"/>
|
||||
|
||||
<localInspection groupPath="Mirai console" language="JAVA" shortName="ResourceNotClosedJava"
|
||||
bundle="messages.InspectionGadgetsBundle"
|
||||
key="resource.not.closed.display.name" groupBundle="messages.InspectionsBundle"
|
||||
groupKey="group.names.mirai.core.issues" enabledByDefault="true" level="WARNING"
|
||||
implementationClass="net.mamoe.mirai.console.intellij.diagnostics.ResourceNotClosedInspection"/>
|
||||
|
||||
<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>
|
||||
<category>Mirai</category>
|
||||
<className>net.mamoe.mirai.console.intellij.diagnostics.fix.WrapWithResourceUseCallIntention</className>
|
||||
<category>Mirai console</category>
|
||||
</intentionAction>
|
||||
-->
|
||||
|
||||
<intentionAction>
|
||||
<className>net.mamoe.mirai.console.intellij.diagnostics.fix.WrapWithResourceUseCallJavaIntention</className>
|
||||
<category>Mirai console</category>
|
||||
</intentionAction>
|
||||
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="org.jetbrains.kotlin">
|
||||
|
@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>检查 ExternalResource 没有 close 的情况。
|
||||
</p>
|
||||
<!-- tooltip end -->
|
||||
<!--<p>Text after this comment will only be shown in the settings of the inspection.</p>-->
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1 @@
|
||||
resource.use { it.uploadAsImage(contact) }
|
@ -0,0 +1 @@
|
||||
resource.uploadAsImage(contact)
|
@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>将资源直接使用转换为 `.use { ... }`
|
||||
</p>
|
||||
<!-- tooltip end -->
|
||||
<p>将 `resource.sendAsImageTo(contact)` 转换为 `resource.use { it.sendAsImageTo(contact) }` </p>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,4 @@
|
||||
ExternalResource resource;
|
||||
Contact contact;
|
||||
|
||||
resource.use { it.uploadAsImage(contact) }
|
@ -0,0 +1,4 @@
|
||||
ExternalResource resource;
|
||||
Contact contact;
|
||||
|
||||
resource.uploadAsImage(contact)
|
@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>将资源直接使用转换为 `.use { ... }`
|
||||
</p>
|
||||
<!-- tooltip end -->
|
||||
<p>将 `resource.sendAsImageTo(contact)` 转换为 `resource.use { it.sendAsImageTo(contact) }` </p>
|
||||
</body>
|
||||
</html>
|
@ -7,4 +7,5 @@
|
||||
# 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
|
||||
using.string.plus.message.display.name=Using string plus message
|
||||
resource.not.closed.display.name=Resource not closed
|
@ -7,4 +7,5 @@
|
||||
# https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
#
|
||||
group.names.plugin.service.issues=Plugin service issues
|
||||
group.names.message.issues=Message issues
|
||||
group.names.message.issues=Message issues
|
||||
group.names.mirai.core.issues=Mirai core issues
|
@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.4.20"
|
||||
kotlin("plugin.serialization") version "1.4.20"
|
||||
id("net.mamoe.mirai-console") version "2.0.0"
|
||||
id("net.mamoe.mirai-console") version "2.3.2"
|
||||
java
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ group = "org.example"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package test;
|
||||
|
||||
import net.mamoe.mirai.contact.Contact;
|
||||
import net.mamoe.mirai.message.data.Image;
|
||||
import net.mamoe.mirai.utils.ExternalResource;
|
||||
import org.example.myplugin.ResourceNotClosedInspectionTestKt;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.example.myplugin.ResourceNotClosedInspectionTestKt.magic;
|
||||
|
||||
public class ResourceNotClosedInspectionTestJava {
|
||||
public static void main(String[] args) {
|
||||
File file = magic();
|
||||
Contact contact = magic();
|
||||
|
||||
// useImage(contact.uploadImage(ExternalResource.create(file)));
|
||||
useImage(Contact.uploadImage(contact, ExternalResource.create(file)));
|
||||
|
||||
useImage(Contact.uploadImage(contact, file));
|
||||
|
||||
try (final ExternalResource resource = ExternalResource.create(file)) {
|
||||
useImage(contact.uploadImage(resource));
|
||||
}
|
||||
}
|
||||
|
||||
static void useImage(Image image) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.example.myplugin
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.Contact.Companion.sendImage
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Image.Key.queryUrl
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
|
||||
import java.io.File
|
||||
|
||||
class ResourceNotClosedInspectionTest {
|
||||
suspend fun useResource() {
|
||||
val file = magic<File>()
|
||||
val contact = magic<Contact>()
|
||||
|
||||
contact.uploadImage(file.toExternalResource()) // should report warning
|
||||
contact.sendImage(file) // should report warning
|
||||
|
||||
//file.toExternalResource().uploadAsImage(contact)
|
||||
|
||||
file.toExternalResource().uploadAsImage(contact)
|
||||
file.toExternalResource().sendAsImageTo(contact)
|
||||
|
||||
// contact.uploadImage(file) // should ok
|
||||
|
||||
// replace to net.mamoe.mirai.contact.Contact.Companion.uploadImage
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> magic(): T = null!!
|
@ -16,5 +16,16 @@ fun main() {
|
||||
val x: String = ""
|
||||
val plain: PlainText = PlainText("")
|
||||
|
||||
PlainText(x) + plain
|
||||
x + plain
|
||||
|
||||
// x + plain
|
||||
run { x } + plain
|
||||
|
||||
run { x.apply { x.let { also { x } } } } + plain
|
||||
run { x.apply { x.let { also { x } } } }.plus(plain)
|
||||
|
||||
x + plain
|
||||
1 + plain
|
||||
x.plus(plain)
|
||||
1.plus(plain)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package org.example.myplugin
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
|
||||
import java.io.File
|
||||
|
||||
class WrapWithResourceUseCallIntentionTest {
|
||||
suspend fun test() {
|
||||
|
||||
val file = magic<File>()
|
||||
val contact = magic<Contact>()
|
||||
val resource = magic<ExternalResource>()
|
||||
|
||||
resource.sendAsImageTo(contact)
|
||||
resource.run { sendAsImageTo(contact) }
|
||||
}
|
||||
}
|
37
tools/intellij-plugin/src/diagnostics/QuickFixUtils.kt
Normal file
37
tools/intellij-plugin/src/diagnostics/QuickFixUtils.kt
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.codeInspection.LocalQuickFix
|
||||
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.kotlin.idea.inspections.KotlinUniversalQuickFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinCrossLanguageQuickFixAction
|
||||
|
||||
|
||||
fun <T: PsiElement> LocalQuickFix(text: String, element: T, invokeAction: QuickFixInvoke<T>.() -> Unit): LocalQuickFix {
|
||||
return object: KotlinCrossLanguageQuickFixAction<T>(element), KotlinUniversalQuickFix {
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
override fun getFamilyName(): String = "Mirai console"
|
||||
override fun getText(): String = text
|
||||
override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) {
|
||||
invokeAction(QuickFixInvoke(project, editor ?: return, file, this.element ?: return))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class QuickFixInvoke<T>(
|
||||
val project: Project,
|
||||
val editor: Editor,
|
||||
val file: PsiFile,
|
||||
val element: T,
|
||||
)
|
@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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.LocalQuickFix
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.*
|
||||
import net.mamoe.mirai.console.intellij.resolve.*
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
|
||||
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
|
||||
import org.jetbrains.kotlin.idea.inspections.KotlinUniversalQuickFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinCrossLanguageQuickFixAction
|
||||
import org.jetbrains.kotlin.idea.search.getKotlinFqName
|
||||
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
|
||||
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.nj2k.postProcessing.resolve
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/*
|
||||
private val bundle by lazy {
|
||||
BundleUtil.loadLanguageBundle(PluginMainServiceNotConfiguredInspection::class.java.classLoader, "messages.InspectionGadgetsBundle")!!
|
||||
}*/
|
||||
|
||||
|
||||
/**
|
||||
* @since 2.4
|
||||
*/
|
||||
class ResourceNotClosedInspection : AbstractKotlinInspection() {
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
|
||||
return object : KtVisitorVoid() {
|
||||
override fun visitCallExpression(callExpression: KtCallExpression) {
|
||||
for (processor in ResourceNotClosedInspectionProcessors.processors) {
|
||||
processor.visitKtExpr(holder, isOnTheFly, callExpression)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitElement(element: PsiElement) {
|
||||
if (element is PsiCallExpression) {
|
||||
for (processor in ResourceNotClosedInspectionProcessors.processors) {
|
||||
processor.visitPsiExpr(holder, isOnTheFly, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val CONTACT_FQ_NAME = FqName("net.mamoe.mirai.contact.Contact")
|
||||
val CONTACT_COMPANION_FQ_NAME = FqName("net.mamoe.mirai.contact.Contact.Companion")
|
||||
|
||||
fun KtReferenceExpression.resolveCalleeFunction(): KtNamedFunction? {
|
||||
val originalCallee = getCalleeExpressionIfAny()?.referenceExpression()?.resolve() ?: return null
|
||||
if (originalCallee !is KtNamedFunction) return null
|
||||
|
||||
return originalCallee
|
||||
}
|
||||
|
||||
fun KtNamedFunction.isNamedMemberFunctionOf(className: String, functionName: String, extensionReceiver: String? = null): Boolean {
|
||||
if (extensionReceiver != null) {
|
||||
if (this.receiverTypeReference?.resolveReferencedType()?.getKotlinFqName()?.toString() != extensionReceiver) return false
|
||||
}
|
||||
return this.name == functionName && this.containingClassOrObject?.allSuperTypes?.any { it.getKotlinFqName()?.toString() == className } == true
|
||||
}
|
||||
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
object ResourceNotClosedInspectionProcessors {
|
||||
val processors = arrayOf(
|
||||
FirstArgumentProcessor,
|
||||
KtExtensionProcessor
|
||||
)
|
||||
|
||||
interface Processor {
|
||||
fun visitKtExpr(holder: ProblemsHolder, isOnTheFly: Boolean, callExpr: KtCallExpression)
|
||||
fun visitPsiExpr(holder: ProblemsHolder, isOnTheFly: Boolean, expr: PsiCallExpression)
|
||||
}
|
||||
|
||||
object KtExtensionProcessor : Processor {
|
||||
// net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImage(net.mamoe.mirai.utils.ExternalResource, C, kotlin.coroutines.Continuation<? super net.mamoe.mirai.message.MessageReceipt<? extends C>>)
|
||||
val SEND_AS_IMAGE_TO = FunctionSignature {
|
||||
name("sendAsImageTo")
|
||||
dispatchReceiver("net.mamoe.mirai.utils.ExternalResource.Companion")
|
||||
extensionReceiver("net.mamoe.mirai.utils.ExternalResource")
|
||||
}
|
||||
val UPLOAD_AS_IMAGE = FunctionSignature {
|
||||
name("uploadAsImage")
|
||||
dispatchReceiver("net.mamoe.mirai.utils.ExternalResource.Companion")
|
||||
extensionReceiver("net.mamoe.mirai.utils.ExternalResource")
|
||||
}
|
||||
|
||||
override fun visitKtExpr(holder: ProblemsHolder, isOnTheFly: Boolean, callExpr: KtCallExpression) {
|
||||
val parent = callExpr.parent
|
||||
if (parent !is KtDotQualifiedExpression) return
|
||||
val callee = callExpr.resolveCalleeFunction() ?: return
|
||||
|
||||
if (!parent.receiverExpression.isCallingExternalResourceCreators()) return
|
||||
|
||||
class Fix(private val functionName: String) : KotlinCrossLanguageQuickFixAction<KtDotQualifiedExpression>(parent), KotlinUniversalQuickFix {
|
||||
override fun getFamilyName(): String = FAMILY_NAME
|
||||
override fun getText(): String = "修复 $functionName"
|
||||
|
||||
override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) {
|
||||
if (editor == null) return
|
||||
val uploadImageExpression = element ?: return
|
||||
val toExternalExpression = uploadImageExpression.receiverExpression
|
||||
|
||||
val toExternalReceiverExpression = toExternalExpression.dotReceiverExpression() ?: return
|
||||
|
||||
toExternalExpression.replace(toExternalReceiverExpression)
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
callee.hasSignature(SEND_AS_IMAGE_TO) -> {
|
||||
// RECEIVER.sendAsImageTo
|
||||
holder.registerResourceNotClosedProblem(
|
||||
parent.receiverExpression,
|
||||
Fix("sendAsImageTo"),
|
||||
)
|
||||
}
|
||||
callee.hasSignature(UPLOAD_AS_IMAGE) -> {
|
||||
holder.registerResourceNotClosedProblem(
|
||||
parent.receiverExpression,
|
||||
Fix("uploadAsImage"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitPsiExpr(holder: ProblemsHolder, isOnTheFly: Boolean, expr: PsiCallExpression) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object FirstArgumentProcessor : Processor {
|
||||
val CONTACT_UPLOAD_IMAGE = FunctionSignature {
|
||||
name("uploadImage")
|
||||
dispatchReceiver(CONTACT_FQ_NAME)
|
||||
parameters("net.mamoe.mirai.utils.ExternalResource")
|
||||
}
|
||||
val CONTACT_UPLOAD_IMAGE_STATIC = FunctionSignature {
|
||||
name("uploadImage")
|
||||
extensionReceiver(CONTACT_FQ_NAME)
|
||||
dispatchReceiver(CONTACT_COMPANION_FQ_NAME)
|
||||
parameters("net.mamoe.mirai.utils.ExternalResource")
|
||||
}
|
||||
val CONTACT_COMPANION_UPLOAD_IMAGE = FunctionSignature {
|
||||
name("uploadImage")
|
||||
extensionReceiver(CONTACT_FQ_NAME)
|
||||
parameters("net.mamoe.mirai.utils.ExternalResource")
|
||||
}
|
||||
|
||||
val CONTACT_COMPANION_SEND_IMAGE = FunctionSignature {
|
||||
name("sendImage")
|
||||
extensionReceiver(CONTACT_FQ_NAME)
|
||||
parameters("net.mamoe.mirai.utils.ExternalResource")
|
||||
}
|
||||
|
||||
private val signatures = arrayOf(
|
||||
CONTACT_UPLOAD_IMAGE,
|
||||
CONTACT_COMPANION_UPLOAD_IMAGE,
|
||||
CONTACT_COMPANION_SEND_IMAGE
|
||||
)
|
||||
|
||||
override fun visitKtExpr(holder: ProblemsHolder, isOnTheFly: Boolean, callExpr: KtCallExpression) {
|
||||
val callee = callExpr.resolveCalleeFunction() ?: return
|
||||
if (signatures.none { callee.hasSignature(it) }) return
|
||||
|
||||
val firstArgument = callExpr.valueArguments.firstOrNull() ?: return
|
||||
val firstArgumentExpr = firstArgument.getArgumentExpression()
|
||||
if (firstArgumentExpr?.isCallingExternalResourceCreators() != true) return
|
||||
|
||||
holder.registerResourceNotClosedProblem(
|
||||
firstArgument,
|
||||
LocalQuickFix("修复", firstArgumentExpr) {
|
||||
fun tryAddImport() {
|
||||
if (file !is KtFile) return
|
||||
val companion = callee.descriptor?.containingDeclaration?.companionObjectDescriptor() ?: return
|
||||
val toImport = companion.findMemberFunction(callee.nameAsName ?: return) ?: return
|
||||
|
||||
// net.mamoe.mirai.contact.Contact.Companion
|
||||
ImportInsertHelper.getInstance(project).importDescriptor(file, toImport)
|
||||
}
|
||||
|
||||
val newArgumentText = element.dotReceiverExpression()?.text ?: return@LocalQuickFix
|
||||
callExpr.replace(KtPsiFactory(project).createExpression(buildString {
|
||||
append(callee.name)
|
||||
append('(')
|
||||
append(newArgumentText)
|
||||
append(')')
|
||||
}))
|
||||
tryAddImport()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun visitPsiExpr(holder: ProblemsHolder, isOnTheFly: Boolean, expr: PsiCallExpression) {
|
||||
if (expr !is PsiMethodCallExpression) return
|
||||
val callee = expr.resolveMethod() ?: return
|
||||
|
||||
val arguments = expr.argumentList.expressions
|
||||
when {
|
||||
callee.hasSignature(CONTACT_UPLOAD_IMAGE) -> {
|
||||
createFixImpl(
|
||||
expr = expr,
|
||||
holder = holder,
|
||||
argument = arguments.firstOrNull() ?: return,
|
||||
fileTypeArgument = arguments.getOrNull(1)
|
||||
) { it.methodExpression.qualifierExpression?.text ?: "this" }
|
||||
}
|
||||
callee.hasSignature(CONTACT_UPLOAD_IMAGE_STATIC) -> {
|
||||
createFixImpl(
|
||||
expr = expr,
|
||||
holder = holder,
|
||||
argument = arguments.getOrNull(1) ?: return,
|
||||
fileTypeArgument = arguments.getOrNull(2)
|
||||
) { arguments.getOrNull(0)?.text ?: "this" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFixImpl(
|
||||
expr: PsiMethodCallExpression,
|
||||
holder: ProblemsHolder,
|
||||
argument: PsiExpression,
|
||||
fileTypeArgument: PsiExpression?,
|
||||
replaceForFirstArgument: (expr: PsiMethodCallExpression) -> String,
|
||||
) {
|
||||
if (!argument.isCallingExternalResourceCreators()) return
|
||||
|
||||
holder.registerResourceNotClosedProblem(
|
||||
argument,
|
||||
LocalQuickFix("修复", argument) {
|
||||
/*
|
||||
useImage(Contact.uploadImage(contact, ExternalResource.create(file))); // before
|
||||
useImage(Contact.uploadImage(contact, file)); // after
|
||||
*/
|
||||
val factory = project.psiElementFactory ?: return@LocalQuickFix
|
||||
val reference = factory.createExpressionFromText(
|
||||
if (fileTypeArgument == null) {
|
||||
"$CONTACT_FQ_NAME.uploadImage(${replaceForFirstArgument(expr)}, ${argument.argumentList?.expressions?.firstOrNull()?.text ?: ""})"
|
||||
} else {
|
||||
"$CONTACT_FQ_NAME.uploadImage(${replaceForFirstArgument(expr)}, ${argument.argumentList?.expressions?.firstOrNull()?.text ?: ""}, ${fileTypeArgument.text})"
|
||||
},
|
||||
expr.context
|
||||
)
|
||||
expr.replace(reference)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ProblemsHolder.registerResourceNotClosedProblem(target: PsiElement, vararg fixes: LocalQuickFix) {
|
||||
registerProblem(
|
||||
target,
|
||||
@Suppress("DialogTitleCapitalization") "资源未关闭",
|
||||
ProblemHighlightType.WARNING,
|
||||
*fixes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val EXTERNAL_RESOURCE_CREATE = FunctionSignature {
|
||||
name("create")
|
||||
dispatchReceiver("net.mamoe.mirai.utils.ExternalResource.Companion")
|
||||
}
|
||||
private val TO_EXTERNAL_RESOURCE = FunctionSignature {
|
||||
name("toExternalResource")
|
||||
dispatchReceiver("net.mamoe.mirai.utils.ExternalResource.Companion")
|
||||
}
|
||||
|
||||
fun KtExpression.isCallingExternalResourceCreators(): Boolean {
|
||||
val callExpr = resolveToCall(BodyResolveMode.PARTIAL)?.resultingDescriptor ?: return false
|
||||
return callExpr.hasSignature(EXTERNAL_RESOURCE_CREATE) || callExpr.hasSignature(TO_EXTERNAL_RESOURCE)
|
||||
}
|
||||
|
||||
fun PsiExpression.isCallingExternalResourceCreators(): Boolean {
|
||||
contract { returns() implies (this@isCallingExternalResourceCreators is PsiCallExpression) }
|
||||
if (this !is PsiCallExpression) return false
|
||||
val callee = resolveMethod() ?: return false
|
||||
return callee.hasSignature(EXTERNAL_RESOURCE_CREATE)
|
||||
}
|
||||
|
||||
private const val FAMILY_NAME = "Mirai console"
|
@ -11,19 +11,24 @@ package net.mamoe.mirai.console.intellij.diagnostics
|
||||
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiElementFactory
|
||||
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 com.intellij.psi.PsiFile
|
||||
import net.mamoe.mirai.console.intellij.resolve.*
|
||||
import org.jetbrains.kotlin.idea.core.ShortenReferences
|
||||
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
|
||||
import org.jetbrains.kotlin.idea.search.declarationsSearch.findDeepestSuperMethodsKotlinAware
|
||||
import org.jetbrains.kotlin.idea.inspections.KotlinUniversalQuickFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinCrossLanguageQuickFixAction
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
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.getQualifiedExpressionForReceiver
|
||||
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
|
||||
import java.util.*
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
|
||||
|
||||
/*
|
||||
private val bundle by lazy {
|
||||
@ -31,44 +36,119 @@ private val bundle by lazy {
|
||||
}*/
|
||||
|
||||
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
|
||||
companion object {
|
||||
const val DESCRIPTION = "使用 String + Message 会导致 Message 被转换为 String 再相加"
|
||||
private const val MESSAGE_FQ_NAME_STR = "net.mamoe.mirai.message.data.Message"
|
||||
private const val CONVERT_TO_PLAIN_TEXT = "将 String 转换为 PlainText"
|
||||
|
||||
val callee = findDeepestSuperMethodsKotlinAware(originalCallee).lastOrNull() as? KtNamedFunction ?: originalCallee
|
||||
|
||||
fun KtReferenceExpression.isCallingStringPlus(): Boolean {
|
||||
val callee = this.referenceExpression()?.resolve() ?: return false
|
||||
if (callee !is KtNamedFunction) return false
|
||||
|
||||
val className = callee.containingClassOrObject?.fqName?.asString()
|
||||
if (className != "kotlin.String") return@visitor
|
||||
if (callee.name != "plus") return@visitor
|
||||
|
||||
val inspectionTarget = when (val parent = expression.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
|
||||
|
||||
holder.registerProblem(
|
||||
inspectionTarget,
|
||||
"使用 String + Message 会导致 Message 被转换为 String 再相加",
|
||||
ProblemHighlightType.WARNING,
|
||||
ConvertToPlainTextFix(inspectionTarget)
|
||||
)
|
||||
if (className != "kotlin.String") return false
|
||||
if (callee.name != "plus") return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private class Visitor(
|
||||
val holder: ProblemsHolder
|
||||
) : KtVisitorVoid() {
|
||||
class BinaryExprFix(left: KtExpression) : ConvertToPlainTextFix<KtExpression>(left) {
|
||||
override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) {
|
||||
if (editor == null || file !is KtFile) return
|
||||
val element = element ?: return
|
||||
|
||||
val referenceExpr = element.referenceExpression()
|
||||
if (referenceExpr == null || element.parent is KtBinaryExpression) {
|
||||
// `+ operator`, e.g. `str + msg`
|
||||
// or
|
||||
// complex expressions, e.g. `str.toString().plus(msg)`, `"".also { }.plus(msg)`
|
||||
val replaced =
|
||||
element.replace(KtPsiFactory(project).createExpression("net.mamoe.mirai.message.data.PlainText(${element.text})"))
|
||||
as? KtElement ?: return
|
||||
ShortenReferences.DEFAULT.process(replaced)
|
||||
return
|
||||
}
|
||||
|
||||
if (element is KtNameReferenceExpression) {
|
||||
val receiver = element.getQualifiedExpressionForReceiver() ?: return
|
||||
val replaced = receiver
|
||||
.replace(KtPsiFactory(project).createExpression("net.mamoe.mirai.message.data.PlainText(${receiver.text})"))
|
||||
as? KtElement ?: return
|
||||
|
||||
ShortenReferences.DEFAULT.process(replaced)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitBinaryExpression(binaryExpression: KtBinaryExpression) {
|
||||
if (binaryExpression.operationToken != KtTokens.PLUS) return
|
||||
if (binaryExpression.left?.getCalleeExpressionIfAny()?.typeFqName()?.toString() != "kotlin.String") return
|
||||
|
||||
val rightType = binaryExpression.right?.type() ?: return
|
||||
if (!rightType.hasSuperType(MESSAGE_FQ_NAME_STR)) return
|
||||
|
||||
val left = binaryExpression.left ?: return
|
||||
|
||||
holder.registerProblem(
|
||||
left,
|
||||
DESCRIPTION,
|
||||
ProblemHighlightType.WARNING,
|
||||
BinaryExprFix(left)
|
||||
)
|
||||
}
|
||||
|
||||
override fun visitCallExpression(expression: KtCallExpression) {
|
||||
if (!expression.isCallingStringPlus()) return
|
||||
val argumentType = expression.valueArguments.singleOrNull()?.type() ?: return
|
||||
|
||||
if (!argumentType.hasSuperType(MESSAGE_FQ_NAME_STR)) return
|
||||
|
||||
val explicitReceiverExpr = expression.siblingDotReceiverExpression()
|
||||
if (explicitReceiverExpr != null) {
|
||||
holder.registerProblem(
|
||||
explicitReceiverExpr,
|
||||
DESCRIPTION,
|
||||
ProblemHighlightType.WARNING,
|
||||
LocalQuickFix(CONVERT_TO_PLAIN_TEXT, explicitReceiverExpr) {
|
||||
element.replaceExpressionAndShortenReferences("net.mamoe.mirai.message.data.PlainText(${element.text})")
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val nameReferenceExpression = expression.findChild<KtNameReferenceExpression>() ?: expression.calleeExpression ?: expression
|
||||
holder.registerProblem(
|
||||
nameReferenceExpression,
|
||||
DESCRIPTION,
|
||||
ProblemHighlightType.WARNING,
|
||||
LocalQuickFix(CONVERT_TO_PLAIN_TEXT, expression) {
|
||||
val callExpression = this.element.calleeExpression ?: return@LocalQuickFix
|
||||
val implicitReceiverText = this.element.implicitExpressionText() ?: return@LocalQuickFix
|
||||
|
||||
this.element.replaceExpressionAndShortenReferences(
|
||||
"net.mamoe.mirai.message.data.PlainText(${implicitReceiverText}).${callExpression.text}"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor = Visitor(holder)
|
||||
|
||||
abstract class ConvertToPlainTextFix<T : PsiElement>(element: T) : KotlinCrossLanguageQuickFixAction<T>(element), KotlinUniversalQuickFix {
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
override fun getFamilyName(): String = "Mirai Console"
|
||||
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
override fun getText(): String = "将 String 转换为 PlainText"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun KtElement.replaceExpressionAndShortenReferences(expression: String) {
|
||||
val replaced = replace(KtPsiFactory(this.project).createExpression(expression)) as? KtElement ?: return
|
||||
ShortenReferences.DEFAULT.process(replaced)
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
|
||||
package net.mamoe.mirai.console.intellij.diagnostics
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
import net.mamoe.mirai.console.compiler.common.castOrNull
|
||||
import net.mamoe.mirai.console.compiler.common.resolve.READ_ONLY_PLUGIN_DATA_FQ_NAME
|
||||
import net.mamoe.mirai.console.intellij.resolve.getResolvedCall
|
||||
@ -18,6 +19,7 @@ import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName
|
||||
import org.jetbrains.kotlin.idea.references.mainReference
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.KtElement
|
||||
import org.jetbrains.kotlin.psi.KtTypeParameter
|
||||
import org.jetbrains.kotlin.psi.KtTypeReference
|
||||
import org.jetbrains.kotlin.psi.KtUserType
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||
@ -37,7 +39,7 @@ fun DeclarationCheckerContext.report(diagnostic: Diagnostic) {
|
||||
|
||||
val DeclarationCheckerContext.bindingContext get() = this.trace.bindingContext
|
||||
|
||||
fun KtElement?.getResolvedCall(
|
||||
fun KtElement.getResolvedCall(
|
||||
context: DeclarationCheckerContext,
|
||||
): ResolvedCall<out CallableDescriptor>? {
|
||||
return this.getResolvedCall(context.bindingContext)
|
||||
@ -49,4 +51,12 @@ fun KtTypeReference.isReferencing(fqName: FqName): Boolean {
|
||||
|
||||
val KtTypeReference.referencedUserType: KtUserType? get() = this.typeElement.castOrNull()
|
||||
|
||||
fun KtTypeReference.resolveReferencedType() = referencedUserType?.referenceExpression?.mainReference?.resolve()
|
||||
fun KtTypeReference.resolveReferencedType(): PsiElement? {
|
||||
val resolved = referencedUserType?.referenceExpression?.mainReference?.resolve()
|
||||
if (resolved is KtTypeParameter) {
|
||||
val bound = resolved.extendsBound ?: return resolved
|
||||
if (bound.name == resolved.name) return null // <C: C> bad type, avoid infinite run
|
||||
return bound.resolveReferencedType()
|
||||
}
|
||||
return resolved
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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.PsiFile
|
||||
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.inspections.KotlinUniversalQuickFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinCrossLanguageQuickFixAction
|
||||
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.referenceExpression
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
val referenceExpr = element.referenceExpression()
|
||||
if (referenceExpr == null || element.parent is KtBinaryExpression) {
|
||||
// + operator, e.g. 'str + msg'
|
||||
// or
|
||||
// complex expressions, e.g. 'str.toString().plus(msg)', '"".also { }.plus(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 = referenceExpr.resolve()
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 com.intellij.openapi.projectRoots.JavaSdkVersion
|
||||
import com.intellij.openapi.projectRoots.JavaVersionService
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiMethodCallExpression
|
||||
import net.mamoe.mirai.console.intellij.diagnostics.ResourceNotClosedInspectionProcessors.KtExtensionProcessor.SEND_AS_IMAGE_TO
|
||||
import net.mamoe.mirai.console.intellij.diagnostics.ResourceNotClosedInspectionProcessors.KtExtensionProcessor.UPLOAD_AS_IMAGE
|
||||
import net.mamoe.mirai.console.intellij.diagnostics.replaceExpressionAndShortenReferences
|
||||
import net.mamoe.mirai.console.intellij.diagnostics.resolveCalleeFunction
|
||||
import net.mamoe.mirai.console.intellij.resolve.hasSignature
|
||||
import org.jetbrains.kotlin.idea.intentions.SelfTargetingIntention
|
||||
import org.jetbrains.kotlin.idea.util.module
|
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
|
||||
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
|
||||
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
|
||||
|
||||
/**
|
||||
* @since 2.4
|
||||
*/
|
||||
class WrapWithResourceUseCallIntention : SelfTargetingIntention<KtDotQualifiedExpression>(KtDotQualifiedExpression::class.java, { "转换为 .use" }) {
|
||||
override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) {
|
||||
val selectorExpression = element.selectorExpression ?: return
|
||||
selectorExpression.replaceExpressionAndShortenReferences("use { it.${selectorExpression.text} }")
|
||||
}
|
||||
|
||||
override fun isApplicableTo(element: KtDotQualifiedExpression, caretOffset: Int): Boolean {
|
||||
val callee = element.selectorExpression?.referenceExpression()?.resolveCalleeFunction() ?: return false
|
||||
if (!callee.hasSignature(UPLOAD_AS_IMAGE) && !callee.hasSignature(SEND_AS_IMAGE_TO)) return false
|
||||
val receiver = element.receiverExpression
|
||||
return receiver is KtSimpleNameExpression
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/mamoe/mirai-console/issues/284
|
||||
/**
|
||||
*
|
||||
* to be supported by 2.5
|
||||
* @since 2.4
|
||||
*/
|
||||
class WrapWithResourceUseCallJavaIntention : SelfTargetingIntention<PsiMethodCallExpression>(PsiMethodCallExpression::class.java, { "转换为 .use" }) {
|
||||
override fun applyTo(element: PsiMethodCallExpression, editor: Editor?) {
|
||||
// val selectorExpression = element.methodExpression
|
||||
|
||||
}
|
||||
|
||||
override fun isApplicableTo(element: PsiMethodCallExpression, caretOffset: Int): Boolean {
|
||||
return false
|
||||
// if (!element.isJavaVersionAtLeast(JavaSdkVersion.JDK_1_9)) return false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun PsiElement.isJavaVersionAtLeast(version: JavaSdkVersion): Boolean {
|
||||
return this.module?.getService(JavaVersionService::class.java)?.isAtLeast(this, JavaSdkVersion.JDK_1_9) == true
|
||||
}
|
153
tools/intellij-plugin/src/resolve/FunctionSignature.kt
Normal file
153
tools/intellij-plugin/src/resolve/FunctionSignature.kt
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.PsiMethod
|
||||
import com.intellij.psi.PsiModifier
|
||||
import net.mamoe.mirai.console.intellij.diagnostics.resolveReferencedType
|
||||
import org.jetbrains.kotlin.asJava.elements.KtLightMethod
|
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
|
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getReturnTypeReference
|
||||
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName
|
||||
import org.jetbrains.kotlin.idea.search.getKotlinFqName
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.nj2k.postProcessing.type
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtFunction
|
||||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
|
||||
inline fun FunctionSignature(builderAction: FunctionSignatureBuilder.() -> Unit): FunctionSignature {
|
||||
return FunctionSignatureBuilder().apply(builderAction).build()
|
||||
}
|
||||
|
||||
data class FunctionSignature(
|
||||
val name: String? = null,
|
||||
val dispatchReceiver: FqName? = null,
|
||||
val extensionReceiver: FqName? = null,
|
||||
val parameters: List<FqName>? = null,
|
||||
val returnType: FqName? = null,
|
||||
)
|
||||
|
||||
class FunctionSignatureBuilder {
|
||||
private var name: String? = null
|
||||
private var dispatchReceiver: FqName? = null
|
||||
private var extensionReceiver: FqName? = null
|
||||
private var parameters: List<FqName>? = null
|
||||
private var returnType: FqName? = null
|
||||
|
||||
fun name(name: String) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun dispatchReceiver(dispatchReceiver: String) {
|
||||
this.dispatchReceiver = FqName(dispatchReceiver)
|
||||
}
|
||||
|
||||
fun extensionReceiver(extensionReceiver: String) {
|
||||
this.extensionReceiver = FqName(extensionReceiver)
|
||||
}
|
||||
|
||||
fun parameters(vararg parameters: String) {
|
||||
this.parameters = parameters.map { FqName(it) }
|
||||
}
|
||||
|
||||
fun returnType(returnType: String) {
|
||||
this.returnType = FqName(returnType)
|
||||
}
|
||||
|
||||
fun build(): FunctionSignature = FunctionSignature(name, dispatchReceiver, extensionReceiver, parameters, returnType)
|
||||
}
|
||||
|
||||
fun FunctionSignatureBuilder.dispatchReceiver(dispatchReceiver: FqName) {
|
||||
dispatchReceiver(dispatchReceiver.toString())
|
||||
}
|
||||
|
||||
fun FunctionSignatureBuilder.extensionReceiver(extensionReceiver: FqName) {
|
||||
extensionReceiver(extensionReceiver.toString())
|
||||
}
|
||||
|
||||
|
||||
fun KtFunction.hasSignature(functionSignature: FunctionSignature): Boolean {
|
||||
if (functionSignature.name != null) {
|
||||
if (this.name != functionSignature.name) return false
|
||||
}
|
||||
if (functionSignature.dispatchReceiver != null) {
|
||||
if (this.containingClassOrObject?.fqName != functionSignature.dispatchReceiver) return false
|
||||
}
|
||||
if (functionSignature.extensionReceiver != null) {
|
||||
if (this.receiverTypeReference?.resolveReferencedType()?.getKotlinFqName() != functionSignature.extensionReceiver) return false
|
||||
}
|
||||
if (functionSignature.parameters != null) {
|
||||
if (this.valueParameters.zip(functionSignature.parameters).any { it.first.type()?.fqName != it.second }) return false
|
||||
}
|
||||
if (functionSignature.returnType != null) {
|
||||
if (this.getReturnTypeReference()?.resolveReferencedType()?.getKotlinFqName() != functionSignature.returnType) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun KtLightMethod.hasSignature(functionSignature: FunctionSignature): Boolean {
|
||||
if (functionSignature.name != null) {
|
||||
if (this.name != functionSignature.name) return false
|
||||
}
|
||||
val parameters = parameterList.parameters.toMutableList()
|
||||
if (functionSignature.dispatchReceiver != null) {
|
||||
val kotlinContainingClassFqn =
|
||||
if (this.modifierList.hasExplicitModifier(PsiModifier.STATIC)) {
|
||||
this.containingClass.kotlinOrigin?.companionObjects?.firstOrNull()?.fqName
|
||||
} else this.containingClass.getKotlinFqName()
|
||||
if (kotlinContainingClassFqn != functionSignature.dispatchReceiver) return false
|
||||
}
|
||||
if (functionSignature.extensionReceiver != null) {
|
||||
val receiver = parameters.removeFirstOrNull() ?: return false
|
||||
if (receiver.type.canonicalText != functionSignature.extensionReceiver.toString()) return false
|
||||
}
|
||||
if (functionSignature.parameters != null) {
|
||||
if (parameters.zip(functionSignature.parameters).any { it.first.type.canonicalText != it.second.toString() }) return false
|
||||
}
|
||||
if (functionSignature.returnType != null) {
|
||||
if (returnType?.canonicalText != functionSignature.returnType.toString()) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun PsiMethod.hasSignature(functionSignature: FunctionSignature): Boolean {
|
||||
if (this is KtLightMethod) {
|
||||
return this.hasSignature(functionSignature)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun KtExpression.isCalling(functionSignature: FunctionSignature): Boolean {
|
||||
val descriptor = resolveToCall(BodyResolveMode.PARTIAL)?.resultingDescriptor ?: return false
|
||||
return descriptor.hasSignature(functionSignature)
|
||||
}
|
||||
|
||||
fun CallableDescriptor.hasSignature(functionSignature: FunctionSignature): Boolean {
|
||||
if (functionSignature.name != null) {
|
||||
if (this.name.toString() != functionSignature.name) return false
|
||||
}
|
||||
if (functionSignature.extensionReceiver != null) {
|
||||
if (this.extensionReceiverParameter?.fqNameUnsafe != functionSignature.extensionReceiver.toUnsafe()) return false
|
||||
}
|
||||
if (functionSignature.dispatchReceiver != null) {
|
||||
if (this.containingDeclaration.fqNameUnsafe != functionSignature.dispatchReceiver.toUnsafe()) return false
|
||||
}
|
||||
if (functionSignature.parameters != null) {
|
||||
if (this.valueParameters.zip(functionSignature.parameters).any { it.first.type.fqName != it.second }) return false
|
||||
}
|
||||
if (functionSignature.returnType != null) {
|
||||
if (this.returnType?.fqName != functionSignature.returnType) return false
|
||||
}
|
||||
return true
|
||||
}
|
96
tools/intellij-plugin/src/resolve/ReceiverExpression.kt
Normal file
96
tools/intellij-plugin/src/resolve/ReceiverExpression.kt
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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 org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.util.getFactoryForImplicitReceiverWithSubtypeOf
|
||||
import org.jetbrains.kotlin.idea.util.getResolutionScope
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtPsiFactory
|
||||
|
||||
sealed class ReceiverExpression {
|
||||
abstract val receiverExpression: KtExpression
|
||||
abstract val receiverText: String
|
||||
|
||||
operator fun component1(): KtExpression = receiverExpression
|
||||
operator fun component2(): String = receiverText
|
||||
|
||||
class Explicit(
|
||||
override val receiverExpression: KtExpression
|
||||
) : ReceiverExpression() {
|
||||
override val receiverText: String
|
||||
get() = receiverExpression.text
|
||||
}
|
||||
|
||||
class Implicit(
|
||||
receiverExpression: Lazy<KtExpression>,
|
||||
override val receiverText: String,
|
||||
) : ReceiverExpression() {
|
||||
override val receiverExpression: KtExpression by receiverExpression
|
||||
}
|
||||
}
|
||||
|
||||
fun KtCallExpression.siblingDotReceiverExpression(): KtExpression? {
|
||||
val dotQualifiedExpression = parent
|
||||
if (dotQualifiedExpression is KtDotQualifiedExpression) {
|
||||
return dotQualifiedExpression.receiverExpression
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun KtExpression.dotReceiverExpression(): KtExpression? {
|
||||
return if (this is KtDotQualifiedExpression) receiverExpression else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Find:
|
||||
* - explicit receiver: `a` for `a.foo()`
|
||||
* - implicit labeled receiver: `this@run` in `a.run { foo() }`
|
||||
*
|
||||
* @receiver identifier reference in a call. e.g. `foo` in `a.foo()` and `foo()`
|
||||
*/
|
||||
fun KtExpression.receiverExpression(psiFactory: KtPsiFactory): ReceiverExpression? {
|
||||
val dotQualifiedExpr = parent
|
||||
if (dotQualifiedExpr is KtDotQualifiedExpression) {
|
||||
return ReceiverExpression.Explicit(dotQualifiedExpr.receiverExpression)
|
||||
} else {
|
||||
val context = analyze()
|
||||
val scope = getResolutionScope(context) ?: return null
|
||||
|
||||
val descriptor = getResolvedCall(context)?.resultingDescriptor ?: return null
|
||||
val receiverDescriptor = descriptor.extensionReceiverParameter
|
||||
?: descriptor.dispatchReceiverParameter
|
||||
?: return null
|
||||
|
||||
val expressionFactory = scope.getFactoryForImplicitReceiverWithSubtypeOf(receiverDescriptor.type) ?: return null
|
||||
val receiverText = if (expressionFactory.isImmediate) "this" else expressionFactory.expressionText
|
||||
return ReceiverExpression.Implicit(lazy { expressionFactory.createExpression(psiFactory, true) }, receiverText)
|
||||
}
|
||||
}
|
||||
|
||||
fun KtExpression.implicitExpressionText(): String? {
|
||||
val dotQualifiedExpr = parent
|
||||
if (dotQualifiedExpr is KtDotQualifiedExpression) {
|
||||
return null
|
||||
} else {
|
||||
val context = analyze()
|
||||
val scope = getResolutionScope(context) ?: return null
|
||||
|
||||
val descriptor = getResolvedCall(context)?.resultingDescriptor ?: return null
|
||||
val receiverDescriptor = descriptor.extensionReceiverParameter
|
||||
?: descriptor.dispatchReceiverParameter
|
||||
?: return null
|
||||
|
||||
val expressionFactory = scope.getFactoryForImplicitReceiverWithSubtypeOf(receiverDescriptor.type) ?: return null
|
||||
return if (expressionFactory.isImmediate) "this" else expressionFactory.expressionText
|
||||
}
|
||||
}
|
@ -9,23 +9,24 @@
|
||||
|
||||
package net.mamoe.mirai.console.intellij.resolve
|
||||
|
||||
import com.intellij.psi.PsiClass
|
||||
import com.intellij.psi.PsiDeclarationStatement
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiModifierListOwner
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.util.parentsWithSelf
|
||||
import net.mamoe.mirai.console.compiler.common.castOrNull
|
||||
import net.mamoe.mirai.console.compiler.common.resolve.*
|
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.VariableDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
|
||||
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.incremental.components.NoLookupLocation
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.nj2k.postProcessing.resolve
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getCall
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
|
||||
@ -36,6 +37,7 @@ 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.TypeProjection
|
||||
import org.jetbrains.kotlin.types.typeUtil.supertypes
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
|
||||
|
||||
@ -203,14 +205,23 @@ val PsiElement.allChildrenFlat: Sequence<PsiElement>
|
||||
|
||||
inline fun <reified E> PsiElement.findChild(): E? = this.children.find { it is E } as E?
|
||||
|
||||
fun KtElement?.getResolvedCall(
|
||||
context: BindingContext,
|
||||
fun KtValueArgument.type() = getArgumentExpression()?.referenceExpression()?.type()
|
||||
fun KtExpression.resultingDescriptor() = resolveToCall(BodyResolveMode.PARTIAL)?.resultingDescriptor
|
||||
fun KtExpression.type() = resultingDescriptor()?.returnType
|
||||
fun KtReferenceExpression.typeFqName() = type()?.fqName
|
||||
fun KtExpression.typeFqName() = referenceExpression()?.typeFqName()
|
||||
|
||||
fun KtElement.getResolvedCall(
|
||||
context: BindingContext = analyze(BodyResolveMode.PARTIAL),
|
||||
): ResolvedCall<out CallableDescriptor>? {
|
||||
return this?.getCall(context)?.getResolvedCall(context)
|
||||
return this.getCall(context)?.getResolvedCall(context)
|
||||
}
|
||||
|
||||
val ResolvedCall<out CallableDescriptor>.valueParameters: List<ValueParameterDescriptor> get() = this.resultingDescriptor.valueParameters
|
||||
|
||||
val Project.psiElementFactory: PsiElementFactory?
|
||||
get() = PsiElementFactory.getInstance(this)
|
||||
|
||||
fun ConstantValue<*>.selfOrChildrenConstantStrings(): Sequence<String> {
|
||||
return when (this) {
|
||||
is StringValue -> sequenceOf(value)
|
||||
@ -221,6 +232,17 @@ fun ConstantValue<*>.selfOrChildrenConstantStrings(): Sequence<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fun ClassDescriptor.findMemberFunction(name: Name, vararg typeProjection: TypeProjection): SimpleFunctionDescriptor? {
|
||||
return getMemberScope(typeProjection.toList()).getContributedFunctions(name, NoLookupLocation.FROM_IDE).firstOrNull()
|
||||
}
|
||||
|
||||
fun DeclarationDescriptor.companionObjectDescriptor(): ClassDescriptor? {
|
||||
if (this !is ClassDescriptor) {
|
||||
return null
|
||||
}
|
||||
return this.companionObjectDescriptor
|
||||
}
|
||||
|
||||
fun KtExpression.resolveStringConstantValues(): Sequence<String> {
|
||||
when (this) {
|
||||
is KtNameReferenceExpression -> {
|
||||
|
Loading…
Reference in New Issue
Block a user