Add USING_DERIVED_XXX_TYPE diagnostic and ConvertToXXXFix for derived collection type usages in PluginData delegation. fix #278

This commit is contained in:
Him188 2021-02-11 18:51:26 +08:00
parent da3d7a7e19
commit 6d02764e2e
8 changed files with 258 additions and 14 deletions

View File

@ -37,6 +37,21 @@ object MiraiConsoleErrors {
@JvmField
val NOT_CONSTRUCTABLE_TYPE = create<KtTypeProjection, KtCallExpression, String>(ERROR)
@JvmField
val USING_DERIVED_MUTABLE_MAP_TYPE = create<KtTypeProjection, KtCallExpression, String>(ERROR)
@JvmField
val USING_DERIVED_MAP_TYPE = create<KtTypeProjection, KtCallExpression, String>(ERROR)
@JvmField
val USING_DERIVED_LIST_TYPE = create<KtTypeProjection, KtCallExpression, String>(ERROR)
@JvmField
val USING_DERIVED_MUTABLE_LIST_TYPE = create<KtTypeProjection, KtCallExpression, String>(ERROR)
@JvmField
val USING_DERIVED_CONCURRENT_MAP_TYPE = create<KtTypeProjection, KtCallExpression, String>(ERROR)
@JvmField
val UNSERIALIZABLE_TYPE = create<PsiElement, ClassDescriptor>(ERROR)

View File

@ -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}'' 无法在顶层函数使用.",

View File

@ -8,6 +8,10 @@ plugins {
group = "org.example"
version = "1.0-SNAPSHOT"
dependencies {
}
repositories {
jcenter()
mavenCentral()

View File

@ -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<MutableMap<String, String>>()
var p2 by value<Map<String, String>>()
var p4 by value<HashMap<String, String>>()
var p3 by value<ConcurrentHashMap<String, String>>()
var p5 by value<ArrayList<String>>()
var p6 by value<AbstractList<String>>()
}

View File

@ -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)
}
}

View File

@ -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<out CallableDescriptor>, 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<ClassDescriptor>() ?: 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<ClassDescriptor>()
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

View File

@ -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<KtTypeProjection>(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
}
}

View File

@ -167,6 +167,11 @@ fun KtDeclaration.bodyCalls(bindingContext: BindingContext): Sequence<ResolvedCa
val call = declaration.getResolvedCall(bindingContext) ?: return null
sequenceOf(ResolvedCallWithExpr(call, declaration))
}
is KtProperty -> {
val expr = declaration.delegateExpression ?: return null
val call = expr.getResolvedCall(bindingContext) ?: return null
sequenceOf(ResolvedCallWithExpr(call, expr))
}
else -> return null
}
}