Add "Change type to mutable collection" quick fix for NO_SET_METHOD

#KT-29193 Fixed
This commit is contained in:
Toshiaki Kameyama
2019-02-06 19:07:05 +09:00
committed by Mikhail Glukhikh
parent 52c2547d95
commit 4113d06767
14 changed files with 221 additions and 35 deletions
@@ -12,15 +12,18 @@ import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.diagnostics.Severity
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.intentions.ReplaceWithOrdinaryAssignmentIntention
import org.jetbrains.kotlin.idea.project.builtIns
import org.jetbrains.kotlin.idea.quickfix.ChangeToMutableCollectionFix
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.lexer.KtSingleValueToken
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
@@ -81,44 +84,14 @@ class SuspiciousCollectionReassignmentInspection : AbstractKotlinInspection() {
val binaryExpression = operationReference.parent as? KtBinaryExpression ?: return
val left = binaryExpression.left ?: return
val property = left.mainReference?.resolve() as? KtProperty ?: return
val initializer = property.initializer ?: return
val fqName = initializer.resolveToCall()?.resultingDescriptor?.fqNameOrNull()?.asString()
val psiFactory = KtPsiFactory(binaryExpression)
val mutableOf = mutableConversionMap[fqName]
if (mutableOf != null) {
(initializer as? KtCallExpression)?.calleeExpression?.replaced(psiFactory.createExpression(mutableOf)) ?: return
} else {
val builtIns = binaryExpression.builtIns
val toMutable = when (type.constructor) {
builtIns.list.defaultType.constructor -> "toMutableList"
builtIns.set.defaultType.constructor -> "toMutableSet"
builtIns.map.defaultType.constructor -> "toMutableMap"
else -> null
} ?: return
val dotQualifiedExpression = initializer.replaced(
psiFactory.createExpressionByPattern("($0).$1()", initializer, toMutable)
) as KtDotQualifiedExpression
val receiver = dotQualifiedExpression.receiverExpression
val deparenthesize = KtPsiUtil.deparenthesize(dotQualifiedExpression.receiverExpression)
if (deparenthesize != null && receiver != deparenthesize) receiver.replace(deparenthesize)
}
property.typeReference?.also { it.replace(psiFactory.createType("Mutable${it.text}")) }
property.valOrVarKeyword.replace(psiFactory.createValKeyword())
ChangeToMutableCollectionFix.applyFix(property, type)
property.valOrVarKeyword.replace(KtPsiFactory(property).createValKeyword())
binaryExpression.findExistingEditor()?.caretModel?.moveToOffset(property.endOffset)
}
companion object {
private const val COLLECTIONS = "kotlin.collections"
private val mutableConversionMap = mapOf(
"$COLLECTIONS.listOf" to "mutableListOf",
"$COLLECTIONS.setOf" to "mutableSetOf",
"$COLLECTIONS.mapOf" to "mutableMapOf"
)
fun isApplicable(property: KtProperty): Boolean {
return property.isLocal && property.initializer != null
return ChangeToMutableCollectionFix.isApplicable(property)
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.quickfix
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.project.builtIns
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.KotlinType
class ChangeToMutableCollectionFix(property: KtProperty, private val type: String) : KotlinQuickFixAction<KtProperty>(property) {
override fun getText() = "Change type to Mutable$type"
override fun getFamilyName() = text
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val property = element ?: return
val context = property.analyze(BodyResolveMode.PARTIAL)
val type = property.initializer?.getType(context) ?: return
applyFix(property, type)
editor?.caretModel?.moveToOffset(property.endOffset)
}
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): KotlinQuickFixAction<KtProperty>? {
val element = Errors.NO_SET_METHOD.cast(diagnostic).psiElement as? KtArrayAccessExpression ?: return null
val arrayExpr = element.arrayExpression ?: return null
val context = arrayExpr.analyze(BodyResolveMode.PARTIAL)
val type = arrayExpr.getType(context) ?: return null
if (!type.isReadOnlyListOrMap(element.builtIns)) return null
val property = arrayExpr.mainReference?.resolve() as? KtProperty ?: return null
if (!isApplicable(property)) return null
val typeName = type.constructor.declarationDescriptor?.name?.asString() ?: return null
return ChangeToMutableCollectionFix(property, typeName)
}
private fun KotlinType.isReadOnlyListOrMap(builtIns: KotlinBuiltIns): Boolean {
val leftDefaultType = constructor.declarationDescriptor?.defaultType ?: return false
return leftDefaultType in listOf(builtIns.list.defaultType, builtIns.map.defaultType)
}
fun isApplicable(property: KtProperty): Boolean {
return property.isLocal && property.initializer != null
}
fun applyFix(property: KtProperty, type: KotlinType) {
val initializer = property.initializer ?: return
val fqName = initializer.resolveToCall()?.resultingDescriptor?.fqNameOrNull()?.asString()
val psiFactory = KtPsiFactory(property)
val mutableOf = mutableConversionMap[fqName]
if (mutableOf != null) {
(initializer as? KtCallExpression)?.calleeExpression?.replaced(psiFactory.createExpression(mutableOf)) ?: return
} else {
val builtIns = property.builtIns
val toMutable = when (type.constructor) {
builtIns.list.defaultType.constructor -> "toMutableList"
builtIns.set.defaultType.constructor -> "toMutableSet"
builtIns.map.defaultType.constructor -> "toMutableMap"
else -> null
} ?: return
val dotQualifiedExpression = initializer.replaced(
psiFactory.createExpressionByPattern("($0).$1()", initializer, toMutable)
) as KtDotQualifiedExpression
val receiver = dotQualifiedExpression.receiverExpression
val deparenthesize = KtPsiUtil.deparenthesize(dotQualifiedExpression.receiverExpression)
if (deparenthesize != null && receiver != deparenthesize) receiver.replace(deparenthesize)
}
property.typeReference?.also { it.replace(psiFactory.createType("Mutable${it.text}")) }
}
private const val COLLECTIONS = "kotlin.collections"
private val mutableConversionMap = mapOf(
"$COLLECTIONS.listOf" to "mutableListOf",
"$COLLECTIONS.setOf" to "mutableSetOf",
"$COLLECTIONS.mapOf" to "mutableMapOf"
)
}
}
@@ -598,5 +598,7 @@ class QuickFixRegistrar : QuickFixContributor {
TYPE_MISMATCH.registerFactory(SurroundWithLambdaFix)
CONSTANT_EXPECTED_TYPE_MISMATCH.registerFactory(SurroundWithLambdaFix)
NO_SET_METHOD.registerFactory(ChangeToMutableCollectionFix)
}
}
@@ -0,0 +1,6 @@
// "Change type to MutableList" "true"
// WITH_RUNTIME
fun main() {
val list = listOf(1, 2, 3)
list[1]<caret> = 10
}
@@ -0,0 +1,6 @@
// "Change type to MutableList" "true"
// WITH_RUNTIME
fun main() {
val list = mutableListOf(1, 2, 3)<caret>
list[1] = 10
}
@@ -0,0 +1,8 @@
// "Change type to MutableList" "true"
// WITH_RUNTIME
fun main() {
val list = foo()
list[1]<caret> = 10
}
fun foo() = listOf(1, 2, 3)
@@ -0,0 +1,8 @@
// "Change type to MutableList" "true"
// WITH_RUNTIME
fun main() {
val list = foo().toMutableList()<caret>
list[1] = 10
}
fun foo() = listOf(1, 2, 3)
@@ -0,0 +1,6 @@
// "Change type to MutableMap" "true"
// WITH_RUNTIME
fun main() {
val map = mapOf(1 to "a")
map[2<caret>] = "b"
}
@@ -0,0 +1,6 @@
// "Change type to MutableMap" "true"
// WITH_RUNTIME
fun main() {
val map = mutableMapOf(1 to "a")<caret>
map[2] = "b"
}
@@ -0,0 +1,8 @@
// "Change type to MutableMap" "true"
// WITH_RUNTIME
fun main() {
val map = foo()
map[2<caret>] = "b"
}
fun foo() = mapOf(1 to "a")
@@ -0,0 +1,8 @@
// "Change type to MutableMap" "true"
// WITH_RUNTIME
fun main() {
val map = foo().toMutableMap()<caret>
map[2] = "b"
}
fun foo() = mapOf(1 to "a")
@@ -0,0 +1,8 @@
// "Change type to MutableSet" "false"
// DISABLE-ERRORS
// ACTION: Replace overloaded operator with function call
// WITH_RUNTIME
fun main() {
val set = setOf(1, 2, 3)
set[1]<caret> = 10
}
@@ -1252,6 +1252,19 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes
}
}
@TestMetadata("idea/testData/quickfix/changeToMutableCollection")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ChangeToMutableCollection extends AbstractQuickFixMultiFileTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInChangeToMutableCollection() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/changeToMutableCollection"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
}
}
@TestMetadata("idea/testData/quickfix/changeToUseSpreadOperator")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -2062,6 +2062,44 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/changeToMutableCollection")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ChangeToMutableCollection extends AbstractQuickFixTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInChangeToMutableCollection() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/changeToMutableCollection"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("list.kt")
public void testList() throws Exception {
runTest("idea/testData/quickfix/changeToMutableCollection/list.kt");
}
@TestMetadata("list2.kt")
public void testList2() throws Exception {
runTest("idea/testData/quickfix/changeToMutableCollection/list2.kt");
}
@TestMetadata("map.kt")
public void testMap() throws Exception {
runTest("idea/testData/quickfix/changeToMutableCollection/map.kt");
}
@TestMetadata("map2.kt")
public void testMap2() throws Exception {
runTest("idea/testData/quickfix/changeToMutableCollection/map2.kt");
}
@TestMetadata("set.kt")
public void testSet() throws Exception {
runTest("idea/testData/quickfix/changeToMutableCollection/set.kt");
}
}
@TestMetadata("idea/testData/quickfix/changeToUseSpreadOperator")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)