Supported indexOfFirst/indexOfLast

This commit is contained in:
Valentin Kipyatkov
2016-04-22 15:42:37 +03:00
parent 5ac2c48879
commit b65ecdd660
21 changed files with 400 additions and 292 deletions
@@ -39,7 +39,7 @@ class LoopToCallChainIntention : SelfTargetingRangeIntention<KtForExpression>(
return if (match != null) element.forKeyword.textRange else null return if (match != null) element.forKeyword.textRange else null
} }
private fun ResultTransformationMatch.buildPresentation(): String { private fun TransformationMatch.Result.buildPresentation(): String {
var transformations = sequenceTransformations + resultTransformation var transformations = sequenceTransformations + resultTransformation
val MAX = 3 val MAX = 3
var result: String? = null var result: String? = null
@@ -57,6 +57,7 @@ fun KtExpression?.isSimpleName(name: Name): Boolean {
} }
fun KtCallableDeclaration.hasUsages(inElement: KtElement): Boolean { fun KtCallableDeclaration.hasUsages(inElement: KtElement): Boolean {
assert(inElement.isPhysical)
return hasUsages(listOf(inElement)) return hasUsages(listOf(inElement))
} }
@@ -19,6 +19,7 @@ package org.jetbrains.kotlin.idea.intentions.loopToCallChain
import org.jetbrains.kotlin.descriptors.VariableDescriptor import org.jetbrains.kotlin.descriptors.VariableDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isNullExpression 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.psi.*
import org.jetbrains.kotlin.types.typeUtil.TypeNullability import org.jetbrains.kotlin.types.typeUtil.TypeNullability
import org.jetbrains.kotlin.types.typeUtil.nullability import org.jetbrains.kotlin.types.typeUtil.nullability
@@ -26,9 +27,14 @@ import org.jetbrains.kotlin.types.typeUtil.nullability
interface FindOperatorGenerator { interface FindOperatorGenerator {
val functionName: String val functionName: String
val hasFilter: Boolean
val presentation: String
get() = functionName + (if (hasFilter) "{}" else "()")
val shouldUseInputVariable: Boolean val shouldUseInputVariable: Boolean
fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression
val chainCallCount: Int val chainCallCount: Int
get() = 1 get() = 1
@@ -38,11 +44,15 @@ fun buildFindOperationGenerator(
valueIfFound: KtExpression, valueIfFound: KtExpression,
valueIfNotFound: KtExpression, valueIfNotFound: KtExpression,
inputVariable: KtCallableDeclaration, inputVariable: KtCallableDeclaration,
indexVariable: KtCallableDeclaration?,
filterTransformation: FilterTransformation?,
findFirst: Boolean findFirst: Boolean
): FindOperatorGenerator? { ): FindOperatorGenerator? {
assert(valueIfFound.isPhysical) assert(valueIfFound.isPhysical)
assert(valueIfNotFound.isPhysical) assert(valueIfNotFound.isPhysical)
val filterCondition = filterTransformation?.effectiveCondition()
fun generateChainedCall(stdlibFunName: String, chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { fun generateChainedCall(stdlibFunName: String, chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression {
return if (filter == null) { return if (filter == null) {
chainedCallGenerator.generate("$stdlibFunName()") chainedCallGenerator.generate("$stdlibFunName()")
@@ -53,97 +63,126 @@ fun buildFindOperationGenerator(
} }
} }
class SimpleGenerator(override val functionName: String, override val shouldUseInputVariable: Boolean) : FindOperatorGenerator { class SimpleGenerator(
override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { override val functionName: String,
return generateChainedCall(functionName, chainedCallGenerator, filter) 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? { //TODO: what if value when not found is not "-1"?
if (valueIfNotFound.isNullExpression()) return this if (valueIfFound.isVariableReference(indexVariable) && valueIfNotFound.text == "-1") {
return SimpleGenerator(if (findFirst) "indexOfFirst" else "indexOfLast", shouldUseInputVariable = false)
// 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)
}
} }
return null
} }
else {
val inputVariableCanHoldNull = (inputVariable.resolveToDescriptor() as VariableDescriptor).type.nullability() != TypeNullability.NOT_NULL
when { fun FindOperatorGenerator.useElvisOperatorIfNeeded(): FindOperatorGenerator? {
valueIfFound.isVariableReference(inputVariable) -> { if (valueIfNotFound.isNullExpression()) return this
val generator = SimpleGenerator(if (findFirst) "firstOrNull" else "lastOrNull", shouldUseInputVariable = true)
return generator.useElvisOperatorIfNeeded()
}
valueIfFound.isTrueConstant() && valueIfNotFound.isFalseConstant() -> return SimpleGenerator("any", shouldUseInputVariable = false) // we cannot use ?: if found value can be null
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 "<input variable>.<some call>" or "<input variable>?.<some call>"
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
if (inputVariableCanHoldNull) return null if (inputVariableCanHoldNull) return null
return object: FindOperatorGenerator { return object: FindOperatorGenerator by this {
override val functionName: String override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression {
get() = "firstOrNull" val generated = this@useElvisOperatorIfNeeded.generate(chainedCallGenerator)
return KtPsiFactory(generated).createExpressionByPattern("$0 ?: $1", generated, valueIfNotFound)
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)
} }
}.useElvisOperatorIfNeeded() }
} }
else -> { when {
return object: FindOperatorGenerator { valueIfFound.isVariableReference(inputVariable) -> {
override val functionName: String val generator = SimpleGenerator(if (findFirst) "firstOrNull" else "lastOrNull", shouldUseInputVariable = true)
get() = "any" return generator.useElvisOperatorIfNeeded()
}
override val shouldUseInputVariable: Boolean valueIfFound.isTrueConstant() && valueIfNotFound.isFalseConstant() -> return SimpleGenerator("any", shouldUseInputVariable = false)
get() = false
override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { valueIfFound.isFalseConstant() && valueIfNotFound.isTrueConstant() -> return SimpleGenerator("none", shouldUseInputVariable = false)
val chainedCall = generateChainedCall(functionName, chainedCallGenerator, filter)
return KtPsiFactory(chainedCall).createExpressionByPattern("if ($0) $1 else $2", chainedCall, valueIfFound, valueIfNotFound) inputVariable.hasUsages(valueIfFound) -> {
if (!findFirst) return null // too dangerous because of side effects
// specially handle the case when the result expression is "<input variable>.<some call>" or "<input variable>?.<some call>"
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)
}
} }
} }
} }
@@ -100,7 +100,11 @@ data class MatchingState(
val initializationStatementsToDelete: Collection<KtExpression> = emptyList() val initializationStatementsToDelete: Collection<KtExpression> = 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. * 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. * In this case they are obliged to deal with them and filter out invalid cases.
@@ -109,36 +113,28 @@ interface BaseMatcher {
get() = false get() = false
} }
/** sealed class TransformationMatch(val sequenceTransformations: List<SequenceTransformation>) {
* A matcher that can recognize one or more [SequenceTransformation]'s abstract val allTransformations: List<Transformation>
*/
interface SequenceTransformationMatcher : BaseMatcher {
fun match(state: MatchingState): SequenceTransformationMatch?
}
class SequenceTransformationMatch( /**
val transformations: List<SequenceTransformation>, * A partial match, includes [newState] for further matching
val newState: MatchingState */
) { class Sequence(transformations: List<SequenceTransformation>, val newState: MatchingState) : TransformationMatch(transformations) {
constructor(transformation: SequenceTransformation, newState: MatchingState) : this(listOf(transformation), newState) constructor(transformation: SequenceTransformation, newState: MatchingState) : this(listOf(transformation), newState)
}
/** override val allTransformations: List<Transformation>
* A matcher that can recognize a [ResultTransformation] (optionally prepended by some [SequenceTransformation]'s). get() = sequenceTransformations
* Should match the whole rest part of the loop. }
*/
interface ResultTransformationMatcher : BaseMatcher {
fun match(state: MatchingState): ResultTransformationMatch?
val indexVariableUsePossible: Boolean /**
} * A match of the whole rest part of the loop
*/
class Result(val resultTransformation: ResultTransformation, sequenceTransformations: List<SequenceTransformation>) : TransformationMatch(sequenceTransformations) {
constructor(resultTransformation: ResultTransformation, vararg sequenceTransformations: SequenceTransformation)
: this(resultTransformation, sequenceTransformations.asList())
class ResultTransformationMatch( override val allTransformations = sequenceTransformations + resultTransformation
val resultTransformation: ResultTransformation, }
val sequenceTransformations: List<SequenceTransformation>
) {
constructor(resultTransformation: ResultTransformation, vararg sequenceTransformations: SequenceTransformation)
: this(resultTransformation, sequenceTransformations.asList())
} }
/** /**
@@ -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.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.AddToCollectionTransformation 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.CountTransformation
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.FindAndAssignTransformation import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.FindTransformationMatcher
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.result.FindAndReturnTransformation
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FilterTransformation 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.FlatMapTransformation
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.IntroduceIndexMatcher import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.IntroduceIndexMatcher
@@ -42,24 +41,20 @@ import org.jetbrains.kotlin.utils.addToStdlib.check
import java.util.* import java.util.*
object MatcherRegistrar { object MatcherRegistrar {
val sequenceMatchers: Collection<SequenceTransformationMatcher> = listOf( val matchers: Collection<TransformationMatcher> = listOf(
FindTransformationMatcher,
AddToCollectionTransformation.Matcher,
CountTransformation.Matcher,
IntroduceIndexMatcher, IntroduceIndexMatcher,
FilterTransformation.Matcher, FilterTransformation.Matcher,
MapTransformation.Matcher, MapTransformation.Matcher,
FlatMapTransformation.Matcher FlatMapTransformation.Matcher
) )
val resultMatchers: Collection<ResultTransformationMatcher> = listOf(
FindAndReturnTransformation.Matcher,
FindAndAssignTransformation.Matcher,
AddToCollectionTransformation.Matcher,
CountTransformation.Matcher
)
} }
data class MatchResult( data class MatchResult(
val sequenceExpression: KtExpression, val sequenceExpression: KtExpression,
val transformationMatch: ResultTransformationMatch, val transformationMatch: TransformationMatch.Result,
val initializationStatementsToDelete: Collection<KtExpression> val initializationStatementsToDelete: Collection<KtExpression>
) )
@@ -91,52 +86,47 @@ fun match(loop: KtForExpression): MatchResult? {
val restContainsEmbeddedBreakOrContinue = loopContainsEmbeddedBreakOrContinue && state.statements.any { it.containsEmbeddedBreakOrContinue() } val restContainsEmbeddedBreakOrContinue = loopContainsEmbeddedBreakOrContinue && state.statements.any { it.containsEmbeddedBreakOrContinue() }
for (matcher in MatcherRegistrar.resultMatchers) { MatchersLoop@
if (state.indexVariable != null && !matcher.indexVariableUsePossible) continue for (matcher in MatcherRegistrar.matchers) {
if (restContainsEmbeddedBreakOrContinue && !matcher.embeddedBreakOrContinuePossible) continue if (state.indexVariable != null && !matcher.indexVariableAllowed) continue
val match = matcher.match(state) val match = matcher.match(state)
if (match != null) { if (match != null) {
if (!inputVariableUsed if (!inputVariableUsed && match.allTransformations.any { it.shouldUseInputVariable }) return null
&& (match.sequenceTransformations.any { it.shouldUseInputVariable }
|| match.resultTransformation.shouldUseInputVariable)) {
return null
}
sequenceTransformations.addAll(match.sequenceTransformations) 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) { when (match) {
val match = matcher.match(state) is TransformationMatch.Sequence -> {
if (match != null) { // check that old input variable is not needed anymore
if (!inputVariableUsed && match.transformations.any { it.shouldUseInputVariable }) { var newState = match.newState
return null 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 return callChain
} }
private fun mergeTransformations(match: ResultTransformationMatch): ResultTransformationMatch { private fun mergeTransformations(match: TransformationMatch.Result): TransformationMatch.Result {
val transformations = ArrayList<Transformation>().apply { addAll(match.sequenceTransformations); add(match.resultTransformation) } val transformations = ArrayList<Transformation>().apply { addAll(match.sequenceTransformations); add(match.resultTransformation) }
var anyChange: Boolean var anyChange: Boolean
@@ -302,6 +292,6 @@ private fun mergeTransformations(match: ResultTransformationMatch): ResultTransf
} while(anyChange) } while(anyChange)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return ResultTransformationMatch(transformations.last() as ResultTransformation, transformations.dropLast(1) as List<SequenceTransformation>) return TransformationMatch.Result(transformations.last() as ResultTransformation, transformations.dropLast(1) as List<SequenceTransformation>)
} }
@@ -84,11 +84,11 @@ class AddToCollectionTransformation(
* collection.add(...) * collection.add(...)
* } * }
*/ */
object Matcher : ResultTransformationMatcher { object Matcher : TransformationMatcher {
override val indexVariableUsePossible: Boolean override val indexVariableAllowed: Boolean
get() = true get() = true
override fun match(state: MatchingState): ResultTransformationMatch? { override fun match(state: MatchingState): TransformationMatch.Result? {
val statement = state.statements.singleOrNull() ?: return null val statement = state.statements.singleOrNull() ?: return null
//TODO: it can be implicit 'this' too //TODO: it can be implicit 'this' too
val qualifiedExpression = statement as? KtDotQualifiedExpression ?: return null val qualifiedExpression = statement as? KtDotQualifiedExpression ?: return null
@@ -108,11 +108,11 @@ class AddToCollectionTransformation(
} }
if (state.indexVariable == null && argumentValue.isVariableReference(state.inputVariable)) { if (state.indexVariable == null && argumentValue.isVariableReference(state.inputVariable)) {
return ResultTransformationMatch(AddToCollectionTransformation(state.outerLoop, targetCollection)) return TransformationMatch.Result(AddToCollectionTransformation(state.outerLoop, targetCollection))
} }
else { else {
//TODO: recognize "?: continue" in the argument //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)) state.outerLoop, state.inputVariable, state.indexVariable, targetCollection, argumentValue, mapNotNull = false))
} }
} }
@@ -121,7 +121,7 @@ class AddToCollectionTransformation(
state: MatchingState, state: MatchingState,
targetCollection: KtExpression, targetCollection: KtExpression,
addOperationArgument: KtExpression addOperationArgument: KtExpression
): ResultTransformationMatch? { ): TransformationMatch.Result? {
val collectionInitialization = targetCollection.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null val collectionInitialization = targetCollection.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null
val collectionKind = collectionInitialization.initializer.isSimpleCollectionInstantiation() ?: return null val collectionKind = collectionInitialization.initializer.isSimpleCollectionInstantiation() ?: return null
val argumentIsInputVariable = addOperationArgument.isVariableReference(state.inputVariable) val argumentIsInputVariable = addOperationArgument.isVariableReference(state.inputVariable)
@@ -136,13 +136,13 @@ class AddToCollectionTransformation(
val mapTransformation = MapTransformation(state.outerLoop, state.inputVariable, null, addOperationArgument, mapNotNull = false) val mapTransformation = MapTransformation(state.outerLoop, state.inputVariable, null, addOperationArgument, mapNotNull = false)
AssignSequenceTransformationResultTransformation(mapTransformation, collectionInitialization) AssignSequenceTransformationResultTransformation(mapTransformation, collectionInitialization)
} }
return ResultTransformationMatch(transformation) return TransformationMatch.Result(transformation)
} }
canChangeInitializerType(collectionInitialization, KotlinBuiltIns.FQ_NAMES.mutableList, state.outerLoop) -> { canChangeInitializerType(collectionInitialization, KotlinBuiltIns.FQ_NAMES.mutableList, state.outerLoop) -> {
if (argumentIsInputVariable) { if (argumentIsInputVariable) {
val transformation = AssignToMutableListTransformation(state.outerLoop, collectionInitialization) val transformation = AssignToMutableListTransformation(state.outerLoop, collectionInitialization)
return ResultTransformationMatch(transformation) return TransformationMatch.Result(transformation)
} }
} }
} }
@@ -162,11 +162,11 @@ class AddToCollectionTransformation(
} }
if (argumentIsInputVariable) { if (argumentIsInputVariable) {
return ResultTransformationMatch(assignToSetTransformation) return TransformationMatch.Result(assignToSetTransformation)
} }
else { else {
val mapTransformation = MapTransformation(state.outerLoop, state.inputVariable, null, addOperationArgument, mapNotNull = false) val mapTransformation = MapTransformation(state.outerLoop, state.inputVariable, null, addOperationArgument, mapNotNull = false)
return ResultTransformationMatch(assignToSetTransformation, mapTransformation) return TransformationMatch.Result(assignToSetTransformation, mapTransformation)
} }
} }
} }
@@ -68,11 +68,11 @@ class CountTransformation(
* variable++ (or ++variable) * variable++ (or ++variable)
* } * }
*/ */
object Matcher : ResultTransformationMatcher { object Matcher : TransformationMatcher {
override val indexVariableUsePossible: Boolean override val indexVariableAllowed: Boolean
get() = false get() = false
override fun match(state: MatchingState): ResultTransformationMatch? { override fun match(state: MatchingState): TransformationMatch.Result? {
val operand = state.statements.singleOrNull()?.isPlusPlusOf() ?: return null val operand = state.statements.singleOrNull()?.isPlusPlusOf() ?: return null
val initialization = operand.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null val initialization = operand.detectInitializationBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = true) ?: return null
@@ -82,7 +82,7 @@ class CountTransformation(
if (!KotlinBuiltIns.isInt(variableType)) return null if (!KotlinBuiltIns.isInt(variableType)) return null
val transformation = CountTransformation(state.outerLoop, state.inputVariable, initialization, null) val transformation = CountTransformation(state.outerLoop, state.inputVariable, initialization, null)
return ResultTransformationMatch(transformation) return TransformationMatch.Result(transformation)
} }
} }
} }
@@ -16,30 +16,21 @@
package org.jetbrains.kotlin.idea.intentions.loopToCallChain.result package org.jetbrains.kotlin.idea.intentions.loopToCallChain.result
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.* import org.jetbrains.kotlin.idea.intentions.loopToCallChain.AssignToVariableResultTransformation
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FilterTransformation import org.jetbrains.kotlin.idea.intentions.loopToCallChain.ChainedCallGenerator
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.idea.intentions.loopToCallChain.FindOperatorGenerator
import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.idea.intentions.loopToCallChain.VariableInitialization
import org.jetbrains.kotlin.psi.KtBreakExpression
import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtForExpression import org.jetbrains.kotlin.psi.KtForExpression
class FindAndAssignTransformation( class FindAndAssignTransformation(
loop: KtForExpression, loop: KtForExpression,
private val generator: FindOperatorGenerator, private val generator: FindOperatorGenerator,
initialization: VariableInitialization, initialization: VariableInitialization
private val filter: KtExpression? = null
) : AssignToVariableResultTransformation(loop, initialization) { ) : 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 override val presentation: String
get() = generator.functionName + (if (filter != null) "{}" else "()") get() = generator.presentation
override val chainCallCount: Int override val chainCallCount: Int
get() = generator.chainCallCount get() = generator.chainCallCount
@@ -48,57 +39,6 @@ class FindAndAssignTransformation(
get() = generator.shouldUseInputVariable get() = generator.shouldUseInputVariable
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
return generator.generate(chainedCallGenerator, filter) return generator.generate(chainedCallGenerator)
}
/**
* 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)
}
} }
} }
@@ -17,7 +17,6 @@
package org.jetbrains.kotlin.idea.intentions.loopToCallChain.result package org.jetbrains.kotlin.idea.intentions.loopToCallChain.result
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.* 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.KtExpression
import org.jetbrains.kotlin.psi.KtForExpression import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtReturnExpression import org.jetbrains.kotlin.psi.KtReturnExpression
@@ -26,21 +25,13 @@ import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
class FindAndReturnTransformation( class FindAndReturnTransformation(
override val loop: KtForExpression, override val loop: KtForExpression,
private val generator: FindOperatorGenerator, private val generator: FindOperatorGenerator,
private val endReturn: KtReturnExpression, private val endReturn: KtReturnExpression
private val filter: KtExpression? = null
) : ResultTransformation { ) : 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 commentSavingRange = PsiChildRange(loop.unwrapIfLabeled(), endReturn)
override val presentation: String override val presentation: String
get() = generator.functionName + (if (filter != null) "{}" else "()") get() = generator.presentation
override val chainCallCount: Int override val chainCallCount: Int
get() = generator.chainCallCount get() = generator.chainCallCount
@@ -49,7 +40,7 @@ class FindAndReturnTransformation(
get() = generator.shouldUseInputVariable get() = generator.shouldUseInputVariable
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression { override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
return generator.generate(chainedCallGenerator, filter) return generator.generate(chainedCallGenerator)
} }
override fun convertLoop(resultCallChain: KtExpression, commentSavingRangeHolder: CommentSavingRangeHolder): KtExpression { override fun convertLoop(resultCallChain: KtExpression, commentSavingRangeHolder: CommentSavingRangeHolder): KtExpression {
@@ -57,31 +48,4 @@ class FindAndReturnTransformation(
loop.deleteWithLabels() loop.deleteWithLabels()
return endReturn 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)
}
}
} }
@@ -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)
}
}
@@ -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.branchedTransformations.isNullExpression
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.* 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.idea.intentions.negate
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
@@ -73,9 +74,14 @@ class FilterTransformation(
* if (<condition>) break * if (<condition>) break
* ... * ...
* } * }
*
* plus optionally consequent find operation (assignment or return)
*/ */
object Matcher : SequenceTransformationMatcher { object Matcher : TransformationMatcher {
override fun match(state: MatchingState): SequenceTransformationMatch? { 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 // we merge filter transformations here instead of FilterTransformation.mergeWithPrevious() because of filterIndexed that won't merge otherwise
var (transformation, currentState) = matchOneTransformation(state) ?: return null 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? transformation = FilterTransformation(state.outerLoop, transformation.inputVariable, indexVariable, mergedCondition, isInverse = false) //TODO: build filterNot in some cases?
currentState = nextState currentState = nextState
} }
FindTransformationMatcher.matchWithFilterBefore(currentState, transformation)
?.let { return it }
} }
return SequenceTransformationMatch(transformation, currentState) return TransformationMatch.Sequence(transformation, currentState)
} }
private fun matchOneTransformation(state: MatchingState): Pair<SequenceTransformation, MatchingState>? { private fun matchOneTransformation(state: MatchingState): Pair<SequenceTransformation, MatchingState>? {
@@ -49,8 +49,11 @@ class FlatMapTransformation(
* } * }
* } * }
*/ */
object Matcher : SequenceTransformationMatcher { object Matcher : TransformationMatcher {
override fun match(state: MatchingState): SequenceTransformationMatch? { override val indexVariableAllowed: Boolean
get() = true
override fun match(state: MatchingState): TransformationMatch.Sequence? {
val nestedLoop = state.statements.singleOrNull() as? KtForExpression ?: return null val nestedLoop = state.statements.singleOrNull() as? KtForExpression ?: return null
val transform = nestedLoop.loopRange ?: return null val transform = nestedLoop.loopRange ?: return null
@@ -73,7 +76,7 @@ class FlatMapTransformation(
statements = listOf(nestedLoopBody), statements = listOf(nestedLoopBody),
inputVariable = newInputVariable inputVariable = newInputVariable
) )
return SequenceTransformationMatch(listOf(mapIndexedTransformation, flatMapTransformation), newState) return TransformationMatch.Sequence(listOf(mapIndexedTransformation, flatMapTransformation), newState)
} }
val transformation = FlatMapTransformation(state.outerLoop, state.inputVariable, transform) val transformation = FlatMapTransformation(state.outerLoop, state.inputVariable, transform)
@@ -82,7 +85,7 @@ class FlatMapTransformation(
statements = listOf(nestedLoopBody), statements = listOf(nestedLoopBody),
inputVariable = newInputVariable inputVariable = newInputVariable
) )
return SequenceTransformationMatch(transformation, newState) return TransformationMatch.Sequence(transformation, newState)
} }
} }
} }
@@ -22,9 +22,11 @@ import org.jetbrains.kotlin.psi.KtContinueExpression
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.isAncestor import org.jetbrains.kotlin.psi.psiUtil.isAncestor
object IntroduceIndexMatcher : SequenceTransformationMatcher { object IntroduceIndexMatcher : TransformationMatcher {
override fun match(state: MatchingState): SequenceTransformationMatch? { override val indexVariableAllowed: Boolean
if (state.indexVariable != null) return null // old index variable is still needed - cannot introduce another one 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 if (state.statements.size < 2) return null
val operand = state.statements.last().isPlusPlusOf() ?: return null val operand = state.statements.last().isPlusPlusOf() ?: return null
val restStatements = state.statements.dropLast(1) val restStatements = state.statements.dropLast(1)
@@ -52,6 +54,6 @@ object IntroduceIndexMatcher : SequenceTransformationMatcher {
val newState = state.copy(statements = restStatements, val newState = state.copy(statements = restStatements,
indexVariable = variable, indexVariable = variable,
initializationStatementsToDelete = state.initializationStatementsToDelete + variableInitialization.initializationStatement) initializationStatementsToDelete = state.initializationStatementsToDelete + variableInitialization.initializationStatement)
return SequenceTransformationMatch(emptyList(), newState) return TransformationMatch.Sequence(emptyList(), newState)
} }
} }
@@ -54,11 +54,14 @@ class MapTransformation(
* ... * ...
* } * }
*/ */
object Matcher : SequenceTransformationMatcher { object Matcher : TransformationMatcher {
override val indexVariableAllowed: Boolean
get() = true
override val embeddedBreakOrContinuePossible: Boolean override val embeddedBreakOrContinuePossible: Boolean
get() = true 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 declaration = state.statements.firstOrNull() as? KtProperty ?: return null //TODO: support multi-variables
val initializer = declaration.initializer ?: return null val initializer = declaration.initializer ?: return null
if (declaration.hasWriteUsages()) return null if (declaration.hasWriteUsages()) return null
@@ -76,7 +79,7 @@ class MapTransformation(
else else
MapTransformation(state.outerLoop, state.inputVariable, null, mapping, mapNotNull = true) MapTransformation(state.outerLoop, state.inputVariable, null, mapping, mapNotNull = true)
val newState = state.copy(statements = restStatements, inputVariable = declaration) val newState = state.copy(statements = restStatements, inputVariable = declaration)
return SequenceTransformationMatch(transformation, newState) return TransformationMatch.Sequence(transformation, newState)
} }
if (initializer.containsEmbeddedBreakOrContinue()) return null if (initializer.containsEmbeddedBreakOrContinue()) return null
@@ -86,7 +89,7 @@ class MapTransformation(
else else
MapTransformation(state.outerLoop, state.inputVariable, null, initializer, mapNotNull = false) MapTransformation(state.outerLoop, state.inputVariable, null, initializer, mapNotNull = false)
val newState = state.copy(statements = restStatements, inputVariable = declaration) val newState = state.copy(statements = restStatements, inputVariable = declaration)
return SequenceTransformationMatch(transformation, newState) return TransformationMatch.Sequence(transformation, newState)
} }
} }
} }
@@ -0,0 +1,11 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'indexOfFirst{}'"
fun foo(list: List<String>) {
var result = -1
<caret>for ((index, s) in list.withIndex()) {
if (s.length > 0) {
result = index
break
}
}
}
@@ -0,0 +1,5 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'indexOfFirst{}'"
fun foo(list: List<String>) {
val <caret>result = list.indexOfFirst { it.length > 0 }
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'indexOfFirst{}'"
fun foo(list: List<String>): Int {
<caret>for ((index, s) in list.withIndex()) {
if (s.length > 0) {
return index
}
}
return -1
}
@@ -0,0 +1,5 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'indexOfFirst{}'"
fun foo(list: List<String>): Int {
<caret>return list.indexOfFirst { it.length > 0 }
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>) {
var result = -1
<caret>for ((index, s) in list.withIndex()) {
if (s.length > index) {
result = index
break
}
}
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'indexOfLast{}'"
fun foo(list: List<String>) {
var result = -1
<caret>for ((index, s) in list.withIndex()) {
if (s.length > 0) {
result = index
}
}
}
@@ -0,0 +1,5 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'indexOfLast{}'"
fun foo(list: List<String>) {
val <caret>result = list.indexOfLast { it.length > 0 }
}