Initial support for indexed transformations
This commit is contained in:
+1
-1
@@ -35,7 +35,7 @@ class LoopToCallChainIntention : SelfTargetingRangeIntention<KtForExpression>(
|
|||||||
) {
|
) {
|
||||||
override fun applicabilityRange(element: KtForExpression): TextRange? {
|
override fun applicabilityRange(element: KtForExpression): TextRange? {
|
||||||
val match = match(element)
|
val match = match(element)
|
||||||
text = if (match != null) "Replace with '${match.buildPresentation()}'" else defaultText
|
text = if (match != null) "Replace with '${match.transformationMatch.buildPresentation()}'" else defaultText
|
||||||
return if (match != null) element.forKeyword.textRange else null
|
return if (match != null) element.forKeyword.textRange else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ data class MatchingState(
|
|||||||
val innerLoop: KtForExpression,
|
val innerLoop: KtForExpression,
|
||||||
val statements: Collection<KtExpression>,
|
val statements: Collection<KtExpression>,
|
||||||
val inputVariable: KtCallableDeclaration,
|
val inputVariable: KtCallableDeclaration,
|
||||||
|
/**
|
||||||
|
* Matchers can assume that indexVariable is null if it's not used in the rest of the loop
|
||||||
|
*/
|
||||||
val indexVariable: KtCallableDeclaration?
|
val indexVariable: KtCallableDeclaration?
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,6 +116,8 @@ class SequenceTransformationMatch(
|
|||||||
*/
|
*/
|
||||||
interface ResultTransformationMatcher {
|
interface ResultTransformationMatcher {
|
||||||
fun match(state: MatchingState): ResultTransformationMatch?
|
fun match(state: MatchingState): ResultTransformationMatch?
|
||||||
|
|
||||||
|
val indexVariableUsePossible: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResultTransformationMatch(
|
class ResultTransformationMatch(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FlatMapTran
|
|||||||
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.MapTransformation
|
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.MapTransformation
|
||||||
import org.jetbrains.kotlin.idea.util.CommentSaver
|
import org.jetbrains.kotlin.idea.util.CommentSaver
|
||||||
import org.jetbrains.kotlin.idea.util.getResolutionScope
|
import org.jetbrains.kotlin.idea.util.getResolutionScope
|
||||||
|
import org.jetbrains.kotlin.name.Name
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.*
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
|
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
|
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
|
||||||
@@ -54,14 +55,21 @@ object MatcherRegistrar {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun match(loop: KtForExpression): ResultTransformationMatch? {
|
data class MatchResult(
|
||||||
|
val sequenceExpression: KtExpression,
|
||||||
|
val transformationMatch: ResultTransformationMatch
|
||||||
|
)
|
||||||
|
|
||||||
|
fun match(loop: KtForExpression): MatchResult? {
|
||||||
|
val (inputVariable, indexVariable, sequenceExpression) = extractLoopData(loop) ?: return null
|
||||||
|
|
||||||
val sequenceTransformations = ArrayList<SequenceTransformation>()
|
val sequenceTransformations = ArrayList<SequenceTransformation>()
|
||||||
var state = MatchingState(
|
var state = MatchingState(
|
||||||
outerLoop = loop,
|
outerLoop = loop,
|
||||||
innerLoop = loop,
|
innerLoop = loop,
|
||||||
statements = listOf(loop.body ?: return null),
|
statements = listOf(loop.body ?: return null),
|
||||||
inputVariable = loop.loopParameter ?: return null,
|
inputVariable = inputVariable,
|
||||||
indexVariable = null
|
indexVariable = indexVariable
|
||||||
)
|
)
|
||||||
|
|
||||||
MatchLoop@
|
MatchLoop@
|
||||||
@@ -73,7 +81,14 @@ fun match(loop: KtForExpression): ResultTransformationMatch? {
|
|||||||
|
|
||||||
val inputVariableUsed = state.inputVariable.hasUsages(state.statements)
|
val inputVariableUsed = state.inputVariable.hasUsages(state.statements)
|
||||||
|
|
||||||
|
// drop index variable if it's not used anymore
|
||||||
|
if (state.indexVariable != null && !state.indexVariable!!.hasUsages(state.statements)) {
|
||||||
|
state = state.copy(indexVariable = null)
|
||||||
|
}
|
||||||
|
|
||||||
for (matcher in MatcherRegistrar.resultMatchers) {
|
for (matcher in MatcherRegistrar.resultMatchers) {
|
||||||
|
if (state.indexVariable != null && !matcher.indexVariableUsePossible) continue
|
||||||
|
|
||||||
val match = matcher.match(state)
|
val match = matcher.match(state)
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
if (!inputVariableUsed
|
if (!inputVariableUsed
|
||||||
@@ -85,6 +100,7 @@ fun match(loop: KtForExpression): ResultTransformationMatch? {
|
|||||||
sequenceTransformations.addAll(match.sequenceTransformations)
|
sequenceTransformations.addAll(match.sequenceTransformations)
|
||||||
return ResultTransformationMatch(match.resultTransformation, sequenceTransformations)
|
return ResultTransformationMatch(match.resultTransformation, sequenceTransformations)
|
||||||
.let { mergeTransformations(it) }
|
.let { mergeTransformations(it) }
|
||||||
|
.let { MatchResult(sequenceExpression, it) }
|
||||||
.check { checkSmartCastsPreserved(loop, it) }
|
.check { checkSmartCastsPreserved(loop, it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,10 +112,16 @@ fun match(loop: KtForExpression): ResultTransformationMatch? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val newState = match.newState
|
var newState = match.newState
|
||||||
// check that old input variable is not needed anymore
|
// check that old input variable is not needed anymore
|
||||||
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 }) {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
sequenceTransformations.addAll(match.transformations)
|
sequenceTransformations.addAll(match.transformations)
|
||||||
state = newState
|
state = newState
|
||||||
continue@MatchLoop
|
continue@MatchLoop
|
||||||
@@ -111,19 +133,44 @@ fun match(loop: KtForExpression): ResultTransformationMatch? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO: offer to use of .asSequence() as an option
|
//TODO: offer to use of .asSequence() as an option
|
||||||
fun convertLoop(loop: KtForExpression, matchResult: ResultTransformationMatch): KtExpression {
|
fun convertLoop(loop: KtForExpression, matchResult: MatchResult): KtExpression {
|
||||||
val commentSaver = CommentSaver(matchResult.resultTransformation.commentSavingRange)
|
val resultTransformation = matchResult.transformationMatch.resultTransformation
|
||||||
|
val commentSaver = CommentSaver(resultTransformation.commentSavingRange)
|
||||||
|
|
||||||
val callChain = matchResult.generateCallChain(loop)
|
val callChain = matchResult.generateCallChain(loop)
|
||||||
|
|
||||||
val result = matchResult.resultTransformation.convertLoop(callChain)
|
val result = resultTransformation.convertLoop(callChain)
|
||||||
|
|
||||||
commentSaver.restore(matchResult.resultTransformation.commentRestoringRange(result))
|
commentSaver.restore(resultTransformation.commentRestoringRange(result))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkSmartCastsPreserved(loop: KtForExpression, matchResult: ResultTransformationMatch): Boolean {
|
data class LoopData(
|
||||||
|
val inputVariable: KtCallableDeclaration,
|
||||||
|
val indexVariable: KtCallableDeclaration?,
|
||||||
|
val sequenceExpression: KtExpression)
|
||||||
|
|
||||||
|
private fun extractLoopData(loop: KtForExpression): LoopData? {
|
||||||
|
val loopRange = loop.loopRange ?: return null
|
||||||
|
|
||||||
|
//TODO: recognize other patterns for loop with index
|
||||||
|
val destructuringParameter = loop.destructuringParameter
|
||||||
|
if (destructuringParameter != null && destructuringParameter.entries.size == 2) {
|
||||||
|
val qualifiedExpression = loopRange as? KtDotQualifiedExpression
|
||||||
|
if (qualifiedExpression != null) {
|
||||||
|
val call = qualifiedExpression.selectorExpression as? KtCallExpression
|
||||||
|
//TODO: check that it's the correct "withIndex"
|
||||||
|
if (call != null && call.calleeExpression.isSimpleName(Name.identifier("withIndex")) && call.valueArguments.isEmpty()) {
|
||||||
|
return LoopData(destructuringParameter.entries[1], destructuringParameter.entries[0], qualifiedExpression.receiverExpression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoopData(loop.loopParameter ?: return null, null, loopRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkSmartCastsPreserved(loop: KtForExpression, matchResult: MatchResult): Boolean {
|
||||||
val bindingContext = loop.analyze(BodyResolveMode.FULL)
|
val bindingContext = loop.analyze(BodyResolveMode.FULL)
|
||||||
|
|
||||||
val SMARTCAST_KEY = Key<KotlinType>("SMARTCAST_KEY")
|
val SMARTCAST_KEY = Key<KotlinType>("SMARTCAST_KEY")
|
||||||
@@ -174,9 +221,9 @@ private fun checkSmartCastsPreserved(loop: KtForExpression, matchResult: ResultT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ResultTransformationMatch.generateCallChain(loop: KtForExpression): KtExpression {
|
private fun MatchResult.generateCallChain(loop: KtForExpression): KtExpression {
|
||||||
var sequenceTransformations = sequenceTransformations
|
var sequenceTransformations = transformationMatch.sequenceTransformations
|
||||||
var resultTransformation = resultTransformation
|
var resultTransformation = transformationMatch.resultTransformation
|
||||||
while(true) {
|
while(true) {
|
||||||
val last = sequenceTransformations.lastOrNull() ?: break
|
val last = sequenceTransformations.lastOrNull() ?: break
|
||||||
resultTransformation = resultTransformation.mergeWithPrevious(last) ?: break
|
resultTransformation = resultTransformation.mergeWithPrevious(last) ?: break
|
||||||
@@ -186,7 +233,7 @@ private fun ResultTransformationMatch.generateCallChain(loop: KtForExpression):
|
|||||||
val chainCallCount = sequenceTransformations.sumBy { it.chainCallCount } + resultTransformation.chainCallCount
|
val chainCallCount = sequenceTransformations.sumBy { it.chainCallCount } + resultTransformation.chainCallCount
|
||||||
val lineBreak = if (chainCallCount > 1) "\n" else ""
|
val lineBreak = if (chainCallCount > 1) "\n" else ""
|
||||||
|
|
||||||
var callChain = loop.loopRange!!
|
var callChain = sequenceExpression
|
||||||
|
|
||||||
val psiFactory = KtPsiFactory(loop)
|
val psiFactory = KtPsiFactory(loop)
|
||||||
val chainedCallGenerator = object : ChainedCallGenerator {
|
val chainedCallGenerator = object : ChainedCallGenerator {
|
||||||
|
|||||||
+17
-11
@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.idea.imports.importableFqName
|
|||||||
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.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.MapIndexedTransformation
|
||||||
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.MapTransformation
|
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.MapTransformation
|
||||||
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
||||||
import org.jetbrains.kotlin.name.FqName
|
import org.jetbrains.kotlin.name.FqName
|
||||||
@@ -33,14 +34,13 @@ import org.jetbrains.kotlin.renderer.render
|
|||||||
|
|
||||||
class AddToCollectionTransformation(
|
class AddToCollectionTransformation(
|
||||||
loop: KtForExpression,
|
loop: KtForExpression,
|
||||||
private val inputVariable: KtCallableDeclaration,
|
|
||||||
private val targetCollection: KtExpression
|
private val targetCollection: KtExpression
|
||||||
) : ReplaceLoopResultTransformation(loop) {
|
) : ReplaceLoopResultTransformation(loop) {
|
||||||
|
|
||||||
override fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? {
|
override fun mergeWithPrevious(previousTransformation: SequenceTransformation): ResultTransformation? {
|
||||||
return when (previousTransformation) {
|
return when (previousTransformation) {
|
||||||
is FilterTransformation -> {
|
is FilterTransformation -> {
|
||||||
FilterToTransformation.create(loop, inputVariable, targetCollection, previousTransformation.effectiveCondition()) //TODO: use filterNotTo?
|
FilterToTransformation.create(loop, previousTransformation.inputVariable, targetCollection, previousTransformation.effectiveCondition()) //TODO: use filterNotTo?
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapTransformation -> {
|
is MapTransformation -> {
|
||||||
@@ -80,10 +80,10 @@ class AddToCollectionTransformation(
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
object Matcher : ResultTransformationMatcher {
|
object Matcher : ResultTransformationMatcher {
|
||||||
override fun match(state: MatchingState): ResultTransformationMatch? {
|
override val indexVariableUsePossible: Boolean
|
||||||
//TODO: pass indexVariable as null if not used
|
get() = true
|
||||||
if (state.indexVariable != null) return null
|
|
||||||
|
|
||||||
|
override fun match(state: MatchingState): ResultTransformationMatch? {
|
||||||
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
|
||||||
@@ -95,16 +95,22 @@ class AddToCollectionTransformation(
|
|||||||
val argument = callExpression.valueArguments.singleOrNull() ?: return null
|
val argument = callExpression.valueArguments.singleOrNull() ?: return null
|
||||||
val argumentValue = argument.getArgumentExpression() ?: return null
|
val argumentValue = argument.getArgumentExpression() ?: return null
|
||||||
|
|
||||||
matchWithCollectionInitializationReplacement(state, targetCollection, argumentValue)
|
if (state.indexVariable == null) {
|
||||||
?.let { return it }
|
matchWithCollectionInitializationReplacement(state, targetCollection, argumentValue)
|
||||||
|
?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
val transformation = if (argumentValue.isVariableReference(state.inputVariable)) {
|
if (state.indexVariable == null && argumentValue.isVariableReference(state.inputVariable)) {
|
||||||
AddToCollectionTransformation(state.outerLoop, state.inputVariable, targetCollection)
|
return ResultTransformationMatch(AddToCollectionTransformation(state.outerLoop, targetCollection))
|
||||||
|
}
|
||||||
|
else if (state.indexVariable != null) {
|
||||||
|
val mapIndexedTransformation = MapIndexedTransformation(state.outerLoop, state.inputVariable, state.indexVariable, argumentValue)
|
||||||
|
val addToCollectionTransformation = AddToCollectionTransformation(state.outerLoop, targetCollection)
|
||||||
|
return ResultTransformationMatch(addToCollectionTransformation, mapIndexedTransformation)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
MapToTransformation.create(state.outerLoop, state.inputVariable, targetCollection, argumentValue)
|
return ResultTransformationMatch(MapToTransformation.create(state.outerLoop, state.inputVariable, targetCollection, argumentValue))
|
||||||
}
|
}
|
||||||
return ResultTransformationMatch(transformation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun matchWithCollectionInitializationReplacement(
|
private fun matchWithCollectionInitializationReplacement(
|
||||||
|
|||||||
+3
@@ -71,6 +71,9 @@ class CountTransformation(
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
object Matcher : ResultTransformationMatcher {
|
object Matcher : ResultTransformationMatcher {
|
||||||
|
override val indexVariableUsePossible: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
override fun match(state: MatchingState): ResultTransformationMatch? {
|
override fun match(state: MatchingState): ResultTransformationMatch? {
|
||||||
val statement = state.statements.singleOrNull() as? KtUnaryExpression ?: return null
|
val statement = state.statements.singleOrNull() as? KtUnaryExpression ?: return null
|
||||||
if (statement.operationToken != KtTokens.PLUSPLUS) return null
|
if (statement.operationToken != KtTokens.PLUSPLUS) return null
|
||||||
|
|||||||
+3
-3
@@ -71,10 +71,10 @@ class FindAndAssignTransformation(
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
object Matcher : ResultTransformationMatcher {
|
object Matcher : ResultTransformationMatcher {
|
||||||
override fun match(state: MatchingState): ResultTransformationMatch? {
|
override val indexVariableUsePossible: Boolean
|
||||||
//TODO: pass indexVariable as null if not used
|
get() = false
|
||||||
if (state.indexVariable != null) return null
|
|
||||||
|
|
||||||
|
override fun match(state: MatchingState): ResultTransformationMatch? {
|
||||||
when (state.statements.size) {
|
when (state.statements.size) {
|
||||||
1 -> {}
|
1 -> {}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -70,10 +70,10 @@ class FindAndReturnTransformation(
|
|||||||
* return ...
|
* return ...
|
||||||
*/
|
*/
|
||||||
object Matcher : ResultTransformationMatcher {
|
object Matcher : ResultTransformationMatcher {
|
||||||
override fun match(state: MatchingState): ResultTransformationMatch? {
|
override val indexVariableUsePossible: Boolean
|
||||||
//TODO: pass indexVariable as null if not used
|
get() = false
|
||||||
if (state.indexVariable != null) return null
|
|
||||||
|
|
||||||
|
override fun match(state: MatchingState): ResultTransformationMatch? {
|
||||||
val returnInLoop = state.statements.singleOrNull() as? KtReturnExpression ?: return null
|
val returnInLoop = state.statements.singleOrNull() as? KtReturnExpression ?: return null
|
||||||
val returnAfterLoop = state.outerLoop.nextStatement() as? KtReturnExpression ?: return null
|
val returnAfterLoop = state.outerLoop.nextStatement() as? KtReturnExpression ?: return null
|
||||||
if (returnInLoop.getLabelName() != returnAfterLoop.getLabelName()) return null
|
if (returnInLoop.getLabelName() != returnAfterLoop.getLabelName()) return null
|
||||||
|
|||||||
+31
-5
@@ -69,22 +69,20 @@ class FilterTransformation(
|
|||||||
*/
|
*/
|
||||||
object Matcher : SequenceTransformationMatcher {
|
object Matcher : SequenceTransformationMatcher {
|
||||||
override fun match(state: MatchingState): SequenceTransformationMatch? {
|
override fun match(state: MatchingState): SequenceTransformationMatch? {
|
||||||
if (state.indexVariable != null) return null //TODO?
|
|
||||||
|
|
||||||
val ifStatement = state.statements.firstOrNull() as? KtIfExpression ?: return null
|
val ifStatement = state.statements.firstOrNull() as? KtIfExpression ?: return null
|
||||||
if (ifStatement.`else` != null) return null
|
if (ifStatement.`else` != null) return null
|
||||||
val condition = ifStatement.condition ?: return null
|
val condition = ifStatement.condition ?: return null
|
||||||
val then = ifStatement.then ?: return null
|
val then = ifStatement.then ?: return null
|
||||||
|
|
||||||
if (state.statements.size == 1) {
|
if (state.statements.size == 1) {
|
||||||
val transformation = createFilterTransformation(state.outerLoop, state.inputVariable, condition, isInverse = false)
|
val transformation = createFilterTransformation(state.outerLoop, state.inputVariable, state.indexVariable, condition, isInverse = false)
|
||||||
val newState = state.copy(statements = listOf(then))
|
val newState = state.copy(statements = listOf(then))
|
||||||
return SequenceTransformationMatch(transformation, newState)
|
return SequenceTransformationMatch(transformation, newState)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val continueExpression = then.blockExpressionsOrSingle().singleOrNull() as? KtContinueExpression ?: return null
|
val continueExpression = then.blockExpressionsOrSingle().singleOrNull() as? KtContinueExpression ?: return null
|
||||||
if (!continueExpression.isBreakOrContinueOfLoop(state.innerLoop)) return null
|
if (!continueExpression.isBreakOrContinueOfLoop(state.innerLoop)) return null
|
||||||
val transformation = createFilterTransformation(state.outerLoop, state.inputVariable, condition, isInverse = true)
|
val transformation = createFilterTransformation(state.outerLoop, state.inputVariable, state.indexVariable, condition, isInverse = true)
|
||||||
val newState = state.copy(statements = state.statements.drop(1))
|
val newState = state.copy(statements = state.statements.drop(1))
|
||||||
return SequenceTransformationMatch(transformation, newState)
|
return SequenceTransformationMatch(transformation, newState)
|
||||||
}
|
}
|
||||||
@@ -94,11 +92,17 @@ class FilterTransformation(
|
|||||||
private fun createFilterTransformation(
|
private fun createFilterTransformation(
|
||||||
loop: KtForExpression,
|
loop: KtForExpression,
|
||||||
inputVariable: KtCallableDeclaration,
|
inputVariable: KtCallableDeclaration,
|
||||||
|
indexVariable: KtCallableDeclaration?,
|
||||||
condition: KtExpression,
|
condition: KtExpression,
|
||||||
isInverse: Boolean): SequenceTransformation {
|
isInverse: Boolean
|
||||||
|
): SequenceTransformation {
|
||||||
|
|
||||||
val effectiveCondition = if (isInverse) condition.negate() else condition
|
val effectiveCondition = if (isInverse) condition.negate() else condition
|
||||||
|
|
||||||
|
if (indexVariable != null && indexVariable.hasUsages(condition)) {
|
||||||
|
return FilterIndexedTransformation(loop, inputVariable, indexVariable, effectiveCondition)
|
||||||
|
}
|
||||||
|
|
||||||
if (effectiveCondition is KtIsExpression
|
if (effectiveCondition is KtIsExpression
|
||||||
&& !effectiveCondition.isNegated
|
&& !effectiveCondition.isNegated
|
||||||
&& effectiveCondition.leftHandSide.isSimpleName(inputVariable.nameAsSafeName) // we cannot use isVariableReference here because expression can be non-physical
|
&& effectiveCondition.leftHandSide.isSimpleName(inputVariable.nameAsSafeName) // we cannot use isVariableReference here because expression can be non-physical
|
||||||
@@ -150,3 +154,25 @@ class FilterNotNullTransformation(override val loop: KtForExpression) : Sequence
|
|||||||
return chainedCallGenerator.generate("filterNotNull()")
|
return chainedCallGenerator.generate("filterNotNull()")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FilterIndexedTransformation(
|
||||||
|
override val loop: KtForExpression,
|
||||||
|
val inputVariable: KtCallableDeclaration,
|
||||||
|
val indexVariable: KtCallableDeclaration,
|
||||||
|
val condition: KtExpression
|
||||||
|
) : SequenceTransformation {
|
||||||
|
|
||||||
|
//TODO: how to handle multiple if's using index?
|
||||||
|
|
||||||
|
override val affectsIndex: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
|
override val presentation: String
|
||||||
|
get() = "filterIndexed{}"
|
||||||
|
|
||||||
|
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
|
||||||
|
val lambda = generateLambda(condition, indexVariable, inputVariable)
|
||||||
|
return chainedCallGenerator.generate("filterIndexed $0:'{}'", lambda)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-6
@@ -20,9 +20,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.*
|
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.*
|
||||||
import org.jetbrains.kotlin.idea.util.FuzzyType
|
import org.jetbrains.kotlin.idea.util.FuzzyType
|
||||||
import org.jetbrains.kotlin.psi.KtCallableDeclaration
|
import org.jetbrains.kotlin.psi.*
|
||||||
import org.jetbrains.kotlin.psi.KtExpression
|
|
||||||
import org.jetbrains.kotlin.psi.KtForExpression
|
|
||||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||||
|
|
||||||
class FlatMapTransformation(
|
class FlatMapTransformation(
|
||||||
@@ -53,8 +51,6 @@ class FlatMapTransformation(
|
|||||||
*/
|
*/
|
||||||
object Matcher : SequenceTransformationMatcher {
|
object Matcher : SequenceTransformationMatcher {
|
||||||
override fun match(state: MatchingState): SequenceTransformationMatch? {
|
override fun match(state: MatchingState): SequenceTransformationMatch? {
|
||||||
if (state.indexVariable != null) return null
|
|
||||||
|
|
||||||
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
|
||||||
@@ -65,8 +61,21 @@ class FlatMapTransformation(
|
|||||||
if (iterableType.checkIsSuperTypeOf(nestedSequenceType) == null) return null
|
if (iterableType.checkIsSuperTypeOf(nestedSequenceType) == null) return null
|
||||||
|
|
||||||
val nestedLoopBody = nestedLoop.body ?: return null
|
val nestedLoopBody = nestedLoop.body ?: return null
|
||||||
|
|
||||||
val newWorkingVariable = nestedLoop.loopParameter ?: return null
|
val newWorkingVariable = nestedLoop.loopParameter ?: return null
|
||||||
|
|
||||||
|
if (state.indexVariable != null && state.indexVariable.hasUsages(transform)) {
|
||||||
|
// if nested loop range uses index, convert to "mapIndexed {...}.flatMap { it }"
|
||||||
|
val mapIndexedTransformation = MapIndexedTransformation(state.outerLoop, state.inputVariable, state.indexVariable, transform)
|
||||||
|
val inputVarExpression = KtPsiFactory(nestedLoop).createExpressionByPattern("$0", state.inputVariable.nameAsSafeName)
|
||||||
|
val flatMapTransformation = FlatMapTransformation(state.outerLoop, state.inputVariable, inputVarExpression)
|
||||||
|
val newState = state.copy(
|
||||||
|
innerLoop = nestedLoop,
|
||||||
|
statements = listOf(nestedLoopBody),
|
||||||
|
inputVariable = newWorkingVariable
|
||||||
|
)
|
||||||
|
return SequenceTransformationMatch(listOf(mapIndexedTransformation, flatMapTransformation), newState)
|
||||||
|
}
|
||||||
|
|
||||||
val transformation = FlatMapTransformation(state.outerLoop, state.inputVariable, transform)
|
val transformation = FlatMapTransformation(state.outerLoop, state.inputVariable, transform)
|
||||||
val newState = state.copy(
|
val newState = state.copy(
|
||||||
innerLoop = nestedLoop,
|
innerLoop = nestedLoop,
|
||||||
|
|||||||
+24
-4
@@ -48,16 +48,36 @@ class MapTransformation(
|
|||||||
*/
|
*/
|
||||||
object Matcher : SequenceTransformationMatcher {
|
object Matcher : SequenceTransformationMatcher {
|
||||||
override fun match(state: MatchingState): SequenceTransformationMatch? {
|
override fun match(state: MatchingState): SequenceTransformationMatch? {
|
||||||
if (state.indexVariable != null) return null //TODO?
|
|
||||||
|
|
||||||
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
|
||||||
val restStatements = state.statements.drop(1)
|
val restStatements = state.statements.drop(1)
|
||||||
|
|
||||||
val transformation = MapTransformation(state.outerLoop, state.inputVariable, initializer)
|
val transformation = if (state.indexVariable != null && state.indexVariable.hasUsages(initializer))
|
||||||
|
MapIndexedTransformation(state.outerLoop, state.inputVariable, state.indexVariable, initializer)
|
||||||
|
else
|
||||||
|
MapTransformation(state.outerLoop, state.inputVariable, initializer)
|
||||||
val newState = state.copy(statements = restStatements, inputVariable = declaration)
|
val newState = state.copy(statements = restStatements, inputVariable = declaration)
|
||||||
return SequenceTransformationMatch(transformation, newState)
|
return SequenceTransformationMatch(transformation, newState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MapIndexedTransformation(
|
||||||
|
override val loop: KtForExpression,
|
||||||
|
val inputVariable: KtCallableDeclaration,
|
||||||
|
val indexVariable: KtCallableDeclaration,
|
||||||
|
val mapping: KtExpression
|
||||||
|
) : SequenceTransformation {
|
||||||
|
|
||||||
|
override val affectsIndex: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override val presentation: String
|
||||||
|
get() = "mapIndexed{}"
|
||||||
|
|
||||||
|
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
|
||||||
|
val lambda = generateLambda(mapping, indexVariable, inputVariable)
|
||||||
|
return chainedCallGenerator.generate("mapIndexed $0:'{}'", lambda)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,6 +81,25 @@ fun generateLambda(inputVariable: KtCallableDeclaration, expression: KtExpressio
|
|||||||
return psiFactory.createExpressionByPattern("{ $0 }", lambdaExpression.bodyExpression!!) as KtLambdaExpression
|
return psiFactory.createExpressionByPattern("{ $0 }", lambdaExpression.bodyExpression!!) as KtLambdaExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun generateLambda(expression: KtExpression, vararg inputVariables: KtCallableDeclaration): KtLambdaExpression {
|
||||||
|
return KtPsiFactory(expression).buildExpression {
|
||||||
|
appendFixedText("{")
|
||||||
|
|
||||||
|
for ((index, variable) in inputVariables.withIndex()) {
|
||||||
|
if (index > 0) {
|
||||||
|
appendFixedText(",")
|
||||||
|
}
|
||||||
|
appendName(variable.nameAsSafeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendFixedText("->")
|
||||||
|
|
||||||
|
appendExpression(expression)
|
||||||
|
|
||||||
|
appendFixedText("}")
|
||||||
|
} as KtLambdaExpression
|
||||||
|
}
|
||||||
|
|
||||||
fun KtExpression?.isTrueConstant()
|
fun KtExpression?.isTrueConstant()
|
||||||
= this != null && node?.elementType == KtNodeTypes.BOOLEAN_CONSTANT && text == "true"
|
= this != null && node?.elementType == KtNodeTypes.BOOLEAN_CONSTANT && text == "true"
|
||||||
|
|
||||||
@@ -100,12 +119,14 @@ fun KtCallableDeclaration.hasUsages(inElement: KtElement): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun KtCallableDeclaration.hasUsages(inElements: Collection<KtElement>): Boolean {
|
fun KtCallableDeclaration.hasUsages(inElements: Collection<KtElement>): Boolean {
|
||||||
|
assert(this.isPhysical)
|
||||||
// TODO: it's a temporary workaround about strange dead-lock when running inspections
|
// TODO: it's a temporary workaround about strange dead-lock when running inspections
|
||||||
return inElements.any { ReferencesSearch.search(this, LocalSearchScope(it)).any() }
|
return inElements.any { ReferencesSearch.search(this, LocalSearchScope(it)).any() }
|
||||||
// return ReferencesSearch.search(this, LocalSearchScope(inElements.toTypedArray())).any()
|
// return ReferencesSearch.search(this, LocalSearchScope(inElements.toTypedArray())).any()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun KtProperty.hasWriteUsages(): Boolean {
|
fun KtProperty.hasWriteUsages(): Boolean {
|
||||||
|
assert(this.isPhysical)
|
||||||
if (!isVar) return false
|
if (!isVar) return false
|
||||||
return ReferencesSearch.search(this, useScope).any {
|
return ReferencesSearch.search(this, useScope).any {
|
||||||
(it as? KtSimpleNameReference)?.element?.readWriteAccess(useResolveForReadWrite = true)?.isWrite == true
|
(it as? KtSimpleNameReference)?.element?.readWriteAccess(useResolveForReadWrite = true)?.isWrite == true
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'filterIndexed{}.any()'"
|
||||||
|
fun foo(list: List<String>) {
|
||||||
|
var found = false
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
if (s.length > index) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'filterIndexed{}.any()'"
|
||||||
|
fun foo(list: List<String>) {
|
||||||
|
val <caret>found = list
|
||||||
|
.filterIndexed { index, s -> s.length > index }
|
||||||
|
.any()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'filterIndexed{}.firstOrNull()'"
|
||||||
|
fun foo(list: List<String>): String? {
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
if (s.length > index) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'filterIndexed{}.firstOrNull()'"
|
||||||
|
fun foo(list: List<String>): String? {
|
||||||
|
<caret>return list
|
||||||
|
.filterIndexed { index, s -> s.length > index }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'mapIndexed{}.flatMap{}.firstOrNull{}'"
|
||||||
|
fun foo(list: List<String>): String? {
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
for (line in s.lines().take(index)) {
|
||||||
|
if (line.isNotBlank()) return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'mapIndexed{}.flatMap{}.firstOrNull{}'"
|
||||||
|
fun foo(list: List<String>): String? {
|
||||||
|
<caret>return list
|
||||||
|
.mapIndexed { index, s -> s.lines().take(index) }
|
||||||
|
.flatMap { it }
|
||||||
|
.firstOrNull { it.isNotBlank() }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'map{}.filterIndexed{}.firstOrNull()'"
|
||||||
|
fun foo(list: List<String>): Int? {
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
val l = s.length
|
||||||
|
if (l > index) {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'map{}.filterIndexed{}.firstOrNull()'"
|
||||||
|
fun foo(list: List<String>): Int? {
|
||||||
|
<caret>return list
|
||||||
|
.map { it.length }
|
||||||
|
.filterIndexed { index, l -> l > index }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'mapIndexed{}.firstOrNull{}'"
|
||||||
|
fun foo(list: List<String>): Int? {
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
val x = s.length * index
|
||||||
|
if (x > 0) return x
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'mapIndexed{}.firstOrNull{}'"
|
||||||
|
fun foo(list: List<String>): Int? {
|
||||||
|
<caret>return list
|
||||||
|
.mapIndexed { index, s -> s.length * index }
|
||||||
|
.firstOrNull { it > 0 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'mapIndexed{}.mapIndexed{}.firstOrNull{}'"
|
||||||
|
fun foo(list: List<String>): Int? {
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
val x = s.length * index
|
||||||
|
val y = x + index
|
||||||
|
if (y > 0) return y
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with 'mapIndexed{}.mapIndexed{}.firstOrNull{}'"
|
||||||
|
fun foo(list: List<String>): Int? {
|
||||||
|
<caret>return list
|
||||||
|
.mapIndexed { index, s -> s.length * index }
|
||||||
|
.mapIndexed { index, x -> x + index }
|
||||||
|
.firstOrNull { it > 0 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with '+= mapIndexed{}'"
|
||||||
|
fun foo(list: List<String>, target: MutableList<Int>) {
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
target.add(s.hashCode() * index)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// INTENTION_TEXT: "Replace with '+= mapIndexed{}'"
|
||||||
|
fun foo(list: List<String>, target: MutableList<Int>) {
|
||||||
|
<caret>target += list.mapIndexed { index, s -> s.hashCode() * index }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// IS_APPLICABLE: false
|
||||||
|
fun foo(list: List<String>): Int? {
|
||||||
|
<caret>for ((index, s) in list.withIndex()) {
|
||||||
|
if (s.isBlank()) continue
|
||||||
|
val x = s.length * index
|
||||||
|
if (x > 1000) return x
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user