diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/LoopToCallChainIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/LoopToCallChainIntention.kt index f39a0a111d1..07693cbf3f7 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/LoopToCallChainIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/LoopToCallChainIntention.kt @@ -39,7 +39,7 @@ class LoopToCallChainIntention : SelfTargetingRangeIntention( return if (match != null) element.forKeyword.textRange else null } - private fun ResultTransformationMatch.buildPresentation(): String { + private fun TransformationMatch.Result.buildPresentation(): String { var transformations = sequenceTransformations + resultTransformation val MAX = 3 var result: String? = null diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt index 7d8d3db2b41..389ad459270 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt @@ -57,6 +57,7 @@ fun KtExpression?.isSimpleName(name: Name): Boolean { } fun KtCallableDeclaration.hasUsages(inElement: KtElement): Boolean { + assert(inElement.isPhysical) return hasUsages(listOf(inElement)) } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/findOperationGeneration.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/findOperationGeneration.kt index c9e0d6bcbdd..4bef0f032e1 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/findOperationGeneration.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/findOperationGeneration.kt @@ -19,6 +19,7 @@ package org.jetbrains.kotlin.idea.intentions.loopToCallChain import org.jetbrains.kotlin.descriptors.VariableDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isNullExpression +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FilterTransformation import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.types.typeUtil.TypeNullability import org.jetbrains.kotlin.types.typeUtil.nullability @@ -26,9 +27,14 @@ import org.jetbrains.kotlin.types.typeUtil.nullability interface FindOperatorGenerator { val functionName: String + val hasFilter: Boolean + + val presentation: String + get() = functionName + (if (hasFilter) "{}" else "()") + val shouldUseInputVariable: Boolean - fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression + fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression val chainCallCount: Int get() = 1 @@ -38,11 +44,15 @@ fun buildFindOperationGenerator( valueIfFound: KtExpression, valueIfNotFound: KtExpression, inputVariable: KtCallableDeclaration, + indexVariable: KtCallableDeclaration?, + filterTransformation: FilterTransformation?, findFirst: Boolean ): FindOperatorGenerator? { assert(valueIfFound.isPhysical) assert(valueIfNotFound.isPhysical) + val filterCondition = filterTransformation?.effectiveCondition() + fun generateChainedCall(stdlibFunName: String, chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { return if (filter == null) { chainedCallGenerator.generate("$stdlibFunName()") @@ -53,97 +63,126 @@ fun buildFindOperationGenerator( } } - class SimpleGenerator(override val functionName: String, override val shouldUseInputVariable: Boolean) : FindOperatorGenerator { - override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { - return generateChainedCall(functionName, chainedCallGenerator, filter) + class SimpleGenerator( + override val functionName: String, + override val shouldUseInputVariable: Boolean + ) : FindOperatorGenerator { + override val hasFilter: Boolean + get() = filterCondition != null + + override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression { + return generateChainedCall(functionName, chainedCallGenerator, filterCondition) } } - val inputVariableCanHoldNull = (inputVariable.resolveToDescriptor() as VariableDescriptor).type.nullability() != TypeNullability.NOT_NULL + if (indexVariable != null) { + if (filterTransformation == null) return null // makes no sense, indexVariable must be always 0 + if (filterTransformation.indexVariable != null) return null // cannot use index in condition for indexOfFirst/indexOfLast - fun FindOperatorGenerator.useElvisOperatorIfNeeded(): FindOperatorGenerator? { - if (valueIfNotFound.isNullExpression()) return this - - // we cannot use ?: if found value can be null - if (inputVariableCanHoldNull) return null - - return object: FindOperatorGenerator by this { - override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { - val generated = this@useElvisOperatorIfNeeded.generate(chainedCallGenerator, filter) - return KtPsiFactory(generated).createExpressionByPattern("$0 ?: $1", generated, valueIfNotFound) - } + //TODO: what if value when not found is not "-1"? + if (valueIfFound.isVariableReference(indexVariable) && valueIfNotFound.text == "-1") { + return SimpleGenerator(if (findFirst) "indexOfFirst" else "indexOfLast", shouldUseInputVariable = false) } + + return null + } + else { + val inputVariableCanHoldNull = (inputVariable.resolveToDescriptor() as VariableDescriptor).type.nullability() != TypeNullability.NOT_NULL - when { - valueIfFound.isVariableReference(inputVariable) -> { - val generator = SimpleGenerator(if (findFirst) "firstOrNull" else "lastOrNull", shouldUseInputVariable = true) - return generator.useElvisOperatorIfNeeded() - } + fun FindOperatorGenerator.useElvisOperatorIfNeeded(): FindOperatorGenerator? { + if (valueIfNotFound.isNullExpression()) return this - valueIfFound.isTrueConstant() && valueIfNotFound.isFalseConstant() -> return SimpleGenerator("any", shouldUseInputVariable = false) - - valueIfFound.isFalseConstant() && valueIfNotFound.isTrueConstant() -> return SimpleGenerator("none", shouldUseInputVariable = false) - - inputVariable.hasUsages(valueIfFound) -> { - if (!findFirst) return null // too dangerous because of side effects - - // specially handle the case when the result expression is "." or "?." - val qualifiedExpression = valueIfFound as? KtQualifiedExpression - if (qualifiedExpression != null) { - val receiver = qualifiedExpression.receiverExpression - val selector = qualifiedExpression.selectorExpression - if (receiver.isVariableReference(inputVariable) && selector != null && !inputVariable.hasUsages(selector)) { - return object: FindOperatorGenerator { - override val functionName: String - get() = "firstOrNull" - - override val shouldUseInputVariable: Boolean - get() = true - - override val chainCallCount: Int - get() = 2 - - override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { - val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filter) - return chainedCallGenerator.generate("$0", selector, receiver = findFirstCall, safeCall = true) - } - }.useElvisOperatorIfNeeded() - } - } - - // in case of nullable input variable we cannot distinguish by the result of "firstOrNull" whether nothing was found or 'null' was found + // we cannot use ?: if found value can be null if (inputVariableCanHoldNull) return null - return object: FindOperatorGenerator { - override val functionName: String - get() = "firstOrNull" - - override val shouldUseInputVariable: Boolean - get() = true - - override val chainCallCount: Int - get() = 2 // also includes "let" - - override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { - val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filter) - val letBody = generateLambda(inputVariable, valueIfFound) - return chainedCallGenerator.generate("let $0:'{}'", letBody, receiver = findFirstCall, safeCall = true) + return object: FindOperatorGenerator by this { + override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression { + val generated = this@useElvisOperatorIfNeeded.generate(chainedCallGenerator) + return KtPsiFactory(generated).createExpressionByPattern("$0 ?: $1", generated, valueIfNotFound) } - }.useElvisOperatorIfNeeded() + } } - else -> { - return object: FindOperatorGenerator { - override val functionName: String - get() = "any" + when { + valueIfFound.isVariableReference(inputVariable) -> { + val generator = SimpleGenerator(if (findFirst) "firstOrNull" else "lastOrNull", shouldUseInputVariable = true) + return generator.useElvisOperatorIfNeeded() + } - override val shouldUseInputVariable: Boolean - get() = false + valueIfFound.isTrueConstant() && valueIfNotFound.isFalseConstant() -> return SimpleGenerator("any", shouldUseInputVariable = false) - override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { - val chainedCall = generateChainedCall(functionName, chainedCallGenerator, filter) - return KtPsiFactory(chainedCall).createExpressionByPattern("if ($0) $1 else $2", chainedCall, valueIfFound, valueIfNotFound) + valueIfFound.isFalseConstant() && valueIfNotFound.isTrueConstant() -> return SimpleGenerator("none", shouldUseInputVariable = false) + + inputVariable.hasUsages(valueIfFound) -> { + if (!findFirst) return null // too dangerous because of side effects + + // specially handle the case when the result expression is "." or "?." + val qualifiedExpression = valueIfFound as? KtQualifiedExpression + if (qualifiedExpression != null) { + val receiver = qualifiedExpression.receiverExpression + val selector = qualifiedExpression.selectorExpression + if (receiver.isVariableReference(inputVariable) && selector != null && !inputVariable.hasUsages(selector)) { + return object: FindOperatorGenerator { + override val functionName: String + get() = "firstOrNull" + + override val hasFilter: Boolean + get() = filterCondition != null + + override val shouldUseInputVariable: Boolean + get() = true + + override val chainCallCount: Int + get() = 2 + + override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression { + val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filterCondition) + return chainedCallGenerator.generate("$0", selector, receiver = findFirstCall, safeCall = true) + } + }.useElvisOperatorIfNeeded() + } + } + + // in case of nullable input variable we cannot distinguish by the result of "firstOrNull" whether nothing was found or 'null' was found + if (inputVariableCanHoldNull) return null + + return object: FindOperatorGenerator { + override val functionName: String + get() = "firstOrNull" + + override val hasFilter: Boolean + get() = filterCondition != null + + override val shouldUseInputVariable: Boolean + get() = true + + override val chainCallCount: Int + get() = 2 // also includes "let" + + override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression { + val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filterCondition) + val letBody = generateLambda(inputVariable, valueIfFound) + return chainedCallGenerator.generate("let $0:'{}'", letBody, receiver = findFirstCall, safeCall = true) + } + }.useElvisOperatorIfNeeded() + } + + else -> { + return object: FindOperatorGenerator { + override val functionName: String + get() = "any" + + override val hasFilter: Boolean + get() = filterCondition != null + + override val shouldUseInputVariable: Boolean + get() = false + + override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression { + val chainedCall = generateChainedCall(functionName, chainedCallGenerator, filterCondition) + return KtPsiFactory(chainedCall).createExpressionByPattern("if ($0) $1 else $2", chainedCall, valueIfFound, valueIfNotFound) + } } } } 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 6809f069d0f..e8efb91b57c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt @@ -100,7 +100,11 @@ data class MatchingState( val initializationStatementsToDelete: Collection = emptyList() ) -interface BaseMatcher { +interface TransformationMatcher { + fun match(state: MatchingState): TransformationMatch? + + val indexVariableAllowed: Boolean + /** * Implementors should return true if they match some constructs with expression-embedded break or continue. * In this case they are obliged to deal with them and filter out invalid cases. @@ -109,36 +113,28 @@ interface BaseMatcher { get() = false } -/** - * A matcher that can recognize one or more [SequenceTransformation]'s - */ -interface SequenceTransformationMatcher : BaseMatcher { - fun match(state: MatchingState): SequenceTransformationMatch? -} +sealed class TransformationMatch(val sequenceTransformations: List) { + abstract val allTransformations: List -class SequenceTransformationMatch( - val transformations: List, - val newState: MatchingState -) { - constructor(transformation: SequenceTransformation, newState: MatchingState) : this(listOf(transformation), newState) -} + /** + * A partial match, includes [newState] for further matching + */ + class Sequence(transformations: List, val newState: MatchingState) : TransformationMatch(transformations) { + constructor(transformation: SequenceTransformation, newState: MatchingState) : this(listOf(transformation), newState) -/** - * A matcher that can recognize a [ResultTransformation] (optionally prepended by some [SequenceTransformation]'s). - * Should match the whole rest part of the loop. - */ -interface ResultTransformationMatcher : BaseMatcher { - fun match(state: MatchingState): ResultTransformationMatch? + override val allTransformations: List + get() = sequenceTransformations + } - val indexVariableUsePossible: Boolean -} + /** + * A match of the whole rest part of the loop + */ + class Result(val resultTransformation: ResultTransformation, sequenceTransformations: List) : TransformationMatch(sequenceTransformations) { + constructor(resultTransformation: ResultTransformation, vararg sequenceTransformations: SequenceTransformation) + : this(resultTransformation, sequenceTransformations.asList()) -class ResultTransformationMatch( - val resultTransformation: ResultTransformation, - val sequenceTransformations: List -) { - constructor(resultTransformation: ResultTransformation, vararg sequenceTransformations: SequenceTransformation) - : this(resultTransformation, sequenceTransformations.asList()) + override val allTransformations = sequenceTransformations + resultTransformation + } } /** 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 f4f3313dc57..792704845be 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt @@ -22,8 +22,7 @@ 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.CountTransformation -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.result.FindTransformationMatcher import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FilterTransformation import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FlatMapTransformation import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.IntroduceIndexMatcher @@ -42,24 +41,20 @@ import org.jetbrains.kotlin.utils.addToStdlib.check import java.util.* object MatcherRegistrar { - val sequenceMatchers: Collection = listOf( + val matchers: Collection = listOf( + FindTransformationMatcher, + AddToCollectionTransformation.Matcher, + CountTransformation.Matcher, IntroduceIndexMatcher, FilterTransformation.Matcher, MapTransformation.Matcher, FlatMapTransformation.Matcher ) - - val resultMatchers: Collection = listOf( - FindAndReturnTransformation.Matcher, - FindAndAssignTransformation.Matcher, - AddToCollectionTransformation.Matcher, - CountTransformation.Matcher - ) } data class MatchResult( val sequenceExpression: KtExpression, - val transformationMatch: ResultTransformationMatch, + val transformationMatch: TransformationMatch.Result, val initializationStatementsToDelete: Collection ) @@ -91,52 +86,47 @@ fun match(loop: KtForExpression): MatchResult? { val restContainsEmbeddedBreakOrContinue = loopContainsEmbeddedBreakOrContinue && state.statements.any { it.containsEmbeddedBreakOrContinue() } - for (matcher in MatcherRegistrar.resultMatchers) { - if (state.indexVariable != null && !matcher.indexVariableUsePossible) continue - if (restContainsEmbeddedBreakOrContinue && !matcher.embeddedBreakOrContinuePossible) continue + MatchersLoop@ + for (matcher in MatcherRegistrar.matchers) { + if (state.indexVariable != null && !matcher.indexVariableAllowed) continue val match = matcher.match(state) if (match != null) { - if (!inputVariableUsed - && (match.sequenceTransformations.any { it.shouldUseInputVariable } - || match.resultTransformation.shouldUseInputVariable)) { - return null - } + if (!inputVariableUsed && match.allTransformations.any { it.shouldUseInputVariable }) return null sequenceTransformations.addAll(match.sequenceTransformations) - return ResultTransformationMatch(match.resultTransformation, sequenceTransformations) - .let { mergeTransformations(it) } - .let { MatchResult(sequenceExpression, it, state.initializationStatementsToDelete) } - .check { checkSmartCastsPreserved(loop, it) } - } - } - for (matcher in MatcherRegistrar.sequenceMatchers) { - val match = matcher.match(state) - if (match != null) { - if (!inputVariableUsed && match.transformations.any { it.shouldUseInputVariable }) { - return null + when (match) { + is TransformationMatch.Sequence -> { + // check that old input variable is not needed anymore + var newState = match.newState + if (state.inputVariable != newState.inputVariable && state.inputVariable.hasUsages(newState.statements)) return null + + if (state.indexVariable != null && match.sequenceTransformations.any { it.affectsIndex }) { + // index variable is still needed but index in the new sequence is different + if (state.indexVariable!!.hasUsages(newState.statements)) return null + newState = newState.copy(indexVariable = null) + } + + if (restContainsEmbeddedBreakOrContinue && !matcher.embeddedBreakOrContinuePossible) { + val countBefore = state.statements.sumBy { it.countEmbeddedBreaksAndContinues() } + val countAfter = newState.statements.sumBy { it.countEmbeddedBreaksAndContinues() } + if (countAfter != countBefore) continue@MatchersLoop // some embedded break or continue in the matched part + } + + state = newState + continue@MatchLoop + } + + is TransformationMatch.Result -> { + if (restContainsEmbeddedBreakOrContinue && !matcher.embeddedBreakOrContinuePossible) continue@MatchersLoop + + return TransformationMatch.Result(match.resultTransformation, sequenceTransformations) + .let { mergeTransformations(it) } + .let { MatchResult(sequenceExpression, it, state.initializationStatementsToDelete) } + .check { checkSmartCastsPreserved(loop, it) } + } } - - // check that old input variable is not needed anymore - var newState = match.newState - if (state.inputVariable != newState.inputVariable && state.inputVariable.hasUsages(newState.statements)) return null - - if (state.indexVariable != null && match.transformations.any { it.affectsIndex }) { - // index variable is still needed but index in the new sequence is different - if (state.indexVariable!!.hasUsages(newState.statements)) return null - newState = newState.copy(indexVariable = null) - } - - if (restContainsEmbeddedBreakOrContinue && !matcher.embeddedBreakOrContinuePossible) { - val countBefore = state.statements.sumBy { it.countEmbeddedBreaksAndContinues() } - val countAfter = newState.statements.sumBy { it.countEmbeddedBreaksAndContinues() } - if (countAfter != countBefore) continue // some embedded break or continue in the matched part - } - - sequenceTransformations.addAll(match.transformations) - state = newState - continue@MatchLoop } } @@ -279,7 +269,7 @@ private fun MatchResult.generateCallChain(loop: KtForExpression): KtExpression { return callChain } -private fun mergeTransformations(match: ResultTransformationMatch): ResultTransformationMatch { +private fun mergeTransformations(match: TransformationMatch.Result): TransformationMatch.Result { val transformations = ArrayList().apply { addAll(match.sequenceTransformations); add(match.resultTransformation) } var anyChange: Boolean @@ -302,6 +292,6 @@ private fun mergeTransformations(match: ResultTransformationMatch): ResultTransf } while(anyChange) @Suppress("UNCHECKED_CAST") - return ResultTransformationMatch(transformations.last() as ResultTransformation, transformations.dropLast(1) as List) + return TransformationMatch.Result(transformations.last() as ResultTransformation, transformations.dropLast(1) as List) } 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 index 0640cf3c819..d8f978502a9 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/AddToCollectionTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/AddToCollectionTransformation.kt @@ -84,11 +84,11 @@ class AddToCollectionTransformation( * collection.add(...) * } */ - object Matcher : ResultTransformationMatcher { - override val indexVariableUsePossible: Boolean + object Matcher : TransformationMatcher { + override val indexVariableAllowed: Boolean get() = true - override fun match(state: MatchingState): ResultTransformationMatch? { + override fun match(state: MatchingState): TransformationMatch.Result? { val statement = state.statements.singleOrNull() ?: return null //TODO: it can be implicit 'this' too val qualifiedExpression = statement as? KtDotQualifiedExpression ?: return null @@ -108,11 +108,11 @@ class AddToCollectionTransformation( } if (state.indexVariable == null && argumentValue.isVariableReference(state.inputVariable)) { - return ResultTransformationMatch(AddToCollectionTransformation(state.outerLoop, targetCollection)) + return TransformationMatch.Result(AddToCollectionTransformation(state.outerLoop, targetCollection)) } else { //TODO: recognize "?: continue" in the argument - return ResultTransformationMatch(MapToTransformation.create( + return TransformationMatch.Result(MapToTransformation.create( state.outerLoop, state.inputVariable, state.indexVariable, targetCollection, argumentValue, mapNotNull = false)) } } @@ -121,7 +121,7 @@ class AddToCollectionTransformation( state: MatchingState, targetCollection: KtExpression, addOperationArgument: KtExpression - ): ResultTransformationMatch? { + ): TransformationMatch.Result? { val collectionInitialization = targetCollection.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null val collectionKind = collectionInitialization.initializer.isSimpleCollectionInstantiation() ?: return null val argumentIsInputVariable = addOperationArgument.isVariableReference(state.inputVariable) @@ -136,13 +136,13 @@ class AddToCollectionTransformation( val mapTransformation = MapTransformation(state.outerLoop, state.inputVariable, null, addOperationArgument, mapNotNull = false) AssignSequenceTransformationResultTransformation(mapTransformation, collectionInitialization) } - return ResultTransformationMatch(transformation) + return TransformationMatch.Result(transformation) } canChangeInitializerType(collectionInitialization, KotlinBuiltIns.FQ_NAMES.mutableList, state.outerLoop) -> { if (argumentIsInputVariable) { val transformation = AssignToMutableListTransformation(state.outerLoop, collectionInitialization) - return ResultTransformationMatch(transformation) + return TransformationMatch.Result(transformation) } } } @@ -162,11 +162,11 @@ class AddToCollectionTransformation( } if (argumentIsInputVariable) { - return ResultTransformationMatch(assignToSetTransformation) + return TransformationMatch.Result(assignToSetTransformation) } else { val mapTransformation = MapTransformation(state.outerLoop, state.inputVariable, null, addOperationArgument, mapNotNull = false) - return ResultTransformationMatch(assignToSetTransformation, mapTransformation) + return TransformationMatch.Result(assignToSetTransformation, mapTransformation) } } } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/CountTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/CountTransformation.kt index c0805563b2e..d2639367e52 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/CountTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/CountTransformation.kt @@ -68,11 +68,11 @@ class CountTransformation( * variable++ (or ++variable) * } */ - object Matcher : ResultTransformationMatcher { - override val indexVariableUsePossible: Boolean + object Matcher : TransformationMatcher { + override val indexVariableAllowed: Boolean get() = false - override fun match(state: MatchingState): ResultTransformationMatch? { + override fun match(state: MatchingState): TransformationMatch.Result? { val operand = state.statements.singleOrNull()?.isPlusPlusOf() ?: return null val initialization = operand.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null @@ -82,7 +82,7 @@ class CountTransformation( if (!KotlinBuiltIns.isInt(variableType)) return null val transformation = CountTransformation(state.outerLoop, state.inputVariable, initialization, null) - return ResultTransformationMatch(transformation) + return TransformationMatch.Result(transformation) } } } \ 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 bf0b0317d01..0448608f9c8 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 @@ -16,30 +16,21 @@ 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.lexer.KtTokens -import org.jetbrains.kotlin.psi.KtBinaryExpression -import org.jetbrains.kotlin.psi.KtBreakExpression +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.AssignToVariableResultTransformation +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.ChainedCallGenerator +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.FindOperatorGenerator +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.VariableInitialization import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtForExpression class FindAndAssignTransformation( loop: KtForExpression, private val generator: FindOperatorGenerator, - initialization: VariableInitialization, - private val filter: KtExpression? = null + initialization: VariableInitialization ) : AssignToVariableResultTransformation(loop, initialization) { - override fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? { - if (previousTransformation !is FilterTransformation) return null - if (previousTransformation.indexVariable != null) return null - assert(filter == null) { "Should not happen because no 2 consecutive FilterTransformation's possible"} - return FindAndAssignTransformation(loop, generator, initialization, previousTransformation.effectiveCondition()) - } - override val presentation: String - get() = generator.functionName + (if (filter != null) "{}" else "()") + get() = generator.presentation override val chainCallCount: Int get() = generator.chainCallCount @@ -48,57 +39,6 @@ class FindAndAssignTransformation( get() = generator.shouldUseInputVariable override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { - return generator.generate(chainedCallGenerator, filter) - } - - /** - * Matches: - * val variable = ... - * for (...) { - * ... - * variable = ... - * break - * } - * or - * val variable = ... - * for (...) { - * ... - * variable = ... - * } - */ - object Matcher : ResultTransformationMatcher { - override val indexVariableUsePossible: Boolean - get() = false - - override fun match(state: MatchingState): ResultTransformationMatch? { - when (state.statements.size) { - 1 -> {} - - 2 -> { - val breakExpression = state.statements.last() as? KtBreakExpression ?: return null - if (breakExpression.targetLoop() != state.outerLoop) return null - } - - else -> return null - } - val findFirst = state.statements.size == 2 - - val binaryExpression = state.statements.first() as? KtBinaryExpression ?: return null - if (binaryExpression.operationToken != KtTokens.EQ) return null - val left = binaryExpression.left ?: return null - val right = binaryExpression.right ?: return null - - val initialization = left.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null - - if (initialization.variable.countUsages(state.outerLoop) != 1) return null // this should be the only usage of this variable inside the loop - - // we do not try to convert anything if the initializer is not compile-time constant because of possible side-effects - if (!initialization.initializer.isConstant()) return null - - val generator = buildFindOperationGenerator(right, initialization.initializer, state.inputVariable, findFirst) ?: return null - - val transformation = FindAndAssignTransformation(state.outerLoop, generator, initialization) - return ResultTransformationMatch(transformation) - } + return generator.generate(chainedCallGenerator) } } \ No newline at end of file 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 b0b525d944d..8975109e309 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 @@ -17,7 +17,6 @@ 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.KtExpression import org.jetbrains.kotlin.psi.KtForExpression import org.jetbrains.kotlin.psi.KtReturnExpression @@ -26,21 +25,13 @@ import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange class FindAndReturnTransformation( override val loop: KtForExpression, private val generator: FindOperatorGenerator, - private val endReturn: KtReturnExpression, - private val filter: KtExpression? = null + private val endReturn: KtReturnExpression ) : ResultTransformation { - override fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? { - if (previousTransformation !is FilterTransformation) return null - if (previousTransformation.indexVariable != null) return null - assert(filter == null) { "Should not happen because no 2 consecutive FilterTransformation's possible"} - return FindAndReturnTransformation(loop, generator, endReturn, previousTransformation.effectiveCondition()) - } - override val commentSavingRange = PsiChildRange(loop.unwrapIfLabeled(), endReturn) override val presentation: String - get() = generator.functionName + (if (filter != null) "{}" else "()") + get() = generator.presentation override val chainCallCount: Int get() = generator.chainCallCount @@ -49,7 +40,7 @@ class FindAndReturnTransformation( get() = generator.shouldUseInputVariable override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { - return generator.generate(chainedCallGenerator, filter) + return generator.generate(chainedCallGenerator) } override fun convertLoop(resultCallChain: KtExpression, commentSavingRangeHolder: CommentSavingRangeHolder): KtExpression { @@ -57,31 +48,4 @@ class FindAndReturnTransformation( loop.deleteWithLabels() return endReturn } - - /** - * Matches: - * for (...) { - * ... - * return ... - * } - * return ... - */ - object Matcher : ResultTransformationMatcher { - override val indexVariableUsePossible: Boolean - get() = false - - override fun match(state: MatchingState): ResultTransformationMatch? { - val returnInLoop = state.statements.singleOrNull() as? KtReturnExpression ?: return null - val returnAfterLoop = state.outerLoop.nextStatement() as? KtReturnExpression ?: return null - if (returnInLoop.getLabelName() != returnAfterLoop.getLabelName()) return null - - val returnValueInLoop = returnInLoop.returnedExpression ?: return null - val returnValueAfterLoop = returnAfterLoop.returnedExpression ?: return null - - val generator = buildFindOperationGenerator(returnValueInLoop, returnValueAfterLoop, state.inputVariable, findFirst = true) ?: return null - - val transformation = FindAndReturnTransformation(state.outerLoop, generator, returnAfterLoop) - return ResultTransformationMatch(transformation) - } - } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindTransformationMatcher.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindTransformationMatcher.kt new file mode 100644 index 00000000000..2ea55bc1ef3 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindTransformationMatcher.kt @@ -0,0 +1,104 @@ +/* + * 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.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtBinaryExpression +import org.jetbrains.kotlin.psi.KtBreakExpression +import org.jetbrains.kotlin.psi.KtReturnExpression + +/** + * Matches: + * val variable = ... + * for (...) { + * ... + * variable = ... + * break + * } + * or + * val variable = ... + * for (...) { + * ... + * variable = ... + * } + * or + * for (...) { + * ... + * return ... + * } + * return ... + */ +object FindTransformationMatcher : TransformationMatcher { + override val indexVariableAllowed: Boolean + get() = false + + override fun match(state: MatchingState): TransformationMatch.Result? { + return matchWithFilterBefore(state, null) + } + + fun matchWithFilterBefore(state: MatchingState, filterTransformation: FilterTransformation?): TransformationMatch.Result? { + matchReturn(state, filterTransformation)?.let { return it } + + when (state.statements.size) { + 1 -> { } + + 2 -> { + val breakExpression = state.statements.last() as? KtBreakExpression ?: return null + if (breakExpression.targetLoop() != state.outerLoop) return null + } + + else -> return null + } + val findFirst = state.statements.size == 2 + + val binaryExpression = state.statements.first() as? KtBinaryExpression ?: return null + if (binaryExpression.operationToken != KtTokens.EQ) return null + val left = binaryExpression.left ?: return null + val right = binaryExpression.right ?: return null + + val initialization = left.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null + + if (initialization.variable.countUsages(state.outerLoop) != 1) return null // this should be the only usage of this variable inside the loop + + // we do not try to convert anything if the initializer is not compile-time constant because of possible side-effects + if (!initialization.initializer.isConstant()) return null + + val generator = buildFindOperationGenerator(right, initialization.initializer, state.inputVariable, state.indexVariable, filterTransformation, findFirst) + ?: return null + + val transformation = FindAndAssignTransformation(state.outerLoop, generator, initialization) + return TransformationMatch.Result(transformation) + } + + private fun matchReturn(state: MatchingState, filterTransformation: FilterTransformation?): TransformationMatch.Result? { + val returnInLoop = state.statements.singleOrNull() as? KtReturnExpression ?: return null + val returnAfterLoop = state.outerLoop.nextStatement() as? KtReturnExpression ?: return null + if (returnInLoop.getLabelName() != returnAfterLoop.getLabelName()) return null + + val returnValueInLoop = returnInLoop.returnedExpression ?: return null + val returnValueAfterLoop = returnAfterLoop.returnedExpression ?: return null + + val generator = buildFindOperationGenerator(returnValueInLoop, returnValueAfterLoop, + state.inputVariable, state.indexVariable, filterTransformation, findFirst = true) + ?: return null + + val transformation = FindAndReturnTransformation(state.outerLoop, generator, returnAfterLoop) + return TransformationMatch.Result(transformation) + } +} \ No newline at end of file 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 e221a5b5418..94d699b2112 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 @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isNullExpression import org.jetbrains.kotlin.idea.intentions.loopToCallChain.* +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.FindTransformationMatcher import org.jetbrains.kotlin.idea.intentions.negate import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.* @@ -73,9 +74,14 @@ class FilterTransformation( * if () break * ... * } + * + * plus optionally consequent find operation (assignment or return) */ - object Matcher : SequenceTransformationMatcher { - override fun match(state: MatchingState): SequenceTransformationMatch? { + object Matcher : TransformationMatcher { + override val indexVariableAllowed: Boolean + get() = true + + override fun match(state: MatchingState): TransformationMatch? { // we merge filter transformations here instead of FilterTransformation.mergeWithPrevious() because of filterIndexed that won't merge otherwise var (transformation, currentState) = matchOneTransformation(state) ?: return null @@ -93,9 +99,12 @@ class FilterTransformation( transformation = FilterTransformation(state.outerLoop, transformation.inputVariable, indexVariable, mergedCondition, isInverse = false) //TODO: build filterNot in some cases? currentState = nextState } + + FindTransformationMatcher.matchWithFilterBefore(currentState, transformation) + ?.let { return it } } - return SequenceTransformationMatch(transformation, currentState) + return TransformationMatch.Sequence(transformation, currentState) } private fun matchOneTransformation(state: MatchingState): Pair? { diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FlatMapTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FlatMapTransformation.kt index 9fd16b340b0..77d33ba6476 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FlatMapTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FlatMapTransformation.kt @@ -49,8 +49,11 @@ class FlatMapTransformation( * } * } */ - object Matcher : SequenceTransformationMatcher { - override fun match(state: MatchingState): SequenceTransformationMatch? { + object Matcher : TransformationMatcher { + override val indexVariableAllowed: Boolean + get() = true + + override fun match(state: MatchingState): TransformationMatch.Sequence? { val nestedLoop = state.statements.singleOrNull() as? KtForExpression ?: return null val transform = nestedLoop.loopRange ?: return null @@ -73,7 +76,7 @@ class FlatMapTransformation( statements = listOf(nestedLoopBody), inputVariable = newInputVariable ) - return SequenceTransformationMatch(listOf(mapIndexedTransformation, flatMapTransformation), newState) + return TransformationMatch.Sequence(listOf(mapIndexedTransformation, flatMapTransformation), newState) } val transformation = FlatMapTransformation(state.outerLoop, state.inputVariable, transform) @@ -82,7 +85,7 @@ class FlatMapTransformation( statements = listOf(nestedLoopBody), inputVariable = newInputVariable ) - return SequenceTransformationMatch(transformation, newState) + return TransformationMatch.Sequence(transformation, newState) } } } \ No newline at end of file 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 9d0c037ffef..d259edf3f6d 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 @@ -22,9 +22,11 @@ import org.jetbrains.kotlin.psi.KtContinueExpression import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType import org.jetbrains.kotlin.psi.psiUtil.isAncestor -object IntroduceIndexMatcher : SequenceTransformationMatcher { - override fun match(state: MatchingState): SequenceTransformationMatch? { - if (state.indexVariable != null) return null // old index variable is still needed - cannot introduce another one +object IntroduceIndexMatcher : TransformationMatcher { + override val indexVariableAllowed: Boolean + get() = false // old index variable is still needed - cannot introduce another one + + override fun match(state: MatchingState): TransformationMatch.Sequence? { if (state.statements.size < 2) return null val operand = state.statements.last().isPlusPlusOf() ?: return null val restStatements = state.statements.dropLast(1) @@ -52,6 +54,6 @@ object IntroduceIndexMatcher : SequenceTransformationMatcher { val newState = state.copy(statements = restStatements, indexVariable = variable, initializationStatementsToDelete = state.initializationStatementsToDelete + variableInitialization.initializationStatement) - return SequenceTransformationMatch(emptyList(), newState) + return TransformationMatch.Sequence(emptyList(), newState) } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/MapTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/MapTransformation.kt index a8748afe357..1e83a923dac 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/MapTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/MapTransformation.kt @@ -54,11 +54,14 @@ class MapTransformation( * ... * } */ - object Matcher : SequenceTransformationMatcher { + object Matcher : TransformationMatcher { + override val indexVariableAllowed: Boolean + get() = true + override val embeddedBreakOrContinuePossible: Boolean get() = true - override fun match(state: MatchingState): SequenceTransformationMatch? { + override fun match(state: MatchingState): TransformationMatch.Sequence? { val declaration = state.statements.firstOrNull() as? KtProperty ?: return null //TODO: support multi-variables val initializer = declaration.initializer ?: return null if (declaration.hasWriteUsages()) return null @@ -76,7 +79,7 @@ class MapTransformation( else MapTransformation(state.outerLoop, state.inputVariable, null, mapping, mapNotNull = true) val newState = state.copy(statements = restStatements, inputVariable = declaration) - return SequenceTransformationMatch(transformation, newState) + return TransformationMatch.Sequence(transformation, newState) } if (initializer.containsEmbeddedBreakOrContinue()) return null @@ -86,7 +89,7 @@ class MapTransformation( else MapTransformation(state.outerLoop, state.inputVariable, null, initializer, mapNotNull = false) val newState = state.copy(statements = restStatements, inputVariable = declaration) - return SequenceTransformationMatch(transformation, newState) + return TransformationMatch.Sequence(transformation, newState) } } } diff --git a/idea/testData/intentions/loopToCallChain/indexOfFirst_ifAssign.kt b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifAssign.kt new file mode 100644 index 00000000000..e7a7c44139f --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifAssign.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'indexOfFirst{}'" +fun foo(list: List) { + var result = -1 + for ((index, s) in list.withIndex()) { + if (s.length > 0) { + result = index + break + } + } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexOfFirst_ifAssign.kt.after b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifAssign.kt.after new file mode 100644 index 00000000000..75d317bd303 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifAssign.kt.after @@ -0,0 +1,5 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'indexOfFirst{}'" +fun foo(list: List) { + val result = list.indexOfFirst { it.length > 0 } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexOfFirst_ifReturn.kt b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifReturn.kt new file mode 100644 index 00000000000..0b0e8488f4e --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifReturn.kt @@ -0,0 +1,10 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'indexOfFirst{}'" +fun foo(list: List): Int { + for ((index, s) in list.withIndex()) { + if (s.length > 0) { + return index + } + } + return -1 +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexOfFirst_ifReturn.kt.after b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifReturn.kt.after new file mode 100644 index 00000000000..1270aeb1630 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexOfFirst_ifReturn.kt.after @@ -0,0 +1,5 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'indexOfFirst{}'" +fun foo(list: List): Int { + return list.indexOfFirst { it.length > 0 } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexOfFirst_mustBeNoIndexInCondition.kt b/idea/testData/intentions/loopToCallChain/indexOfFirst_mustBeNoIndexInCondition.kt new file mode 100644 index 00000000000..ebd06773402 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexOfFirst_mustBeNoIndexInCondition.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +// IS_APPLICABLE: false +fun foo(list: List) { + var result = -1 + for ((index, s) in list.withIndex()) { + if (s.length > index) { + result = index + break + } + } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexOfLast_ifAssign.kt b/idea/testData/intentions/loopToCallChain/indexOfLast_ifAssign.kt new file mode 100644 index 00000000000..1a6f9bdda90 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexOfLast_ifAssign.kt @@ -0,0 +1,10 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'indexOfLast{}'" +fun foo(list: List) { + var result = -1 + for ((index, s) in list.withIndex()) { + if (s.length > 0) { + result = index + } + } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexOfLast_ifAssign.kt.after b/idea/testData/intentions/loopToCallChain/indexOfLast_ifAssign.kt.after new file mode 100644 index 00000000000..59b12551752 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexOfLast_ifAssign.kt.after @@ -0,0 +1,5 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'indexOfLast{}'" +fun foo(list: List) { + val result = list.indexOfLast { it.length > 0 } +} \ No newline at end of file