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