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,12 +63,31 @@ 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)
} }
} }
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
//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 val inputVariableCanHoldNull = (inputVariable.resolveToDescriptor() as VariableDescriptor).type.nullability() != TypeNullability.NOT_NULL
fun FindOperatorGenerator.useElvisOperatorIfNeeded(): FindOperatorGenerator? { fun FindOperatorGenerator.useElvisOperatorIfNeeded(): FindOperatorGenerator? {
@@ -68,8 +97,8 @@ fun buildFindOperationGenerator(
if (inputVariableCanHoldNull) return null if (inputVariableCanHoldNull) return null
return object: FindOperatorGenerator by this { return object: FindOperatorGenerator by this {
override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression {
val generated = this@useElvisOperatorIfNeeded.generate(chainedCallGenerator, filter) val generated = this@useElvisOperatorIfNeeded.generate(chainedCallGenerator)
return KtPsiFactory(generated).createExpressionByPattern("$0 ?: $1", generated, valueIfNotFound) return KtPsiFactory(generated).createExpressionByPattern("$0 ?: $1", generated, valueIfNotFound)
} }
} }
@@ -98,14 +127,17 @@ fun buildFindOperationGenerator(
override val functionName: String override val functionName: String
get() = "firstOrNull" get() = "firstOrNull"
override val hasFilter: Boolean
get() = filterCondition != null
override val shouldUseInputVariable: Boolean override val shouldUseInputVariable: Boolean
get() = true get() = true
override val chainCallCount: Int override val chainCallCount: Int
get() = 2 get() = 2
override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression {
val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filter) val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filterCondition)
return chainedCallGenerator.generate("$0", selector, receiver = findFirstCall, safeCall = true) return chainedCallGenerator.generate("$0", selector, receiver = findFirstCall, safeCall = true)
} }
}.useElvisOperatorIfNeeded() }.useElvisOperatorIfNeeded()
@@ -119,14 +151,17 @@ fun buildFindOperationGenerator(
override val functionName: String override val functionName: String
get() = "firstOrNull" get() = "firstOrNull"
override val hasFilter: Boolean
get() = filterCondition != null
override val shouldUseInputVariable: Boolean override val shouldUseInputVariable: Boolean
get() = true get() = true
override val chainCallCount: Int override val chainCallCount: Int
get() = 2 // also includes "let" get() = 2 // also includes "let"
override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression {
val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filter) val findFirstCall = generateChainedCall(functionName, chainedCallGenerator, filterCondition)
val letBody = generateLambda(inputVariable, valueIfFound) val letBody = generateLambda(inputVariable, valueIfFound)
return chainedCallGenerator.generate("let $0:'{}'", letBody, receiver = findFirstCall, safeCall = true) return chainedCallGenerator.generate("let $0:'{}'", letBody, receiver = findFirstCall, safeCall = true)
} }
@@ -138,14 +173,18 @@ fun buildFindOperationGenerator(
override val functionName: String override val functionName: String
get() = "any" get() = "any"
override val hasFilter: Boolean
get() = filterCondition != null
override val shouldUseInputVariable: Boolean override val shouldUseInputVariable: Boolean
get() = false get() = false
override fun generate(chainedCallGenerator: ChainedCallGenerator, filter: KtExpression?): KtExpression { override fun generate(chainedCallGenerator: ChainedCallGenerator): KtExpression {
val chainedCall = generateChainedCall(functionName, chainedCallGenerator, filter) val chainedCall = generateChainedCall(functionName, chainedCallGenerator, filterCondition)
return KtPsiFactory(chainedCall).createExpressionByPattern("if ($0) $1 else $2", chainedCall, valueIfFound, valueIfNotFound) 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. }
/**
* A match of the whole rest part of the loop
*/ */
interface ResultTransformationMatcher : BaseMatcher { class Result(val resultTransformation: ResultTransformation, sequenceTransformations: List<SequenceTransformation>) : TransformationMatch(sequenceTransformations) {
fun match(state: MatchingState): ResultTransformationMatch?
val indexVariableUsePossible: Boolean
}
class ResultTransformationMatch(
val resultTransformation: ResultTransformation,
val sequenceTransformations: List<SequenceTransformation>
) {
constructor(resultTransformation: ResultTransformation, vararg sequenceTransformations: SequenceTransformation) constructor(resultTransformation: ResultTransformation, vararg sequenceTransformations: SequenceTransformation)
: this(resultTransformation, sequenceTransformations.asList()) : this(resultTransformation, sequenceTransformations.asList())
override val allTransformations = sequenceTransformations + resultTransformation
}
} }
/** /**
@@ -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,38 +86,23 @@ 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) {
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 // check that old input variable is not needed anymore
var newState = match.newState var newState = match.newState
if (state.inputVariable != newState.inputVariable && state.inputVariable.hasUsages(newState.statements)) return null if (state.inputVariable != newState.inputVariable && state.inputVariable.hasUsages(newState.statements)) return null
if (state.indexVariable != null && match.transformations.any { it.affectsIndex }) { if (state.indexVariable != null && match.sequenceTransformations.any { it.affectsIndex }) {
// index variable is still needed but index in the new sequence is different // index variable is still needed but index in the new sequence is different
if (state.indexVariable!!.hasUsages(newState.statements)) return null if (state.indexVariable!!.hasUsages(newState.statements)) return null
newState = newState.copy(indexVariable = null) newState = newState.copy(indexVariable = null)
@@ -131,13 +111,23 @@ fun match(loop: KtForExpression): MatchResult? {
if (restContainsEmbeddedBreakOrContinue && !matcher.embeddedBreakOrContinuePossible) { if (restContainsEmbeddedBreakOrContinue && !matcher.embeddedBreakOrContinuePossible) {
val countBefore = state.statements.sumBy { it.countEmbeddedBreaksAndContinues() } val countBefore = state.statements.sumBy { it.countEmbeddedBreaksAndContinues() }
val countAfter = newState.statements.sumBy { it.countEmbeddedBreaksAndContinues() } val countAfter = newState.statements.sumBy { it.countEmbeddedBreaksAndContinues() }
if (countAfter != countBefore) continue // some embedded break or continue in the matched part if (countAfter != countBefore) continue@MatchersLoop // some embedded break or continue in the matched part
} }
sequenceTransformations.addAll(match.transformations)
state = newState state = newState
continue@MatchLoop 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) }
}
}
}
} }
return null return null
@@ -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 }
}