KT-14294 for to stdlib to firstOrNull/lastOrNull: intention is absent if there is additional var before for loop

#KT-14294 Fixed
This commit is contained in:
Valentin Kipyatkov
2016-10-14 21:43:47 +03:00
parent a89ef54578
commit eae23b548f
20 changed files with 287 additions and 18 deletions
@@ -16,12 +16,18 @@
package org.jetbrains.kotlin.idea.intentions.loopToCallChain
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
import org.jetbrains.kotlin.idea.core.copied
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
import org.jetbrains.kotlin.resolve.scopes.utils.findFunction
import org.jetbrains.kotlin.resolve.scopes.utils.findPackage
import org.jetbrains.kotlin.resolve.scopes.utils.findVariable
/**
* Base class for [ResultTransformation]'s that replaces the loop-expression with the result call chain
@@ -30,8 +36,9 @@ abstract class ReplaceLoopResultTransformation(final override val loop: KtForExp
override val commentSavingRange = PsiChildRange.singleElement(loop.unwrapIfLabeled())
override val expressionToBeReplacedByResultCallChain: KtExpression
get() = loop.unwrapIfLabeled()
override fun generateExpressionToReplaceLoopAndCheckErrors(resultCallChain: KtExpression): KtExpression {
return resultCallChain
}
override fun convertLoop(resultCallChain: KtExpression, commentSavingRangeHolder: CommentSavingRangeHolder): KtExpression {
return loop.unwrapIfLabeled().replaced(resultCallChain)
@@ -48,8 +55,30 @@ abstract class AssignToVariableResultTransformation(
override val commentSavingRange = PsiChildRange(initialization.initializationStatement, loop.unwrapIfLabeled())
override val expressionToBeReplacedByResultCallChain: KtExpression
get() = initialization.initializer
override fun generateExpressionToReplaceLoopAndCheckErrors(resultCallChain: KtExpression): KtExpression {
val psiFactory = KtPsiFactory(resultCallChain)
val initializationStatement = initialization.initializationStatement
if (initializationStatement is KtVariableDeclaration) {
val resolutionScope = loop.getResolutionScope()
fun isUniqueName(name: String): Boolean {
val identifier = Name.identifier(name)
return resolutionScope.findVariable(identifier, NoLookupLocation.FROM_IDE) == null
&& resolutionScope.findFunction(identifier, NoLookupLocation.FROM_IDE) == null
&& resolutionScope.findClassifier(identifier, NoLookupLocation.FROM_IDE) == null
&& resolutionScope.findPackage(identifier) == null
}
val uniqueName = KotlinNameSuggester.suggestNameByName("test", ::isUniqueName)
val copy = initializationStatement.copied()
copy.initializer!!.replace(resultCallChain)
copy.setName(uniqueName)
return copy
}
else {
return psiFactory.createExpressionByPattern("$0 = $1", initialization.variable.nameAsSafeName, resultCallChain)
}
}
override fun convertLoop(resultCallChain: KtExpression, commentSavingRangeHolder: CommentSavingRangeHolder): KtExpression {
initialization.initializer.replace(resultCallChain)
@@ -79,7 +79,7 @@ interface ResultTransformation : Transformation {
val commentSavingRange: PsiChildRange
val expressionToBeReplacedByResultCallChain: KtExpression
fun generateExpressionToReplaceLoopAndCheckErrors(resultCallChain: KtExpression): KtExpression
/**
* Implementations of this method are obliged to update [commentSavingRangeHolder] when deleting or adding any element into the tree
@@ -293,8 +293,8 @@ private fun checkSmartCastsPreserved(loop: KtForExpression, matchResult: MatchRe
if (preservedSmartCastCount == smartCastCount) return true
// not all smart cast expressions has been found in the result or have the same type after conversion, perform more expensive check
val expressionToBeReplaced = matchResult.transformationMatch.resultTransformation.expressionToBeReplacedByResultCallChain
if (!tryChangeAndCheckErrors(expressionToBeReplaced, loop) { it.replace(callChain) }) return false
val expression = matchResult.transformationMatch.resultTransformation.generateExpressionToReplaceLoopAndCheckErrors(callChain)
if (!tryChangeAndCheckErrors(loop) { it.replace(expression) }) return false
return true
}
@@ -135,8 +135,9 @@ object FindTransformationMatcher : TransformationMatcher {
return generator.generate(chainedCallGenerator)
}
override val expressionToBeReplacedByResultCallChain: KtExpression
get() = endReturn.returnedExpression!!
override fun generateExpressionToReplaceLoopAndCheckErrors(resultCallChain: KtExpression): KtExpression {
return KtPsiFactory(resultCallChain).createExpressionByPattern("return $0", resultCallChain)
}
override fun convertLoop(resultCallChain: KtExpression, commentSavingRangeHolder: CommentSavingRangeHolder): KtExpression {
endReturn.returnedExpression!!.replace(resultCallChain)
@@ -230,7 +230,7 @@ fun canChangeLocalVariableType(variable: KtProperty, newTypeText: String, loop:
fun <TExpression : KtExpression> tryChangeAndCheckErrors(
expressionToChange: TExpression,
scopeToExclude: KtElement,
scopeToExclude: KtElement? = null,
performChange: (TExpression) -> Unit
): Boolean {
val bindingContext = expressionToChange.analyze(BodyResolveMode.FULL)
@@ -246,19 +246,19 @@ fun <TExpression : KtExpression> tryChangeAndCheckErrors(
val SCOPE_TO_EXCLUDE = Key<Unit>("SCOPE_TO_EXCLUDE")
expressionToChange.putCopyableUserData(EXPRESSION, Unit)
scopeToExclude.putCopyableUserData(SCOPE_TO_EXCLUDE, Unit)
scopeToExclude?.putCopyableUserData(SCOPE_TO_EXCLUDE, Unit)
val blockCopy = block.copied()
val expressionCopy: TExpression
val scopeToExcludeCopy: KtElement
val scopeToExcludeCopy: KtElement?
@Suppress("UNCHECKED_CAST")
try {
expressionCopy = blockCopy.findDescendantOfType<KtExpression> { it.getCopyableUserData(EXPRESSION) != null } as TExpression
scopeToExcludeCopy = blockCopy.findDescendantOfType<KtElement> { it.getCopyableUserData(SCOPE_TO_EXCLUDE) != null }!!
scopeToExcludeCopy = blockCopy.findDescendantOfType<KtElement> { it.getCopyableUserData(SCOPE_TO_EXCLUDE) != null }
}
finally {
expressionToChange.putCopyableUserData(EXPRESSION, null)
scopeToExclude.putCopyableUserData(SCOPE_TO_EXCLUDE, null)
scopeToExclude?.putCopyableUserData(SCOPE_TO_EXCLUDE, null)
}
performChange(expressionCopy)
@@ -0,0 +1,18 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'filterNotNull().firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().filterNotNull().firstOrNull{}'"
fun getFirstValue() = "value"
fun foo(list: List<String?>): String? {
var found: String? = null
val value = getFirstValue()
<caret>for (s in list)
if (s != null)
if(!s.startsWith("IMG:"))
if (s.contains(value)) {
found = s
break
}
return found
}
@@ -0,0 +1,13 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'filterNotNull().firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().filterNotNull().firstOrNull{}'"
fun getFirstValue() = "value"
fun foo(list: List<String?>): String? {
val value = getFirstValue()
val <caret>found: String? = list
.filterNotNull()
.firstOrNull { !it.startsWith("IMG:") && it.contains(value) }
return found
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'filterNotNull().firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().filterNotNull().firstOrNull{}'"
fun getFirstValue() = "value"
fun foo(list: List<String?>): String? {
val value = getFirstValue()
val <caret>found: String? = list
.asSequence()
.filterNotNull()
.firstOrNull { !it.startsWith("IMG:") && it.contains(value) }
return found
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.firstOrNull{}'"
fun foo(list: List<String>, o: Any) {
if (o is CharSequence) {
var result: Any? = null
<caret>for (s in list) {
val a = s.length + (o as String).capitalize().hashCode()
val x = a * o.length
if (x > 1000) {
result = x
break
}
}
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.firstOrNull{}'"
fun foo(list: List<String>, o: Any) {
if (o is CharSequence) {
val result: Any? = list
.map { it.length + (o as String).capitalize().hashCode() }
.map { it * o.length }
.firstOrNull { it > 1000 }
}
}
@@ -0,0 +1,12 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.firstOrNull{}'"
fun foo(list: List<String>, o: Any) {
if (o is CharSequence) {
val result: Any? = list
.asSequence()
.map { it.length + (o as String).capitalize().hashCode() }
.map { it * o.length }
.firstOrNull { it > 1000 }
}
}
@@ -0,0 +1,17 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.firstOrNull{}'"
fun foo(list: List<String>, o: Any) {
var result: Any? = ""
if (o is CharSequence) {
result = null
<caret>for (s in list) {
val a = s.length + (o as String).capitalize().hashCode()
val x = a * o.length
if (x > 1000) {
result = x
break
}
}
}
}
@@ -0,0 +1,12 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.firstOrNull{}'"
fun foo(list: List<String>, o: Any) {
var result: Any? = ""
if (o is CharSequence) {
result = list
.map { it.length + (o as String).capitalize().hashCode() }
.map { it * o.length }
.firstOrNull { it > 1000 }
}
}
@@ -0,0 +1,13 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.firstOrNull{}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.firstOrNull{}'"
fun foo(list: List<String>, o: Any) {
var result: Any? = ""
if (o is CharSequence) {
result = list
.asSequence()
.map { it.length + (o as String).capitalize().hashCode() }
.map { it * o.length }
.firstOrNull { it > 1000 }
}
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.filterTo(){}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.filterTo(){}'"
fun foo(list: List<String>, o: Any, result: MutableCollection<Int>) {
if (o is CharSequence) {
<caret>for (s in list) {
val a = s.length + (o as String).capitalize().hashCode()
val x = a * o.length
if (x > 1000) {
result.add(x)
}
}
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.filterTo(){}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.filterTo(){}'"
fun foo(list: List<String>, o: Any, result: MutableCollection<Int>) {
if (o is CharSequence) {
list
.map { it.length + (o as String).capitalize().hashCode() }
.map { it * o.length }
.filterTo(result) { it > 1000 }
}
}
@@ -0,0 +1,12 @@
// WITH_RUNTIME
// INTENTION_TEXT: "Replace with 'map{}.map{}.filterTo(){}'"
// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.map{}.filterTo(){}'"
fun foo(list: List<String>, o: Any, result: MutableCollection<Int>) {
if (o is CharSequence) {
list
.asSequence()
.map { it.length + (o as String).capitalize().hashCode() }
.map { it * o.length }
.filterTo(result) { it > 1000 }
}
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
// IS_APPLICABLE_2: false
fun foo(list: List<Any>) {
var v: String?
v = null
<caret>for (o in list) {
if (bar(o as String)) {
v = o
break
}
}
}
fun bar(s: String): Boolean = true
@@ -613,6 +613,12 @@ public class IntentionTest2Generated extends AbstractIntentionTest2 {
doTest(fileName);
}
@TestMetadata("KT14292.kt")
public void testKT14292() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/firstOrNull/KT14292.kt");
doTest(fileName);
}
@TestMetadata("let.kt")
public void testLet() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/firstOrNull/let.kt");
@@ -1174,6 +1180,24 @@ public class IntentionTest2Generated extends AbstractIntentionTest2 {
doTest(fileName);
}
@TestMetadata("smartCastNotBroken3.kt")
public void testSmartCastNotBroken3() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotBroken3.kt");
doTest(fileName);
}
@TestMetadata("smartCastNotBroken4.kt")
public void testSmartCastNotBroken4() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotBroken4.kt");
doTest(fileName);
}
@TestMetadata("smartCastNotBroken5.kt")
public void testSmartCastNotBroken5() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotBroken5.kt");
doTest(fileName);
}
@TestMetadata("smartCastNotNullRequired.kt")
public void testSmartCastNotNullRequired() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotNullRequired.kt");
@@ -1216,6 +1240,12 @@ public class IntentionTest2Generated extends AbstractIntentionTest2 {
doTest(fileName);
}
@TestMetadata("smartCastRequired6.kt")
public void testSmartCastRequired6() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastRequired6.kt");
doTest(fileName);
}
@TestMetadata("smartCastThisRequired.kt")
public void testSmartCastThisRequired() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastThisRequired.kt");
@@ -8521,6 +8521,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
doTest(fileName);
}
@TestMetadata("KT14292.kt")
public void testKT14292() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/firstOrNull/KT14292.kt");
doTest(fileName);
}
@TestMetadata("let.kt")
public void testLet() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/firstOrNull/let.kt");
@@ -9082,6 +9088,24 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
doTest(fileName);
}
@TestMetadata("smartCastNotBroken3.kt")
public void testSmartCastNotBroken3() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotBroken3.kt");
doTest(fileName);
}
@TestMetadata("smartCastNotBroken4.kt")
public void testSmartCastNotBroken4() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotBroken4.kt");
doTest(fileName);
}
@TestMetadata("smartCastNotBroken5.kt")
public void testSmartCastNotBroken5() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotBroken5.kt");
doTest(fileName);
}
@TestMetadata("smartCastNotNullRequired.kt")
public void testSmartCastNotNullRequired() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastNotNullRequired.kt");
@@ -9124,6 +9148,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
doTest(fileName);
}
@TestMetadata("smartCastRequired6.kt")
public void testSmartCastRequired6() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastRequired6.kt");
doTest(fileName);
}
@TestMetadata("smartCastThisRequired.kt")
public void testSmartCastThisRequired() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/smartCasts/smartCastThisRequired.kt");