diff --git a/tools/intellij-plugin/resources/META-INF/plugin.xml b/tools/intellij-plugin/resources/META-INF/plugin.xml index be28484d2..a597d1735 100644 --- a/tools/intellij-plugin/resources/META-INF/plugin.xml +++ b/tools/intellij-plugin/resources/META-INF/plugin.xml @@ -44,18 +44,34 @@ groupKey="group.names.plugin.service.issues" enabledByDefault="true" level="WARNING" implementationClass="net.mamoe.mirai.console.intellij.diagnostics.PluginMainServiceNotConfiguredInspection"/> + + + + - + + + net.mamoe.mirai.console.intellij.diagnostics.fix.WrapWithResourceUseCallJavaIntention + Mirai console + + 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>) + 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 -> {