Initial implementation of loop-to-call-chain intention

This commit is contained in:
Valentin Kipyatkov
2016-03-08 18:12:23 +01:00
parent c50cf13611
commit 12b1a99a6a
80 changed files with 3462 additions and 17 deletions
File diff suppressed because it is too large Load Diff
@@ -17,6 +17,7 @@
package org.jetbrains.kotlin.psi;
import com.intellij.lang.ASTNode;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.KtNodeTypes;
@@ -48,7 +49,12 @@ public class KtForExpression extends KtLoopExpression {
}
@Nullable @IfNotParsed
public ASTNode getInKeywordNode() {
return getNode().findChildByType(KtTokens.IN_KEYWORD);
public PsiElement getInKeyword() {
return findChildByType(KtTokens.IN_KEYWORD);
}
@NotNull
public PsiElement getForKeyword() {
return findChildByType(KtTokens.FOR_KEYWORD);
}
}
@@ -691,6 +691,11 @@ public abstract class KotlinBuiltIns {
return getString().getDefaultType();
}
@NotNull
public KotlinType getIterableType() {
return getIterable().getDefaultType();
}
@NotNull
public KotlinType getArrayElementType(@NotNull KotlinType arrayType) {
if (isArray(arrayType)) {
@@ -37,18 +37,22 @@ abstract class IntentionBasedInspection<TElement : PsiElement>(
) : AbstractKotlinInspection() {
constructor(
intention: SelfTargetingRangeIntention<TElement>
) : this(listOf(IntentionData(intention)), null, intention.elementType)
intention: SelfTargetingRangeIntention<TElement>,
problemText: String? = null
) : this(listOf(IntentionData(intention)), problemText, intention.elementType)
constructor(
intention: SelfTargetingRangeIntention<TElement>,
additionalChecker: (TElement, IntentionBasedInspection<TElement>) -> Boolean
) : this(listOf(IntentionData(intention, additionalChecker)), null, intention.elementType)
additionalChecker: (TElement, IntentionBasedInspection<TElement>) -> Boolean,
problemText: String? = null
) : this(listOf(IntentionData(intention, additionalChecker)), problemText, intention.elementType)
constructor(
intention: SelfTargetingRangeIntention<TElement>,
additionalChecker: (TElement) -> Boolean
) : this(listOf(IntentionData(intention, { element, inspection -> additionalChecker(element) } )), null, intention.elementType)
additionalChecker: (TElement) -> Boolean,
problemText: String? = null
) : this(listOf(IntentionData(intention, { element, inspection -> additionalChecker(element) } )), problemText, intention.elementType)
data class IntentionData<TElement : PsiElement>(
@@ -25,12 +25,12 @@ import java.util.*
class KtForLoopInReference(element: KtForExpression) : KtMultiReference<KtForExpression>(element) {
override fun getRangeInElement(): TextRange {
val inKeywordNode = expression.inKeywordNode
if (inKeywordNode == null)
val inKeyword = expression.inKeyword
if (inKeyword == null)
return TextRange.EMPTY_RANGE
val offset = inKeywordNode.psi!!.startOffsetInParent
return TextRange(offset, offset + inKeywordNode.textLength)
val offset = inKeyword.startOffsetInParent
return TextRange(offset, offset + inKeyword.textLength)
}
override fun getTargetDescriptors(context: BindingContext): Collection<DeclarationDescriptor> {
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports for-loops that can be replaced with a sequence of stdlib-operations (like "map", "filter" etc)
</body>
</html>
@@ -0,0 +1,3 @@
fun foo(list: List<String>): String? {
<spot>return list.firstOrNull { s -> s.length > 0 }</spot>
}
@@ -0,0 +1,8 @@
fun foo(list: List<String>): String? {
<spot>for (s in list) {
if (s.length > 0) {
return s
}
}
return null</spot>
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention converts a for-loop into a sequence of stdlib-operations (like "map", "filter" etc)
</body>
</html>
+12
View File
@@ -1013,6 +1013,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.loopToCallChain.LoopToCallChainIntention</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.SwapBinaryExpressionIntention</className>
<category>Kotlin</category>
@@ -1507,6 +1512,13 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.loopToCallChain.LoopToCallChainInspection"
displayName="Loop can be replaced with stdlib operations"
groupName="Kotlin"
enabledByDefault="true"
level="INFO"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ConflictingExtensionPropertyInspection"
displayName="Extension property conflicting with synthetic one"
groupName="Kotlin"
@@ -32,9 +32,8 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class RemoveForLoopIndicesInspection : IntentionBasedInspection<KtForExpression>(
listOf(IntentionBasedInspection.IntentionData(RemoveForLoopIndicesIntention())),
"Index is not used in the loop body",
KtForExpression::class.java
RemoveForLoopIndicesIntention(),
problemText = "Index is not used in the loop body"
) {
override val problemHighlightType: ProblemHighlightType
get() = ProblemHighlightType.LIKE_UNUSED_SYMBOL
@@ -0,0 +1,44 @@
/*
* 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
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.idea.core.moveCaret
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.psiUtil.startOffset
class LoopToCallChainInspection : IntentionBasedInspection<KtForExpression>(
LoopToCallChainIntention(),
problemText = "Loop can be replaced with stdlib operations")
class LoopToCallChainIntention : SelfTargetingRangeIntention<KtForExpression>(
KtForExpression::class.java,
"Replace with stdlib operations"
) {
override fun applicabilityRange(element: KtForExpression): TextRange? {
return if (match(element) != null) element.forKeyword.textRange else null
}
override fun applyTo(element: KtForExpression, editor: Editor?) {
val match = match(element)!!
val result = convertLoop(element, match)
editor?.moveCaret(result.startOffset)
}
}
@@ -0,0 +1,85 @@
/*
* 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
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
interface ChainedCallGenerator {
fun generate(pattern: String, vararg args: Any): KtExpression
}
interface Transformation {
val inputVariable: KtCallableDeclaration
}
interface SequenceTransformation : Transformation {
val affectsIndex: Boolean
fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression
}
interface ResultTransformation : Transformation {
val canIncludeFilter: Boolean
val canIncludeMap: Boolean
val commentSavingRange: PsiChildRange
val commentRestoringRange: PsiChildRange
fun generateCode(
chainedCallGenerator: ChainedCallGenerator,
filter: FilterOrMap?,
map: FilterOrMap?
): KtExpression
fun convertLoop(resultCallChain: KtExpression): KtExpression
}
data class FilterOrMap(val expression: KtExpression, val workingVariable: KtCallableDeclaration)
data class MatchingState(
val statements: Collection<KtExpression>,
val workingVariable: KtCallableDeclaration,
val indexVariable: KtCallableDeclaration?,
val loop: KtForExpression
)
interface SequenceTransformationMatcher {
fun match(state: MatchingState): SequenceTransformationMatch?
}
class SequenceTransformationMatch(
val transformations: Collection<SequenceTransformation>,
val newState: MatchingState
) {
init {
assert(transformations.isNotEmpty())
}
constructor(transformation: SequenceTransformation, newState: MatchingState) : this(listOf(transformation), newState)
}
interface ResultTransformationMatcher {
fun match(state: MatchingState): ResultTransformationMatch?
}
class ResultTransformationMatch(
val resultTransformation: ResultTransformation,
val sequenceTransformations: Collection<SequenceTransformation> = listOf()
)
@@ -0,0 +1,124 @@
/*
* 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
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.sequence.FilterTransformation
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.FlatMapTransformation
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.MapTransformation
import org.jetbrains.kotlin.idea.intentions.negate
import org.jetbrains.kotlin.idea.util.CommentSaver
import org.jetbrains.kotlin.psi.*
import java.util.*
object MatcherRegistrar {
val sequenceMatchers: Collection<SequenceTransformationMatcher> = listOf(
FilterTransformation.Matcher,
MapTransformation.Matcher,
FlatMapTransformation.Matcher
)
val resultMatchers: Collection<ResultTransformationMatcher> = listOf(
FindAndReturnTransformation.Matcher,
FindAndAssignTransformation.Matcher
)
}
fun match(loop: KtForExpression): ResultTransformationMatch? {
val sequenceTransformations = ArrayList<SequenceTransformation>()
var state = MatchingState(
statements = listOf(loop.body ?: return null),
workingVariable = loop.loopParameter ?: return null,
indexVariable = null,
loop = loop
)
MatchLoop@
while (true) {
val block = state.statements.singleOrNull() as? KtBlockExpression
if (block != null) {
state = state.copy(statements = block.statements)
}
for (matcher in MatcherRegistrar.resultMatchers) {
val match = matcher.match(state)
if (match != null) {
sequenceTransformations.addAll(match.sequenceTransformations)
return ResultTransformationMatch(match.resultTransformation, sequenceTransformations)
}
}
for (matcher in MatcherRegistrar.sequenceMatchers) {
val match = matcher.match(state)
if (match != null) {
sequenceTransformations.addAll(match.transformations)
state = match.newState
continue@MatchLoop
}
}
return null
}
}
//TODO: offer to use of .asSequence() as an option
fun convertLoop(loop: KtForExpression, matchResult: ResultTransformationMatch): KtExpression {
val commentSaver = CommentSaver(matchResult.resultTransformation.commentSavingRange)
var callChain = matchResult.generateCallChain(loop)
val result = matchResult.resultTransformation.convertLoop(callChain)
commentSaver.restore(matchResult.resultTransformation.commentRestoringRange)
return result
}
private fun ResultTransformationMatch.generateCallChain(loop: KtForExpression): KtExpression {
var sequenceTransformations = sequenceTransformations
val last = sequenceTransformations.lastOrNull()
var lastFilter: FilterOrMap? = null
var lastMap: FilterOrMap? = null //TODO
if (last is FilterTransformation && resultTransformation.canIncludeFilter) {
val condition = if (last.isInverse) last.condition.negate() else last.condition
lastFilter = FilterOrMap(condition, last.inputVariable)
sequenceTransformations = sequenceTransformations.take(sequenceTransformations.size - 1)
}
val lineBreak = if (sequenceTransformations.isNotEmpty()) "\n" else ""
var callChain = loop.loopRange!!
val psiFactory = KtPsiFactory(loop)
fun chainedCallGenerator(): ChainedCallGenerator {
return object : ChainedCallGenerator {
override fun generate(pattern: String, vararg args: Any): KtExpression {
val newPattern = "\$${args.size}$lineBreak.$pattern"
return psiFactory.createExpressionByPattern(newPattern, *args, callChain)
}
}
}
for (transformation in sequenceTransformations) {
callChain = transformation.generateCode(chainedCallGenerator())
}
callChain = resultTransformation.generateCode(chainedCallGenerator(), lastFilter, lastMap)
return callChain
}
@@ -0,0 +1,92 @@
/*
* 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 com.intellij.psi.search.LocalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.*
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
class FindAndAssignTransformation(
private val loop: KtForExpression,
override val inputVariable: KtCallableDeclaration,
private val stdlibFunName: String,
private val initialDeclaration: KtProperty
) : ResultTransformation {
override val commentSavingRange = PsiChildRange(initialDeclaration, loop.unwrapIfLabeled())
override val commentRestoringRange = commentSavingRange.withoutLastStatement()
override val canIncludeFilter: Boolean
get() = true
override val canIncludeMap: Boolean
get() = false
override fun generateCode(chainedCallGenerator: ChainedCallGenerator, filter: FilterOrMap?, map: FilterOrMap?): KtExpression {
return if (filter == null) {
chainedCallGenerator.generate("$stdlibFunName()")
}
else {
val lambda = generateLambda(filter.workingVariable, filter.expression)
chainedCallGenerator.generate("$stdlibFunName $0:'{}'", lambda)
}
}
override fun convertLoop(resultCallChain: KtExpression): KtExpression {
initialDeclaration.initializer!!.replace(resultCallChain)
loop.deleteWithLabels()
if (!initialDeclaration.hasWriteUsages()) { // change variable to 'val' if possible
initialDeclaration.valOrVarKeyword.replace(KtPsiFactory(initialDeclaration).createValKeyword())
}
return initialDeclaration
}
object Matcher : ResultTransformationMatcher {
override fun match(state: MatchingState): ResultTransformationMatch? {
//TODO: pass indexVariable as null if not used
if (state.indexVariable != null) return null
if (state.statements.size != 2) return null
val breakExpression = state.statements.last() as? KtBreakExpression ?: return null
if (!breakExpression.isBreakOrContinueOfLoop(state.loop)) return null
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
//TODO: support also assignment instead of declaration
val declarationBeforeLoop = state.loop.previousStatement() as? KtProperty ?: return null
val initializer = declarationBeforeLoop.initializer ?: return null
if (!left.isVariableReference(declarationBeforeLoop)) return null
val usageCountInLoop = ReferencesSearch.search(declarationBeforeLoop, LocalSearchScope(state.loop)).count()
if (usageCountInLoop != 1) return null // this should be the only usage of this variable inside the loop
val stdlibFunName = stdlibFunNameForFind(right, initializer, state.workingVariable) ?: return null
val transformation = FindAndAssignTransformation(state.loop, state.workingVariable, stdlibFunName, declarationBeforeLoop)
return ResultTransformationMatch(transformation)
}
}
}
@@ -0,0 +1,77 @@
/*
* 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.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtReturnExpression
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
class FindAndReturnTransformation(
private val loop: KtForExpression,
override val inputVariable: KtCallableDeclaration,
private val stdlibFunName: String,
private val endReturn: KtReturnExpression
) : ResultTransformation {
override val commentSavingRange = PsiChildRange(loop.unwrapIfLabeled(), endReturn)
override val commentRestoringRange = commentSavingRange.withoutFirstStatement()
override val canIncludeFilter: Boolean
get() = true
override val canIncludeMap: Boolean
get() = false
override fun generateCode(chainedCallGenerator: ChainedCallGenerator, filter: FilterOrMap?, map: FilterOrMap?): KtExpression {
return if (filter == null) {
chainedCallGenerator.generate("$stdlibFunName()")
}
else {
val lambda = generateLambda(filter.workingVariable, filter.expression)
chainedCallGenerator.generate("$stdlibFunName $0:'{}'", lambda)
}
}
override fun convertLoop(resultCallChain: KtExpression): KtExpression {
endReturn.returnedExpression!!.replace(resultCallChain)
loop.deleteWithLabels()
return endReturn
}
object Matcher : ResultTransformationMatcher {
override fun match(state: MatchingState): ResultTransformationMatch? {
//TODO: pass indexVariable as null if not used
if (state.indexVariable != null) return null
val returnInLoop = state.statements.singleOrNull() as? KtReturnExpression ?: return null
val returnAfterLoop = state.loop.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 stdlibFunName = stdlibFunNameForFind(returnValueInLoop, returnValueAfterLoop, state.workingVariable) ?: return null
val transformation = FindAndReturnTransformation(state.loop, state.workingVariable, stdlibFunName, returnAfterLoop)
return ResultTransformationMatch(transformation)
}
}
}
@@ -0,0 +1,123 @@
/*
* 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.sequence
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isNullExpression
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.*
import org.jetbrains.kotlin.idea.intentions.negate
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.blockExpressionsOrSingle
class FilterTransformation(
override val inputVariable: KtCallableDeclaration,
val condition: KtExpression,
val isInverse: Boolean
) : SequenceTransformation {
init {
assert(condition.isPhysical)
}
override val affectsIndex: Boolean
get() = true
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
val lambda = generateLambda(inputVariable, condition)
val name = if (isInverse) "filterNot" else "filter"
return chainedCallGenerator.generate("$0$1:'{}'", name, lambda)
}
//TODO: merge subsequent filters
object Matcher : SequenceTransformationMatcher {
override fun match(state: MatchingState): SequenceTransformationMatch? {
if (state.indexVariable != null) return null //TODO?
val ifStatement = state.statements.firstOrNull() as? KtIfExpression ?: return null
if (ifStatement.`else` != null) return null
val condition = ifStatement.condition ?: return null
val then = ifStatement.then ?: return null
if (state.statements.size == 1) {
val transformation = createFilterTransformation(state.workingVariable, condition, isInverse = false)
val newState = state.copy(statements = listOf(then))
return SequenceTransformationMatch(transformation, newState)
}
else {
val continueExpression = then.blockExpressionsOrSingle().singleOrNull() as? KtContinueExpression ?: return null
if (!continueExpression.isBreakOrContinueOfLoop(state.loop)) return null
val transformation = createFilterTransformation(state.workingVariable, condition, isInverse = true)
val newState = state.copy(statements = state.statements.drop(1))
return SequenceTransformationMatch(transformation, newState)
}
}
//TODO: choose filter or filterNot depending on condition
private fun createFilterTransformation(
inputVariable: KtCallableDeclaration,
condition: KtExpression,
isInverse: Boolean): SequenceTransformation {
val realCondition = if (isInverse) condition.negate() else condition
if (realCondition is KtIsExpression
&& !realCondition.isNegated
&& realCondition.leftHandSide.isSimpleName(inputVariable.nameAsSafeName) // we cannot use isVariableReference here because expression can be non-physical
) {
val typeRef = realCondition.typeReference
if (typeRef != null) {
return FilterIsInstanceTransformation(inputVariable, typeRef)
}
}
if (realCondition is KtBinaryExpression
&& realCondition.operationToken == KtTokens.EXCLEQ
&& realCondition.right.isNullExpression()
&& realCondition.left.isSimpleName(inputVariable.nameAsSafeName)
) {
return FilterNotNullTransformation(inputVariable)
}
return FilterTransformation(inputVariable, condition, isInverse)
}
}
}
class FilterIsInstanceTransformation(
override val inputVariable: KtCallableDeclaration,
private val type: KtTypeReference
) : SequenceTransformation {
override val affectsIndex: Boolean
get() = true
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
return chainedCallGenerator.generate("filterIsInstance<$0>()", type)
}
}
class FilterNotNullTransformation(
override val inputVariable: KtCallableDeclaration
) : SequenceTransformation {
override val affectsIndex: Boolean
get() = true
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
return chainedCallGenerator.generate("filterNotNull()")
}
}
@@ -0,0 +1,68 @@
/*
* 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.sequence
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.*
import org.jetbrains.kotlin.idea.util.FuzzyType
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class FlatMapTransformation(
override val inputVariable: KtCallableDeclaration,
private val transform: KtExpression
) : SequenceTransformation {
init {
assert(transform.isPhysical)
}
override val affectsIndex: Boolean
get() = true
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
val lambda = generateLambda(inputVariable, transform)
return chainedCallGenerator.generate("flatMap$0:'{}'", lambda)
}
object Matcher : SequenceTransformationMatcher {
override fun match(state: MatchingState): SequenceTransformationMatch? {
if (state.indexVariable != null) return null
val nestedLoop = state.statements.singleOrNull() as? KtForExpression ?: return null
val transform = nestedLoop.loopRange ?: return null
// check that we iterate over Iterable
val nestedSequenceType = transform.analyze(BodyResolveMode.PARTIAL).getType(transform) ?: return null
val builtIns = transform.getResolutionFacade().moduleDescriptor.builtIns
val iterableType = FuzzyType(builtIns.iterableType, builtIns.iterable.declaredTypeParameters)
if (iterableType.checkIsSuperTypeOf(nestedSequenceType) == null) return null
val newWorkingVariable = nestedLoop.loopParameter ?: return null
val loopBody = nestedLoop.body ?: return null
val transformation = FlatMapTransformation(state.workingVariable, transform)
val newState = state.copy(
statements = listOf(loopBody),
workingVariable = newWorkingVariable
)
return SequenceTransformationMatch(transformation, newState)
}
}
}
@@ -0,0 +1,56 @@
/*
* 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.sequence
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.*
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtProperty
class MapTransformation(
override val inputVariable: KtCallableDeclaration,
val mapping: KtExpression
) : SequenceTransformation {
init {
assert(mapping.isPhysical)
}
override val affectsIndex: Boolean
get() = false
override fun generateCode(chainedCallGenerator: ChainedCallGenerator): KtExpression {
val lambda = generateLambda(inputVariable, mapping)
return chainedCallGenerator.generate("map$0:'{}'", lambda)
}
object Matcher : SequenceTransformationMatcher {
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 initializer = declaration.initializer ?: return null
if (declaration.hasWriteUsages()) return null
val restStatements = state.statements.drop(1)
if (state.workingVariable.hasUsages(restStatements)) return null // workingVariable is still needed - cannot transform to map()
val transformation = MapTransformation(state.workingVariable, initializer)
val newState = state.copy(statements = restStatements, workingVariable = declaration)
return SequenceTransformationMatch(transformation, newState)
}
}
}
@@ -0,0 +1,147 @@
/*
* 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
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.search.LocalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.idea.analysis.analyzeInContext
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isNullExpression
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.references.readWriteAccess
import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
fun generateLambda(workingVariable: KtCallableDeclaration, expression: KtExpression): KtLambdaExpression {
val psiFactory = KtPsiFactory(expression)
val lambdaExpression = psiFactory.createExpressionByPattern("{ $0 -> $1 }", workingVariable.nameAsSafeName, expression) as KtLambdaExpression
val isItUsedInside = expression.anyDescendantOfType<KtNameReferenceExpression> {
it.getQualifiedExpressionForSelector() == null && it.getReferencedName() == "it"
}
if (isItUsedInside) return lambdaExpression
val resolutionScope = workingVariable.getResolutionScope(workingVariable.analyze(BodyResolveMode.FULL), workingVariable.getResolutionFacade())
val bindingContext = lambdaExpression.analyzeInContext(resolutionScope, contextExpression = workingVariable)
val lambdaParam = lambdaExpression.valueParameters.single()
val lambdaParamDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, lambdaParam]
val usages = lambdaExpression.collectDescendantsOfType<KtNameReferenceExpression> {
it.mainReference.resolveToDescriptors(bindingContext).singleOrNull() == lambdaParamDescriptor
}
val itExpr = psiFactory.createSimpleName("it")
for (usage in usages) {
usage.replace(itExpr)
}
return psiFactory.createExpressionByPattern("{ $0 }", lambdaExpression.bodyExpression!!) as KtLambdaExpression
}
fun KtExpression?.isTrueConstant()
= this != null && node?.elementType == KtNodeTypes.BOOLEAN_CONSTANT && text == "true"
fun KtExpression?.isFalseConstant()
= this != null && node?.elementType == KtNodeTypes.BOOLEAN_CONSTANT && text == "false"
fun KtExpression?.isVariableReference(variable: KtCallableDeclaration): Boolean {
return this is KtNameReferenceExpression && this.mainReference.isReferenceTo(variable)
}
fun KtExpression?.isSimpleName(name: Name): Boolean {
return this is KtNameReferenceExpression && this.getQualifiedExpressionForSelector() == null && this.getReferencedNameAsName() == name
}
fun KtCallableDeclaration.hasUsages(inElements: Collection<KtElement>): Boolean {
return ReferencesSearch.search(this, LocalSearchScope(inElements.toTypedArray())).any()
}
fun KtProperty.hasWriteUsages(): Boolean {
if (!isVar) return false
return ReferencesSearch.search(this, useScope).any {
(it as? KtSimpleNameReference)?.element?.readWriteAccess(useResolveForReadWrite = true)?.isWrite == true
}
}
fun stdlibFunNameForFind(valueIfFound: KtExpression, valueIfNotFound: KtExpression, workingVariable: KtCallableDeclaration): String? {
return when {
valueIfNotFound.isNullExpression() && valueIfFound.isVariableReference(workingVariable) -> "firstOrNull"
valueIfFound.isTrueConstant() && valueIfNotFound.isFalseConstant() -> "any"
valueIfFound.isFalseConstant() && valueIfNotFound.isTrueConstant() -> "none"
else -> /*TODO: allow other constants*/ null
}
}
fun KtExpressionWithLabel.isBreakOrContinueOfLoop(loop: KtLoopExpression): Boolean {
val label = getTargetLabel()
if (label == null) {
val closestLoop = parents.firstIsInstance<KtLoopExpression>()
return loop == closestLoop
}
else {
//TODO: does PARTIAL always work here?
val targetLoop = analyze(BodyResolveMode.PARTIAL)[BindingContext.LABEL_TARGET, label]
return targetLoop == loop
}
}
fun KtExpression.previousStatement(): KtExpression? {
var statement = unwrapIfLabeled()
if (statement.parent !is KtBlockExpression) return null
return statement.siblings(forward = false, withItself = false).firstIsInstanceOrNull<KtExpression>()
}
fun KtExpression.nextStatement(): KtExpression? {
var statement = unwrapIfLabeled()
if (statement.parent !is KtBlockExpression) return null
return statement.siblings(forward = true, withItself = false).firstIsInstanceOrNull<KtExpression>()
}
fun KtExpression.unwrapIfLabeled(): KtExpression {
var statement = this
while (true) {
statement = statement.parent as? KtLabeledExpression ?: return statement
}
}
fun KtLoopExpression.deleteWithLabels() {
unwrapIfLabeled().delete()
}
fun PsiChildRange.withoutFirstStatement(): PsiChildRange {
val newFirst = first!!.siblings(forward = true, withItself = false).first { it !is PsiWhiteSpace }
return PsiChildRange(newFirst, last)
}
fun PsiChildRange.withoutLastStatement(): PsiChildRange {
val newLast = last!!.siblings(forward = false, withItself = false).first { it !is PsiWhiteSpace }
return PsiChildRange(first, newLast)
}
@@ -1 +1 @@
fun getText(): String = "xxx" //TODO
fun getText(): String = "xxx" //TODO
@@ -1,2 +1,2 @@
fun getText(): String = // let's return xxx
"xxx" //TODO
"xxx" //TODO
+5
View File
@@ -0,0 +1,5 @@
fun foo(p: Int) {
<caret>if (p !in 1..10) {
}
}
@@ -0,0 +1,3 @@
fun foo(p: Int) {
if (p in 1..10) return
}
+1
View File
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.loopToCallChain.LoopToCallChainIntention
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
var found = false
<caret>for (s in list) {
if (s.length > 0) {
found = true
break
}
}
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
<caret>val found = list.any { it.length > 0 }
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo(list: List<String>): Boolean {
<caret>for (s in list) {
if (s.length > 0) {
return true
}
}
return false
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): Boolean {
<caret>return list.any { it.length > 0 }
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<String>): Boolean {
<caret>for (s in list) {
return true
}
return false
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): Boolean {
<caret>return list.any()
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun foo(list: List<Any>): Int? {
<caret>for (o in list) {
if (o is String) {
val length = o.length
return length
}
}
return null
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<Any>): Int? {
return list
.filterIsInstance<String>()
.map { it.length }
.firstOrNull()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo(list: List<Any>): Int? {
<caret>for (o in list) {
if (o !is String) continue
val length = o.length
return length
}
return null
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<Any>): Int? {
return list
.filterIsInstance<String>()
.map { it.length }
.firstOrNull()
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun foo(list: List<String?>): Int? {
<caret>for (s in list) {
if (s != null) {
val length = s.length
return length
}
}
return null
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<String?>): Int? {
<caret>return list
.filterNotNull()
.map { it.length }
.firstOrNull()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo(list: List<Any?>): Int? {
<caret>for (o in list) {
if (o == null) continue
val code = o.hashCode()
return code
}
return null
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<Any?>): Int? {
<caret>return list
.filterNotNull()
.map { it.hashCode() }
.firstOrNull()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo(list: List<String>): Int? {
<caret>for (s in list) {
if (s.isEmpty()) continue
val l = s.length
return l
}
return null
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<String>): Int? {
<caret>return list
.filterNot { it.isEmpty() }
.map { it.length }
.firstOrNull()
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>for (s in list) {
if (s.isEmpty()) continue
return s
}
return null
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>return list.firstOrNull { !it.isEmpty() }
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>for (s in list) {
if (!s.isEmpty()) continue
return s
}
return null
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>return list.firstOrNull { it.isEmpty() }
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>for (s in list) {
if (s.isEmpty()) {
continue
}
return s
}
return null
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>return list.firstOrNull { !it.isEmpty() }
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(): String? {
Loop@
while (x()) {
<caret>for (s in list()) {
if (s.isEmpty()) continue@Loop
return s
}
return null
}
return null
}
fun x(): Boolean = false
fun list(): List<String> = listOf()
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
var result: String? = null
<caret>for (s in list) {
if (s.length > 0) {
result = s
break
}
}
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
<caret>val result: String? = list.firstOrNull { it.length > 0 }
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo() {
MainLoop@
for (i in 1..10) {
var result: String? = null
<caret>for (s in list()) {
if (s.length > 0) {
result = s
break@MainLoop
}
}
}
}
fun list(): List<String> = listOf()
@@ -0,0 +1,12 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
var result: String? = null
<caret>for (s in list) {
if (s.length > 0) {
result = s
break
}
}
result += "1"
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
<caret>var result: String? = list.firstOrNull { it.length > 0 }
result += "1"
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
var result: String? = null
<caret>for (s in list) { // search for first non-empty string in the list
if (s.length > 0) { // string should be non-empty
result = s // save it into result
break
}
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<String>) {
<caret>val result: String? = list.firstOrNull { // search for first non-empty string in the list
it.length > 0
}// string should be non-empty
// save it into result
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>) {
var result: String? = null
<caret>for (s in list) {
if (s != result) {
result = s
break
}
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>for (s in list) {
if (s.length > 0) {
return s
}
}
return null
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>return list.firstOrNull { it.length > 0 }
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>for (s in list) {
return s
}
return null
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>return list.firstOrNull()
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>): Int? {
<caret>for (s in list) {
if (s.isNotEmpty()) {
return s.length
}
}
return null
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>): String {
<caret>for (s in list) {
if (s.isNotEmpty()) {
return s
}
}
return ""
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>for (s in list) {
return s
}
// return null if not found
return null
}
@@ -0,0 +1,5 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
// return null if not found
return list.firstOrNull()
}
+9
View File
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>for (s in list) {
for (line in s.lines()) {
if (line.isNotBlank()) return line
}
}
return null
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>return list
.flatMap { it.lines() }
.firstOrNull { it.isNotBlank() }
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
var result: String? = null
MainLoop@
<caret>for (s in list) {
for (line in s.lines()) {
if (line.isNotBlank()) {
result = line
break@MainLoop
}
}
}
return result
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo(list: List<String>): String? {
<caret>val result: String? = list
.flatMap { it.lines() }
.firstOrNull { it.isNotBlank() }
return result
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>): String? {
var result: String? = null
<caret>for (s in list) {
for (line in s.lines()) {
if (line.isNotBlank()) {
result = line
break
}
}
}
return result
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>): Char? {
<caret>for (s in list) {
for (c in s) {
if (c != ' ') return c
}
}
return null
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun foo(list: List<String>, it: Int) {
var found = false
<caret>for (s in list) {
if (s.length > it) {
found = true
break
}
}
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>, it: Int) {
<caret>val found = list.any { s -> s.length > it }
}
+8
View File
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun foo(list: List<String>): Int? {
<caret>for (s in list) {
val length = s.length
if (length > 0) return length
}
return null
}
+6
View File
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo(list: List<String>): Int? {
<caret>return list
.map { it.length }
.firstOrNull { it > 0 }
}
+8
View File
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun foo(list: List<String>): Int? {
<caret>for (s in list) {
var length = s.length
if (length > 0) return length
}
return null
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo(list: List<String>): Int? {
<caret>return list
.map { it.length }
.firstOrNull { it > 0 }
}
+9
View File
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>): Int? {
<caret>for (s in list) {
var length = s.length
if (length > 0) return ++length
}
return null
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun foo(list: List<String>): String? {
<caret>for (s in list) {
val length = s.length
if (length > 0) return s + length
}
return null
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo(list: List<String>): Boolean {
<caret>for (s in list) {
if (s.length > 0) {
return false
}
}
return true
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo(list: List<String>): Boolean {
<caret>return list.none { it.length > 0 }
}