[kotlin] k2: cascade if to when inspection

^KTIJ-22890 fixed

GitOrigin-RevId: 37aee0b758296b6316cfcbf4ea70542ccb7bcab3
This commit is contained in:
Anna Kozlova
2024-12-03 15:44:02 +00:00
committed by intellij-monorepo-bot
parent c37d60477a
commit 0158444eae
10 changed files with 466 additions and 251 deletions
@@ -1,38 +1,32 @@
<html>
<body>
Reports <code>if</code> statements with three or more branches that can be replaced with the <code>when</code> expression.
Reports <code>if</code> statements with three or more branches that can be replaced with the <code>when</code> expression with a subject.
<p><b>Example:</b></p>
<pre><code>
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 {
"???"
}
}
</code></pre>
<p>The quick-fix converts the <code>if</code> expression to <code>when</code>:</p>
<pre><code>
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 -> {
"???"
}
}
}
</code></pre>
<!-- tooltip end -->
@@ -247,6 +247,15 @@
language="kotlin"
key="inspection.enum.values.method.soft.deprecate.migration.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.k2.codeinsight.inspections.CascadeIfInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.style.issues"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
key="inspection.cascade.if.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.k2.codeinsight.inspections.expressions.WhenWithOnlyElseInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.redundant.constructs"
@@ -0,0 +1,90 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.kotlin.idea.k2.codeinsight.inspections
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInspection.util.InspectionMessage
import com.intellij.codeInspection.util.IntentionFamilyName
import com.intellij.modcommand.ModPsiUpdater
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.idea.base.psi.isOneLiner
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix
import org.jetbrains.kotlin.idea.codeinsight.utils.branches
import org.jetbrains.kotlin.idea.codeinsights.impl.base.applicators.ApplicabilityRanges
import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.branchedTransformations.convertIfToWhen
import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.branchedTransformations.getWhenConditionSubjectCandidate
import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.branchedTransformations.matches
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.lastBlockStatementOrThis
class CascadeIfInspection : KotlinApplicableInspectionBase.Simple<KtIfExpression, Unit>() {
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<KtIfExpression> = object : KotlinModCommandQuickFix<KtIfExpression>() {
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<TextRange> = 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<KtExpressionWithLabel> {
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)
}
}
}
@@ -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 {
@@ -58,5 +58,6 @@
<orderEntry type="module" module-name="kotlin.code-insight.override-implement.shared" />
<orderEntry type="module" module-name="kotlin.code-insight.override-implement.k2" />
<orderEntry type="module" module-name="kotlin.fir.frontend-independent" />
<orderEntry type="module" module-name="kotlin.code-insight.k2" />
</component>
</module>
@@ -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<KtIfExpressi
override fun getFamilyName(): String = KotlinBundle.message("replace.if.with.when")
context(KaSession)
override fun prepareContext(element: KtIfExpression) {
}
override fun prepareContext(element: KtIfExpression) {}
override fun invoke(
actionContext: ActionContext,
@@ -37,233 +37,11 @@ internal class IfToWhenIntention : KotlinApplicableModCommandAction<KtIfExpressi
elementContext: Unit,
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<KtLoopExpression>()
val loopJumpVisitor = LabelLoopJumpVisitor(loop)
val toDelete = ArrayList<PsiElement>()
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<PsiElement>
): Pair<KtWhenExpression, Boolean> {
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<KtExpression>()
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<TextRange> =
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<KtExpression>.addOrBranches(expression: KtExpression): List<KtExpression> {
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<PsiElement> {
val result = ArrayList<PsiElement>()
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<KtLoopExpression>() != 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)
}
}
@@ -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<KtLoopExpression>()
val loopJumpVisitor = LabelLoopJumpVisitor(loop)
val toDelete = ArrayList<PsiElement>()
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<PsiElement>
): Pair<KtWhenExpression, Boolean> {
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<KtExpression>()
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<KtExpression>.addOrBranches(expression: KtExpression): List<KtExpression> {
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<PsiElement> {
val result = ArrayList<PsiElement>()
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<KtLoopExpression>() != 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)
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.k2.codeinsight.inspections.CascadeIfInspection
@@ -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")