KT-4820: Intention to introduce/remove indices from a for-loop (update to modern Kotlin)

This commit is contained in:
Dmitry Jemerov
2015-07-02 13:16:34 +02:00
parent 7f3f834011
commit 21e25e8509
19 changed files with 184 additions and 80 deletions
-5
View File
@@ -803,11 +803,6 @@
<category>Kotlin</category> <category>Kotlin</category>
</intentionAction>S </intentionAction>S
<intentionAction>
<className>org.jetbrains.jet.plugin.intentions.SwapBinaryExpression</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction> <intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.SwapBinaryExpressionIntention</className> <className>org.jetbrains.kotlin.idea.intentions.SwapBinaryExpressionIntention</className>
<category>Kotlin</category> <category>Kotlin</category>
@@ -16,34 +16,33 @@
package org.jetbrains.kotlin.idea.intentions package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import org.jetbrains.jet.lang.psi.JetForExpression
import org.jetbrains.jet.lang.psi.JetPsiFactory
import org.jetbrains.jet.lang.psi.JetDotQualifiedExpression
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.PsiDocumentManager
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
import org.jetbrains.jet.lang.resolve.BindingContext
import org.jetbrains.jet.lang.psi.JetParenthesizedExpression
import com.intellij.codeInsight.template.TemplateBuilderImpl import com.intellij.codeInsight.template.TemplateBuilderImpl
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateManagerImpl import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import org.jetbrains.jet.lang.resolve.DescriptorUtils import com.intellij.openapi.editor.Editor
import org.jetbrains.jet.analyzer.analyzeInContext import com.intellij.psi.PsiDocumentManager
import org.jetbrains.jet.lang.psi.JetCallExpression import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.analyzer.analyzeInContext
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
public class AddForLoopIndicesIntention : JetSelfTargetingIntention<JetForExpression>( public class AddForLoopIndicesIntention : JetSelfTargetingIntention<JetForExpression>(
"add.for.loop.indices", javaClass()) { javaClass(), "Add indices to 'for' loop") {
override fun applyTo(element: JetForExpression, editor: Editor) { override fun applyTo(element: JetForExpression, editor: Editor) {
val loopRange = element.getLoopRange()!! val loopRange = element.getLoopRange()!!
val newRangeText = "${loopRange.getText()}.withIndices()" val newRangeText = "${loopRange.getText()}.withIndex()"
val newRange = JetPsiFactory.createExpression(editor.getProject(), newRangeText) val project = editor.getProject()!!
val psiFactory = JetPsiFactory(project)
val newRange = psiFactory.createExpression(newRangeText)
//Roundabout way to create new multiparameter element so as not to incorrectly trigger syntax error highlighting //Roundabout way to create new multiparameter element so as not to incorrectly trigger syntax error highlighting
val loopParameter = element.getLoopParameter()!! val loopParameter = element.getLoopParameter()!!
val parenthesizedParam = JetPsiFactory.createExpression(editor.getProject(), "(index)") as JetParenthesizedExpression val parenthesizedParam = psiFactory.createExpression("(index)") as JetParenthesizedExpression
val indexElement = parenthesizedParam.getExpression()!! val indexElement = parenthesizedParam.getExpression()!!
val comma = JetPsiFactory.createComma(editor.getProject()) val comma = psiFactory.createComma()
val newParamElement = JetPsiFactory.createExpression(editor.getProject(), " ${loopParameter.getText()}") val newParamElement = psiFactory.createExpression(loopParameter.getText())
parenthesizedParam.addAfter(newParamElement, indexElement) parenthesizedParam.addAfter(newParamElement, indexElement)
parenthesizedParam.addAfter(comma, indexElement) parenthesizedParam.addAfter(comma, indexElement)
@@ -55,29 +54,31 @@ public class AddForLoopIndicesIntention : JetSelfTargetingIntention<JetForExpres
editor.getCaretModel().moveToOffset(multiParameter.getTextOffset()) editor.getCaretModel().moveToOffset(multiParameter.getTextOffset())
val templateBuilder = TemplateBuilderImpl(multiParameter) val templateBuilder = TemplateBuilderImpl(multiParameter)
templateBuilder.replaceElement(multiParameter, "index") templateBuilder.replaceElement(multiParameter, "index")
val manager = TemplateManagerImpl(editor.getProject()) PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument())
PsiDocumentManager.getInstance(editor.getProject()!!).doPostponedOperationsAndUnblockDocument(editor.getDocument()) TemplateManager.getInstance(project).startTemplate(editor, templateBuilder.buildInlineTemplate()!!)
manager.startTemplate(editor, templateBuilder.buildInlineTemplate()!!)
} }
override fun isApplicableTo(element: JetForExpression): Boolean { override fun isApplicableTo(element: JetForExpression, caretOffset: Int): Boolean {
if (element.getLoopParameter() == null) return false if (element.getLoopParameter() == null) return false
val body = element.getBody()
if (body != null && caretOffset >= body.getTextRange().getStartOffset()) return false
val range = element.getLoopRange() ?: return false val range = element.getLoopRange() ?: return false
if (range is JetDotQualifiedExpression) { if (range is JetDotQualifiedExpression) {
val selector = range.getSelectorExpression() ?: return true val selector = range.getSelectorExpression() ?: return true
if (selector.getText() == "withIndices()") return false if (selector.getText() == "withIndex()") return false
} }
val potentialExpression = JetPsiFactory.createExpression(element.getProject(), val psiFactory = JetPsiFactory(element.getProject())
"${range.getText()}.withIndices()") as JetDotQualifiedExpression val potentialExpression = psiFactory.createExpression("${range.getText()}.withIndex()") as JetDotQualifiedExpression
val bindingContext = AnalyzerFacadeWithCache.getContextForElement(element) val bindingContext = element.analyze()
val scope = bindingContext[BindingContext.RESOLUTION_SCOPE, element] ?: return false val scope = bindingContext[BindingContext.RESOLUTION_SCOPE, element] ?: return false
val functionSelector = potentialExpression.getSelectorExpression() as JetCallExpression val functionSelector = potentialExpression.getSelectorExpression() as JetCallExpression
val updatedContext = potentialExpression.analyzeInContext(scope) val updatedContext = potentialExpression.analyzeInContext(scope)
val callScope = updatedContext[BindingContext.RESOLVED_CALL, functionSelector.getCalleeExpression()!!] ?: return false val call = updatedContext[BindingContext.CALL, functionSelector.getCalleeExpression()] ?: return false
val callScope = updatedContext[BindingContext.RESOLVED_CALL, call] ?: return false
val callFqName = DescriptorUtils.getFqNameSafe(callScope.getCandidateDescriptor()) val callFqName = DescriptorUtils.getFqNameSafe(callScope.getCandidateDescriptor())
return callFqName.toString() == "kotlin.withIndices" return callFqName.toString() == "kotlin.withIndex"
} }
}
}
@@ -16,55 +16,48 @@
package org.jetbrains.kotlin.idea.intentions package org.jetbrains.kotlin.idea.intentions
import org.jetbrains.jet.lang.psi.JetForExpression
import com.intellij.openapi.editor.Editor
import org.jetbrains.jet.lang.psi.JetDotQualifiedExpression
import org.jetbrains.jet.lang.psi.JetPsiUtil
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
import com.intellij.find.FindManager import com.intellij.find.FindManager
import com.intellij.find.impl.FindManagerImpl import com.intellij.find.impl.FindManagerImpl
import com.intellij.openapi.editor.Editor
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.usageView.UsageInfo import com.intellij.usageView.UsageInfo
import org.jetbrains.jet.plugin.findUsages.KotlinPropertyFindUsagesOptions import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.jet.lang.resolve.BindingContext import org.jetbrains.kotlin.idea.findUsages.KotlinPropertyFindUsagesOptions
import org.jetbrains.jet.lang.psi.JetCallExpression import org.jetbrains.kotlin.psi.*
import org.jetbrains.jet.lang.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
public class RemoveForLoopIndicesIntention : JetSelfTargetingIntention<JetForExpression>( public class RemoveForLoopIndicesIntention : JetSelfTargetingIntention<JetForExpression>(
"remove.for.loop.indices", javaClass()) { javaClass(), "Remove indices in 'for' loop") {
override fun applyTo(element: JetForExpression, editor: Editor) { override fun applyTo(element: JetForExpression, editor: Editor) {
val parameter = element.getMultiParameter()!! val parameter = element.getMultiParameter()!!
val range = element.getLoopRange() as JetDotQualifiedExpression val range = element.getLoopRange() as JetDotQualifiedExpression
val parameters = parameter.getEntries() val parameters = parameter.getEntries()
if (parameters.size() == 2) {
parameter.replace(parameters[1]) val loop = JetPsiFactory(element).createExpression("for (${parameters[1].getText()} in _) {}") as JetForExpression
} parameter.replace(loop.getLoopParameter()!!)
else {
JetPsiUtil.deleteElementWithDelimiters(parameters[0])
}
range.replace(range.getReceiverExpression()) range.replace(range.getReceiverExpression())
} }
override fun isApplicableTo(element: JetForExpression): Boolean { override fun isApplicableTo(element: JetForExpression, caretOffset: Int): Boolean {
val multiParameter = element.getMultiParameter() ?: return false val multiParameter = element.getMultiParameter() ?: return false
if (multiParameter.getEntries().size() != 2) return false
val range = element.getLoopRange() as? JetDotQualifiedExpression ?: return false val range = element.getLoopRange() as? JetDotQualifiedExpression ?: return false
val selector = range.getSelectorExpression() as? JetCallExpression ?: return false val selector = range.getSelectorExpression() as? JetCallExpression ?: return false
if (!selector.textMatches("withIndices()")) return false if (!selector.textMatches("withIndex()")) return false
val bindingContext = AnalyzerFacadeWithCache.getContextForElement(element) val body = element.getBody()
val callResolution = bindingContext[BindingContext.RESOLVED_CALL, selector.getCalleeExpression()!!] ?: return false if (body != null && caretOffset >= body.getTextRange().getStartOffset()) return false
val bindingContext = element.analyze()
val call = bindingContext[BindingContext.CALL, selector.getCalleeExpression()] ?: return false
val callResolution = bindingContext[BindingContext.RESOLVED_CALL, call] ?: return false
val fqName = DescriptorUtils.getFqNameSafe(callResolution.getCandidateDescriptor()) val fqName = DescriptorUtils.getFqNameSafe(callResolution.getCandidateDescriptor())
if (fqName.toString() != "kotlin.withIndices") return false if (fqName.toString() != "kotlin.withIndex") return false
val indexVar = multiParameter.getEntries()[0] val indexVar = multiParameter.getEntries()[0]
val findManager = FindManager.getInstance(element.getProject()) as FindManagerImpl return ReferencesSearch.search(indexVar).findFirst() == null
val findHandler = findManager.getFindUsagesManager().getFindUsagesHandler(indexVar, false) ?: return false
val options = KotlinPropertyFindUsagesOptions(element.getProject())
var usageCount = 0
val processorLambda: (UsageInfo?) -> Boolean = { t -> usageCount++; false }
findHandler.processElementUsages(indexVar, processorLambda, options)
return usageCount == 0
} }
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.AddForLoopIndicesIntention
@@ -1,7 +1,7 @@
//IS_APPLICABLE: FALSE //IS_APPLICABLE: FALSE
// WITH_RUNTIME // WITH_RUNTIME
fun b(c: List<String>) { fun b(c: List<String>) {
for ((<caret>indexVariable, d) in c.withIndices()) { for ((<caret>indexVariable, d) in c.withIndex()) {
} }
} }
@@ -1,6 +1,6 @@
// WITH_RUNTIME // WITH_RUNTIME
// IS_APPLICABLE: FALSE // IS_APPLICABLE: FALSE
fun String.withIndices(): Int = 42 fun String.withIndex(): Int = 42
fun foo(s: String) { fun foo(s: String) {
for (<caret>a in s) { for (<caret>a in s) {
@@ -1,6 +1,6 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(bar: IntArray) { fun foo(bar: IntArray) {
for ((index, a) in bar.withIndices()) { for ((index, a) in bar.withIndex()) {
} }
} }
@@ -1,6 +1,6 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(bar: Iterable<Int>) { fun foo(bar: Iterable<Int>) {
for ((index, a) in bar.withIndices()) { for ((index, a) in bar.withIndex()) {
} }
} }
@@ -1,7 +1,7 @@
// WITH_RUNTIME // WITH_RUNTIME
fun a() { fun a() {
val b = listOf(1,2,3,4,5) val b = listOf(1,2,3,4,5)
for ((index, c : Int) in b.withIndices()) { for ((index, c : Int) in b.withIndex()) {
} }
} }
@@ -1,6 +1,6 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(bar: Array<Object>) { fun foo(bar: Array<Object>) {
for ((index, a) in bar.withIndices()) { for ((index, a) in bar.withIndex()) {
} }
} }
@@ -1,7 +1,7 @@
// WITH_RUNTIME // WITH_RUNTIME
fun a() { fun a() {
val b = listOf(1,2,3,4,5) val b = listOf(1,2,3,4,5)
for ((index, c) in b.withIndices()) { for ((index, c) in b.withIndex()) {
} }
} }
@@ -1,6 +1,6 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(bar: Stream<String>) { fun foo(bar: Stream<String>) {
for ((index, a) in bar.withIndices()) { for ((index, a) in bar.withIndex()) {
} }
} }
@@ -1,6 +1,6 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(bar: String) { fun foo(bar: String) {
for ((index, a) in bar.withIndices()) { for ((index, a) in bar.withIndex()) {
} }
} }
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.RemoveForLoopIndicesIntention
@@ -2,7 +2,7 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(b: List<Int>) : Int { fun foo(b: List<Int>) : Int {
for ((i, <caret>c) in b.withIndices()) { for ((i, <caret>c) in b.withIndex()) {
return i return i
} }
return 0 return 0
@@ -1,11 +1,10 @@
// WITH_RUNTIME // WITH_RUNTIME
// IS_APPLICABLE: FALSE // IS_APPLICABLE: FALSE
import java.util.LinkedList
fun Int.withIndices(): List<Pair<Int, Int>> = LinkedList<Pair<Int, Int>>() fun Int.withIndex(): List<Pair<Int, Int>> = linkedListOf<Pair<Int, Int>>()
fun foo(s: Int) { fun foo(s: Int) {
for ((index<caret>, a) in s.withIndices()) { for ((index<caret>, a) in s.withIndex()) {
} }
} }
@@ -1,6 +1,6 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(bar: List<Int>) { fun foo(bar: List<Int>) {
for ((i : <caret>Int, b: Int) in bar.withIndices()) { for ((i : <caret>Int, b: Int) in bar.withIndex()) {
} }
} }
@@ -1,6 +1,6 @@
//WITH_RUNTIME //WITH_RUNTIME
fun foo(bar: List<String>) { fun foo(bar: List<String>) {
for ((i,<caret>a) in bar.withIndices()) { for ((i,<caret>a) in bar.withIndex()) {
} }
} }
@@ -92,6 +92,81 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
} }
} }
@TestMetadata("idea/testData/intentions/addForLoopIndicesIntention")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddForLoopIndicesIntention extends AbstractIntentionTest {
public void testAllFilesPresentInAddForLoopIndicesIntention() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/addForLoopIndicesIntention"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("inapplicableExistingIndices.kt")
public void testInapplicableExistingIndices() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/inapplicableExistingIndices.kt");
doTest(fileName);
}
@TestMetadata("inapplicableOverridenFunction.kt")
public void testInapplicableOverridenFunction() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/inapplicableOverridenFunction.kt");
doTest(fileName);
}
@TestMetadata("inapplicableSyntaxError.kt")
public void testInapplicableSyntaxError() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/inapplicableSyntaxError.kt");
doTest(fileName);
}
@TestMetadata("inapplicableUnorderedCollection.kt")
public void testInapplicableUnorderedCollection() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/inapplicableUnorderedCollection.kt");
doTest(fileName);
}
@TestMetadata("intArray.kt")
public void testIntArray() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/intArray.kt");
doTest(fileName);
}
@TestMetadata("iterable.kt")
public void testIterable() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/iterable.kt");
doTest(fileName);
}
@TestMetadata("listWithType.kt")
public void testListWithType() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/listWithType.kt");
doTest(fileName);
}
@TestMetadata("objectArray.kt")
public void testObjectArray() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/objectArray.kt");
doTest(fileName);
}
@TestMetadata("simpleIntList.kt")
public void testSimpleIntList() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/simpleIntList.kt");
doTest(fileName);
}
@TestMetadata("stream.kt")
public void testStream() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/stream.kt");
doTest(fileName);
}
@TestMetadata("string.kt")
public void testString() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/addForLoopIndicesIntention/string.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/addNameToArgument") @TestMetadata("idea/testData/intentions/addNameToArgument")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)
@@ -6089,6 +6164,45 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
} }
@TestMetadata("idea/testData/intentions/removeForLoopIndicesIntention")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class RemoveForLoopIndicesIntention extends AbstractIntentionTest {
public void testAllFilesPresentInRemoveForLoopIndicesIntention() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/removeForLoopIndicesIntention"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("inapplicableForLoop.kt")
public void testInapplicableForLoop() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/removeForLoopIndicesIntention/inapplicableForLoop.kt");
doTest(fileName);
}
@TestMetadata("inapplicableIndexUse.kt")
public void testInapplicableIndexUse() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/removeForLoopIndicesIntention/inapplicableIndexUse.kt");
doTest(fileName);
}
@TestMetadata("inapplicableOverridenFunction.kt")
public void testInapplicableOverridenFunction() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/removeForLoopIndicesIntention/inapplicableOverridenFunction.kt");
doTest(fileName);
}
@TestMetadata("loopWithType.kt")
public void testLoopWithType() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/removeForLoopIndicesIntention/loopWithType.kt");
doTest(fileName);
}
@TestMetadata("simpleLoopWithIndices.kt")
public void testSimpleLoopWithIndices() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/removeForLoopIndicesIntention/simpleLoopWithIndices.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/removeUnnecessaryParentheses") @TestMetadata("idea/testData/intentions/removeUnnecessaryParentheses")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)