If to when: label break / continue if necessary #KT-11427 Fixed

This commit is contained in:
Mikhail Glukhikh
2018-04-28 18:12:50 +03:00
parent 2812601e2d
commit a8ad980910
19 changed files with 389 additions and 68 deletions
@@ -18,18 +18,20 @@ package org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.codeStyle.CodeStyleManager
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.getSubjectToIntroduce
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.introduceSubject
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.unwrapBlockOrParenthesis
import org.jetbrains.kotlin.idea.quickfix.AddLoopLabelFix
import org.jetbrains.kotlin.idea.util.CommentSaver
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.psi.psiUtil.*
import java.util.*
class IfToWhenIntention : SelfTargetingRangeIntention<KtIfExpression>(KtIfExpression::class.java, "Replace 'if' with 'when'") {
@@ -80,6 +82,36 @@ class IfToWhenIntention : SelfTargetingRangeIntention<KtIfExpression>(KtIfExpres
return result
}
private class LabelLoopJumpVisitor(private val nearestLoopIfAny: KtLoopExpression?) : KtVisitorVoid() {
val labelName: String? by lazy {
nearestLoopIfAny?.let { loop ->
(loop.parent as? KtLabeledExpression)?.getLabelName() ?: AddLoopLabelFix.getUniqueLabelName(loop)
}
}
fun KtExpressionWithLabel.addLabelIfNecessary(): KtExpressionWithLabel {
if (this.getLabelName() != null) return this
if (this.getStrictParentOfType<KtLoopExpression>() != nearestLoopIfAny) return this
if (labelName != null) {
val jumpWithLabel = KtPsiFactory(project).createExpression("$text@$labelName") as KtExpressionWithLabel
return replaced(jumpWithLabel)
}
return this
}
override fun visitBreakExpression(expression: KtBreakExpression) {
expression.addLabelIfNecessary()
}
override fun visitContinueExpression(expression: KtContinueExpression) {
expression.addLabelIfNecessary()
}
override fun visitKtElement(element: KtElement) {
element.acceptChildren(this)
}
}
private fun BuilderByPattern<*>.appendElseBlock(block: KtExpression?) {
appendFixedText("else->")
appendExpression(block?.unwrapBlockOrParenthesis())
@@ -93,6 +125,8 @@ class IfToWhenIntention : SelfTargetingRangeIntention<KtIfExpression>(KtIfExpres
val toDelete = ArrayList<PsiElement>()
var applyFullCommentSaver = true
val loop = element.getStrictParentOfType<KtLoopExpression>()
val loopJumpVisitor = LabelLoopJumpVisitor(loop)
var whenExpression = KtPsiFactory(element).buildExpression {
appendFixedText("when {\n")
@@ -146,9 +180,27 @@ class IfToWhenIntention : SelfTargetingRangeIntention<KtIfExpression>(KtIfExpres
whenExpression = whenExpression.introduceSubject()
}
val result = element.replace(whenExpression)
val result = element.replaced(whenExpression)
(if (applyFullCommentSaver) fullCommentSaver else elementCommentSaver).restore(result)
toDelete.forEach(PsiElement::delete)
result.accept(loopJumpVisitor)
val labelName = loopJumpVisitor.labelName
if (loop != null && labelName != null && loop.parent !is KtLabeledExpression) {
val labeledLoopExpression = KtPsiFactory(result).createLabeledExpression(labelName)
labeledLoopExpression.baseExpression!!.replace(loop)
val replacedLabeledLoopExpression = loop.replace(labeledLoopExpression)
// For some reason previous operation can break adjustments
val project = loop.project
if (editor != null) {
val documentManager = PsiDocumentManager.getInstance(project)
documentManager.commitDocument(editor.document)
documentManager.doPostponedOperationsAndUnblockDocument(editor.document)
val psiFile = documentManager.getPsiFile(editor.document)!!
CodeStyleManager.getInstance(project).adjustLineIndent(psiFile, replacedLabeledLoopExpression.textRange)
}
}
}
private fun MutableList<KtExpression>.addOrBranches(expression: KtExpression): List<KtExpression> {
@@ -40,7 +40,7 @@ class AddLoopLabelFix(
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return
val labelName = existingLabelName ?: getUniqueLabelName(collectUsedLabels(element))
val labelName = existingLabelName ?: getUniqueLabelName(element)
val jumpWithLabel = KtPsiFactory(project).createExpression(jumpExpression.text + "@" + labelName)
jumpExpression.replace(jumpWithLabel)
@@ -55,31 +55,6 @@ class AddLoopLabelFix(
// TODO(yole) We should initiate in-place rename for the label here, but in-place rename for labels is not yet implemented
}
private fun collectUsedLabels(element: KtElement): Set<String> {
val usedLabels = hashSetOf<String>()
element.acceptChildren(object : KtTreeVisitorVoid() {
override fun visitLabeledExpression(expression: KtLabeledExpression) {
super.visitLabeledExpression(expression)
usedLabels.add(expression.getLabelName()!!)
}
})
element.parents.forEach {
if (it is KtLabeledExpression) {
usedLabels.add(it.getLabelName()!!)
}
}
return usedLabels
}
private fun getUniqueLabelName(existingNames: Collection<String>): String {
var index = 0
var result = "loop"
while (result in existingNames) {
result = "loop${++index}"
}
return result
}
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val element = diagnostic.psiElement as? KtExpressionWithLabel
@@ -88,5 +63,33 @@ class AddLoopLabelFix(
val loop = element?.getStrictParentOfType<KtLoopExpression>() ?: return null
return AddLoopLabelFix(loop, element)
}
private fun collectUsedLabels(element: KtElement): Set<String> {
val usedLabels = hashSetOf<String>()
element.acceptChildren(object : KtTreeVisitorVoid() {
override fun visitLabeledExpression(expression: KtLabeledExpression) {
super.visitLabeledExpression(expression)
usedLabels.add(expression.getLabelName()!!)
}
})
element.parents.forEach {
if (it is KtLabeledExpression) {
usedLabels.add(it.getLabelName()!!)
}
}
return usedLabels
}
private fun getUniqueLabelName(existingNames: Collection<String>): String {
var index = 0
var result = "loop"
while (result in existingNames) {
result = "loop${++index}"
}
return result
}
fun getUniqueLabelName(loop: KtLoopExpression): String =
getUniqueLabelName(collectUsedLabels(loop))
}
}
@@ -0,0 +1,17 @@
// WITH_RUNTIME
fun testIf(xs: List<Any>) {
for (x in xs) {
<caret>if (x is String) {
for (c in x) {
continue // do not change
}
}
else if (x is Int) {
break
}
else {
println(x)
}
}
}
@@ -0,0 +1,13 @@
// WITH_RUNTIME
fun testIf(xs: List<Any>) {
loop@ for (x in xs) {
when (x) {
is String -> for (c in x) {
continue // do not change
}
is Int -> break@loop
else -> println(x)
}
}
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
fun testIf(x: Any) {
<caret>if (x is String) {
println(x)
for (c in x) {
if (c == ' ')
break // do not change
}
}
else {
println(x)
}
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
fun testIf(x: Any) {
when (x) {
is String -> {
println(x)
for (c in x) {
if (c == ' ')
break // do not change
}
}
else -> println(x)
}
}
@@ -0,0 +1,6 @@
fun test() {
for (i in -2..2) {
<caret>if (i > 0) i.hashCode()
else continue
}
}
@@ -0,0 +1,8 @@
fun test() {
loop@ for (i in -2..2) {
when {
i > 0 -> i.hashCode()
else -> continue@loop
}
}
}
@@ -0,0 +1,22 @@
fun test() {
loop@ while (true) {
for (i in -10..10) {
<caret>if (i < 0) {
if (i < -5) {
break
} else {
continue@loop
}
} else {
if (i == 0) {
i.hashCode()
break
} else if (i > 5) {
i.hashCode()
} else {
continue
}
}
}
}
}
@@ -0,0 +1,21 @@
fun test() {
loop@ while (true) {
loop1@ for (i in -10..10) {
when {
i < 0 -> if (i < -5) {
break@loop1
} else {
continue@loop
}
else -> if (i == 0) {
i.hashCode()
break@loop1
} else if (i > 5) {
i.hashCode()
} else {
continue@loop1
}
}
}
}
}
@@ -0,0 +1,31 @@
fun test() {
// Comment 1
loop@ while (true) {
// Comment 2
for (i in -10..10) {
// Comment 3
if (i < 0) {
// Comment 4
if (i < -5) {
break
} else {
// Comment 5
continue@loop
}
} else {
// Comment 6
<caret>if (i == 0) {
i.hashCode()
// Comment 7
break
} else if (i > 5) {
// Comment 8
i.hashCode()
} else {
// Comment 9
continue
}
}
}
}
}
@@ -0,0 +1,31 @@
fun test() {
// Comment 1
loop@ while (true) {
// Comment 2
loop1@ for (i in -10..10) {
// Comment 3
if (i < 0) {
// Comment 4
if (i < -5) {
break
} else {
// Comment 5
continue@loop
}
} else {
// Comment 6
when {
i == 0 -> {
i.hashCode()
// Comment 7
break@loop1
}
i > 5 -> // Comment 8
i.hashCode()
else -> // Comment 9
continue@loop1
}
}
}
}
}
@@ -0,0 +1,6 @@
fun test() {
myLoop@ for (i in -2..2) {
<caret>if (i > 0) i.hashCode()
else continue
}
}
@@ -0,0 +1,8 @@
fun test() {
myLoop@ for (i in -2..2) {
when {
i > 0 -> i.hashCode()
else -> continue@myLoop
}
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun testIf(xs: List<Any>) {
for (x in xs) {
<caret>if (x is String) continue
else if (x is Int) break
else println(x)
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
fun testIf(xs: List<Any>) {
loop@ for (x in xs) {
when (x) {
is String -> continue@loop
is Int -> break@loop
else -> println(x)
}
}
}
@@ -0,0 +1,14 @@
fun test() {
// Loop comment
for (i in -2..2) {
// Some comment
<caret>if (i < 0) {
// Very important comment
break
}
else {
// More comments
i.hashCode()
}
}
}
@@ -0,0 +1,12 @@
fun test() {
// Loop comment
loop@ for (i in -2..2) {
// Some comment
when {
i < 0 -> // Very important comment
break@loop
else -> // More comments
i.hashCode()
}
}
}
@@ -2545,6 +2545,46 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
public void testWithAnnotation() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withAnnotation.kt");
}
@TestMetadata("withInternalLoop.kt")
public void testWithInternalLoop() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withInternalLoop.kt");
}
@TestMetadata("withInternalLoopOnly.kt")
public void testWithInternalLoopOnly() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withInternalLoopOnly.kt");
}
@TestMetadata("withLoop.kt")
public void testWithLoop() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withLoop.kt");
}
@TestMetadata("withLoopDeep.kt")
public void testWithLoopDeep() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withLoopDeep.kt");
}
@TestMetadata("withLoopDeepAndComments.kt")
public void testWithLoopDeepAndComments() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withLoopDeepAndComments.kt");
}
@TestMetadata("withLoopExistingLabel.kt")
public void testWithLoopExistingLabel() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withLoopExistingLabel.kt");
}
@TestMetadata("withLoopOriginal.kt")
public void testWithLoopOriginal() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withLoopOriginal.kt");
}
@TestMetadata("withLoopThen.kt")
public void testWithLoopThen() throws Exception {
runTest("idea/testData/intentions/branched/ifWhen/ifToWhen/withLoopThen.kt");
}
}
@TestMetadata("idea/testData/intentions/branched/ifWhen/whenToIf")
@@ -3998,80 +4038,72 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ConvertArrayParameterToVararg extends AbstractIntentionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInConvertArrayParameterToVararg() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertArrayParameterToVararg"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("arrayGenericType.kt")
public void testArrayGenericType() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/arrayGenericType.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/arrayGenericType.kt");
}
@TestMetadata("arrayInt.kt")
public void testArrayInt() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/arrayInt.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/arrayInt.kt");
}
@TestMetadata("arrayString.kt")
public void testArrayString() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/arrayString.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/arrayString.kt");
}
@TestMetadata("inConstructor.kt")
public void testInConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/inConstructor.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/inConstructor.kt");
}
@TestMetadata("inLambda.kt")
public void testInLambda() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/inLambda.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/inLambda.kt");
}
@TestMetadata("intArray.kt")
public void testIntArray() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/intArray.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/intArray.kt");
}
@TestMetadata("longArray.kt")
public void testLongArray() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/longArray.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/longArray.kt");
}
@TestMetadata("starProjection.kt")
public void testStarProjection() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/starProjection.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/starProjection.kt");
}
@TestMetadata("vararg.kt")
public void testVararg() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/vararg.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/vararg.kt");
}
@TestMetadata("withContravariant.kt")
public void testWithContravariant() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/withContravariant.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/withContravariant.kt");
}
@TestMetadata("withCovariance.kt")
public void testWithCovariance() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/withCovariance.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/withCovariance.kt");
}
@TestMetadata("withDefaultValue.kt")
public void testWithDefaultValue() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertArrayParameterToVararg/withDefaultValue.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertArrayParameterToVararg/withDefaultValue.kt");
}
}
@@ -7460,50 +7492,47 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ConvertVarargParameterToArray extends AbstractIntentionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInConvertVarargParameterToArray() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertVarargParameterToArray"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("genericType.kt")
public void testGenericType() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertVarargParameterToArray/genericType.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertVarargParameterToArray/genericType.kt");
}
@TestMetadata("inConstructor.kt")
public void testInConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertVarargParameterToArray/inConstructor.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertVarargParameterToArray/inConstructor.kt");
}
@TestMetadata("int.kt")
public void testInt() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertVarargParameterToArray/int.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertVarargParameterToArray/int.kt");
}
@TestMetadata("long.kt")
public void testLong() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertVarargParameterToArray/long.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertVarargParameterToArray/long.kt");
}
@TestMetadata("noVararg.kt")
public void testNoVararg() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertVarargParameterToArray/noVararg.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertVarargParameterToArray/noVararg.kt");
}
@TestMetadata("string.kt")
public void testString() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertVarargParameterToArray/string.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertVarargParameterToArray/string.kt");
}
@TestMetadata("withDefaultValue.kt")
public void testWithDefaultValue() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertVarargParameterToArray/withDefaultValue.kt");
doTest(fileName);
runTest("idea/testData/intentions/convertVarargParameterToArray/withDefaultValue.kt");
}
}