From d5d5b86b211d25fd5c1877fed3f65b76ea54a23a Mon Sep 17 00:00:00 2001 From: Andrey Cherkasov Date: Fri, 3 Nov 2023 14:17:08 +0400 Subject: [PATCH] [kotlin k2] Support K2 the "Invert 'if' condition" intention ^KTIJ-27469 GitOrigin-RevId: c8f7bcc4eb5264e29a4e05201fadc26dd9561998 --- .../kotlin.code-insight.intentions-k2.xml | 7 + ...naryExpressionWithDemorgansLawIntention.kt | 6 +- .../intentions/InvertIfConditionIntention.kt | 289 +++++++++++++++++ .../tests/K2IntentionTestGenerated.java | 293 ++++++++++++++++++ .../codeinsight/utils/DemorgansLawUtils.kt | 8 +- .../intentions/invertIfCondition/.k2Intention | 1 + .../codeinsight/GenerateK2IntentionTests.kt | 1 + 7 files changed, 597 insertions(+), 8 deletions(-) create mode 100644 plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/InvertIfConditionIntention.kt create mode 100644 plugins/kotlin/idea/tests/testData/intentions/invertIfCondition/.k2Intention diff --git a/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions-k2.xml b/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions-k2.xml index 5c52f9032136..ecc6bedb0bc7 100644 --- a/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions-k2.xml +++ b/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions-k2.xml @@ -235,5 +235,12 @@ messages.KotlinBundle group.names.kotlin + + + kotlin + org.jetbrains.kotlin.idea.k2.codeinsight.intentions.InvertIfConditionIntention + messages.KotlinBundle + group.names.kotlin + diff --git a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ConvertBinaryExpressionWithDemorgansLawIntention.kt b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ConvertBinaryExpressionWithDemorgansLawIntention.kt index a3c7a4215313..88d5cf3778dc 100644 --- a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ConvertBinaryExpressionWithDemorgansLawIntention.kt +++ b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ConvertBinaryExpressionWithDemorgansLawIntention.kt @@ -23,12 +23,14 @@ internal class ConvertBinaryExpressionWithDemorgansLawIntention : override fun apply(element: KtBinaryExpression, context: AnalysisActionContext, updater: ModPsiUpdater) { val expr = element.topmostBinaryExpression() if (splitBooleanSequence(expr) == null) return - applyDemorgansLaw(element, context.analyzeContext) + applyDemorgansLaw(expr, context.analyzeContext) } context(KtAnalysisSession) override fun prepareContext(element: KtBinaryExpression): DemorgansLawContext? { - return prepareDemorgansLawContext(element) + val operands = element.topmostBinaryExpression().let(::splitBooleanSequence) ?: return null + if (operands.any { !it.isBoolean }) return null + return prepareDemorgansLawContext(operands) } override fun getActionName(element: KtBinaryExpression, context: DemorgansLawContext): String { diff --git a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/InvertIfConditionIntention.kt b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/InvertIfConditionIntention.kt new file mode 100644 index 000000000000..5303a5bcd1f5 --- /dev/null +++ b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/InvertIfConditionIntention.kt @@ -0,0 +1,289 @@ +// Copyright 2000-2023 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 + +import com.intellij.modcommand.ModPsiUpdater +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.SmartPsiElementPointer +import com.intellij.refactoring.suggested.createSmartPointer +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.base.psi.getLineNumber +import org.jetbrains.kotlin.idea.base.psi.replaced +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.AbstractKotlinModCommandWithContext +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.AnalysisActionContext +import org.jetbrains.kotlin.idea.codeinsight.api.applicators.KotlinApplicabilityRange +import org.jetbrains.kotlin.idea.codeinsight.api.applicators.applicabilityTarget +import org.jetbrains.kotlin.idea.codeinsight.utils.* +import org.jetbrains.kotlin.idea.util.CommentSaver +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance +import org.jetbrains.kotlin.utils.addToStdlib.lastIsInstanceOrNull + +internal class InvertIfConditionIntention : AbstractKotlinModCommandWithContext(KtIfExpression::class) { + override fun getFamilyName(): String = KotlinBundle.message("invert.if.condition") + override fun getActionName(element: KtIfExpression, context: Context): String = familyName + + context(KtAnalysisSession) + override fun prepareContext(element: KtIfExpression): Context { + val rBrace = parentBlockRBrace(element) + val commentSavingRange = if (rBrace != null) + PsiChildRange(element, rBrace) + else + PsiChildRange.singleElement(element) + + val commentSaver = CommentSaver(commentSavingRange) + + val condition = element.condition!! + val areAllOperandsBoolean = + (condition is KtBinaryExpression && splitBooleanSequence(condition)?.all { it.isBoolean } == true) || condition.isBoolean + val newCondition = (condition as? KtQualifiedExpression)?.invertSelectorFunction() ?: condition.negate() + + val isParentFunUnit = element.getParentOfType(true) + val isUnit = isParentFunUnit != null && isParentFunUnit.getReturnKtType().isUnit + + val demorgansLawContext = if (areAllOperandsBoolean) { + getBinaryExpression(newCondition)?.let(::splitBooleanSequence)?.let { expressions -> + prepareDemorgansLawContext(expressions) + } + } else null + + return Context(newCondition.createSmartPointer(), demorgansLawContext, isUnit, commentSaver) + } + + private fun getBinaryExpression(expression: KtExpression): KtBinaryExpression? { + (expression as? KtPrefixExpression)?.let { + if (it.operationReference.getReferencedNameElementType() == KtTokens.EXCL) { + val binaryExpr = (it.baseExpression as? KtParenthesizedExpression)?.expression as? KtBinaryExpression + return binaryExpr?.topmostBinaryExpression() + } + } + return null + } + + override fun getApplicabilityRange(): KotlinApplicabilityRange = applicabilityTarget { ifExpression: KtIfExpression -> + ifExpression.ifKeyword + } + + override fun isApplicableByPsi(element: KtIfExpression): Boolean { + return element.condition != null && element.then != null + } + + override fun apply(element: KtIfExpression, context: AnalysisActionContext, updater: ModPsiUpdater) { + val rBrace = parentBlockRBrace(element) + if (rBrace != null) element.nextEolCommentOnSameLine()?.delete() + + val analyzeContext = context.analyzeContext + val newIf = handleSpecialCases(element, analyzeContext) ?: handleStandardCase(element, analyzeContext) + + val commentRestoreRange = if (rBrace != null) + PsiChildRange(newIf, rBrace) + else + PsiChildRange(newIf, parentBlockRBrace(newIf) ?: newIf) + + context.analyzeContext.commentSaver.restore(commentRestoreRange) + + val binaryExpr = newIf.condition?.let(::getBinaryExpression) + if (binaryExpr != null) { + context.analyzeContext.demorgansLawContext?.let { demorgansLawContext -> + applyDemorgansLaw(binaryExpr, demorgansLawContext) + } + } + + updater.moveTo(newIf) + } + + private fun handleStandardCase(ifExpression: KtIfExpression, context: Context): KtIfExpression { + val psiFactory = KtPsiFactory(ifExpression.project) + + val thenBranch = ifExpression.then!! + val elseBranch = ifExpression.`else` ?: psiFactory.createEmptyBody() + + val newThen = if (elseBranch is KtIfExpression) + psiFactory.createSingleStatementBlock(elseBranch) + else + elseBranch + + val newElse = if (thenBranch is KtBlockExpression && thenBranch.statements.isEmpty()) + null + else + thenBranch + + val conditionLineNumber = ifExpression.condition?.getLineNumber(false) + val thenBranchLineNumber = thenBranch.getLineNumber(false) + val elseKeywordLineNumber = ifExpression.elseKeyword?.getLineNumber() + val afterCondition = if (newThen !is KtBlockExpression && elseKeywordLineNumber != elseBranch.getLineNumber(false)) "\n" else "" + val beforeElse = if (newThen !is KtBlockExpression && conditionLineNumber != elseKeywordLineNumber) "\n" else " " + val afterElse = if (newElse !is KtBlockExpression && conditionLineNumber != thenBranchLineNumber) "\n" else " " + + val newCondition = context.newCondition.element!! + val newIf = if (newElse == null) { + psiFactory.createExpressionByPattern("if ($0)$afterCondition$1", newCondition, newThen) + } else { + psiFactory.createExpressionByPattern("if ($0)$afterCondition$1${beforeElse}else$afterElse$2", newCondition, newThen, newElse) + } as KtIfExpression + + return ifExpression.replaced(newIf) + } + + private fun handleSpecialCases(ifExpression: KtIfExpression, context: Context): KtIfExpression? { + val elseBranch = ifExpression.`else` + if (elseBranch != null) return null + + val psiFactory = KtPsiFactory(ifExpression.project) + + val thenBranch = ifExpression.then!! + val lastThenStatement = thenBranch.lastBlockStatementOrThis() + val newCondition = context.newCondition.element!! + if (lastThenStatement.isExitStatement()) { + val block = ifExpression.parent as? KtBlockExpression + if (block != null) { + val rBrace = block.rBrace + val afterIfInBlock = ifExpression.siblings(withItself = false).takeWhile { it != rBrace }.toList() + val lastStatementInBlock = afterIfInBlock.lastIsInstanceOrNull() + if (lastStatementInBlock != null) { + val exitStatementAfterIf = if (lastStatementInBlock.isExitStatement()) + lastStatementInBlock + else + exitStatementExecutedAfter(lastStatementInBlock, context) + if (exitStatementAfterIf != null) { + val first = afterIfInBlock.first() + val last = afterIfInBlock.last() + // build new then branch from statements after if (we will add exit statement if necessary later) + val newThenRange = if (isEmptyReturn(lastThenStatement) && isEmptyReturn(lastStatementInBlock)) { + PsiChildRange(first, lastStatementInBlock.prevSibling).trimWhiteSpaces() + } else { + PsiChildRange(first, last).trimWhiteSpaces() + } + val newIf = + psiFactory.createExpressionByPattern("if ($0) { $1 }", newCondition, newThenRange) as KtIfExpression + + // remove statements after if as they are moving under if + block.deleteChildRange(first, last) + + if (isEmptyReturn(lastThenStatement)) { + if (block.parent is KtDeclarationWithBody && block.parent !is KtFunctionLiteral) { + lastThenStatement.delete() + } + } + val updatedIf = copyThenBranchAfter(ifExpression) + + // check if we need to add exit statement to then branch + if (exitStatementAfterIf != lastStatementInBlock) { + // don't insert the exit statement, if the new if statement placement has the same exit statement executed after it + val exitAfterNewIf = exitStatementExecutedAfter(updatedIf, context) + if (exitAfterNewIf == null || !matches(exitAfterNewIf, exitStatementAfterIf)) { + val newThen = newIf.then as KtBlockExpression + newThen.addBefore(exitStatementAfterIf, newThen.rBrace) + } + } + + return updatedIf.replace(newIf) as KtIfExpression + } + } + } + } + + val exitStatement = exitStatementExecutedAfter(ifExpression, context) ?: return null + + val updatedIf = copyThenBranchAfter(ifExpression) + val newIf = psiFactory.createExpressionByPattern("if ($0) $1", newCondition, exitStatement) + return updatedIf.replace(newIf) as KtIfExpression + } + + private fun matches(exitExpr1: KtExpression, exitExpr2: KtExpression): Boolean { + return if (exitExpr1 is KtReturnExpression && exitExpr2 is KtReturnExpression) { + return exitExpr1 == exitExpr2 || (exitExpr1.returnedExpression == null && exitExpr2.returnedExpression == null) + } else exitExpr1.javaClass == exitExpr2.javaClass + } + + private fun isEmptyReturn(statement: KtExpression) = + statement is KtReturnExpression && statement.returnedExpression == null && statement.labeledExpression == null + + private fun copyThenBranchAfter(ifExpression: KtIfExpression): KtIfExpression { + val psiFactory = KtPsiFactory(ifExpression.project) + val thenBranch = ifExpression.then ?: return ifExpression + + val parent = ifExpression.parent + if (parent !is KtBlockExpression) { + assert(parent is KtContainerNode) + val block = psiFactory.createEmptyBody() + block.addAfter(ifExpression, block.lBrace) + val newBlock = ifExpression.replaced(block) + val newIf = newBlock.statements.single() as KtIfExpression + return copyThenBranchAfter(newIf) + } + + if (thenBranch is KtBlockExpression) { + (thenBranch.statements.lastOrNull() as? KtContinueExpression)?.delete() + val range = thenBranch.contentRange() + if (!range.isEmpty) { + parent.addRangeAfter(range.first, range.last, ifExpression) + parent.addAfter(psiFactory.createNewLine(), ifExpression) + } + } else if (thenBranch !is KtContinueExpression) { + parent.addAfter(thenBranch, ifExpression) + parent.addAfter(psiFactory.createNewLine(), ifExpression) + } + return ifExpression + } + + private fun exitStatementExecutedAfter(expression: KtExpression, context: Context): KtExpression? { + when (val parent = expression.parent) { + is KtBlockExpression -> { + val lastStatement = parent.statements.last() + return if (expression == lastStatement) { + exitStatementExecutedAfter(parent, context) + } else if (lastStatement.isExitStatement() && + expression.siblings(withItself = false).firstIsInstance() == lastStatement + ) { + lastStatement + } else { + null + } + } + is KtNamedFunction -> { + if (parent.bodyExpression == expression && parent.hasBlockBody() && context.isParentFunUnit) { + return KtPsiFactory(expression.project).createExpression("return") + } + } + + is KtContainerNode -> when (val pparent = parent.parent) { + is KtLoopExpression -> { + if (expression == pparent.body) { + return KtPsiFactory(expression.project).createExpression("continue") + } + } + + is KtIfExpression -> { + if (expression == pparent.then || expression == pparent.`else`) { + return exitStatementExecutedAfter(pparent, context) + } + } + } + } + return null + } + + private fun KtExpression.isExitStatement(): Boolean = when (this) { + is KtContinueExpression, is KtBreakExpression, is KtThrowExpression, is KtReturnExpression -> true + else -> false + } + + private fun parentBlockRBrace(element: KtIfExpression): PsiElement? = (element.parent as? KtBlockExpression)?.rBrace + + private fun KtIfExpression.nextEolCommentOnSameLine(): PsiElement? = getLineNumber(false).let { lastLineNumber -> + siblings(withItself = false) + .takeWhile { it.getLineNumber() == lastLineNumber } + .firstOrNull { it is PsiComment && it.node.elementType == KtTokens.EOL_COMMENT } + } +} + +internal class Context( + val newCondition: SmartPsiElementPointer, + val demorgansLawContext: DemorgansLawContext?, + val isParentFunUnit: Boolean, + val commentSaver: CommentSaver, +) diff --git a/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java b/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java index 2cd88b869637..b30f413c3ad2 100644 --- a/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java +++ b/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java @@ -4676,4 +4676,297 @@ public abstract class K2IntentionTestGenerated extends AbstractK2IntentionTest { runTest("../../../idea/tests/testData/intentions/convertBinaryExpressionWithDemorgansLaw/retainedParens.kt"); } } + + @RunWith(JUnit3RunnerWithInners.class) + @TestMetadata("../../../idea/tests/testData/intentions/invertIfCondition") + public static class InvertIfCondition extends AbstractK2IntentionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + @TestMetadata("addSurroundingBlock_preserveComments.kt") + public void testAddSurroundingBlock_preserveComments() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/addSurroundingBlock_preserveComments.kt"); + } + + @TestMetadata("assignedToValue.kt") + public void testAssignedToValue() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/assignedToValue.kt"); + } + + @TestMetadata("binaryExpression.kt") + public void testBinaryExpression() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/binaryExpression.kt"); + } + + @TestMetadata("booleanLiteral.kt") + public void testBooleanLiteral() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/booleanLiteral.kt"); + } + + @TestMetadata("branchingIfStatements.kt") + public void testBranchingIfStatements() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/branchingIfStatements.kt"); + } + + @TestMetadata("endOfLineCommentBug.kt") + public void testEndOfLineCommentBug() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/endOfLineCommentBug.kt"); + } + + @TestMetadata("forLoopWithMultipleExpressions.kt") + public void testForLoopWithMultipleExpressions() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/forLoopWithMultipleExpressions.kt"); + } + + @TestMetadata("functionWithReturnExpression.kt") + public void testFunctionWithReturnExpression() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/functionWithReturnExpression.kt"); + } + + @TestMetadata("ifExpressionInsideForLoop.kt") + public void testIfExpressionInsideForLoop() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifExpressionInsideForLoop.kt"); + } + + @TestMetadata("ifExpressionWithReturn.kt") + public void testIfExpressionWithReturn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifExpressionWithReturn.kt"); + } + + @TestMetadata("ifThenReturn.kt") + public void testIfThenReturn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifThenReturn.kt"); + } + + @TestMetadata("ifThenReturn2.kt") + public void testIfThenReturn2() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifThenReturn2.kt"); + } + + @TestMetadata("ifThenReturn3.kt") + public void testIfThenReturn3() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifThenReturn3.kt"); + } + + @TestMetadata("ifThenReturn4.kt") + public void testIfThenReturn4() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifThenReturn4.kt"); + } + + @TestMetadata("ifWithBothBranchesReturn.kt") + public void testIfWithBothBranchesReturn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifWithBothBranchesReturn.kt"); + } + + @TestMetadata("ifWithBothBranchesSetter.kt") + public void testIfWithBothBranchesSetter() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/ifWithBothBranchesSetter.kt"); + } + + @TestMetadata("in.kt") + public void testIn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/in.kt"); + } + + @TestMetadata("invertableOperator.kt") + public void testInvertableOperator() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/invertableOperator.kt"); + } + + @TestMetadata("is.kt") + public void testIs() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/is.kt"); + } + + @TestMetadata("isBlank.kt") + public void testIsBlank() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/isBlank.kt"); + } + + @TestMetadata("isEmpty.kt") + public void testIsEmpty() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/isEmpty.kt"); + } + + @TestMetadata("isNotBlank.kt") + public void testIsNotBlank() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/isNotBlank.kt"); + } + + @TestMetadata("isNotEmpty.kt") + public void testIsNotEmpty() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/isNotEmpty.kt"); + } + + @TestMetadata("lambdaNonLocalAndLocalReturn.kt") + public void testLambdaNonLocalAndLocalReturn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lambdaNonLocalAndLocalReturn.kt"); + } + + @TestMetadata("lambdaNonLocalReturn.kt") + public void testLambdaNonLocalReturn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lambdaNonLocalReturn.kt"); + } + + @TestMetadata("lastStatement1.kt") + public void testLastStatement1() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatement1.kt"); + } + + @TestMetadata("lastStatement2.kt") + public void testLastStatement2() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatement2.kt"); + } + + @TestMetadata("lastStatement3.kt") + public void testLastStatement3() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatement3.kt"); + } + + @TestMetadata("lastStatementBeforeBreak.kt") + public void testLastStatementBeforeBreak() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatementBeforeBreak.kt"); + } + + @TestMetadata("lastStatementBeforeContinue.kt") + public void testLastStatementBeforeContinue() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatementBeforeContinue.kt"); + } + + @TestMetadata("lastStatementBeforeReturn.kt") + public void testLastStatementBeforeReturn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatementBeforeReturn.kt"); + } + + @TestMetadata("lastStatementInLambda.kt") + public void testLastStatementInLambda() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatementInLambda.kt"); + } + + @TestMetadata("lastStatementInLoop.kt") + public void testLastStatementInLoop() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatementInLoop.kt"); + } + + @TestMetadata("lastStatementInLoop2.kt") + public void testLastStatementInLoop2() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatementInLoop2.kt"); + } + + @TestMetadata("lastStatementNonUnitMethod.kt") + public void testLastStatementNonUnitMethod() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/lastStatementNonUnitMethod.kt"); + } + + @TestMetadata("negatedExpression.kt") + public void testNegatedExpression() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/negatedExpression.kt"); + } + + @TestMetadata("negatedIsBlank.kt") + public void testNegatedIsBlank() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/negatedIsBlank.kt"); + } + + @TestMetadata("negatedIsEmpty.kt") + public void testNegatedIsEmpty() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/negatedIsEmpty.kt"); + } + + @TestMetadata("negatedIsNotBlank.kt") + public void testNegatedIsNotBlank() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/negatedIsNotBlank.kt"); + } + + @TestMetadata("negatedIsNotEmpty.kt") + public void testNegatedIsNotEmpty() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/negatedIsNotEmpty.kt"); + } + + @TestMetadata("nestedIfWithReturn.kt") + public void testNestedIfWithReturn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/nestedIfWithReturn.kt"); + } + + @TestMetadata("notBlock.kt") + public void testNotBlock() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock.kt"); + } + + @TestMetadata("notBlock2.kt") + public void testNotBlock2() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock2.kt"); + } + + @TestMetadata("notBlock3.kt") + public void testNotBlock3() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock3.kt"); + } + + @TestMetadata("notBlock4.kt") + public void testNotBlock4() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock4.kt"); + } + + @TestMetadata("notBlock5.kt") + public void testNotBlock5() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock5.kt"); + } + + @TestMetadata("notBlock6.kt") + public void testNotBlock6() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock6.kt"); + } + + @TestMetadata("notBlock7.kt") + public void testNotBlock7() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock7.kt"); + } + + @TestMetadata("notBlock8.kt") + public void testNotBlock8() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notBlock8.kt"); + } + + @TestMetadata("notIn.kt") + public void testNotIn() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notIn.kt"); + } + + @TestMetadata("notIs.kt") + public void testNotIs() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/notIs.kt"); + } + + @TestMetadata("returnIfExpression.kt") + public void testReturnIfExpression() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/returnIfExpression.kt"); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/simple.kt"); + } + + @TestMetadata("unnecessaryContinue.kt") + public void testUnnecessaryContinue() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/unnecessaryContinue.kt"); + } + + @TestMetadata("unnecessaryContinue2.kt") + public void testUnnecessaryContinue2() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/unnecessaryContinue2.kt"); + } + + @TestMetadata("unnecessaryContinue3.kt") + public void testUnnecessaryContinue3() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/unnecessaryContinue3.kt"); + } + + @TestMetadata("valueAndReturnBranches.kt") + public void testValueAndReturnBranches() throws Exception { + runTest("../../../idea/tests/testData/intentions/invertIfCondition/valueAndReturnBranches.kt"); + } + } } diff --git a/plugins/kotlin/code-insight/utils/src/org/jetbrains/kotlin/idea/codeinsight/utils/DemorgansLawUtils.kt b/plugins/kotlin/code-insight/utils/src/org/jetbrains/kotlin/idea/codeinsight/utils/DemorgansLawUtils.kt index caa8ba3b0112..3aeaeaaa5cb1 100644 --- a/plugins/kotlin/code-insight/utils/src/org/jetbrains/kotlin/idea/codeinsight/utils/DemorgansLawUtils.kt +++ b/plugins/kotlin/code-insight/utils/src/org/jetbrains/kotlin/idea/codeinsight/utils/DemorgansLawUtils.kt @@ -14,18 +14,14 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs context(KtAnalysisSession) @OptIn(UnsafeCastFunction::class) -fun prepareDemorgansLawContext(element: KtBinaryExpression): DemorgansLawContext? { - val expr = element.topmostBinaryExpression() - val operands = splitBooleanSequence(expr) ?: return null - if (!expr.left.isBoolean || !expr.right.isBoolean) return null +fun prepareDemorgansLawContext(operands: List): DemorgansLawContext { val pointers = operands.asReversed().map { operand -> operand.safeAs()?.invertSelectorFunction() ?: operand.negate(false) { it.isBoolean } }.map { it.createSmartPointer() } return DemorgansLawContext(pointers) } -fun applyDemorgansLaw(element: KtBinaryExpression, context: DemorgansLawContext) { - val expression = element.topmostBinaryExpression() +fun applyDemorgansLaw(expression: KtBinaryExpression, context: DemorgansLawContext) { val operatorText = when (expression.operationToken) { KtTokens.ANDAND -> KtTokens.OROR.value KtTokens.OROR -> KtTokens.ANDAND.value diff --git a/plugins/kotlin/idea/tests/testData/intentions/invertIfCondition/.k2Intention b/plugins/kotlin/idea/tests/testData/intentions/invertIfCondition/.k2Intention new file mode 100644 index 000000000000..452ef239015d --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/intentions/invertIfCondition/.k2Intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.k2.codeinsight.intentions.InvertIfConditionIntention diff --git a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt index ee61adeda933..a6fb4e31f95b 100644 --- a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt +++ b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt @@ -48,6 +48,7 @@ internal fun MutableTWorkspace.generateK2IntentionTests() { model("${idea}intentions/branched/ifWhen/whenToIf", pattern = pattern) model("code-insight/intentions-k2/tests/testData/intentions", pattern = pattern) model("${idea}intentions/convertBinaryExpressionWithDemorgansLaw", pattern = pattern) + model("${idea}intentions/invertIfCondition", pattern = pattern) } testClass {