mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[kotlin] k2: cascade if to when inspection
^KTIJ-22890 fixed GitOrigin-RevId: 37aee0b758296b6316cfcbf4ea70542ccb7bcab3
This commit is contained in:
committed by
intellij-monorepo-bot
parent
c37d60477a
commit
0158444eae
+20
-26
@@ -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 -->
|
||||
|
||||
+9
@@ -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"
|
||||
|
||||
+90
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -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>
|
||||
+3
-225
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+267
@@ -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
|
||||
+1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user