diff --git a/compiler/frontend/src/org/jetbrains/kotlin/cfg/PseudocodeTraverser.kt b/compiler/frontend/src/org/jetbrains/kotlin/cfg/PseudocodeTraverser.kt index 5090dbaf0c5..c53e4808421 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/cfg/PseudocodeTraverser.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/cfg/PseudocodeTraverser.kt @@ -181,8 +181,8 @@ enum class TraverseInstructionResult { // returns false when interrupted by handler fun traverseFollowingInstructions( rootInstruction: Instruction, - visited: MutableSet, - order: TraversalOrder, + visited: MutableSet = HashSet(), + order: TraversalOrder = FORWARD, // true to continue traversal handler: ((Instruction) -> TraverseInstructionResult)? ): Boolean { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/Pseudocode.kt b/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/Pseudocode.kt index b7bb5390131..746beea90b1 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/Pseudocode.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/Pseudocode.kt @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.cfg.pseudocode import org.jetbrains.kotlin.cfg.pseudocode.instructions.Instruction +import org.jetbrains.kotlin.cfg.pseudocode.instructions.KtElementInstruction import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.LocalFunctionDeclarationInstruction import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.SubroutineEnterInstruction import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.SubroutineExitInstruction @@ -51,4 +52,6 @@ interface Pseudocode { fun isSideEffectFree(instruction: Instruction): Boolean fun copy(): Pseudocode + + fun instructionForElement(element: KtElement): KtElementInstruction? } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/PseudocodeImpl.kt b/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/PseudocodeImpl.kt index c4856c5f84f..15ca239b951 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/PseudocodeImpl.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/cfg/pseudocode/PseudocodeImpl.kt @@ -30,17 +30,14 @@ import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.LocalFunctionDec import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.SubroutineEnterInstruction import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.SubroutineExitInstruction import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.SubroutineSinkInstruction -import org.jetbrains.kotlin.cfg.pseudocodeTraverser.* -import org.jetbrains.kotlin.cfg.pseudocodeTraverser.TraverseInstructionResult -import org.jetbrains.kotlin.psi.KtElement - -import java.util.* - import org.jetbrains.kotlin.cfg.pseudocodeTraverser.TraversalOrder.BACKWARD import org.jetbrains.kotlin.cfg.pseudocodeTraverser.TraversalOrder.FORWARD +import org.jetbrains.kotlin.cfg.pseudocodeTraverser.TraverseInstructionResult +import org.jetbrains.kotlin.cfg.pseudocodeTraverser.traverseFollowingInstructions +import org.jetbrains.kotlin.psi.KtElement +import java.util.* class PseudocodeImpl(override val correspondingElement: KtElement) : Pseudocode { - inner class PseudocodeLabel internal constructor(private val name: String, private val comment: String?) : Label { var targetInstructionIndex: Int? = null private set @@ -89,8 +86,8 @@ class PseudocodeImpl(override val correspondingElement: KtElement) : Pseudocode override val localDeclarations: Set by lazy { getLocalDeclarations(this) } - //todo getters - private val representativeInstructions = HashMap() + + private val representativeInstructions = HashMap() private val labels = ArrayList() @@ -191,7 +188,9 @@ class PseudocodeImpl(override val correspondingElement: KtElement) : Pseudocode instruction.owner = this if (instruction is KtElementInstruction) { - representativeInstructions.put(instruction.element, instruction) + if (!representativeInstructions.containsKey(instruction.element)) { + representativeInstructions.put(instruction.element, instruction) + } } if (instruction is MergeInstruction) { @@ -379,6 +378,10 @@ class PseudocodeImpl(override val correspondingElement: KtElement) : Pseudocode return result } + override fun instructionForElement(element: KtElement): KtElementInstruction? { + return representativeInstructions[element] + } + private fun repeatWhole(originalPseudocode: PseudocodeImpl) { repeatInternal(originalPseudocode, null, null, 0) parent = originalPseudocode.parent 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 3aad5e3a1ec..478330a4ca3 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.intentions.loopToCallChain import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace +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 @@ -102,7 +103,8 @@ data class MatchingState( */ val indexVariable: KtCallableDeclaration?, val initializationStatementsToDelete: Collection = emptyList(), - val previousTransformations: List + val previousTransformations: List, + val pseudocodeProvider: () -> Pseudocode ) 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 8e98f688d5f..d1dea98415e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt @@ -18,6 +18,9 @@ package org.jetbrains.kotlin.idea.intentions.loopToCallChain import com.intellij.openapi.util.Key import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.cfg.pseudocode.Pseudocode +import org.jetbrains.kotlin.cfg.pseudocode.PseudocodeUtil +import org.jetbrains.kotlin.cfg.pseudocode.containingDeclarationForPseudocode import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.idea.analysis.analyzeInContext import org.jetbrains.kotlin.idea.caches.resolve.analyze @@ -71,13 +74,25 @@ fun match(loop: KtForExpression): MatchResult? { val loopContainsEmbeddedBreakOrContinue = loop.containsEmbeddedBreakOrContinue() val sequenceTransformations = ArrayList() + + val pseudocodeProvider: () -> Pseudocode = object : () -> Pseudocode { + val pseudocode: Pseudocode by lazy { + val declaration = loop.containingDeclarationForPseudocode!! + val bindingContext = loop.analyze(BodyResolveMode.FULL) + PseudocodeUtil.generatePseudocode(declaration, bindingContext) + } + + override fun invoke() = pseudocode + } + var state = MatchingState( outerLoop = loop, innerLoop = loop, statements = listOf(loop.body ?: return null), inputVariable = inputVariable, indexVariable = indexVariable, - previousTransformations = sequenceTransformations + previousTransformations = sequenceTransformations, + pseudocodeProvider = pseudocodeProvider ) MatchLoop@ 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 bd2d938369b..79a98716d0d 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 @@ -16,28 +16,37 @@ package org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence +import org.jetbrains.kotlin.cfg.pseudocode.instructions.Instruction +import org.jetbrains.kotlin.cfg.pseudocode.instructions.eval.AccessTarget +import org.jetbrains.kotlin.cfg.pseudocode.instructions.eval.ReadValueInstruction +import org.jetbrains.kotlin.cfg.pseudocodeTraverser.TraverseInstructionResult +import org.jetbrains.kotlin.cfg.pseudocodeTraverser.traverseFollowingInstructions +import org.jetbrains.kotlin.descriptors.VariableDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.idea.intentions.loopToCallChain.* -import org.jetbrains.kotlin.psi.KtConstantExpression -import org.jetbrains.kotlin.psi.KtContinueExpression -import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType import org.jetbrains.kotlin.psi.psiUtil.isAncestor +import java.util.* 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) - - fun isContinueOfThisLoopOrOuter(continueExpression: KtContinueExpression): Boolean { - val targetLoop = continueExpression.targetLoop() ?: return true - return targetLoop.isAncestor(state.innerLoop, strict = false) + for (statement in state.statements) { + val unaryExpressions = statement.collectDescendantsOfType( + canGoInside = { it !is KtBlockExpression && it !is KtFunction } + ) + for (unaryExpression in unaryExpressions) { + checkIndexCandidate(unaryExpression, state)?.let { return it } + } } + return null + } - // there should be no continuation of the loop in statements before index increment - if (restStatements.any { statement -> statement.anyDescendantOfType(::isContinueOfThisLoopOrOuter) }) return null + private fun checkIndexCandidate(incrementExpression: KtUnaryExpression, state: MatchingState): TransformationMatch.Sequence? { + val operand = incrementExpression.isPlusPlusOf() ?: return null val variableInitialization = operand.isVariableInitializedBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = false) ?: return null @@ -51,9 +60,51 @@ object IntroduceIndexMatcher : TransformationMatcher { //TODO: preform more precise analysis when variable can be used earlier or used later but value overwritten before that if (variable.countUsages() != variable.countUsages(state.statements + variableInitialization.initializationStatement)) return null + val pseudocode = state.pseudocodeProvider() + val firstStatement = state.statements.first() + val firstInstruction = pseudocode.instructionForElement(firstStatement)!! + val incrementInstruction = pseudocode.instructionForElement(incrementExpression)!! + if (!isAlwaysReachedOrExitedLoop(firstInstruction, incrementInstruction, state.outerLoop, state.innerLoop)) return null + + val variableDescriptor = variable.resolveToDescriptor() as VariableDescriptor + if (isAccessedAfter(variableDescriptor, incrementInstruction, state.innerLoop)) return null // index accessed inside loop after increment + + 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) return TransformationMatch.Sequence(emptyList(), newState) } + + private fun isAlwaysReachedOrExitedLoop( + from: Instruction, + to: Instruction, + outerLoop: KtForExpression, + innerLoop: KtForExpression + ): Boolean { + val visited = HashSet() + return traverseFollowingInstructions(from, visited) { instruction -> + val nextInstructionScope = instruction.blockScope.block + when { + instruction == to -> TraverseInstructionResult.SKIP + !outerLoop.isAncestor(nextInstructionScope, strict = false) -> TraverseInstructionResult.SKIP // we are out of the outer loop - it's ok + !innerLoop.isAncestor(nextInstructionScope, strict = true) -> TraverseInstructionResult.HALT // we exited or continued inner loop + else -> TraverseInstructionResult.CONTINUE + } + } && visited.contains(to) + } + + private fun isAccessedAfter(variableDescriptor: VariableDescriptor, instruction: Instruction, loop: KtForExpression): Boolean { + return !traverseFollowingInstructions(instruction) { instruction -> + when { + !loop.isAncestor(instruction.blockScope.block, strict = true) -> TraverseInstructionResult.SKIP // we are outside the loop or going to the next iteration + instruction.isReadOfVariable(variableDescriptor) -> TraverseInstructionResult.HALT + else -> TraverseInstructionResult.CONTINUE + } + } + } + + private fun Instruction.isReadOfVariable(descriptor: VariableDescriptor): Boolean { + return ((this as? ReadValueInstruction)?.target as? AccessTarget.Call)?.resolvedCall?.resultingDescriptor == descriptor + } } \ No newline at end of file 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 8f7a39ad6d6..319358ba0e2 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt @@ -53,13 +53,7 @@ fun generateLambda(inputVariable: KtCallableDeclaration, expression: KtExpressio if (isItUsedInside) return lambdaExpression - val resolutionScope = inputVariable.getResolutionScope(inputVariable.analyze(BodyResolveMode.FULL), inputVariable.getResolutionFacade()) - val bindingContext = lambdaExpression.analyzeInContext(resolutionScope, contextExpression = inputVariable) - val lambdaParam = lambdaExpression.valueParameters.single() - val lambdaParamDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, lambdaParam] - val usages = lambdaExpression.collectDescendantsOfType { - it.mainReference.resolveToDescriptors(bindingContext).singleOrNull() == lambdaParamDescriptor - } + val usages = lambdaExpression.findParameterUsages(lambdaExpression.valueParameters.single(), inputVariable) val itExpr = psiFactory.createSimpleName("it") for (usage in usages) { @@ -72,6 +66,34 @@ fun generateLambda(inputVariable: KtCallableDeclaration, expression: KtExpressio return psiFactory.createExpressionByPattern("{ $0 }", lambdaExpression.bodyExpression!!) as KtLambdaExpression } +fun generateLambda(expression: KtExpression, indexVariable: KtCallableDeclaration, inputVariable: KtCallableDeclaration): KtLambdaExpression { + val lambdaExpression = generateLambda(expression, *arrayOf(indexVariable, inputVariable)) + + // replace "index++" with "index" or "index + 1" (see IntroduceIndexMatcher) + val indexPlusPlus = lambdaExpression.findDescendantOfType { unaryExpression -> + val operand = unaryExpression.isPlusPlusOf() as? KtNameReferenceExpression + if (operand != null && operand.getReferencedName() == indexVariable.name) { + val bindingContext = lambdaExpression.analyzeInContext(inputVariable) + val parameter = lambdaExpression.valueParameters[0] + val parameterDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, parameter] + operand.mainReference.resolveToDescriptors(bindingContext).singleOrNull() == parameterDescriptor + } + else { + false + } + } + 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) + } + + return lambdaExpression +} + fun generateLambda(expression: KtExpression, vararg inputVariables: KtCallableDeclaration): KtLambdaExpression { return KtPsiFactory(expression).buildExpression { appendFixedText("{") @@ -91,6 +113,19 @@ fun generateLambda(expression: KtExpression, vararg inputVariables: KtCallableDe } as KtLambdaExpression } +private fun KtLambdaExpression.findParameterUsages(lambdaParam: KtParameter, context: KtExpression): Collection { + val bindingContext = analyzeInContext(context) + val lambdaParamDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, lambdaParam] + return collectDescendantsOfType { + it.mainReference.resolveToDescriptors(bindingContext).singleOrNull() == lambdaParamDescriptor + } +} + +private fun KtLambdaExpression.analyzeInContext(context: KtExpression): BindingContext { + val resolutionScope = context.getResolutionScope(context.analyze(BodyResolveMode.FULL), context.getResolutionFacade()) + return analyzeInContext(resolutionScope, contextExpression = context) +} + data class VariableInitialization( val variable: KtProperty, val initializationStatement: KtExpression, diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractionEngine/extractableAnalysisUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractionEngine/extractableAnalysisUtil.kt index fd0913376cf..d8ef049099f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractionEngine/extractableAnalysisUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractionEngine/extractableAnalysisUtil.kt @@ -46,8 +46,8 @@ import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.core.KotlinNameSuggester import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator import org.jetbrains.kotlin.idea.core.compareDescriptors -import org.jetbrains.kotlin.idea.refactoring.createTempCopy import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle +import org.jetbrains.kotlin.idea.refactoring.createTempCopy import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.AnalysisResult.ErrorMessage import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.AnalysisResult.Status import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.OutputValue.* @@ -106,7 +106,7 @@ private fun List.getVarDescriptorsAccessedAfterwards(bindingContext val visitedInstructions = HashSet() fun doTraversal(instruction: Instruction) { - traverseFollowingInstructions(instruction, visitedInstructions, TraversalOrder.FORWARD) { + traverseFollowingInstructions(instruction, visitedInstructions) { when { it is AccessValueInstruction && it !in this -> PseudocodeUtil.extractVariableDescriptorIfAny(it, false, bindingContext)?.let { accessedAfterwards.add(it) } diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression.kt b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression.kt new file mode 100644 index 00000000000..26854070c55 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'filterNot{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + if (s.isBlank()) continue + val x = s.length * index++ + if (x > 0) return x + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression.kt.after b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression.kt.after new file mode 100644 index 00000000000..000cbbd54ed --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'filterNot{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + return list + .filterNot { it.isBlank() } + .mapIndexed { index, s -> s.length * index } + .firstOrNull { it > 0 } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedAfterIncrement.kt b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedAfterIncrement.kt new file mode 100644 index 00000000000..c812da36d2b --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedAfterIncrement.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +//TODO: should not be available without "asSequence()"! +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + if (s.isBlank()) continue + val x = s.length * index++ + if (x * 100 > index * index) return x + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedAfterIncrement.kt.after b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedAfterIncrement.kt.after new file mode 100644 index 00000000000..c01b9e545fc --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedAfterIncrement.kt.after @@ -0,0 +1,9 @@ +// WITH_RUNTIME +//TODO: should not be available without "asSequence()"! +fun foo(list: List): Int? { + var index = 0 + return list + .filterNot { it.isBlank() } + .map { it.length * index++ } + .firstOrNull { it * 100 > index * index } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement.kt b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement.kt new file mode 100644 index 00000000000..6cd03c0188f --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with '...mapIndexed{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + if (s.isBlank()) continue + val x = s.length * index + val y = x * index++ + if (y > 1000) return y + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement.kt.after b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement.kt.after new file mode 100644 index 00000000000..b175d3d21f6 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement.kt.after @@ -0,0 +1,9 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with '...mapIndexed{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + return list + .filterNot { it.isBlank() } + .mapIndexed { index, s -> s.length * index } + .mapIndexed { index, x -> x * index } + .firstOrNull { it > 1000 } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement2.kt b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement2.kt new file mode 100644 index 00000000000..e9edce2b13a --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement2.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'mapIndexed{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + val x = s.length * index + val y = x * index++ + if (y > 1000) continue + return y + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement2.kt.after b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement2.kt.after new file mode 100644 index 00000000000..c64bb0aba28 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusInsideExpression_indexUsedBeforeIncrement2.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'mapIndexed{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + return list + .mapIndexed { index, s -> s.length * index } + .mapIndexed { index, x -> x * index } + .firstOrNull { it <= 1000 } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement.kt b/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement.kt new file mode 100644 index 00000000000..cf0a606f8d6 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'filterNot{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + if (s.isBlank()) continue + 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/loopToCallChain/indexPlusPlusIsNotLastStatement.kt.after b/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement.kt.after new file mode 100644 index 00000000000..000cbbd54ed --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'filterNot{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + return list + .filterNot { it.isBlank() } + .mapIndexed { index, s -> s.length * index } + .firstOrNull { it > 0 } +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement_indexUsedAfterIncrement.kt b/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement_indexUsedAfterIncrement.kt new file mode 100644 index 00000000000..a2a242deb4e --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/indexPlusPlusIsNotLastStatement_indexUsedAfterIncrement.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// IS_APPLICABLE: false +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + if (s.isBlank()) continue + val x = s.length * index + index++ + if ((x + index) % 3 == 0) return x + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/mapIndexed_afterFilter.kt b/idea/testData/intentions/loopToCallChain/mapIndexed_afterFilter.kt index a64c1ac9db8..cf0a606f8d6 100644 --- a/idea/testData/intentions/loopToCallChain/mapIndexed_afterFilter.kt +++ b/idea/testData/intentions/loopToCallChain/mapIndexed_afterFilter.kt @@ -5,8 +5,8 @@ fun foo(list: List): Int? { for (s in list) { if (s.isBlank()) continue val x = s.length * index - if (x > 0) return x index++ + if (x > 0) return x } return null } \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/prefixIndexPlusPlusInsideExpression.kt b/idea/testData/intentions/loopToCallChain/prefixIndexPlusPlusInsideExpression.kt new file mode 100644 index 00000000000..012a540de95 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/prefixIndexPlusPlusInsideExpression.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'filterNot{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + var index = 0 + for (s in list) { + if (s.isBlank()) continue + val x = s.length * ++index + if (x > 0) return x + } + return null +} \ No newline at end of file diff --git a/idea/testData/intentions/loopToCallChain/prefixIndexPlusPlusInsideExpression.kt.after b/idea/testData/intentions/loopToCallChain/prefixIndexPlusPlusInsideExpression.kt.after new file mode 100644 index 00000000000..b56d24f81f8 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/prefixIndexPlusPlusInsideExpression.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'filterNot{}.mapIndexed{}.firstOrNull{}'" +fun foo(list: List): Int? { + return list + .filterNot { it.isBlank() } + .mapIndexed { index, s -> s.length * (index + 1) } + .firstOrNull { it > 0 } +} \ No newline at end of file