From 0158444eae1ecd8cd8f51fa0dfe0de9675d79505 Mon Sep 17 00:00:00 2001 From: Anna Kozlova Date: Tue, 3 Dec 2024 13:52:49 +0100 Subject: [PATCH] [kotlin] k2: cascade if to when inspection ^KTIJ-22890 fixed GitOrigin-RevId: 37aee0b758296b6316cfcbf4ea70542ccb7bcab3 --- .../inspectionDescriptions/CascadeIf.html | 46 ++- .../kotlin.code-insight.inspections.k2.xml | 9 + .../inspections/CascadeIfInspection.kt | 90 ++++++ .../tests/K2LocalInspectionTestGenerated.java | 74 +++++ .../kotlin.code-insight.intentions.k2.iml | 1 + .../intentions/IfToWhenIntention.kt | 228 +-------------- .../branchedTransformations/IfToWhenUtil.kt | 267 ++++++++++++++++++ .../branchedTransformationUtils.kt | 0 .../inspectionsLocal/cascadeIf/.k2Inspection | 1 + .../codeinsight/GenerateK2InspectionTests.kt | 1 + 10 files changed, 466 insertions(+), 251 deletions(-) create mode 100644 plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/CascadeIfInspection.kt create mode 100644 plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/IfToWhenUtil.kt rename plugins/kotlin/code-insight/{intentions-k2 => kotlin.code-insight.k2}/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/branchedTransformationUtils.kt (100%) create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/cascadeIf/.k2Inspection diff --git a/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/CascadeIf.html b/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/CascadeIf.html index 734cc050f387..cda33b78f302 100644 --- a/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/CascadeIf.html +++ b/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/CascadeIf.html @@ -1,38 +1,32 @@ -Reports if statements with three or more branches that can be replaced with the when expression. +Reports if statements with three or more branches that can be replaced with the when expression with a subject.

Example:


-  fun checkIdentifier(id: String) {
-      fun Char.isIdentifierStart() = this in 'A'..'z'
-      fun Char.isIdentifierPart() = isIdentifierStart() || this in '0'..'9'
-
-      if (id.isEmpty()) {
-          print("Identifier is empty")
-      } else if (!id.first().isIdentifierStart()) {
-          print("Identifier should start with a letter")
-      } else if (!id.subSequence(1, id.length).all(Char::isIdentifierPart)) {
-          print("Identifier should contain only letters and numbers")
-      }
+  fun translateNumber(n: Int): String {
+    return if (n == 1) {
+      "one"
+    } else if (n == 2) {
+      "two"
+    } else {
+      "???"
+    }
   }
 

The quick-fix converts the if expression to when:


-  fun checkIdentifier(id: String) {
-      fun Char.isIdentifierStart() = this in 'A'..'z'
-      fun Char.isIdentifierPart() = isIdentifierStart() || this in '0'..'9'
-
-      when {
-          id.isEmpty() -> {
-              print("Identifier is empty")
-          }
-          !id.first().isIdentifierStart() -> {
-              print("Identifier should start with a letter")
-          }
-          !id.subSequence(1, id.length).all(Char::isIdentifierPart) -> {
-              print("Identifier should contain only letters and numbers")
-          }
+  fun translateNumber(n: Int): String {
+    return when (n) {
+      1 -> {
+        "one"
       }
+      2 -> {
+        "two"
+      }
+      else -> {
+        "???"
+      }
+    }
   }
 
diff --git a/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml b/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml index 0b6432014fb9..5f2293ff99c8 100644 --- a/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml +++ b/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml @@ -247,6 +247,15 @@ language="kotlin" key="inspection.enum.values.method.soft.deprecate.migration.display.name" bundle="messages.KotlinBundle"/> + + + () { + + override fun getProblemDescription( + element: KtIfExpression, + context: Unit + ): @InspectionMessage String = KotlinBundle.message("cascade.if.should.be.replaced.with.when") + + override fun createQuickFix( + element: KtIfExpression, + context: Unit + ): KotlinModCommandQuickFix = object : KotlinModCommandQuickFix() { + override fun getFamilyName(): @IntentionFamilyName String = KotlinBundle.message("replace.if.with.when") + + override fun applyFix( + project: Project, + element: KtIfExpression, + updater: ModPsiUpdater + ) { + convertIfToWhen(element, updater) + } + } + + override fun getApplicableRanges(element: KtIfExpression): List = ApplicabilityRanges.ifKeyword(element) + + context(KaSession) + override fun prepareContext(element: KtIfExpression): Unit? { + var current: KtIfExpression? = element + var lastSubjectCandidate: KtExpression? = null + while (current != null) { + val subjectCandidate = current.condition.getWhenConditionSubjectCandidate(checkConstants = false) ?: return null + if (lastSubjectCandidate != null && !lastSubjectCandidate.matches(subjectCandidate)) return null + lastSubjectCandidate = subjectCandidate + current = current.`else` as? KtIfExpression + } + return Unit + } + + override fun isApplicableByPsi(element: KtIfExpression): Boolean { + val branches = element.branches + if (branches.size <= 2) return false + if (element.isOneLiner()) return false + + if (branches.any { + it == null || it.lastBlockStatementOrThis() is KtIfExpression + } + ) return false + + if (element.parent.node.elementType == KtNodeTypes.ELSE) return false + + if (element.anyDescendantOfType { + it is KtBreakExpression || it is KtContinueExpression + } + ) return false + return true + } + + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean + ): KtVisitor<*, *> = object : KtVisitorVoid() { + override fun visitIfExpression(expression: KtIfExpression) { + visitTargetElement(expression, holder, isOnTheFly) + } + } +} diff --git a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java index 229f1d9275b7..002820640ca4 100644 --- a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java +++ b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java @@ -1446,6 +1446,80 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp } } + @RunWith(JUnit3RunnerWithInners.class) + @TestMetadata("../../../idea/tests/testData/inspectionsLocal/cascadeIf") + public static class CascadeIf extends AbstractK2LocalInspectionTest { + @java.lang.Override + @org.jetbrains.annotations.NotNull + public final KotlinPluginMode getPluginMode() { + return KotlinPluginMode.K2; + } + + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + @TestMetadata("cannotIntroduceSubject.kt") + public void testCannotIntroduceSubject() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/cannotIntroduceSubject.kt"); + } + + @TestMetadata("complex.kt") + public void testComplex() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/complex.kt"); + } + + @TestMetadata("four.kt") + public void testFour() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/four.kt"); + } + + @TestMetadata("ifTree.kt") + public void testIfTree() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/ifTree.kt"); + } + + @TestMetadata("insideOtherIf.kt") + public void testInsideOtherIf() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/insideOtherIf.kt"); + } + + @TestMetadata("middleIf.kt") + public void testMiddleIf() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/middleIf.kt"); + } + + @TestMetadata("noSecondElse.kt") + public void testNoSecondElse() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/noSecondElse.kt"); + } + + @TestMetadata("normal.kt") + public void testNormal() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/normal.kt"); + } + + @TestMetadata("oneLiner.kt") + public void testOneLiner() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/oneLiner.kt"); + } + + @TestMetadata("shortIf.kt") + public void testShortIf() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/shortIf.kt"); + } + + @TestMetadata("withAnnotation.kt") + public void testWithAnnotation() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/withAnnotation.kt"); + } + + @TestMetadata("withBreak.kt") + public void testWithBreak() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/cascadeIf/withBreak.kt"); + } + } + @RunWith(JUnit3RunnerWithInners.class) @TestMetadata("../../../idea/tests/testData/inspectionsLocal/nullableBooleanElvis") public static class NullableBooleanElvis extends AbstractK2LocalInspectionTest { diff --git a/plugins/kotlin/code-insight/intentions-k2/kotlin.code-insight.intentions.k2.iml b/plugins/kotlin/code-insight/intentions-k2/kotlin.code-insight.intentions.k2.iml index 834db932735a..c69871795fa2 100644 --- a/plugins/kotlin/code-insight/intentions-k2/kotlin.code-insight.intentions.k2.iml +++ b/plugins/kotlin/code-insight/intentions-k2/kotlin.code-insight.intentions.k2.iml @@ -58,5 +58,6 @@ + \ No newline at end of file diff --git a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/IfToWhenIntention.kt b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/IfToWhenIntention.kt index da04e0fa93c3..4fdf957d7df6 100644 --- a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/IfToWhenIntention.kt +++ b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/IfToWhenIntention.kt @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.idea.base.util.reformat import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.KotlinApplicableModCommandAction import org.jetbrains.kotlin.idea.codeinsights.impl.base.applicators.ApplicabilityRanges import org.jetbrains.kotlin.idea.codeinsights.impl.base.quickFix.AddLoopLabelFix +import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.branchedTransformations.convertIfToWhen import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.branchedTransformations.getSubjectToIntroduce import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.branchedTransformations.introduceSubjectIfPossible import org.jetbrains.kotlin.idea.util.CommentSaver @@ -28,8 +29,7 @@ internal class IfToWhenIntention : KotlinApplicableModCommandAction() - val loopJumpVisitor = LabelLoopJumpVisitor(loop) - - val toDelete = ArrayList() - - val (whenExpression, applyFullCommentSaver) = createWhenExpression(ifExpression, toDelete) - - val commentSaver = if (applyFullCommentSaver) fullCommentSaver else elementCommentSaver - - val subjectedWhenExpression = analyze(element) { - val analysableWhenExpression = - KtPsiFactory(element.project).createExpressionCodeFragment(whenExpression.text, ifExpression) - .getContentElement() as KtWhenExpression - - val subject = analysableWhenExpression.getSubjectToIntroduce(false) - whenExpression.introduceSubjectIfPossible(subject, ifExpression) - } - - val result = ifExpression.replaced(subjectedWhenExpression) - - updater.moveCaretTo(result.startOffset) - commentSaver.restore(result) - - if (toDelete.isNotEmpty()) { - parent.deleteChildRange( - toDelete.first().let { it.prevSibling as? PsiWhiteSpace ?: it }, - toDelete.last() - ) - } - - result.accept(loopJumpVisitor) - val labelName = loopJumpVisitor.labelName - if (loop != null && loopJumpVisitor.labelRequired && labelName != null && loop.parent !is KtLabeledExpression) { - val labeledLoopExpression = KtPsiFactory(result.project).createLabeledExpression(labelName) - labeledLoopExpression.baseExpression!!.replace(loop) - - val replacedLabeledLoopExpression = loop.replace(labeledLoopExpression) - replacedLabeledLoopExpression.reformat() - } - } - - private fun createWhenExpression( - ifExpression: KtIfExpression, - toDelete: ArrayList - ): Pair { - var applyFullCommentSaver = true - val whenExpression = KtPsiFactory(ifExpression.project).buildExpression { - appendFixedText("when {\n") - - var currentIfExpression = ifExpression - var baseIfExpressionForSyntheticBranch = currentIfExpression - var canPassThrough = false - while (true) { - val condition = currentIfExpression.condition - val orBranches = ArrayList() - if (condition != null) { - orBranches.addOrBranches(condition) - } - - appendExpressions(orBranches, separator = "||") - - appendFixedText("->") - - val currentThenBranch = currentIfExpression.then - appendExpression(currentThenBranch) - appendFixedText("\n") - - canPassThrough = canPassThrough || canPassThrough(currentThenBranch) - - val currentElseBranch = currentIfExpression.`else` - if (currentElseBranch == null) { - // Try to build synthetic if / else according to KT-10750 - val syntheticElseBranch = if (canPassThrough) null else buildNextBranch(baseIfExpressionForSyntheticBranch) - if (syntheticElseBranch == null) { - applyFullCommentSaver = false - break - } - toDelete.addAll(baseIfExpressionForSyntheticBranch.siblingsUpTo(syntheticElseBranch)) - if (syntheticElseBranch is KtIfExpression) { - baseIfExpressionForSyntheticBranch = syntheticElseBranch - currentIfExpression = syntheticElseBranch - toDelete.add(syntheticElseBranch) - } else { - appendElseBlock(syntheticElseBranch, unwrapBlockOrParenthesis = true) - break - } - } else if (currentElseBranch is KtIfExpression) { - currentIfExpression = currentElseBranch - } else { - appendElseBlock(currentElseBranch) - applyFullCommentSaver = false - break - } - } - - appendFixedText("}") - } as KtWhenExpression - - return Pair(whenExpression, applyFullCommentSaver) + convertIfToWhen(element, updater) } override fun getApplicableRanges(element: KtIfExpression): List = ApplicabilityRanges.ifKeyword(element) override fun isApplicableByPsi(element: KtIfExpression): Boolean = element.then != null - - private fun KtIfExpression.topmostIfExpression(): KtIfExpression { - var target = this - while (true) { - val container = target.parent as? KtContainerNodeForControlStructureBody ?: break - val parent = container.parent as? KtIfExpression ?: break - if (parent.`else` != target) break - target = parent - } - return target - } - - private fun canPassThrough(expression: KtExpression?): Boolean = when (expression) { - is KtReturnExpression, is KtThrowExpression, is KtCallExpression, is KtStringTemplateExpression -> - false - is KtBlockExpression -> - expression.statements.all { canPassThrough(it) } - is KtIfExpression -> - canPassThrough(expression.then) || canPassThrough(expression.`else`) - else -> - true - } - - private fun buildNextBranch(ifExpression: KtIfExpression): KtExpression? { - var nextSibling = ifExpression.getNextSiblingIgnoringWhitespaceAndComments() ?: return null - return when (nextSibling) { - is KtIfExpression -> - if (nextSibling.then == null) null else nextSibling - - else -> { - val builder = StringBuilder() - while (true) { - builder.append(nextSibling.text) - nextSibling = nextSibling.nextSibling ?: break - } - KtPsiFactory(ifExpression.project).createBlock(builder.toString()).takeIf { it.statements.isNotEmpty() } - } - } - } - - private fun MutableList.addOrBranches(expression: KtExpression): List { - if (expression is KtBinaryExpression && expression.operationToken == KtTokens.OROR) { - val left = expression.left - val right = expression.right - if (left != null && right != null) { - addOrBranches(left) - addOrBranches(right) - return this - } - } - - add(KtPsiUtil.safeDeparenthesize(expression, true)) - return this - } - - private fun BuilderByPattern<*>.appendElseBlock(block: KtExpression?, unwrapBlockOrParenthesis: Boolean = false) { - appendFixedText("else->") - appendExpression(if (unwrapBlockOrParenthesis) block?.getSingleUnwrappedStatementOrThis() else block) - appendFixedText("\n") - } - - private fun KtIfExpression.siblingsUpTo(other: KtExpression): List { - val result = ArrayList() - var nextSibling = nextSibling - // We delete elements up to the next if (or up to the end of the surrounding block) - while (nextSibling != null && nextSibling != other) { - // RBRACE closes the surrounding block, so it should not be copied / deleted - if (nextSibling !is PsiWhiteSpace && nextSibling.node.elementType != KtTokens.RBRACE) { - result.add(nextSibling) - } - nextSibling = nextSibling.nextSibling - } - - 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) - } - } - - var labelRequired = false - - fun KtExpressionWithLabel.addLabelIfNecessary(): KtExpressionWithLabel { - if (this.getLabelName() != null) { - // Label is already present, no need to add - return this - } - - if (this.getStrictParentOfType() != nearestLoopIfAny) { - // 'for' inside 'if' - return this - } - - if (!languageVersionSettings.supportsFeature(LanguageFeature.AllowBreakAndContinueInsideWhen) && labelName != null) { - val jumpWithLabel = KtPsiFactory(project).createExpression("$text@$labelName") as KtExpressionWithLabel - labelRequired = true - 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) - } } diff --git a/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/IfToWhenUtil.kt b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/IfToWhenUtil.kt new file mode 100644 index 000000000000..af130152dcae --- /dev/null +++ b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/IfToWhenUtil.kt @@ -0,0 +1,267 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.k2.codeinsight.intentions.branchedTransformations + +import com.intellij.modcommand.ModPsiUpdater +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.idea.base.projectStructure.languageVersionSettings +import org.jetbrains.kotlin.idea.base.psi.getSingleUnwrappedStatementOrThis +import org.jetbrains.kotlin.idea.base.psi.replaced +import org.jetbrains.kotlin.idea.base.util.reformat +import org.jetbrains.kotlin.idea.codeinsights.impl.base.quickFix.AddLoopLabelFix +import org.jetbrains.kotlin.idea.util.CommentSaver +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.BuilderByPattern +import org.jetbrains.kotlin.psi.KtBinaryExpression +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtBreakExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtContainerNodeForControlStructureBody +import org.jetbrains.kotlin.psi.KtContinueExpression +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtExpressionWithLabel +import org.jetbrains.kotlin.psi.KtIfExpression +import org.jetbrains.kotlin.psi.KtLabeledExpression +import org.jetbrains.kotlin.psi.KtLoopExpression +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtPsiUtil +import org.jetbrains.kotlin.psi.KtReturnExpression +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.jetbrains.kotlin.psi.KtThrowExpression +import org.jetbrains.kotlin.psi.KtVisitorVoid +import org.jetbrains.kotlin.psi.KtWhenExpression +import org.jetbrains.kotlin.psi.buildExpression +import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange +import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.psi.psiUtil.startOffset + +fun convertIfToWhen(element: KtIfExpression, updater: ModPsiUpdater) { + val ifExpression = element.topmostIfExpression() + val parent = ifExpression.parent + + val elementCommentSaver = CommentSaver(ifExpression, saveLineBreaks = true) + val fullCommentSaver = CommentSaver(PsiChildRange(ifExpression, ifExpression.siblings().last()), saveLineBreaks = true) + + val loop = ifExpression.getStrictParentOfType() + val loopJumpVisitor = LabelLoopJumpVisitor(loop) + + val toDelete = ArrayList() + + val (whenExpression, applyFullCommentSaver) = createWhenExpression(ifExpression, toDelete) + + val commentSaver = if (applyFullCommentSaver) fullCommentSaver else elementCommentSaver + + val subjectedWhenExpression = analyze(element) { + val analysableWhenExpression = + org.jetbrains.kotlin.psi.KtPsiFactory(element.project).createExpressionCodeFragment(whenExpression.text, ifExpression) + .getContentElement() as KtWhenExpression + + val subject = analysableWhenExpression.getSubjectToIntroduce(false) + whenExpression.introduceSubjectIfPossible(subject, ifExpression) + } + + val result = ifExpression.replaced(subjectedWhenExpression) + + updater.moveCaretTo(result.startOffset) + commentSaver.restore(result) + + if (toDelete.isNotEmpty()) { + parent.deleteChildRange( + toDelete.first().let { it.prevSibling as? PsiWhiteSpace ?: it }, + toDelete.last() + ) + } + + result.accept(loopJumpVisitor) + val labelName = loopJumpVisitor.labelName + if (loop != null && loopJumpVisitor.labelRequired && labelName != null && loop.parent !is KtLabeledExpression) { + val labeledLoopExpression = org.jetbrains.kotlin.psi.KtPsiFactory(result.project).createLabeledExpression(labelName) + labeledLoopExpression.baseExpression!!.replace(loop) + + val replacedLabeledLoopExpression = loop.replace(labeledLoopExpression) + replacedLabeledLoopExpression.reformat() + } +} + +private fun createWhenExpression( + ifExpression: KtIfExpression, + toDelete: ArrayList +): Pair { + var applyFullCommentSaver = true + val whenExpression = KtPsiFactory(ifExpression.project).buildExpression { + appendFixedText("when {\n") + + var currentIfExpression = ifExpression + var baseIfExpressionForSyntheticBranch = currentIfExpression + var canPassThrough = false + while (true) { + val condition = currentIfExpression.condition + val orBranches = ArrayList() + if (condition != null) { + orBranches.addOrBranches(condition) + } + + appendExpressions(orBranches, separator = "||") + + appendFixedText("->") + + val currentThenBranch = currentIfExpression.then + appendExpression(currentThenBranch) + appendFixedText("\n") + + canPassThrough = canPassThrough || canPassThrough(currentThenBranch) + + val currentElseBranch = currentIfExpression.`else` + if (currentElseBranch == null) { + // Try to build synthetic if / else according to KT-10750 + val syntheticElseBranch = if (canPassThrough) null else buildNextBranch(baseIfExpressionForSyntheticBranch) + if (syntheticElseBranch == null) { + applyFullCommentSaver = false + break + } + toDelete.addAll(baseIfExpressionForSyntheticBranch.siblingsUpTo(syntheticElseBranch)) + if (syntheticElseBranch is KtIfExpression) { + baseIfExpressionForSyntheticBranch = syntheticElseBranch + currentIfExpression = syntheticElseBranch + toDelete.add(syntheticElseBranch) + } else { + appendElseBlock(syntheticElseBranch, unwrapBlockOrParenthesis = true) + break + } + } else if (currentElseBranch is KtIfExpression) { + currentIfExpression = currentElseBranch + } else { + appendElseBlock(currentElseBranch) + applyFullCommentSaver = false + break + } + } + + appendFixedText("}") + } as KtWhenExpression + + return Pair(whenExpression, applyFullCommentSaver) +} + +private fun KtIfExpression.topmostIfExpression(): KtIfExpression { + var target = this + while (true) { + val container = target.parent as? KtContainerNodeForControlStructureBody ?: break + val parent = container.parent as? KtIfExpression ?: break + if (parent.`else` != target) break + target = parent + } + return target +} + +private fun canPassThrough(expression: KtExpression?): Boolean = when (expression) { + is KtReturnExpression, is KtThrowExpression, is KtCallExpression, is KtStringTemplateExpression -> + false + is KtBlockExpression -> + expression.statements.all { canPassThrough(it) } + is KtIfExpression -> + canPassThrough(expression.then) || canPassThrough(expression.`else`) + else -> + true +} + +private fun buildNextBranch(ifExpression: KtIfExpression): KtExpression? { + var nextSibling = ifExpression.getNextSiblingIgnoringWhitespaceAndComments() ?: return null + return when (nextSibling) { + is KtIfExpression -> + if (nextSibling.then == null) null else nextSibling + + else -> { + val builder = StringBuilder() + while (true) { + builder.append(nextSibling.text) + nextSibling = nextSibling.nextSibling ?: break + } + KtPsiFactory(ifExpression.project).createBlock(builder.toString()).takeIf { it.statements.isNotEmpty() } + } + } +} + +private fun MutableList.addOrBranches(expression: KtExpression): List { + if (expression is KtBinaryExpression && expression.operationToken == KtTokens.OROR) { + val left = expression.left + val right = expression.right + if (left != null && right != null) { + addOrBranches(left) + addOrBranches(right) + return this + } + } + + add(KtPsiUtil.safeDeparenthesize(expression, true)) + return this +} + +private fun BuilderByPattern<*>.appendElseBlock(block: KtExpression?, unwrapBlockOrParenthesis: Boolean = false) { + appendFixedText("else->") + appendExpression(if (unwrapBlockOrParenthesis) block?.getSingleUnwrappedStatementOrThis() else block) + appendFixedText("\n") +} + +private fun KtIfExpression.siblingsUpTo(other: KtExpression): List { + val result = ArrayList() + var nextSibling = nextSibling + // We delete elements up to the next if (or up to the end of the surrounding block) + while (nextSibling != null && nextSibling != other) { + // RBRACE closes the surrounding block, so it should not be copied / deleted + if (nextSibling !is PsiWhiteSpace && nextSibling.node.elementType != KtTokens.RBRACE) { + result.add(nextSibling) + } + nextSibling = nextSibling.nextSibling + } + + 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) + } + } + + var labelRequired = false + + fun KtExpressionWithLabel.addLabelIfNecessary(): KtExpressionWithLabel { + if (this.getLabelName() != null) { + // Label is already present, no need to add + return this + } + + if (this.getStrictParentOfType() != nearestLoopIfAny) { + // 'for' inside 'if' + return this + } + + if (!languageVersionSettings.supportsFeature(LanguageFeature.AllowBreakAndContinueInsideWhen) && labelName != null) { + val jumpWithLabel = KtPsiFactory(project).createExpression("$text@$labelName") as KtExpressionWithLabel + labelRequired = true + 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) + } +} \ No newline at end of file diff --git a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/branchedTransformationUtils.kt b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/branchedTransformationUtils.kt similarity index 100% rename from plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/branchedTransformationUtils.kt rename to plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/branchedTransformations/branchedTransformationUtils.kt diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/cascadeIf/.k2Inspection b/plugins/kotlin/idea/tests/testData/inspectionsLocal/cascadeIf/.k2Inspection new file mode 100644 index 000000000000..f89fa894f927 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/cascadeIf/.k2Inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.k2.codeinsight.inspections.CascadeIfInspection \ No newline at end of file diff --git a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt index 52c7bae1ceb6..b0572117e71d 100644 --- a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt +++ b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt @@ -27,6 +27,7 @@ internal fun MutableTWorkspace.generateK2InspectionTests() { model("${idea}/inspectionsLocal/branched/ifThenToElvis", pattern = Patterns.KT_WITHOUT_DOTS) model("${idea}/inspectionsLocal/branched/ifThenToSafeAccess", pattern = Patterns.KT_WITHOUT_DOTS) model("${idea}/inspectionsLocal/conventionNameCalls/replaceGetOrSet") + model("${idea}/inspectionsLocal/cascadeIf") model("${idea}/inspectionsLocal/nullableBooleanElvis") model("${idea}/inspectionsLocal/redundantElvisReturnNull") model("${idea}/inspectionsLocal/replaceCollectionCountWithSize")