diff --git a/tools/intellij-plugin/resources/inspectionDescriptions/ResourceNotClosed.html b/tools/intellij-plugin/resources/inspectionDescriptions/ResourceNotClosed.html
new file mode 100644
index 000000000..a46f78d57
--- /dev/null
+++ b/tools/intellij-plugin/resources/inspectionDescriptions/ResourceNotClosed.html
@@ -0,0 +1,8 @@
+
+
+检查 ExternalResource 没有 close 的情况。
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/after.receiver.template b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/after.receiver.template
new file mode 100644
index 000000000..0376bed2c
--- /dev/null
+++ b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/after.receiver.template
@@ -0,0 +1 @@
+resource.use { it.uploadAsImage(contact) }
\ No newline at end of file
diff --git a/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/before.receiver.template b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/before.receiver.template
new file mode 100644
index 000000000..de1177ddb
--- /dev/null
+++ b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/before.receiver.template
@@ -0,0 +1 @@
+resource.uploadAsImage(contact)
\ No newline at end of file
diff --git a/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/description.html b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/description.html
new file mode 100644
index 000000000..60ee35f8e
--- /dev/null
+++ b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallIntention/description.html
@@ -0,0 +1,8 @@
+
+
+将资源直接使用转换为 `.use { ... }`
+
+
+将 `resource.sendAsImageTo(contact)` 转换为 `resource.use { it.sendAsImageTo(contact) }`
+
+
\ No newline at end of file
diff --git a/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/after.action.template b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/after.action.template
new file mode 100644
index 000000000..a89174a24
--- /dev/null
+++ b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/after.action.template
@@ -0,0 +1,4 @@
+ExternalResource resource;
+Contact contact;
+
+resource.use { it.uploadAsImage(contact) }
\ No newline at end of file
diff --git a/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/before.action.template b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/before.action.template
new file mode 100644
index 000000000..2c3f0ccdb
--- /dev/null
+++ b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/before.action.template
@@ -0,0 +1,4 @@
+ExternalResource resource;
+Contact contact;
+
+resource.uploadAsImage(contact)
\ No newline at end of file
diff --git a/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/description.html b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/description.html
new file mode 100644
index 000000000..60ee35f8e
--- /dev/null
+++ b/tools/intellij-plugin/resources/intentionDescriptions/WrapWithResourceUseCallJavaIntention/description.html
@@ -0,0 +1,8 @@
+
+
+将资源直接使用转换为 `.use { ... }`
+
+
+将 `resource.sendAsImageTo(contact)` 转换为 `resource.use { it.sendAsImageTo(contact) }`
+
+
\ 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 ae2906e9a..0325fbfde 100644
--- a/tools/intellij-plugin/resources/messages/InspectionGadgetsBundle.properties
+++ b/tools/intellij-plugin/resources/messages/InspectionGadgetsBundle.properties
@@ -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
\ No newline at end of file
+using.string.plus.message.display.name=Using string plus message
+resource.not.closed.display.name=Resource not closed
\ 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 6a4f1e3a4..917c55a6a 100644
--- a/tools/intellij-plugin/resources/messages/InspectionsBundle.properties
+++ b/tools/intellij-plugin/resources/messages/InspectionsBundle.properties
@@ -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
\ No newline at end of file
+group.names.message.issues=Message issues
+group.names.mirai.core.issues=Mirai core issues
\ No newline at end of file
diff --git a/tools/intellij-plugin/run/projects/test-project/build.gradle.kts b/tools/intellij-plugin/run/projects/test-project/build.gradle.kts
index 6161f7658..555761955 100644
--- a/tools/intellij-plugin/run/projects/test-project/build.gradle.kts
+++ b/tools/intellij-plugin/run/projects/test-project/build.gradle.kts
@@ -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()
}
\ No newline at end of file
diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/java/test/ResourceNotClosedInspectionTestJava.java b/tools/intellij-plugin/run/projects/test-project/src/main/java/test/ResourceNotClosedInspectionTestJava.java
new file mode 100644
index 000000000..3034e5657
--- /dev/null
+++ b/tools/intellij-plugin/run/projects/test-project/src/main/java/test/ResourceNotClosedInspectionTestJava.java
@@ -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) {
+
+ }
+}
diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/ResourceNotClosedInspectionTest.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/ResourceNotClosedInspectionTest.kt
new file mode 100644
index 000000000..1ad87a8fc
--- /dev/null
+++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/ResourceNotClosedInspectionTest.kt
@@ -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()
+ val contact = magic()
+
+ 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 magic(): T = null!!
\ No newline at end of file
diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/StringPlusMessageInspectionTest.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/StringPlusMessageInspectionTest.kt
index 4a2e0028d..b980c073c 100644
--- a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/StringPlusMessageInspectionTest.kt
+++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/StringPlusMessageInspectionTest.kt
@@ -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)
}
\ No newline at end of file
diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/WrapWithResourceUseCallIntentionTest.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/WrapWithResourceUseCallIntentionTest.kt
new file mode 100644
index 000000000..909526a4d
--- /dev/null
+++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/WrapWithResourceUseCallIntentionTest.kt
@@ -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()
+ val contact = magic()
+ val resource = magic()
+
+ resource.sendAsImageTo(contact)
+ resource.run { sendAsImageTo(contact) }
+ }
+}
\ No newline at end of file
diff --git a/tools/intellij-plugin/src/diagnostics/QuickFixUtils.kt b/tools/intellij-plugin/src/diagnostics/QuickFixUtils.kt
new file mode 100644
index 000000000..2c40dd25e
--- /dev/null
+++ b/tools/intellij-plugin/src/diagnostics/QuickFixUtils.kt
@@ -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 LocalQuickFix(text: String, element: T, invokeAction: QuickFixInvoke.() -> Unit): LocalQuickFix {
+ return object: KotlinCrossLanguageQuickFixAction(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(
+ val project: Project,
+ val editor: Editor,
+ val file: PsiFile,
+ val element: T,
+)
\ No newline at end of file
diff --git a/tools/intellij-plugin/src/diagnostics/ResourceNotClosedInspection.kt b/tools/intellij-plugin/src/diagnostics/ResourceNotClosedInspection.kt
new file mode 100644
index 000000000..aa551a299
--- /dev/null
+++ b/tools/intellij-plugin/src/diagnostics/ResourceNotClosedInspection.kt
@@ -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(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"
\ No newline at end of file
diff --git a/tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt b/tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt
index 1d6416233..c7ec72779 100644
--- a/tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt
+++ b/tools/intellij-plugin/src/diagnostics/UsingStringPlusMessageInspection.kt
@@ -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()
- ?.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
-
- 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(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() ?: 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(element: T) : KotlinCrossLanguageQuickFixAction(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)
}
\ No newline at end of file
diff --git a/tools/intellij-plugin/src/diagnostics/diagnosticsUtil.kt b/tools/intellij-plugin/src/diagnostics/diagnosticsUtil.kt
index 4eab126ea..eb3635208 100644
--- a/tools/intellij-plugin/src/diagnostics/diagnosticsUtil.kt
+++ b/tools/intellij-plugin/src/diagnostics/diagnosticsUtil.kt
@@ -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? {
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()
\ No newline at end of file
+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 // bad type, avoid infinite run
+ return bound.resolveReferencedType()
+ }
+ return resolved
+}
\ 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
deleted file mode 100644
index dc314655f..000000000
--- a/tools/intellij-plugin/src/diagnostics/fix/ConvertToPlainTextFix.kt
+++ /dev/null
@@ -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(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)
-
- }
- }
-}
diff --git a/tools/intellij-plugin/src/diagnostics/fix/WrapWithResourceUseCallIntention.kt b/tools/intellij-plugin/src/diagnostics/fix/WrapWithResourceUseCallIntention.kt
new file mode 100644
index 000000000..539d201db
--- /dev/null
+++ b/tools/intellij-plugin/src/diagnostics/fix/WrapWithResourceUseCallIntention.kt
@@ -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::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::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
+}
\ No newline at end of file
diff --git a/tools/intellij-plugin/src/resolve/FunctionSignature.kt b/tools/intellij-plugin/src/resolve/FunctionSignature.kt
new file mode 100644
index 000000000..50b244aea
--- /dev/null
+++ b/tools/intellij-plugin/src/resolve/FunctionSignature.kt
@@ -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? = 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? = 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
+}
\ No newline at end of file
diff --git a/tools/intellij-plugin/src/resolve/ReceiverExpression.kt b/tools/intellij-plugin/src/resolve/ReceiverExpression.kt
new file mode 100644
index 000000000..921e53a17
--- /dev/null
+++ b/tools/intellij-plugin/src/resolve/ReceiverExpression.kt
@@ -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,
+ 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
+ }
+}
\ No newline at end of file
diff --git a/tools/intellij-plugin/src/resolve/resolveIdea.kt b/tools/intellij-plugin/src/resolve/resolveIdea.kt
index eeb470978..9344b3b48 100644
--- a/tools/intellij-plugin/src/resolve/resolveIdea.kt
+++ b/tools/intellij-plugin/src/resolve/resolveIdea.kt
@@ -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
inline fun 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? {
- return this?.getCall(context)?.getResolvedCall(context)
+ return this.getCall(context)?.getResolvedCall(context)
}
val ResolvedCall.valueParameters: List get() = this.resultingDescriptor.valueParameters
+val Project.psiElementFactory: PsiElementFactory?
+ get() = PsiElementFactory.getInstance(this)
+
fun ConstantValue<*>.selfOrChildrenConstantStrings(): Sequence {
return when (this) {
is StringValue -> sequenceOf(value)
@@ -221,6 +232,17 @@ fun ConstantValue<*>.selfOrChildrenConstantStrings(): Sequence {
}
}
+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 {
when (this) {
is KtNameReferenceExpression -> {