diff --git a/.idea/inspectionProfiles/Loop_to_call_chain.xml b/.idea/inspectionProfiles/Loop_to_call_chain.xml
new file mode 100644
index 00000000000..3d5a104dcb6
--- /dev/null
+++ b/.idea/inspectionProfiles/Loop_to_call_chain.xml
@@ -0,0 +1,2133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtForExpression.java b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtForExpression.java
index 49e66113b29..04648c79759 100644
--- a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtForExpression.java
+++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtForExpression.java
@@ -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);
}
}
diff --git a/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java b/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java
index 6bf34144d20..8bc8054c510 100644
--- a/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java
+++ b/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java
@@ -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)) {
diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/IntentionBasedInspection.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/IntentionBasedInspection.kt
index f8df4c2c88a..583e0946615 100644
--- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/IntentionBasedInspection.kt
+++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/inspections/IntentionBasedInspection.kt
@@ -37,18 +37,22 @@ abstract class IntentionBasedInspection(
) : AbstractKotlinInspection() {
constructor(
- intention: SelfTargetingRangeIntention
- ) : this(listOf(IntentionData(intention)), null, intention.elementType)
+ intention: SelfTargetingRangeIntention,
+ problemText: String? = null
+ ) : this(listOf(IntentionData(intention)), problemText, intention.elementType)
constructor(
intention: SelfTargetingRangeIntention,
- additionalChecker: (TElement, IntentionBasedInspection) -> Boolean
- ) : this(listOf(IntentionData(intention, additionalChecker)), null, intention.elementType)
+ additionalChecker: (TElement, IntentionBasedInspection) -> Boolean,
+ problemText: String? = null
+ ) : this(listOf(IntentionData(intention, additionalChecker)), problemText, intention.elementType)
constructor(
intention: SelfTargetingRangeIntention,
- 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(
diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/references/KtForLoopInReference.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/references/KtForLoopInReference.kt
index 11f8dc42fba..fca1dc5c756 100644
--- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/references/KtForLoopInReference.kt
+++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/references/KtForLoopInReference.kt
@@ -25,12 +25,12 @@ import java.util.*
class KtForLoopInReference(element: KtForExpression) : KtMultiReference(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 {
diff --git a/idea/resources/inspectionDescriptions/LoopToCallChain.html b/idea/resources/inspectionDescriptions/LoopToCallChain.html
new file mode 100644
index 00000000000..a733fbb15e2
--- /dev/null
+++ b/idea/resources/inspectionDescriptions/LoopToCallChain.html
@@ -0,0 +1,5 @@
+
+
+This inspection reports for-loops that can be replaced with a sequence of stdlib-operations (like "map", "filter" etc)
+
+
diff --git a/idea/resources/intentionDescriptions/LoopToCallChainIntention/after.kt.template b/idea/resources/intentionDescriptions/LoopToCallChainIntention/after.kt.template
new file mode 100644
index 00000000000..01134af2f4e
--- /dev/null
+++ b/idea/resources/intentionDescriptions/LoopToCallChainIntention/after.kt.template
@@ -0,0 +1,3 @@
+fun foo(list: List): String? {
+ return list.firstOrNull { s -> s.length > 0 }
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/LoopToCallChainIntention/before.kt.template b/idea/resources/intentionDescriptions/LoopToCallChainIntention/before.kt.template
new file mode 100644
index 00000000000..46811f931d8
--- /dev/null
+++ b/idea/resources/intentionDescriptions/LoopToCallChainIntention/before.kt.template
@@ -0,0 +1,8 @@
+fun foo(list: List): String? {
+ for (s in list) {
+ if (s.length > 0) {
+ return s
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/LoopToCallChainIntention/description.html b/idea/resources/intentionDescriptions/LoopToCallChainIntention/description.html
new file mode 100644
index 00000000000..bb27c3fd502
--- /dev/null
+++ b/idea/resources/intentionDescriptions/LoopToCallChainIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention converts a for-loop into a sequence of stdlib-operations (like "map", "filter" etc)
+
+
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index cbb88e978e9..3b5012f385a 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1013,6 +1013,11 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.intentions.loopToCallChain.LoopToCallChainIntention
+ Kotlin
+
+
org.jetbrains.kotlin.idea.intentions.SwapBinaryExpressionIntention
Kotlin
@@ -1507,6 +1512,13 @@
language="kotlin"
/>
+
+
(
- 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
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/LoopToCallChainIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/LoopToCallChainIntention.kt
new file mode 100644
index 00000000000..16759cb517b
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/LoopToCallChainIntention.kt
@@ -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(
+ LoopToCallChainIntention(),
+ problemText = "Loop can be replaced with stdlib operations")
+
+class LoopToCallChainIntention : SelfTargetingRangeIntention(
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt
new file mode 100644
index 00000000000..7817206b317
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/interfaces.kt
@@ -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,
+ val workingVariable: KtCallableDeclaration,
+ val indexVariable: KtCallableDeclaration?,
+ val loop: KtForExpression
+)
+
+interface SequenceTransformationMatcher {
+ fun match(state: MatchingState): SequenceTransformationMatch?
+}
+
+class SequenceTransformationMatch(
+ val transformations: Collection,
+ 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 = listOf()
+)
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt
new file mode 100644
index 00000000000..3639f41a027
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/matchAndConvert.kt
@@ -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 = listOf(
+ FilterTransformation.Matcher,
+ MapTransformation.Matcher,
+ FlatMapTransformation.Matcher
+ )
+
+ val resultMatchers: Collection = listOf(
+ FindAndReturnTransformation.Matcher,
+ FindAndAssignTransformation.Matcher
+ )
+}
+
+fun match(loop: KtForExpression): ResultTransformationMatch? {
+ val sequenceTransformations = ArrayList()
+ 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
+}
+
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndAssignTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndAssignTransformation.kt
new file mode 100644
index 00000000000..98fc162c61d
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndAssignTransformation.kt
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndReturnTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndReturnTransformation.kt
new file mode 100644
index 00000000000..7dddf6a1af8
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/FindAndReturnTransformation.kt
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FilterTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FilterTransformation.kt
new file mode 100644
index 00000000000..051f6fe8ee0
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FilterTransformation.kt
@@ -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()")
+ }
+}
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FlatMapTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FlatMapTransformation.kt
new file mode 100644
index 00000000000..4254038345c
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/FlatMapTransformation.kt
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/MapTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/MapTransformation.kt
new file mode 100644
index 00000000000..6e73f5cc1ca
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/sequence/MapTransformation.kt
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt
new file mode 100644
index 00000000000..528e012b3c1
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/utils.kt
@@ -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 {
+ 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 {
+ 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): 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()
+ 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()
+}
+
+fun KtExpression.nextStatement(): KtExpression? {
+ var statement = unwrapIfLabeled()
+ if (statement.parent !is KtBlockExpression) return null
+ return statement.siblings(forward = true, withItself = false).firstIsInstanceOrNull()
+}
+
+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)
+}
diff --git a/idea/testData/intentions/convertToExpressionBody/keepComments/1.kt.after b/idea/testData/intentions/convertToExpressionBody/keepComments/1.kt.after
index 39a4b2901af..1ab84ac75db 100644
--- a/idea/testData/intentions/convertToExpressionBody/keepComments/1.kt.after
+++ b/idea/testData/intentions/convertToExpressionBody/keepComments/1.kt.after
@@ -1 +1 @@
-fun getText(): String = "xxx" //TODO
\ No newline at end of file
+fun getText(): String = "xxx" //TODO
diff --git a/idea/testData/intentions/convertToExpressionBody/keepComments/2.kt.after b/idea/testData/intentions/convertToExpressionBody/keepComments/2.kt.after
index 54e6c1ad0bf..87ff51102a2 100644
--- a/idea/testData/intentions/convertToExpressionBody/keepComments/2.kt.after
+++ b/idea/testData/intentions/convertToExpressionBody/keepComments/2.kt.after
@@ -1,2 +1,2 @@
fun getText(): String = // let's return xxx
- "xxx" //TODO
\ No newline at end of file
+ "xxx" //TODO
diff --git a/idea/testData/intentions/invertIfCondition/notIn.kt b/idea/testData/intentions/invertIfCondition/notIn.kt
new file mode 100644
index 00000000000..9ca2606a0f6
--- /dev/null
+++ b/idea/testData/intentions/invertIfCondition/notIn.kt
@@ -0,0 +1,5 @@
+fun foo(p: Int) {
+ if (p !in 1..10) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/invertIfCondition/notIn.kt.after b/idea/testData/intentions/invertIfCondition/notIn.kt.after
new file mode 100644
index 00000000000..f22de52c951
--- /dev/null
+++ b/idea/testData/intentions/invertIfCondition/notIn.kt.after
@@ -0,0 +1,3 @@
+fun foo(p: Int) {
+ if (p in 1..10) return
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/.intention b/idea/testData/intentions/loopToCallChain/.intention
new file mode 100644
index 00000000000..4f0d984b5fb
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.loopToCallChain.LoopToCallChainIntention
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/any_ifAssign.kt b/idea/testData/intentions/loopToCallChain/any_ifAssign.kt
new file mode 100644
index 00000000000..b868dd48e20
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/any_ifAssign.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ var found = false
+ for (s in list) {
+ if (s.length > 0) {
+ found = true
+ break
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/any_ifAssign.kt.after b/idea/testData/intentions/loopToCallChain/any_ifAssign.kt.after
new file mode 100644
index 00000000000..fd09759adb2
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/any_ifAssign.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ val found = list.any { it.length > 0 }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/any_ifReturn.kt b/idea/testData/intentions/loopToCallChain/any_ifReturn.kt
new file mode 100644
index 00000000000..4bc14f4a29e
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/any_ifReturn.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+fun foo(list: List): Boolean {
+ for (s in list) {
+ if (s.length > 0) {
+ return true
+ }
+ }
+ return false
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/any_ifReturn.kt.after b/idea/testData/intentions/loopToCallChain/any_ifReturn.kt.after
new file mode 100644
index 00000000000..9ea88dff52e
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/any_ifReturn.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): Boolean {
+ return list.any { it.length > 0 }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/any_return.kt b/idea/testData/intentions/loopToCallChain/any_return.kt
new file mode 100644
index 00000000000..70ee9498b99
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/any_return.kt
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): Boolean {
+ for (s in list) {
+ return true
+ }
+ return false
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/any_return.kt.after b/idea/testData/intentions/loopToCallChain/any_return.kt.after
new file mode 100644
index 00000000000..9fbc88d15c3
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/any_return.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): Boolean {
+ return list.any()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterIsInstance.kt b/idea/testData/intentions/loopToCallChain/filterIsInstance.kt
new file mode 100644
index 00000000000..50c2f1a3972
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterIsInstance.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ for (o in list) {
+ if (o is String) {
+ val length = o.length
+ return length
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterIsInstance.kt.after b/idea/testData/intentions/loopToCallChain/filterIsInstance.kt.after
new file mode 100644
index 00000000000..520d2755b99
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterIsInstance.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ return list
+ .filterIsInstance()
+ .map { it.length }
+ .firstOrNull()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterIsInstance_ifContinue.kt b/idea/testData/intentions/loopToCallChain/filterIsInstance_ifContinue.kt
new file mode 100644
index 00000000000..7a361f37264
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterIsInstance_ifContinue.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ for (o in list) {
+ if (o !is String) continue
+ val length = o.length
+ return length
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterIsInstance_ifContinue.kt.after b/idea/testData/intentions/loopToCallChain/filterIsInstance_ifContinue.kt.after
new file mode 100644
index 00000000000..520d2755b99
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterIsInstance_ifContinue.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ return list
+ .filterIsInstance()
+ .map { it.length }
+ .firstOrNull()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterNotNull.kt b/idea/testData/intentions/loopToCallChain/filterNotNull.kt
new file mode 100644
index 00000000000..9854f3b53ef
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterNotNull.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ for (s in list) {
+ if (s != null) {
+ val length = s.length
+ return length
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterNotNull.kt.after b/idea/testData/intentions/loopToCallChain/filterNotNull.kt.after
new file mode 100644
index 00000000000..e058daf6cd1
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterNotNull.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ return list
+ .filterNotNull()
+ .map { it.length }
+ .firstOrNull()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterNotNull_ifContinue.kt b/idea/testData/intentions/loopToCallChain/filterNotNull_ifContinue.kt
new file mode 100644
index 00000000000..4cad52eab77
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterNotNull_ifContinue.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ for (o in list) {
+ if (o == null) continue
+ val code = o.hashCode()
+ return code
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterNotNull_ifContinue.kt.after b/idea/testData/intentions/loopToCallChain/filterNotNull_ifContinue.kt.after
new file mode 100644
index 00000000000..bd2c62248b0
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterNotNull_ifContinue.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ return list
+ .filterNotNull()
+ .map { it.hashCode() }
+ .firstOrNull()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterNot_ifContinue.kt b/idea/testData/intentions/loopToCallChain/filterNot_ifContinue.kt
new file mode 100644
index 00000000000..4c5d9b99104
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterNot_ifContinue.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ for (s in list) {
+ if (s.isEmpty()) continue
+ val l = s.length
+ return l
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filterNot_ifContinue.kt.after b/idea/testData/intentions/loopToCallChain/filterNot_ifContinue.kt.after
new file mode 100644
index 00000000000..83a3cdd96ff
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filterNot_ifContinue.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ return list
+ .filterNot { it.isEmpty() }
+ .map { it.length }
+ .firstOrNull()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filter_ifContinue.kt b/idea/testData/intentions/loopToCallChain/filter_ifContinue.kt
new file mode 100644
index 00000000000..f7ee7de0713
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filter_ifContinue.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ for (s in list) {
+ if (s.isEmpty()) continue
+ return s
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filter_ifContinue.kt.after b/idea/testData/intentions/loopToCallChain/filter_ifContinue.kt.after
new file mode 100644
index 00000000000..f0cb4a66221
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filter_ifContinue.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ return list.firstOrNull { !it.isEmpty() }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filter_ifContinue2.kt b/idea/testData/intentions/loopToCallChain/filter_ifContinue2.kt
new file mode 100644
index 00000000000..abe2d4462e8
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filter_ifContinue2.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ for (s in list) {
+ if (!s.isEmpty()) continue
+ return s
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filter_ifContinue2.kt.after b/idea/testData/intentions/loopToCallChain/filter_ifContinue2.kt.after
new file mode 100644
index 00000000000..68cbd908b51
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filter_ifContinue2.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ return list.firstOrNull { it.isEmpty() }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filter_ifContinueInBlock.kt b/idea/testData/intentions/loopToCallChain/filter_ifContinueInBlock.kt
new file mode 100644
index 00000000000..9c7df778b7f
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filter_ifContinueInBlock.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ for (s in list) {
+ if (s.isEmpty()) {
+ continue
+ }
+ return s
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filter_ifContinueInBlock.kt.after b/idea/testData/intentions/loopToCallChain/filter_ifContinueInBlock.kt.after
new file mode 100644
index 00000000000..f0cb4a66221
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filter_ifContinueInBlock.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ return list.firstOrNull { !it.isEmpty() }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/filter_ifContinueWithLabel.kt b/idea/testData/intentions/loopToCallChain/filter_ifContinueWithLabel.kt
new file mode 100644
index 00000000000..b81b4c6ff44
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/filter_ifContinueWithLabel.kt
@@ -0,0 +1,16 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(): String? {
+ Loop@
+ while (x()) {
+ for (s in list()) {
+ if (s.isEmpty()) continue@Loop
+ return s
+ }
+ return null
+ }
+ return null
+}
+
+fun x(): Boolean = false
+fun list(): List = listOf()
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign.kt
new file mode 100644
index 00000000000..913ba2f7717
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ var result: String? = null
+ for (s in list) {
+ if (s.length > 0) {
+ result = s
+ break
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign.kt.after b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign.kt.after
new file mode 100644
index 00000000000..59861dac227
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ val result: String? = list.firstOrNull { it.length > 0 }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_breakWithLabel.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_breakWithLabel.kt
new file mode 100644
index 00000000000..da5f9e0d895
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_breakWithLabel.kt
@@ -0,0 +1,16 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo() {
+ MainLoop@
+ for (i in 1..10) {
+ var result: String? = null
+ for (s in list()) {
+ if (s.length > 0) {
+ result = s
+ break@MainLoop
+ }
+ }
+ }
+}
+
+fun list(): List = listOf()
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_cannotBeVal.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_cannotBeVal.kt
new file mode 100644
index 00000000000..68f50b6227c
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_cannotBeVal.kt
@@ -0,0 +1,12 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ var result: String? = null
+ for (s in list) {
+ if (s.length > 0) {
+ result = s
+ break
+ }
+ }
+
+ result += "1"
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_cannotBeVal.kt.after b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_cannotBeVal.kt.after
new file mode 100644
index 00000000000..62b74844596
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_cannotBeVal.kt.after
@@ -0,0 +1,6 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ var result: String? = list.firstOrNull { it.length > 0 }
+
+ result += "1"
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_preserveComments.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_preserveComments.kt
new file mode 100644
index 00000000000..ee520cce9b1
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_preserveComments.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ var result: String? = null
+ 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
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_preserveComments.kt.after b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_preserveComments.kt.after
new file mode 100644
index 00000000000..a9b987bec34
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_preserveComments.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List) {
+ 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
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_variableUsedBefore.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_variableUsedBefore.kt
new file mode 100644
index 00000000000..16769bfd896
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifAssign_variableUsedBefore.kt
@@ -0,0 +1,11 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(list: List) {
+ var result: String? = null
+ for (s in list) {
+ if (s != result) {
+ result = s
+ break
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifReturn.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_ifReturn.kt
new file mode 100644
index 00000000000..dbeb013cf82
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifReturn.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ for (s in list) {
+ if (s.length > 0) {
+ return s
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_ifReturn.kt.after b/idea/testData/intentions/loopToCallChain/firstOrNull_ifReturn.kt.after
new file mode 100644
index 00000000000..8e5db467919
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_ifReturn.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ return list.firstOrNull { it.length > 0 }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_return.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_return.kt
new file mode 100644
index 00000000000..1ddc0d2242f
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_return.kt
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ for (s in list) {
+ return s
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_return.kt.after b/idea/testData/intentions/loopToCallChain/firstOrNull_return.kt.after
new file mode 100644
index 00000000000..8b73d9828be
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_return.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ return list.firstOrNull()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_returnExpression.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_returnExpression.kt
new file mode 100644
index 00000000000..0b5313bec07
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_returnExpression.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(list: List): Int? {
+ for (s in list) {
+ if (s.isNotEmpty()) {
+ return s.length
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_returnNotNullIfNone.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_returnNotNullIfNone.kt
new file mode 100644
index 00000000000..a2ff283ea35
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_returnNotNullIfNone.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(list: List): String {
+ for (s in list) {
+ if (s.isNotEmpty()) {
+ return s
+ }
+ }
+ return ""
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_return_comment.kt b/idea/testData/intentions/loopToCallChain/firstOrNull_return_comment.kt
new file mode 100644
index 00000000000..f6361b940e7
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_return_comment.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ for (s in list) {
+ return s
+ }
+ // return null if not found
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/firstOrNull_return_comment.kt.after b/idea/testData/intentions/loopToCallChain/firstOrNull_return_comment.kt.after
new file mode 100644
index 00000000000..6c995a36a6d
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/firstOrNull_return_comment.kt.after
@@ -0,0 +1,5 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ // return null if not found
+ return list.firstOrNull()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/flatMap.kt b/idea/testData/intentions/loopToCallChain/flatMap.kt
new file mode 100644
index 00000000000..c47aa331112
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/flatMap.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ for (s in list) {
+ for (line in s.lines()) {
+ if (line.isNotBlank()) return line
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/flatMap.kt.after b/idea/testData/intentions/loopToCallChain/flatMap.kt.after
new file mode 100644
index 00000000000..5f01a657855
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/flatMap.kt.after
@@ -0,0 +1,6 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ return list
+ .flatMap { it.lines() }
+ .firstOrNull { it.isNotBlank() }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/flatMapWithBreak.kt b/idea/testData/intentions/loopToCallChain/flatMapWithBreak.kt
new file mode 100644
index 00000000000..2640ad0df8f
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/flatMapWithBreak.kt
@@ -0,0 +1,14 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ var result: String? = null
+ MainLoop@
+ for (s in list) {
+ for (line in s.lines()) {
+ if (line.isNotBlank()) {
+ result = line
+ break@MainLoop
+ }
+ }
+ }
+ return result
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/flatMapWithBreak.kt.after b/idea/testData/intentions/loopToCallChain/flatMapWithBreak.kt.after
new file mode 100644
index 00000000000..5032a175aef
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/flatMapWithBreak.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+fun foo(list: List): String? {
+ val result: String? = list
+ .flatMap { it.lines() }
+ .firstOrNull { it.isNotBlank() }
+ return result
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/flatMapWithWrongBreak.kt b/idea/testData/intentions/loopToCallChain/flatMapWithWrongBreak.kt
new file mode 100644
index 00000000000..bc287cf6c91
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/flatMapWithWrongBreak.kt
@@ -0,0 +1,14 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(list: List): String? {
+ var result: String? = null
+ for (s in list) {
+ for (line in s.lines()) {
+ if (line.isNotBlank()) {
+ result = line
+ break
+ }
+ }
+ }
+ return result
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/flatMap_notIterable.kt b/idea/testData/intentions/loopToCallChain/flatMap_notIterable.kt
new file mode 100644
index 00000000000..d785cf65247
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/flatMap_notIterable.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(list: List): Char? {
+ for (s in list) {
+ for (c in s) {
+ if (c != ' ') return c
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/itAlreadyUsed.kt b/idea/testData/intentions/loopToCallChain/itAlreadyUsed.kt
new file mode 100644
index 00000000000..49a0b5f5737
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/itAlreadyUsed.kt
@@ -0,0 +1,10 @@
+// WITH_RUNTIME
+fun foo(list: List, it: Int) {
+ var found = false
+ for (s in list) {
+ if (s.length > it) {
+ found = true
+ break
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/itAlreadyUsed.kt.after b/idea/testData/intentions/loopToCallChain/itAlreadyUsed.kt.after
new file mode 100644
index 00000000000..5f97566217b
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/itAlreadyUsed.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List, it: Int) {
+ val found = list.any { s -> s.length > it }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/map.kt b/idea/testData/intentions/loopToCallChain/map.kt
new file mode 100644
index 00000000000..0d8950238d0
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/map.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ for (s in list) {
+ val length = s.length
+ if (length > 0) return length
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/map.kt.after b/idea/testData/intentions/loopToCallChain/map.kt.after
new file mode 100644
index 00000000000..5311e57e96a
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/map.kt.after
@@ -0,0 +1,6 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ return list
+ .map { it.length }
+ .firstOrNull { it > 0 }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/mapVar.kt b/idea/testData/intentions/loopToCallChain/mapVar.kt
new file mode 100644
index 00000000000..eb80ed93774
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/mapVar.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ for (s in list) {
+ var length = s.length
+ if (length > 0) return length
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/mapVar.kt.after b/idea/testData/intentions/loopToCallChain/mapVar.kt.after
new file mode 100644
index 00000000000..5311e57e96a
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/mapVar.kt.after
@@ -0,0 +1,6 @@
+// WITH_RUNTIME
+fun foo(list: List): Int? {
+ return list
+ .map { it.length }
+ .firstOrNull { it > 0 }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/map_var.kt b/idea/testData/intentions/loopToCallChain/map_var.kt
new file mode 100644
index 00000000000..f3b31c57f11
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/map_var.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(list: List): Int? {
+ for (s in list) {
+ var length = s.length
+ if (length > 0) return ++length
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/map_variableStillNeeded.kt b/idea/testData/intentions/loopToCallChain/map_variableStillNeeded.kt
new file mode 100644
index 00000000000..45118e9ca89
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/map_variableStillNeeded.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+fun foo(list: List): String? {
+ for (s in list) {
+ val length = s.length
+ if (length > 0) return s + length
+ }
+ return null
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/none_ifReturn.kt b/idea/testData/intentions/loopToCallChain/none_ifReturn.kt
new file mode 100644
index 00000000000..b96faa94eb8
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/none_ifReturn.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+fun foo(list: List): Boolean {
+ for (s in list) {
+ if (s.length > 0) {
+ return false
+ }
+ }
+ return true
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/loopToCallChain/none_ifReturn.kt.after b/idea/testData/intentions/loopToCallChain/none_ifReturn.kt.after
new file mode 100644
index 00000000000..2113034825a
--- /dev/null
+++ b/idea/testData/intentions/loopToCallChain/none_ifReturn.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun foo(list: List): Boolean {
+ return list.none { it.length > 0 }
+}
\ No newline at end of file