diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/SuspiciousCollectionReassignmentInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/SuspiciousCollectionReassignmentInspection.kt index 59c8b73f03a..999fe05cc60 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/SuspiciousCollectionReassignmentInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/SuspiciousCollectionReassignmentInspection.kt @@ -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) } } } diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ChangeToMutableCollectionFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ChangeToMutableCollectionFix.kt new file mode 100644 index 00000000000..06476f0596c --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ChangeToMutableCollectionFix.kt @@ -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(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? { + 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" + ) + + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index 9dcba9aa2c3..22300595dc1 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -598,5 +598,7 @@ class QuickFixRegistrar : QuickFixContributor { TYPE_MISMATCH.registerFactory(SurroundWithLambdaFix) CONSTANT_EXPECTED_TYPE_MISMATCH.registerFactory(SurroundWithLambdaFix) + + NO_SET_METHOD.registerFactory(ChangeToMutableCollectionFix) } } diff --git a/idea/testData/quickfix/changeToMutableCollection/list.kt b/idea/testData/quickfix/changeToMutableCollection/list.kt new file mode 100644 index 00000000000..93b1a596edc --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/list.kt @@ -0,0 +1,6 @@ +// "Change type to MutableList" "true" +// WITH_RUNTIME +fun main() { + val list = listOf(1, 2, 3) + list[1] = 10 +} \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/list.kt.after b/idea/testData/quickfix/changeToMutableCollection/list.kt.after new file mode 100644 index 00000000000..53569efec24 --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/list.kt.after @@ -0,0 +1,6 @@ +// "Change type to MutableList" "true" +// WITH_RUNTIME +fun main() { + val list = mutableListOf(1, 2, 3) + list[1] = 10 +} \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/list2.kt b/idea/testData/quickfix/changeToMutableCollection/list2.kt new file mode 100644 index 00000000000..f230d687337 --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/list2.kt @@ -0,0 +1,8 @@ +// "Change type to MutableList" "true" +// WITH_RUNTIME +fun main() { + val list = foo() + list[1] = 10 +} + +fun foo() = listOf(1, 2, 3) \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/list2.kt.after b/idea/testData/quickfix/changeToMutableCollection/list2.kt.after new file mode 100644 index 00000000000..66d357d8838 --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/list2.kt.after @@ -0,0 +1,8 @@ +// "Change type to MutableList" "true" +// WITH_RUNTIME +fun main() { + val list = foo().toMutableList() + list[1] = 10 +} + +fun foo() = listOf(1, 2, 3) \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/map.kt b/idea/testData/quickfix/changeToMutableCollection/map.kt new file mode 100644 index 00000000000..16bc238df97 --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/map.kt @@ -0,0 +1,6 @@ +// "Change type to MutableMap" "true" +// WITH_RUNTIME +fun main() { + val map = mapOf(1 to "a") + map[2] = "b" +} \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/map.kt.after b/idea/testData/quickfix/changeToMutableCollection/map.kt.after new file mode 100644 index 00000000000..ba9cae1cf70 --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/map.kt.after @@ -0,0 +1,6 @@ +// "Change type to MutableMap" "true" +// WITH_RUNTIME +fun main() { + val map = mutableMapOf(1 to "a") + map[2] = "b" +} \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/map2.kt b/idea/testData/quickfix/changeToMutableCollection/map2.kt new file mode 100644 index 00000000000..e06dc009087 --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/map2.kt @@ -0,0 +1,8 @@ +// "Change type to MutableMap" "true" +// WITH_RUNTIME +fun main() { + val map = foo() + map[2] = "b" +} + +fun foo() = mapOf(1 to "a") \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/map2.kt.after b/idea/testData/quickfix/changeToMutableCollection/map2.kt.after new file mode 100644 index 00000000000..d80dd5f6a47 --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/map2.kt.after @@ -0,0 +1,8 @@ +// "Change type to MutableMap" "true" +// WITH_RUNTIME +fun main() { + val map = foo().toMutableMap() + map[2] = "b" +} + +fun foo() = mapOf(1 to "a") \ No newline at end of file diff --git a/idea/testData/quickfix/changeToMutableCollection/set.kt b/idea/testData/quickfix/changeToMutableCollection/set.kt new file mode 100644 index 00000000000..b339396a22d --- /dev/null +++ b/idea/testData/quickfix/changeToMutableCollection/set.kt @@ -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] = 10 +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java index b6a504c6176..b39081869c8 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -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) diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index fb9e93371ba..6d866bb9e70 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -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)