From d658d3b9cf30b32de5e7aa28e7b43c935886e7b5 Mon Sep 17 00:00:00 2001 From: Valentin Kipyatkov Date: Fri, 13 May 2016 17:02:40 +0300 Subject: [PATCH] "Use withIndex()" intention&inspection --- .../inspectionDescriptions/UseWithIndex.html | 5 ++ .../UseWithIndexIntention/after.kt.template | 5 ++ .../UseWithIndexIntention/before.kt.template | 6 ++ .../UseWithIndexIntention/description.html | 5 ++ idea/src/META-INF/plugin.xml | 12 ++++ .../loopToCallChain/UseWithIndexIntention.kt | 59 +++++++++++++++++++ .../intentions/loopToCallChain/interfaces.kt | 4 +- .../loopToCallChain/matchAndConvert.kt | 20 +++++++ .../result/ForEachTransformation.kt | 2 +- .../sequence/IntroduceIndexMatcher.kt | 3 +- .../idea/intentions/loopToCallChain/utils.kt | 16 +++-- .../forEachIndexed_nothingElse.kt | 2 +- .../forEachIndexed_nothingElse.kt.after | 5 -- .../intentions/useWithIndex/.intention | 1 + .../useWithIndex/alreadyWithIndex.kt | 11 ++++ .../useWithIndex/customTypeWithIterator.kt | 16 +++++ .../indexPlusPlusInsideExpression.kt | 9 +++ .../indexPlusPlusInsideExpression.kt.after | 8 +++ .../prefixPlusPlusInsideExpression.kt | 9 +++ .../prefixPlusPlusInsideExpression.kt.after | 8 +++ .../intentions/useWithIndex/simple.kt | 10 ++++ .../intentions/useWithIndex/simple.kt.after | 8 +++ .../intentions/IntentionTestGenerated.java | 39 ++++++++++++ 23 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 idea/resources/inspectionDescriptions/UseWithIndex.html create mode 100644 idea/resources/intentionDescriptions/UseWithIndexIntention/after.kt.template create mode 100644 idea/resources/intentionDescriptions/UseWithIndexIntention/before.kt.template create mode 100644 idea/resources/intentionDescriptions/UseWithIndexIntention/description.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/UseWithIndexIntention.kt delete mode 100644 idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt.after create mode 100644 idea/testData/intentions/useWithIndex/.intention create mode 100644 idea/testData/intentions/useWithIndex/alreadyWithIndex.kt create mode 100644 idea/testData/intentions/useWithIndex/customTypeWithIterator.kt create mode 100644 idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt create mode 100644 idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt.after create mode 100644 idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt create mode 100644 idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt.after create mode 100644 idea/testData/intentions/useWithIndex/simple.kt create mode 100644 idea/testData/intentions/useWithIndex/simple.kt.after diff --git a/idea/resources/inspectionDescriptions/UseWithIndex.html b/idea/resources/inspectionDescriptions/UseWithIndex.html new file mode 100644 index 00000000000..c3831d05d5c --- /dev/null +++ b/idea/resources/inspectionDescriptions/UseWithIndex.html @@ -0,0 +1,5 @@ + + +This inspection reports for-loops with manually incremented index variable that can be replaced with use of "withIndex()" function + + diff --git a/idea/resources/intentionDescriptions/UseWithIndexIntention/after.kt.template b/idea/resources/intentionDescriptions/UseWithIndexIntention/after.kt.template new file mode 100644 index 00000000000..dd478994463 --- /dev/null +++ b/idea/resources/intentionDescriptions/UseWithIndexIntention/after.kt.template @@ -0,0 +1,5 @@ +fun foo(list: List) { + for ((index, s) in list.withIndex()) { + print(s.substring(index)) + } +} \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/UseWithIndexIntention/before.kt.template b/idea/resources/intentionDescriptions/UseWithIndexIntention/before.kt.template new file mode 100644 index 00000000000..18c0bedebf5 --- /dev/null +++ b/idea/resources/intentionDescriptions/UseWithIndexIntention/before.kt.template @@ -0,0 +1,6 @@ +fun foo(list: List) { + var index = 0 + for (s in list) { + print(s.substring(index++)) + } +} \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/UseWithIndexIntention/description.html b/idea/resources/intentionDescriptions/UseWithIndexIntention/description.html new file mode 100644 index 00000000000..f4af95aebe1 --- /dev/null +++ b/idea/resources/intentionDescriptions/UseWithIndexIntention/description.html @@ -0,0 +1,5 @@ + + +This intention replaces manually incremented index variable for a for-loop with use of "withIndex()" function + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 3b5012f385a..44edeab90ae 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1018,6 +1018,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.loopToCallChain.UseWithIndexIntention + Kotlin + + org.jetbrains.kotlin.idea.intentions.SwapBinaryExpressionIntention Kotlin @@ -1519,6 +1524,13 @@ level="INFO" /> + + (UseWithIndexIntention()) + +class UseWithIndexIntention : SelfTargetingRangeIntention( + KtForExpression::class.java, + "Use withIndex() instead of manual index increment" +) { + override fun applicabilityRange(element: KtForExpression): TextRange? { + return if (matchIndexToIntroduce(element) != null) element.forKeyword.textRange else null + } + + override fun applyTo(element: KtForExpression, editor: Editor?) { + val (indexVariable, initializationStatement, incrementExpression) = matchIndexToIntroduce(element)!! + + val factory = KtPsiFactory(element) + val loopRange = element.loopRange!! + val loopParameter = element.loopParameter!! + + val newLoopRange = factory.createExpressionByPattern("$0.withIndex()", loopRange) + loopRange.replace(newLoopRange) + + val multiParameter = (factory.createExpressionByPattern("for(($0, $1) in x){}", indexVariable.nameAsSafeName, loopParameter.text) as KtForExpression).destructuringParameter!! + loopParameter.replace(multiParameter) + + initializationStatement.delete() + if (incrementExpression.parent is KtBlockExpression) { + incrementExpression.delete() + } + else { + removePlusPlus(incrementExpression) + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt index 17259cd2b09..e8a6457f97e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.cfg.pseudocode.Pseudocode import org.jetbrains.kotlin.psi.KtCallableDeclaration import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtForExpression +import org.jetbrains.kotlin.psi.KtUnaryExpression import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.psiUtil.siblings @@ -102,9 +103,10 @@ data class MatchingState( * Matchers can assume that indexVariable is null if it's not used in the rest of the loop */ val indexVariable: KtCallableDeclaration?, + val pseudocodeProvider: () -> Pseudocode, val initializationStatementsToDelete: Collection = emptyList(), val previousTransformations: MutableList = arrayListOf(), - val pseudocodeProvider: () -> Pseudocode + val incrementExpressions: Collection = emptyList() ) interface TransformationMatcher { diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt index ba8f2194b6a..066dc170360 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt @@ -355,3 +355,23 @@ private fun mergeTransformations(match: TransformationMatch.Result): Transformat return TransformationMatch.Result(transformations.last() as ResultTransformation, transformations.dropLast(1) as List) } +data class IntroduceIndexData( + val indexVariable: KtCallableDeclaration, + val initializationStatement: KtExpression, + val incrementExpression: KtUnaryExpression +) + +fun matchIndexToIntroduce(loop: KtForExpression): IntroduceIndexData? { + val (inputVariable, indexVariable) = extractLoopData(loop) ?: return null + if (indexVariable != null) return null // loop is already with "withIndex" + + val state = createInitialMatchingState(loop, inputVariable, indexVariable)?.unwrapBlock() ?: return null + + val match = IntroduceIndexMatcher.match(state) ?: return null + assert(match.sequenceTransformations.isEmpty()) + val newState = match.newState + + val initializationStatement = newState.initializationStatementsToDelete.single() + val incrementExpression = newState.incrementExpressions.single() + return IntroduceIndexData(newState.indexVariable!!, initializationStatement, incrementExpression) +} diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/ForEachTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/ForEachTransformation.kt index 9f971b32a1a..4480d461386 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/ForEachTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/ForEachTransformation.kt @@ -53,7 +53,7 @@ class ForEachTransformation( get() = true override fun match(state: MatchingState): TransformationMatch.Result? { - if (state.previousTransformations.isEmpty() && state.indexVariable == null) return null // do not suggest conversion to just ".forEach{}" + if (state.previousTransformations.isEmpty()) return null // do not suggest conversion to just ".forEach{}" or ".forEachIndexed{}" val statement = state.statements.singleOrNull() ?: return null //TODO: should we disallow it for complicated statements like loops, if, when? diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/IntroduceIndexMatcher.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/IntroduceIndexMatcher.kt index 79a98716d0d..70beabbe960 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/IntroduceIndexMatcher.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/IntroduceIndexMatcher.kt @@ -72,7 +72,8 @@ object IntroduceIndexMatcher : TransformationMatcher { val restStatements = state.statements - incrementExpression // if it is among statements then drop it, otherwise "index++" will be replaced with "index" by generateLambda() val newState = state.copy(statements = restStatements, indexVariable = variable, - initializationStatementsToDelete = state.initializationStatementsToDelete + variableInitialization.initializationStatement) + initializationStatementsToDelete = state.initializationStatementsToDelete + variableInitialization.initializationStatement, + incrementExpressions = state.incrementExpressions + incrementExpression) return TransformationMatch.Sequence(emptyList(), newState) } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt index 319358ba0e2..5a7c140d19f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt @@ -83,17 +83,21 @@ fun generateLambda(expression: KtExpression, indexVariable: KtCallableDeclaratio } } if (indexPlusPlus != null) { - val operand = indexPlusPlus.baseExpression!! - val replacement = if (indexPlusPlus is KtPostfixExpression) // index++ - operand - else // ++index - KtPsiFactory(operand).createExpressionByPattern("$0 + 1", operand) - indexPlusPlus.replace(replacement) + removePlusPlus(indexPlusPlus) } return lambdaExpression } +fun removePlusPlus(indexPlusPlus: KtUnaryExpression) { + val operand = indexPlusPlus.baseExpression!! + val replacement = if (indexPlusPlus is KtPostfixExpression) // index++ + operand + else // ++index + KtPsiFactory(operand).createExpressionByPattern("$0 + 1", operand) + indexPlusPlus.replace(replacement) +} + fun generateLambda(expression: KtExpression, vararg inputVariables: KtCallableDeclaration): KtLambdaExpression { return KtPsiFactory(expression).buildExpression { appendFixedText("{") diff --git a/idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt b/idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt index 0562dc78f1a..7d9c70e2ec5 100644 --- a/idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt +++ b/idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt @@ -1,5 +1,5 @@ // WITH_RUNTIME -// INTENTION_TEXT: "Replace with 'forEachIndexed{}'" +// IS_APPLICABLE: false fun foo(list: List) { for ((index, s) in list.withIndex()) { println(s.hashCode() * index) diff --git a/idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt.after b/idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt.after deleted file mode 100644 index 5c0035f31ff..00000000000 --- a/idea/testData/intentions/loopToCallChain/forEachIndexed_nothingElse.kt.after +++ /dev/null @@ -1,5 +0,0 @@ -// WITH_RUNTIME -// INTENTION_TEXT: "Replace with 'forEachIndexed{}'" -fun foo(list: List) { - list.forEachIndexed { index, s -> println(s.hashCode() * index) } -} \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/.intention b/idea/testData/intentions/useWithIndex/.intention new file mode 100644 index 00000000000..1d67afd291a --- /dev/null +++ b/idea/testData/intentions/useWithIndex/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.loopToCallChain.UseWithIndexIntention \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/alreadyWithIndex.kt b/idea/testData/intentions/useWithIndex/alreadyWithIndex.kt new file mode 100644 index 00000000000..b60f65f386b --- /dev/null +++ b/idea/testData/intentions/useWithIndex/alreadyWithIndex.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +// IS_APPLICABLE: false +fun foo(list: List): Int? { + var index = 0 + for ((i, s) in list.withIndex()) { + val x = s.length * index * i + index++ + if (x > 0) return x + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/customTypeWithIterator.kt b/idea/testData/intentions/useWithIndex/customTypeWithIterator.kt new file mode 100644 index 00000000000..e0dce3a23b7 --- /dev/null +++ b/idea/testData/intentions/useWithIndex/customTypeWithIterator.kt @@ -0,0 +1,16 @@ +// IS_APPLICABLE: false +// WITH_RUNTIME +class X { + operator fun iterator(): Iterator{ + return emptyList().iterator() + } +} + +fun foo(x: X) { + var index = 0 + for (s in x) { + if (s.length > index++) { + println(s) + } + } +} diff --git a/idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt b/idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt new file mode 100644 index 00000000000..02e39bacc2a --- /dev/null +++ b/idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt @@ -0,0 +1,9 @@ +// WITH_RUNTIME +fun foo(list: List) { + var i = 0 + for (s in list) { + println(i) + val x = s.length * i++ + if (x > 1000) break + } +} \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt.after b/idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt.after new file mode 100644 index 00000000000..70e29311332 --- /dev/null +++ b/idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +fun foo(list: List) { + for ((i, s) in list.withIndex()) { + println(i) + val x = s.length * i + if (x > 1000) break + } +} \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt b/idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt new file mode 100644 index 00000000000..8e667f37656 --- /dev/null +++ b/idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt @@ -0,0 +1,9 @@ +// WITH_RUNTIME +fun foo(list: List) { + var i = 0 + for (s in list) { + println(i) + val x = s.length * ++i + if (x > 1000) break + } +} \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt.after b/idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt.after new file mode 100644 index 00000000000..e42c07526f4 --- /dev/null +++ b/idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +fun foo(list: List) { + for ((i, s) in list.withIndex()) { + println(i) + val x = s.length * (i + 1) + if (x > 1000) break + } +} \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/simple.kt b/idea/testData/intentions/useWithIndex/simple.kt new file mode 100644 index 00000000000..bb0e857593c --- /dev/null +++ b/idea/testData/intentions/useWithIndex/simple.kt @@ -0,0 +1,10 @@ +// WITH_RUNTIME +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + val x = s.length * index + index++ + if (x > 0) return x + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/useWithIndex/simple.kt.after b/idea/testData/intentions/useWithIndex/simple.kt.after new file mode 100644 index 00000000000..870c4084c01 --- /dev/null +++ b/idea/testData/intentions/useWithIndex/simple.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +fun foo(list: List): Int? { + for ((index, s) in list.withIndex()) { + val x = s.length * index + if (x > 0) return x + } + return null +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 3de253d4eb5..d3ec05073b6 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -10517,4 +10517,43 @@ public class IntentionTestGenerated extends AbstractIntentionTest { doTest(fileName); } } + + @TestMetadata("idea/testData/intentions/useWithIndex") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class UseWithIndex extends AbstractIntentionTest { + public void testAllFilesPresentInUseWithIndex() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/useWithIndex"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); + } + + @TestMetadata("alreadyWithIndex.kt") + public void testAlreadyWithIndex() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/useWithIndex/alreadyWithIndex.kt"); + doTest(fileName); + } + + @TestMetadata("customTypeWithIterator.kt") + public void testCustomTypeWithIterator() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/useWithIndex/customTypeWithIterator.kt"); + doTest(fileName); + } + + @TestMetadata("indexPlusPlusInsideExpression.kt") + public void testIndexPlusPlusInsideExpression() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/useWithIndex/indexPlusPlusInsideExpression.kt"); + doTest(fileName); + } + + @TestMetadata("prefixPlusPlusInsideExpression.kt") + public void testPrefixPlusPlusInsideExpression() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/useWithIndex/prefixPlusPlusInsideExpression.kt"); + doTest(fileName); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/useWithIndex/simple.kt"); + doTest(fileName); + } + } }