diff --git a/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt b/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt index ff3989766..c80ad2d7c 100644 --- a/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt +++ b/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt @@ -37,6 +37,21 @@ object MiraiConsoleErrors { @JvmField val NOT_CONSTRUCTABLE_TYPE = create(ERROR) + @JvmField + val USING_DERIVED_MUTABLE_MAP_TYPE = create(ERROR) + + @JvmField + val USING_DERIVED_MAP_TYPE = create(ERROR) + + @JvmField + val USING_DERIVED_LIST_TYPE = create(ERROR) + + @JvmField + val USING_DERIVED_MUTABLE_LIST_TYPE = create(ERROR) + + @JvmField + val USING_DERIVED_CONCURRENT_MAP_TYPE = create(ERROR) + @JvmField val UNSERIALIZABLE_TYPE = create(ERROR) diff --git a/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt b/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt index fac10869c..84e8d040a 100644 --- a/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt +++ b/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt @@ -22,6 +22,11 @@ import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.NO import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.READ_ONLY_VALUE_CANNOT_BE_VAR import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.RESTRICTED_CONSOLE_COMMAND_OWNER import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.UNSERIALIZABLE_TYPE +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.USING_DERIVED_CONCURRENT_MAP_TYPE +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.USING_DERIVED_LIST_TYPE +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.USING_DERIVED_MAP_TYPE +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.USING_DERIVED_MUTABLE_LIST_TYPE +import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.USING_DERIVED_MUTABLE_MAP_TYPE import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap import org.jetbrains.kotlin.diagnostics.rendering.Renderers @@ -114,6 +119,41 @@ object MiraiConsoleErrorsRendering : DefaultErrorMessages.Extension { "在 ReadOnlyPluginData 中不可定义 'var' by value", ) + put( + USING_DERIVED_MAP_TYPE, + "使用 'Map' 的派生类型 '{1}'.", + Renderers.EMPTY, + Renderers.STRING, + ) + + put( + USING_DERIVED_MUTABLE_MAP_TYPE, + "使用 'MutableMap' 的派生类型 '{1}'.", + Renderers.EMPTY, + Renderers.STRING, + ) + + put( + USING_DERIVED_LIST_TYPE, + "使用 'List' 的派生类型 '{1}'.", + Renderers.EMPTY, + Renderers.STRING, + ) + + put( + USING_DERIVED_MUTABLE_LIST_TYPE, + "使用 'MutableList' 的派生类型 '{1}'.", + Renderers.EMPTY, + Renderers.STRING, + ) + + put( + USING_DERIVED_CONCURRENT_MAP_TYPE, + "使用 'ConcurrentMap' 的派生类型 '{1}'.", + Renderers.EMPTY, + Renderers.STRING, + ) + // put( // INAPPLICABLE_COMMAND_ANNOTATION, // "''{0}'' 无法在顶层函数使用.", 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 0311d44b7..3188dee80 100644 --- a/tools/intellij-plugin/run/projects/test-project/build.gradle.kts +++ b/tools/intellij-plugin/run/projects/test-project/build.gradle.kts @@ -8,6 +8,10 @@ plugins { group = "org.example" version = "1.0-SNAPSHOT" +dependencies { + +} + repositories { jcenter() mavenCentral() diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/UsingDerivedMap.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/UsingDerivedMap.kt new file mode 100644 index 000000000..728287321 --- /dev/null +++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/UsingDerivedMap.kt @@ -0,0 +1,28 @@ +/* + * 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 org.example.myplugin + +import net.mamoe.mirai.console.data.AutoSavePluginConfig +import net.mamoe.mirai.console.data.ReadOnlyPluginConfig +import net.mamoe.mirai.console.data.value +import org.example.myplugin.DataTest1.provideDelegate +import java.util.concurrent.ConcurrentHashMap + + +object UsingDerivedMap : AutoSavePluginConfig("data") { + var p1 by value>() + var p2 by value>() + + var p4 by value>() + var p3 by value>() + + var p5 by value>() + var p6 by value>() +} diff --git a/tools/intellij-plugin/src/QuickFixRegistrar.kt b/tools/intellij-plugin/src/QuickFixRegistrar.kt index 334195a3d..a088cecff 100644 --- a/tools/intellij-plugin/src/QuickFixRegistrar.kt +++ b/tools/intellij-plugin/src/QuickFixRegistrar.kt @@ -11,9 +11,7 @@ package net.mamoe.mirai.console.intellij import com.intellij.codeInsight.intention.IntentionAction import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors -import net.mamoe.mirai.console.intellij.diagnostics.fix.AddSerializerFix -import net.mamoe.mirai.console.intellij.diagnostics.fix.ConvertToValFix -import net.mamoe.mirai.console.intellij.diagnostics.fix.ProvideDefaultValueFix +import net.mamoe.mirai.console.intellij.diagnostics.fix.* import org.jetbrains.kotlin.diagnostics.DiagnosticFactory import org.jetbrains.kotlin.idea.quickfix.KotlinIntentionActionsFactory import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor @@ -33,5 +31,11 @@ class QuickFixRegistrar : QuickFixContributor { MiraiConsoleErrors.UNSERIALIZABLE_TYPE.registerFactory(AddSerializerFix) MiraiConsoleErrors.NOT_CONSTRUCTABLE_TYPE.registerFactory(ProvideDefaultValueFix) MiraiConsoleErrors.READ_ONLY_VALUE_CANNOT_BE_VAR.registerFactory(ConvertToValFix) + + MiraiConsoleErrors.USING_DERIVED_MAP_TYPE.registerFactory(ConvertToMapFix) + MiraiConsoleErrors.USING_DERIVED_MUTABLE_MAP_TYPE.registerFactory(ConvertToMutableMapFix) + MiraiConsoleErrors.USING_DERIVED_CONCURRENT_MAP_TYPE.registerFactory(ConvertToConcurrentMapFix) + MiraiConsoleErrors.USING_DERIVED_LIST_TYPE.registerFactory(ConvertToListFix) + MiraiConsoleErrors.USING_DERIVED_MUTABLE_LIST_TYPE.registerFactory(ConvertToMutableListFix) } } diff --git a/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt b/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt index ef035c242..06dd5715e 100644 --- a/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt +++ b/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt @@ -21,6 +21,7 @@ import org.jetbrains.kotlin.descriptors.CallableDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.idea.inspections.collections.isCalling +import org.jetbrains.kotlin.idea.project.builtIns import org.jetbrains.kotlin.idea.refactoring.fqName.fqName import org.jetbrains.kotlin.js.descriptorUtils.getJetTypeFqName import org.jetbrains.kotlin.psi.* @@ -28,9 +29,9 @@ import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext +import org.jetbrains.kotlin.resolve.descriptorUtil.isSubclassOf import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.SimpleType -import org.jetbrains.kotlinx.serialization.compiler.resolve.* class PluginDataValuesChecker : DeclarationChecker { /** @@ -42,7 +43,7 @@ class PluginDataValuesChecker : DeclarationChecker { //println(declaration::class.qualifiedName + "\t:" + declaration.text.take(10)) if (declaration is KtProperty) { - if (checkReadOnly(declaration, context)) return // it reports an error on property so no need to check further + checkReadOnly(declaration, context) } val calls = declaration.bodyCalls(bindingContext) ?: return @@ -67,7 +68,7 @@ class PluginDataValuesChecker : DeclarationChecker { || receiverTypeReference?.hasSuperType(READ_ONLY_PLUGIN_DATA_FQ_NAME) == true // extension } - private fun checkReadOnly(property: KtProperty, context: DeclarationCheckerContext): Boolean { + private fun checkReadOnly(property: KtProperty, context: DeclarationCheckerContext) { // first parent is KtPropertyDelegate, next is KtProperty if (property.isVar // var @@ -75,10 +76,7 @@ class PluginDataValuesChecker : DeclarationChecker { && property.isInsideOrExtensionOfReadOnlyPluginData() // extensionReceiver is ReadOnlyPluginData or null ) { context.report(MiraiConsoleErrors.READ_ONLY_VALUE_CANNOT_BE_VAR.on(property.valOrVarKeyword)) - return true } - - return false } private fun checkConstructableAndSerializable(call: ResolvedCall, expr: KtCallExpression, context: DeclarationCheckerContext) { @@ -90,19 +88,48 @@ class PluginDataValuesChecker : DeclarationChecker { ) { checkConstructableAndSerializable(kotlinType, expr, context) + checkFixType(kotlinType, expr, context) } } } + private fun checkFixType(type: KotlinType, callExpr: KtCallExpression, context: DeclarationCheckerContext) { + val inspectionTarget = retrieveInspectionTarget(type, callExpr) ?: return + val classDescriptor = type.classDescriptor() ?: return + val jetTypeFqn = type.getJetTypeFqName(false) + + val builtIns = callExpr.builtIns + val factory = when { + jetTypeFqn == "java.util.concurrent.ConcurrentHashMap" -> MiraiConsoleErrors.USING_DERIVED_CONCURRENT_MAP_TYPE + + classDescriptor.isSubclassOf(builtIns.list) && jetTypeFqn != "kotlin.collections.List" -> { + if (classDescriptor.isSubclassOf(builtIns.mutableList)) { + if (jetTypeFqn != "kotlin.collections.MutableList" && jetTypeFqn != "java.util.List") { + MiraiConsoleErrors.USING_DERIVED_MUTABLE_LIST_TYPE + } else null + } else MiraiConsoleErrors.USING_DERIVED_LIST_TYPE + } + + classDescriptor.isSubclassOf(builtIns.map) && jetTypeFqn != "kotlin.collections.Map" -> { + if (classDescriptor.isSubclassOf(builtIns.mutableMap)) { + if (jetTypeFqn != "kotlin.collections.MutableMap" && jetTypeFqn != "java.util.Map") { + MiraiConsoleErrors.USING_DERIVED_MUTABLE_MAP_TYPE + } else null + } else MiraiConsoleErrors.USING_DERIVED_MAP_TYPE + } + + else -> return + } ?: return + + context.report(factory.on(inspectionTarget, callExpr, jetTypeFqn.substringAfterLast('.'))) + } + private fun checkConstructableAndSerializable(type: KotlinType, callExpr: KtCallExpression, context: DeclarationCheckerContext) { - val classDescriptor = type.constructor.declarationDescriptor?.castOrNull() ?: return + val classDescriptor = type.classDescriptor() ?: return if (canBeSerializedInternally(classDescriptor)) return - val inspectionTarget = kotlin.run { - val fqName = type.fqName ?: return@run null - callExpr.typeArguments.find { it.typeReference?.isReferencing(fqName) == true } - } ?: return + val inspectionTarget = retrieveInspectionTarget(type, callExpr) ?: return if (!classDescriptor.hasNoArgConstructor()) return context.report( @@ -121,10 +148,18 @@ class PluginDataValuesChecker : DeclarationChecker { ) ) } + + private fun KotlinType.classDescriptor() = constructor.declarationDescriptor?.castOrNull() + + private fun retrieveInspectionTarget(type: KotlinType, callExpr: KtCallExpression): KtTypeProjection? { + val fqName = type.fqName ?: return null + return callExpr.typeArguments.find { it.typeReference?.isReferencing(fqName) == true } + } } private fun canBeSerializedInternally(descriptor: ClassDescriptor): Boolean { @Suppress("UNUSED_VARIABLE") val name = when (descriptor.defaultType.getJetTypeFqName(false)) { + // kotlinx.serialization "kotlin.Unit" -> "UnitSerializer" "Z", "kotlin.Boolean" -> "BooleanSerializer" "B", "kotlin.Byte" -> "ByteSerializer" @@ -168,6 +203,11 @@ private fun canBeSerializedInternally(descriptor: ClassDescriptor): Boolean { "java.util.Map", "java.util.LinkedHashMap" -> "LinkedHashMapSerializer" "java.util.HashMap" -> "HashMapSerializer" "java.util.Map.Entry" -> "MapEntrySerializer" + + // mirai + "java.util.concurrent.ConcurrentMap", + "java.util.concurrent.ConcurrentHashMap", + -> "ConcurrentMap" // dummy name else -> return false } return true diff --git a/tools/intellij-plugin/src/diagnostics/fix/TypeProjectionFix.kt b/tools/intellij-plugin/src/diagnostics/fix/TypeProjectionFix.kt new file mode 100644 index 000000000..034546ccb --- /dev/null +++ b/tools/intellij-plugin/src/diagnostics/fix/TypeProjectionFix.kt @@ -0,0 +1,108 @@ +/* + * 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.codeInsight.intention.IntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.core.replaced +import org.jetbrains.kotlin.idea.inspections.KotlinUniversalQuickFix +import org.jetbrains.kotlin.idea.quickfix.KotlinCrossLanguageQuickFixAction +import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtTypeProjection + +abstract class AbstractTypeProjectionFix( + element: KtTypeProjection, + private val newTypeFqn: String, +) : KotlinCrossLanguageQuickFixAction(element), KotlinUniversalQuickFix { + override fun getFamilyName(): String = "Mirai console" + override fun getText(): String = "转化为 ${newTypeFqn.substringAfterLast('.')}" + + override fun invokeImpl(project: Project, editor: Editor?, file: PsiFile) { + val element = element ?: return + project.executeWriteCommand(name) { + val arguments = element.text.substringAfter('<', "") + + val e = element.replaced( + KtPsiFactory(project).createTypeArgument( + if (arguments.isBlank()) { + newTypeFqn + } else "$newTypeFqn<$arguments" + ) + ) + ShortenReferences.DEFAULT.process(e) + } + } +} + +class ConvertToMutableMapFix( + element: KtTypeProjection, +) : AbstractTypeProjectionFix(element, "kotlin.collections.MutableMap") { + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + return ConvertToMutableMapFix(diagnostic.psiElement as? KtTypeProjection ?: return null) + } + + override fun isApplicableForCodeFragment(): Boolean = false + } +} + +class ConvertToMapFix( + element: KtTypeProjection, +) : AbstractTypeProjectionFix(element, "kotlin.collections.Map") { + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + return ConvertToMapFix(diagnostic.psiElement as? KtTypeProjection ?: return null) + } + + override fun isApplicableForCodeFragment(): Boolean = false + } +} + +class ConvertToConcurrentMapFix( + element: KtTypeProjection, +) : AbstractTypeProjectionFix(element, "java.util.concurrent.ConcurrentHashMap") { + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + return ConvertToConcurrentMapFix(diagnostic.psiElement as? KtTypeProjection ?: return null) + } + + override fun isApplicableForCodeFragment(): Boolean = false + } +} + +class ConvertToListFix( + element: KtTypeProjection, +) : AbstractTypeProjectionFix(element, "kotlin.collections.List") { + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + return ConvertToListFix(diagnostic.psiElement as? KtTypeProjection ?: return null) + } + + override fun isApplicableForCodeFragment(): Boolean = false + } +} + +class ConvertToMutableListFix( + element: KtTypeProjection, +) : AbstractTypeProjectionFix(element, "kotlin.collections.MutableList") { + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + return ConvertToListFix(diagnostic.psiElement as? KtTypeProjection ?: return null) + } + + override fun isApplicableForCodeFragment(): Boolean = false + } +} diff --git a/tools/intellij-plugin/src/resolve/resolveIdea.kt b/tools/intellij-plugin/src/resolve/resolveIdea.kt index 9344b3b48..d0d36bc57 100644 --- a/tools/intellij-plugin/src/resolve/resolveIdea.kt +++ b/tools/intellij-plugin/src/resolve/resolveIdea.kt @@ -167,6 +167,11 @@ fun KtDeclaration.bodyCalls(bindingContext: BindingContext): Sequence { + val expr = declaration.delegateExpression ?: return null + val call = expr.getResolvedCall(bindingContext) ?: return null + sequenceOf(ResolvedCallWithExpr(call, expr)) + } else -> return null } }