Add quickfix for ASSIGN_OPERATOR_AMBIGUITY on mutable collection '+=', '-='

KT-26236 Fixed
This commit is contained in:
Toshiaki Kameyama
2018-08-20 22:40:21 +09:00
committed by Natalia Selezneva
parent 725cb88f41
commit b0c3461eab
29 changed files with 403 additions and 0 deletions
@@ -0,0 +1,76 @@
/*
* Copyright 2010-2018 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.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.lexer.KtTokens
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.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.KotlinType
object AssignOperatorAmbiguityFactory : KotlinIntentionActionsFactory() {
override fun doCreateActions(diagnostic: Diagnostic): List<IntentionAction> {
val fixes = mutableListOf<IntentionAction>()
val element = diagnostic.psiElement.parent
if (element is KtBinaryExpression) {
val left = element.left
val right = element.right
val operationText = when (element.operationToken) {
KtTokens.PLUSEQ -> "plus"
KtTokens.MINUSEQ -> "minus"
else -> null
}
if (left != null && right != null && operationText != null) {
val context = element.analyze(BodyResolveMode.PARTIAL)
if (left.getType(context).isMutableCollection()) {
val property = left.mainReference?.resolve() as? KtProperty
val propertyName = property?.name
if (property != null && propertyName != null && property.isLocal) {
fixes.add(ChangeVariableMutabilityFix(property, false, "Change '$propertyName' to val"))
}
fixes.add(ReplaceWithAssignFunctionCallFix(element, operationText))
}
}
}
return fixes
}
}
private fun KotlinType?.isMutableCollection(): Boolean {
if (this == null) return false
return JavaToKotlinClassMap.isMutable(this) || constructor.supertypes.reversed().any { JavaToKotlinClassMap.isMutable(it) }
}
private class ReplaceWithAssignFunctionCallFix(
element: KtBinaryExpression,
private val operationText: String
) : KotlinQuickFixAction<KtBinaryExpression>(element) {
override fun getText() = "Replace with '${operationText}Assign()' call"
override fun getFamilyName() = text
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val binaryExpression = element ?: return
val left = binaryExpression.left ?: return
val right = binaryExpression.right ?: return
val replaced = binaryExpression.replace(
KtPsiFactory(binaryExpression).createExpressionByPattern("$0.${operationText}Assign($1)", left, right)
)
editor?.caretModel?.moveToOffset(replaced.endOffset)
}
}
@@ -578,5 +578,7 @@ class QuickFixRegistrar : QuickFixContributor {
NOTHING_TO_INLINE.registerFactory(RemoveModifierFix.createRemoveModifierFactory(isRedundant = false))
DECLARATION_CANT_BE_INLINED.registerFactory(DeclarationCantBeInlinedFactory)
ASSIGN_OPERATOR_AMBIGUITY.registerFactory(AssignOperatorAmbiguityFactory)
}
}
@@ -0,0 +1,7 @@
// "Change 'list' to val" "true"
// WITH_RUNTIME
fun test() {
var list = ArrayList<Int>()
list <caret>-= 2
}
@@ -0,0 +1,7 @@
// "Change 'list' to val" "true"
// WITH_RUNTIME
fun test() {
val list = ArrayList<Int>()
list <caret>-= 2
}
@@ -0,0 +1,14 @@
// "Change 'list' to val" "false"
// ACTION: Replace overloaded operator with function call
// ACTION: Replace with 'plusAssign()' call
// ACTION: Replace with ordinary assignment
// ERROR: Assignment operators ambiguity: <br>public operator fun <T> Collection<Int>.plus(element: Int): List<Int> defined in kotlin.collections<br>@InlineOnly public inline operator fun <T> MutableCollection<in Int>.plusAssign(element: Int): Unit defined in kotlin.collections
// WITH_RUNTIME
class Test {
var list = mutableListOf(1)
fun test() {
list <caret>+= 2
}
}
@@ -0,0 +1,7 @@
// "Change 'set' to val" "true"
// WITH_RUNTIME
fun test() {
var set = HashMap<Int, Int>()
set <caret>+= 2 to 2
}
@@ -0,0 +1,7 @@
// "Change 'set' to val" "true"
// WITH_RUNTIME
fun test() {
val set = HashMap<Int, Int>()
set <caret>+= 2 to 2
}
@@ -0,0 +1,7 @@
// "Change 'set' to val" "true"
// WITH_RUNTIME
fun test() {
var set = HashSet<Int>()
set <caret>-= 2
}
@@ -0,0 +1,7 @@
// "Change 'set' to val" "true"
// WITH_RUNTIME
fun test() {
val set = HashSet<Int>()
set <caret>-= 2
}
@@ -0,0 +1,7 @@
// "Change 'list' to val" "true"
// WITH_RUNTIME
fun test() {
var list = mutableListOf(1)
list <caret>+= 2
}
@@ -0,0 +1,7 @@
// "Change 'list' to val" "true"
// WITH_RUNTIME
fun test() {
val list = mutableListOf(1)
list <caret>+= 2
}
@@ -0,0 +1,7 @@
// "Change 'map' to val" "true"
// WITH_RUNTIME
fun test() {
var map = mutableMapOf(1 to 1)
map <caret>+= 2 to 2
}
@@ -0,0 +1,7 @@
// "Change 'map' to val" "true"
// WITH_RUNTIME
fun test() {
val map = mutableMapOf(1 to 1)
map <caret>+= 2 to 2
}
@@ -0,0 +1,7 @@
// "Change 'set' to val" "true"
// WITH_RUNTIME
fun test() {
var set = mutableSetOf(1)
set <caret>+= 2
}
@@ -0,0 +1,7 @@
// "Change 'set' to val" "true"
// WITH_RUNTIME
fun test() {
val set = mutableSetOf(1)
set <caret>+= 2
}
@@ -0,0 +1,7 @@
// "Replace with 'minusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var list = ArrayList<Int>()
list <caret>-= 2
}
@@ -0,0 +1,7 @@
// "Replace with 'minusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var list = ArrayList<Int>()
list.minusAssign(2)<caret>
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var set = HashMap<Int, Int>()
set <caret>+= 2 to 2
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var set = HashMap<Int, Int>()
set.plusAssign(2 to 2)
}
@@ -0,0 +1,7 @@
// "Replace with 'minusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var set = HashSet<Int>()
set <caret>-= 2
}
@@ -0,0 +1,7 @@
// "Replace with 'minusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var set = HashSet<Int>()
set.minusAssign(2)<caret>
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var list = mutableListOf(1)
list <caret>+= 2
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var list = mutableListOf(1)
list.plusAssign(2)<caret>
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var map = mutableMapOf(1 to 1)
map <caret>+= 2 to 2
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var map = mutableMapOf(1 to 1)
map.plusAssign(2 to 2)<caret>
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var set = mutableSetOf(1)
set <caret>+= 2
}
@@ -0,0 +1,7 @@
// "Replace with 'plusAssign()' call" "true"
// WITH_RUNTIME
fun test() {
var set = mutableSetOf(1)
set.plusAssign(2)<caret>
}
@@ -364,6 +364,45 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes
}
}
@TestMetadata("idea/testData/quickfix/assignOperatorAmbiguity")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AssignOperatorAmbiguity extends AbstractQuickFixMultiFileTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInAssignOperatorAmbiguity() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/assignOperatorAmbiguity"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
}
@TestMetadata("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ChangeToVal extends AbstractQuickFixMultiFileTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInChangeToVal() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
}
}
@TestMetadata("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceWithAssignCall extends AbstractQuickFixMultiFileTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInReplaceWithAssignCall() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
}
}
}
@TestMetadata("idea/testData/quickfix/assignToProperty")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -1224,6 +1224,110 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/assignOperatorAmbiguity")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AssignOperatorAmbiguity extends AbstractQuickFixTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInAssignOperatorAmbiguity() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/assignOperatorAmbiguity"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ChangeToVal extends AbstractQuickFixTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInChangeToVal() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("arrayList.kt")
public void testArrayList() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal/arrayList.kt");
}
@TestMetadata("classVariable.kt")
public void testClassVariable() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal/classVariable.kt");
}
@TestMetadata("hashMap.kt")
public void testHashMap() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal/hashMap.kt");
}
@TestMetadata("hashSet.kt")
public void testHashSet() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal/hashSet.kt");
}
@TestMetadata("mutableList.kt")
public void testMutableList() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal/mutableList.kt");
}
@TestMetadata("mutableMap.kt")
public void testMutableMap() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal/mutableMap.kt");
}
@TestMetadata("mutableSet.kt")
public void testMutableSet() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/changeToVal/mutableSet.kt");
}
}
@TestMetadata("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceWithAssignCall extends AbstractQuickFixTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInReplaceWithAssignCall() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("arrayList.kt")
public void testArrayList() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall/arrayList.kt");
}
@TestMetadata("hashMap.kt")
public void testHashMap() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall/hashMap.kt");
}
@TestMetadata("hashSet.kt")
public void testHashSet() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall/hashSet.kt");
}
@TestMetadata("mutableList.kt")
public void testMutableList() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall/mutableList.kt");
}
@TestMetadata("mutableMap.kt")
public void testMutableMap() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall/mutableMap.kt");
}
@TestMetadata("mutableSet.kt")
public void testMutableSet() throws Exception {
runTest("idea/testData/quickfix/assignOperatorAmbiguity/replaceWithAssignCall/mutableSet.kt");
}
}
}
@TestMetadata("idea/testData/quickfix/assignToProperty")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)