From 22fb3976625541781b653bfae12ea584589abb74 Mon Sep 17 00:00:00 2001 From: Valentin Kipyatkov Date: Wed, 6 Apr 2016 15:40:33 +0300 Subject: [PATCH] Initial support for "+=", filterTo and mapTo for collections --- .../intentions/loopToCallChain/interfaces.kt | 4 +- .../loopToCallChain/matchAndConvert.kt | 9 +- .../result/AddToCollectionTransformation.kt | 99 +++++++++++++++++++ .../result/FindAndAssignTransformation.kt | 7 +- .../result/FindAndReturnTransformation.kt | 6 +- .../sequence/FilterTransformation.kt | 22 ++--- .../idea/intentions/loopToCallChain/utils.kt | 15 ++- .../loopToCallChain/addToCollection.kt | 6 ++ .../loopToCallChain/addToCollection.kt.after | 4 + .../intentions/loopToCallChain/filterTo.kt | 7 ++ .../loopToCallChain/filterTo.kt.after | 4 + .../intentions/loopToCallChain/mapTo.kt | 7 ++ .../intentions/loopToCallChain/mapTo.kt.after | 6 ++ 13 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/AddToCollectionTransformation.kt create mode 100644 idea/testData/intentions/loopToCallChain/addToCollection.kt create mode 100644 idea/testData/intentions/loopToCallChain/addToCollection.kt.after create mode 100644 idea/testData/intentions/loopToCallChain/filterTo.kt create mode 100644 idea/testData/intentions/loopToCallChain/filterTo.kt.after create mode 100644 idea/testData/intentions/loopToCallChain/mapTo.kt create mode 100644 idea/testData/intentions/loopToCallChain/mapTo.kt.after 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 71e2bab0cad..7d867c75cdf 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt @@ -25,6 +25,8 @@ import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange * An abstraction for generating a chained call that knows about receiver expression and handles proper formatting */ interface ChainedCallGenerator { + val receiver: KtExpression + /** * @param pattern pattern string for generating the part of the call to the right from the dot */ @@ -56,7 +58,7 @@ interface ResultTransformation : Transformation { fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? = null val commentSavingRange: PsiChildRange - val commentRestoringRange: PsiChildRange + fun commentRestoringRange(convertLoopResult: KtExpression): PsiChildRange fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression 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 207d4eb2305..b0bdbe3a36d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt @@ -20,6 +20,7 @@ import com.intellij.openapi.util.Key import org.jetbrains.kotlin.idea.analysis.analyzeInContext import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.AddToCollectionTransformation import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.FindAndAssignTransformation import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.FindAndReturnTransformation import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FilterTransformation @@ -46,7 +47,8 @@ object MatcherRegistrar { val resultMatchers: Collection = listOf( FindAndReturnTransformation.Matcher, - FindAndAssignTransformation.Matcher + FindAndAssignTransformation.Matcher, + AddToCollectionTransformation.Matcher ) } @@ -98,7 +100,7 @@ fun convertLoop(loop: KtForExpression, matchResult: ResultTransformationMatch): val result = matchResult.resultTransformation.convertLoop(callChain) - commentSaver.restore(matchResult.resultTransformation.commentRestoringRange) + commentSaver.restore(matchResult.resultTransformation.commentRestoringRange(result)) return result } @@ -169,6 +171,9 @@ private fun ResultTransformationMatch.generateCallChain(loop: KtForExpression): val psiFactory = KtPsiFactory(loop) val chainedCallGenerator = object : ChainedCallGenerator { + override val receiver: KtExpression + get() = callChain + override fun generate(pattern: String, vararg args: Any): KtExpression { val newPattern = "\$${args.size}$lineBreak.$pattern" return psiFactory.createExpressionByPattern(newPattern, *args, callChain) diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/AddToCollectionTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/AddToCollectionTransformation.kt new file mode 100644 index 00000000000..6eaf896ee9a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/AddToCollectionTransformation.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.intentions.loopToCallChain.result + +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.* +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FilterTransformation +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getCallNameExpression + +class AddToCollectionTransformation( + loop: KtForExpression, + inputVariable: KtCallableDeclaration, + private val targetCollection: KtExpression +) : ReplaceLoopTransformation(loop, inputVariable) { + + override fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? { + if (previousTransformation !is FilterTransformation) return null + return FilterToTransformation(loop, inputVariable, targetCollection, previousTransformation.effectiveCondition()) //TODO: use filterNotTo? + } + + override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { + return KtPsiFactory(loop).createExpressionByPattern("$0 += $1", targetCollection, chainedCallGenerator.receiver) + } + + /** + * Matches: + * for (...) { + * ... + * collection.add(...) + * } + */ + object Matcher : ResultTransformationMatcher { + override fun match(state: MatchingState): ResultTransformationMatch? { + //TODO: pass indexVariable as null if not used + if (state.indexVariable != null) return null + + val statement = state.statements.singleOrNull() ?: return null + //TODO: it can be implicit 'this' too + val qualifiedExpression = statement as? KtDotQualifiedExpression ?: return null + val targetCollection = qualifiedExpression.receiverExpression + //TODO: check that receiver is stable! + val callExpression = qualifiedExpression.selectorExpression as? KtCallExpression ?: return null + if (callExpression.getCallNameExpression()?.getReferencedName() != "add") return null + //TODO: check that it's MutableCollection's add + val argument = callExpression.valueArguments.singleOrNull() ?: return null + val argumentValue = argument.getArgumentExpression() ?: return null + + //TODO: if variable is initialized with new collection than generate toList(), toSet() + + val transformation = if (argumentValue.isVariableReference(state.workingVariable)) { + AddToCollectionTransformation(state.outerLoop, state.workingVariable, targetCollection) + } + else { + MapToTransformation(state.outerLoop, state.workingVariable, targetCollection, argumentValue) + } + return ResultTransformationMatch(transformation) + } + } +} + +class FilterToTransformation( + loop: KtForExpression, + inputVariable: KtCallableDeclaration, + private val targetCollection: KtExpression, + private val filter: KtExpression +) : ReplaceLoopTransformation(loop, inputVariable) { + + override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { + val lambda = generateLambda(inputVariable, filter) + return chainedCallGenerator.generate("filterTo($0) $1:'{}'", targetCollection, lambda) + } +} + +class MapToTransformation( + loop: KtForExpression, + inputVariable: KtCallableDeclaration, + private val targetCollection: KtExpression, + private val mapping: KtExpression +) : ReplaceLoopTransformation(loop, inputVariable) { + + override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { + val lambda = generateLambda(inputVariable, mapping) + return chainedCallGenerator.generate("mapTo($0) $1:'{}'", targetCollection, lambda) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndAssignTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndAssignTransformation.kt index 23caf00576c..3ecc05e13b2 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndAssignTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndAssignTransformation.kt @@ -38,11 +38,14 @@ class FindAndAssignTransformation( override fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? { if (previousTransformation !is FilterTransformation) return null assert(filter == null) { "Should not happen because no 2 consecutive FilterTransformation's possible"} - return FindAndAssignTransformation(loop, previousTransformation.inputVariable, generator, initialization, previousTransformation.buildRealCondition()) + return FindAndAssignTransformation(loop, previousTransformation.inputVariable, generator, initialization, previousTransformation.effectiveCondition()) } override val commentSavingRange = PsiChildRange(initialization.initializationStatement, loop.unwrapIfLabeled()) - override val commentRestoringRange = commentSavingRange.withoutLastStatement() + + private val commentRestoringRange = commentSavingRange.withoutLastStatement() + + override fun commentRestoringRange(convertLoopResult: KtExpression) = commentRestoringRange override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { return generator(chainedCallGenerator, filter) diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndReturnTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndReturnTransformation.kt index c1b636c7d0b..47115874302 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndReturnTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndReturnTransformation.kt @@ -35,12 +35,14 @@ class FindAndReturnTransformation( override fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? { if (previousTransformation !is FilterTransformation) return null assert(filter == null) { "Should not happen because no 2 consecutive FilterTransformation's possible"} - return FindAndReturnTransformation(loop, previousTransformation.inputVariable, generator, endReturn, previousTransformation.buildRealCondition()) + return FindAndReturnTransformation(loop, previousTransformation.inputVariable, generator, endReturn, previousTransformation.effectiveCondition()) } override val commentSavingRange = PsiChildRange(loop.unwrapIfLabeled(), endReturn) - override val commentRestoringRange = commentSavingRange.withoutFirstStatement() + private val commentRestoringRange = commentSavingRange.withoutFirstStatement() + + override fun commentRestoringRange(convertLoopResult: KtExpression) = commentRestoringRange override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { return generator(chainedCallGenerator, filter) diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FilterTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FilterTransformation.kt index 3ef417ea542..13ec41e565d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FilterTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FilterTransformation.kt @@ -29,13 +29,13 @@ class FilterTransformation( val isInverse: Boolean ) : SequenceTransformation { - fun buildRealCondition() = if (isInverse) condition.negate() else condition + fun effectiveCondition() = if (isInverse) condition.negate() else condition override fun mergeWithPrevious(previousTransformation: SequenceTransformation): SequenceTransformation? { if (previousTransformation !is FilterTransformation) return null assert(previousTransformation.inputVariable == inputVariable) val mergedCondition = KtPsiFactory(condition).createExpressionByPattern( - "$0 && $1", previousTransformation.buildRealCondition(), buildRealCondition()) + "$0 && $1", previousTransformation.effectiveCondition(), effectiveCondition()) return FilterTransformation(inputVariable, mergedCondition, isInverse = false) //TODO: build filterNot in some cases? } @@ -92,22 +92,22 @@ class FilterTransformation( condition: KtExpression, isInverse: Boolean): SequenceTransformation { - val realCondition = if (isInverse) condition.negate() else condition + val effectiveCondition = if (isInverse) condition.negate() else condition - if (realCondition is KtIsExpression - && !realCondition.isNegated - && realCondition.leftHandSide.isSimpleName(inputVariable.nameAsSafeName) // we cannot use isVariableReference here because expression can be non-physical + if (effectiveCondition is KtIsExpression + && !effectiveCondition.isNegated + && effectiveCondition.leftHandSide.isSimpleName(inputVariable.nameAsSafeName) // we cannot use isVariableReference here because expression can be non-physical ) { - val typeRef = realCondition.typeReference + val typeRef = effectiveCondition.typeReference if (typeRef != null) { return FilterIsInstanceTransformation(inputVariable, typeRef) } } - if (realCondition is KtBinaryExpression - && realCondition.operationToken == KtTokens.EXCLEQ - && realCondition.right.isNullExpression() - && realCondition.left.isSimpleName(inputVariable.nameAsSafeName) + if (effectiveCondition is KtBinaryExpression + && effectiveCondition.operationToken == KtTokens.EXCLEQ + && effectiveCondition.right.isNullExpression() + && effectiveCondition.left.isSimpleName(inputVariable.nameAsSafeName) ) { return FilterNotNullTransformation(inputVariable) } 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 cf3cde3787c..9f90b36e12d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt @@ -36,7 +36,6 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.resolve.BindingContext -import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.types.typeUtil.TypeNullability import org.jetbrains.kotlin.types.typeUtil.nullability @@ -257,3 +256,17 @@ fun KtExpression.detectInitializationBeforeLoop(loop: KtForExpression): Variable val initializer = assignment.right ?: return null return VariableInitialization(variable, assignment, initializer) } + +abstract class ReplaceLoopTransformation( + protected val loop: KtForExpression, + override val inputVariable: KtCallableDeclaration +): ResultTransformation { + + override val commentSavingRange = PsiChildRange.singleElement(loop.unwrapIfLabeled()) + + override fun commentRestoringRange(convertLoopResult: KtExpression) = PsiChildRange.singleElement(convertLoopResult) + + override fun convertLoop(resultCallChain: KtExpression): KtExpression { + return loop.unwrapIfLabeled().replaced(resultCallChain) + } +} diff --git a/idea/testData/intentions/loopToCallChain/addToCollection.kt b/idea/testData/intentions/loopToCallChain/addToCollection.kt new file mode 100644 index 00000000000..89468ad2141 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/addToCollection.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +fun foo(list: List, target: MutableList) { + for (s in list) { + target.add(s) + } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/addToCollection.kt.after b/idea/testData/intentions/loopToCallChain/addToCollection.kt.after new file mode 100644 index 00000000000..bdf72b472a5 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/addToCollection.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun foo(list: List, target: MutableList) { + target += list +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/filterTo.kt b/idea/testData/intentions/loopToCallChain/filterTo.kt new file mode 100644 index 00000000000..ac352e6d2e8 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/filterTo.kt @@ -0,0 +1,7 @@ +// WITH_RUNTIME +fun foo(list: List, target: MutableList) { + for (s in list) { + if (s.length > 0) + target.add(s) + } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/filterTo.kt.after b/idea/testData/intentions/loopToCallChain/filterTo.kt.after new file mode 100644 index 00000000000..4157c689b97 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/filterTo.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun foo(list: List, target: MutableList) { + list.filterTo(target) { it.length > 0 } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/mapTo.kt b/idea/testData/intentions/loopToCallChain/mapTo.kt new file mode 100644 index 00000000000..4e41d41c043 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/mapTo.kt @@ -0,0 +1,7 @@ +// WITH_RUNTIME +fun foo(list: List, target: MutableList) { + for (s in list) { + if (s.length > 0) + target.add(s.hashCode()) + } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/mapTo.kt.after b/idea/testData/intentions/loopToCallChain/mapTo.kt.after new file mode 100644 index 00000000000..f50cc4c7bdd --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/mapTo.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME +fun foo(list: List, target: MutableList) { + list + .filter { it.length > 0 } + .mapTo(target) { it.hashCode() } +} \ No newline at end of file