diff --git a/idea/resources-en/messages/KotlinBundle.properties b/idea/resources-en/messages/KotlinBundle.properties index e1505b05311..49b213e7250 100644 --- a/idea/resources-en/messages/KotlinBundle.properties +++ b/idea/resources-en/messages/KotlinBundle.properties @@ -2212,6 +2212,7 @@ codestyle.name.kotlin=Kotlin add.missing.class.keyword=Add missing 'class' keyword fix.move.typealias.to.top.level=Move typealias to top level fix.change.jvm.name=Change JVM name +expand.boolean.expression.to.if.else=Expand boolean expression to 'if else' inspection.logger.initialized.with.foreign.class.display.name=Logger initialized with foreign class logger.initialized.with.foreign.class=Logger initialized with foreign class ''{0}'' logger.factory.method.name=Logger factory method name diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index 6d6a85e76b7..e1d0bf0e26d 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -1642,6 +1642,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.ExpandBooleanExpressionIntention + Kotlin + + + +This intention replaces a boolean expression with the equivalent if-else expression. + + diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ExpandBooleanExpressionIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ExpandBooleanExpressionIntention.kt new file mode 100644 index 00000000000..3a7b60dc5c5 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ExpandBooleanExpressionIntention.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.intentions + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.psi2ir.deparenthesize +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.typeUtil.isBoolean + +class ExpandBooleanExpressionIntention : SelfTargetingRangeIntention( + KtExpression::class.java, + KotlinBundle.lazyMessage("expand.boolean.expression.to.if.else") +) { + override fun applicabilityRange(element: KtExpression): TextRange? { + val target = element.parents.takeWhile { + it is KtCallExpression || it is KtQualifiedExpression || it is KtOperationExpression || it is KtParenthesizedExpression + }.lastOrNull() ?: element + if (element != target) return null + if (element.deparenthesize() is KtConstantExpression) return null + val parent = element.parent + if (parent is KtValueArgument || parent is KtParameter || parent is KtStringTemplateEntry) return null + val context = element.analyze(BodyResolveMode.PARTIAL) + if (context[BindingContext.EXPRESSION_TYPE_INFO, element]?.type?.isBoolean() != true) return null + return element.textRange + } + + override fun applyTo(element: KtExpression, editor: Editor?) { + val ifExpression = KtPsiFactory(element).createExpressionByPattern("if ($0) {\ntrue\n} else {\nfalse\n}", element) + val replaced = element.replace(ifExpression) + if (replaced != null) { + editor?.caretModel?.moveToOffset(replaced.startOffset) + } + } +} diff --git a/idea/testData/intentions/expandBooleanExpression/.intention b/idea/testData/intentions/expandBooleanExpression/.intention new file mode 100644 index 00000000000..d0bcde871ef --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.ExpandBooleanExpressionIntention diff --git a/idea/testData/intentions/expandBooleanExpression/binary.kt b/idea/testData/intentions/expandBooleanExpression/binary.kt new file mode 100644 index 00000000000..643577aff6a --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/binary.kt @@ -0,0 +1,3 @@ +fun test(i: Int): Boolean { + return i == 1 || i == 2 || i == 3 +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/binary.kt.after b/idea/testData/intentions/expandBooleanExpression/binary.kt.after new file mode 100644 index 00000000000..8d22ff07f14 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/binary.kt.after @@ -0,0 +1,7 @@ +fun test(i: Int): Boolean { + return if (i == 1 || i == 2 || i == 3) { + true + } else { + false + } +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/call.kt b/idea/testData/intentions/expandBooleanExpression/call.kt new file mode 100644 index 00000000000..3393dfdc512 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/call.kt @@ -0,0 +1,5 @@ +fun test(b: Boolean): Boolean { + return foo(b) +} + +fun foo(b: Boolean) = true \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/call.kt.after b/idea/testData/intentions/expandBooleanExpression/call.kt.after new file mode 100644 index 00000000000..e01e1c10a40 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/call.kt.after @@ -0,0 +1,9 @@ +fun test(b: Boolean): Boolean { + return if (foo(b)) { + true + } else { + false + } +} + +fun foo(b: Boolean) = true \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/constant.kt b/idea/testData/intentions/expandBooleanExpression/constant.kt new file mode 100644 index 00000000000..22df78f87c6 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/constant.kt @@ -0,0 +1,4 @@ +// IS_APPLICABLE: false +fun test(): Boolean { + return true +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/inArgument.kt b/idea/testData/intentions/expandBooleanExpression/inArgument.kt new file mode 100644 index 00000000000..9894b098fda --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/inArgument.kt @@ -0,0 +1,6 @@ +// IS_APPLICABLE: false +fun test(b: Boolean) { + foo(b) +} + +fun foo(b: Boolean) {} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/inParameter.kt b/idea/testData/intentions/expandBooleanExpression/inParameter.kt new file mode 100644 index 00000000000..8dcb5b9865c --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/inParameter.kt @@ -0,0 +1,3 @@ +// IS_APPLICABLE: false +fun test(i: Int, b: Boolean = i == 1) { +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/inStringTemplate.kt b/idea/testData/intentions/expandBooleanExpression/inStringTemplate.kt new file mode 100644 index 00000000000..76816a1bad9 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/inStringTemplate.kt @@ -0,0 +1,4 @@ +// IS_APPLICABLE: false +fun test(b: Boolean) { + val s = "b is $b" +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/is.kt b/idea/testData/intentions/expandBooleanExpression/is.kt new file mode 100644 index 00000000000..71d3f6abf1c --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/is.kt @@ -0,0 +1,3 @@ +fun test(a: Any): Boolean { + return a is String +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/is.kt.after b/idea/testData/intentions/expandBooleanExpression/is.kt.after new file mode 100644 index 00000000000..c3c6a9cba91 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/is.kt.after @@ -0,0 +1,7 @@ +fun test(a: Any): Boolean { + return if (a is String) { + true + } else { + false + } +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/parenthesized.kt b/idea/testData/intentions/expandBooleanExpression/parenthesized.kt new file mode 100644 index 00000000000..0428b470ae0 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/parenthesized.kt @@ -0,0 +1,3 @@ +fun test(i: Int): Boolean { + return (i == 1 || i == 2 || i == 3) +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/parenthesized.kt.after b/idea/testData/intentions/expandBooleanExpression/parenthesized.kt.after new file mode 100644 index 00000000000..71db0e16be3 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/parenthesized.kt.after @@ -0,0 +1,7 @@ +fun test(i: Int): Boolean { + return if ((i == 1 || i == 2 || i == 3)) { + true + } else { + false + } +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/postfix.kt b/idea/testData/intentions/expandBooleanExpression/postfix.kt new file mode 100644 index 00000000000..d474f6e293b --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/postfix.kt @@ -0,0 +1,3 @@ +fun test(b: Boolean?): Boolean { + return b!! +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/postfix.kt.after b/idea/testData/intentions/expandBooleanExpression/postfix.kt.after new file mode 100644 index 00000000000..c72c1b27754 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/postfix.kt.after @@ -0,0 +1,7 @@ +fun test(b: Boolean?): Boolean { + return if (b!!) { + true + } else { + false + } +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/prefix.kt b/idea/testData/intentions/expandBooleanExpression/prefix.kt new file mode 100644 index 00000000000..f71c0090736 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/prefix.kt @@ -0,0 +1,3 @@ +fun test(i: Int): Boolean { + return !(i == 1 || i == 2 || i == 3) +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/prefix.kt.after b/idea/testData/intentions/expandBooleanExpression/prefix.kt.after new file mode 100644 index 00000000000..51d418f1000 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/prefix.kt.after @@ -0,0 +1,7 @@ +fun test(i: Int): Boolean { + return if (!(i == 1 || i == 2 || i == 3)) { + true + } else { + false + } +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/qualified.kt b/idea/testData/intentions/expandBooleanExpression/qualified.kt new file mode 100644 index 00000000000..dff39bb6179 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/qualified.kt @@ -0,0 +1,7 @@ +fun test(b: Boolean) { + val b = Foo().bar() +} + +class Foo { + fun bar() = true +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/qualified.kt.after b/idea/testData/intentions/expandBooleanExpression/qualified.kt.after new file mode 100644 index 00000000000..a8964664514 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/qualified.kt.after @@ -0,0 +1,11 @@ +fun test(b: Boolean) { + val b = if (Foo().bar()) { + true + } else { + false + } +} + +class Foo { + fun bar() = true +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/variable.kt b/idea/testData/intentions/expandBooleanExpression/variable.kt new file mode 100644 index 00000000000..f0976abce45 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/variable.kt @@ -0,0 +1,3 @@ +fun test(b: Boolean): Boolean { + return b +} \ No newline at end of file diff --git a/idea/testData/intentions/expandBooleanExpression/variable.kt.after b/idea/testData/intentions/expandBooleanExpression/variable.kt.after new file mode 100644 index 00000000000..42565c59f65 --- /dev/null +++ b/idea/testData/intentions/expandBooleanExpression/variable.kt.after @@ -0,0 +1,7 @@ +fun test(b: Boolean): Boolean { + return if (b) { + true + } else { + false + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createFromUsage/createFunction/call/kt10983.kt b/idea/testData/quickfix/createFromUsage/createFunction/call/kt10983.kt index 8413673bcf1..1e526823c30 100644 --- a/idea/testData/quickfix/createFromUsage/createFunction/call/kt10983.kt +++ b/idea/testData/quickfix/createFromUsage/createFunction/call/kt10983.kt @@ -7,6 +7,7 @@ // ACTION: Create property 'maximumSizeOfGroup' // ACTION: Introduce local variable // ACTION: Rename reference +// ACTION: Expand boolean expression to 'if else' // ERROR: A 'return' expression required in a function with a block body ('{...}') // ERROR: The expression cannot be a selector (occur after a dot) // ERROR: Type inference failed: inline fun Iterable.firstOrNull(predicate: (T) -> Boolean): T?
cannot be applied to
receiver: Collection> arguments: ((List) -> () -> Boolean)
diff --git a/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter.kt b/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter.kt index 70465923196..83ac67c0133 100644 --- a/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter.kt +++ b/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter.kt @@ -2,5 +2,6 @@ // ACTION: Change type arguments to <*> // ACTION: Convert to block body // ACTION: Introduce local variable +// ACTION: Expand boolean expression to 'if else' // ERROR: Cannot check for instance of erased type: List fun test(a: List) = a is List \ No newline at end of file diff --git a/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter2.kt b/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter2.kt index 178f43dc687..34be81f9818 100644 --- a/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter2.kt +++ b/idea/testData/quickfix/makeTypeParameterReified/noTypeParameter2.kt @@ -2,5 +2,6 @@ // ACTION: Change type arguments to <*> // ACTION: Convert to block body // ACTION: Introduce local variable +// ACTION: Expand boolean expression to 'if else' // ERROR: Cannot check for instance of erased type: List fun test(a: List) = a is List \ No newline at end of file diff --git a/idea/testData/quickfix/nullables/unsafeInfixCall/noComparison.kt b/idea/testData/quickfix/nullables/unsafeInfixCall/noComparison.kt index cb009caa6b9..c5aeca71007 100644 --- a/idea/testData/quickfix/nullables/unsafeInfixCall/noComparison.kt +++ b/idea/testData/quickfix/nullables/unsafeInfixCall/noComparison.kt @@ -3,6 +3,7 @@ // ACTION: Add non-null asserted (!!) call // ACTION: Flip '>' // ACTION: Replace overloaded operator with function call +// ACTION: Expand boolean expression to 'if else' class SafeType { operator fun compareTo(other : SafeType) = 0 diff --git a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInCondition.kt b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInCondition.kt index 73a26b535c6..2f18b41299c 100644 --- a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInCondition.kt +++ b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInCondition.kt @@ -2,6 +2,7 @@ // ACTION: Add non-null asserted (!!) call // ACTION: Flip '<' // ACTION: Replace overloaded operator with function call +// ACTION: Expand boolean expression to 'if else' // ERROR: Operator call corresponds to a dot-qualified call 'w?.x.compareTo(42)' which is not allowed on a nullable receiver 'w?.x'. class Wrapper(val x: Int) diff --git a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInLogic.kt b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInLogic.kt index e540af92cc1..6a502f99740 100644 --- a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInLogic.kt +++ b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInLogic.kt @@ -4,6 +4,7 @@ // ACTION: Replace '&&' with '||' // ACTION: Replace overloaded operator with function call // ACTION: Simplify boolean expression +// ACTION: Expand boolean expression to 'if else' // ERROR: Operator call corresponds to a dot-qualified call 'w?.x.compareTo(42)' which is not allowed on a nullable receiver 'w?.x'. class Wrapper(val x: Int) diff --git a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhen.kt b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhen.kt index 6e3023d88b8..8ed2b9bb709 100644 --- a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhen.kt +++ b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhen.kt @@ -3,6 +3,7 @@ // ACTION: Flip '<=' // ACTION: Enable a trailing comma by default in the formatter // ACTION: Replace overloaded operator with function call +// ACTION: Expand boolean expression to 'if else' // ERROR: Operator call corresponds to a dot-qualified call 'w?.x.compareTo(42)' which is not allowed on a nullable receiver 'w?.x'. class Wrapper(val x: Int) diff --git a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhile.kt b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhile.kt index ab6aa7b5d78..d3d68dd908f 100644 --- a/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhile.kt +++ b/idea/testData/quickfix/nullables/unsafeInfixCall/unsafeComparisonInWhile.kt @@ -2,6 +2,7 @@ // ACTION: Add non-null asserted (!!) call // ACTION: Flip '>=' // ACTION: Replace overloaded operator with function call +// ACTION: Expand boolean expression to 'if else' // ERROR: Operator call corresponds to a dot-qualified call 'w?.x.compareTo(42)' which is not allowed on a nullable receiver 'w?.x'. class Wrapper(val x: Int) diff --git a/idea/testData/quickfix/typeMismatch/typeMismatchOnReturnedExpression/multiFakeOverrideForOperatorConvention.kt b/idea/testData/quickfix/typeMismatch/typeMismatchOnReturnedExpression/multiFakeOverrideForOperatorConvention.kt index 112a32a94c4..5f26a1a2134 100644 --- a/idea/testData/quickfix/typeMismatch/typeMismatchOnReturnedExpression/multiFakeOverrideForOperatorConvention.kt +++ b/idea/testData/quickfix/typeMismatch/typeMismatchOnReturnedExpression/multiFakeOverrideForOperatorConvention.kt @@ -1,6 +1,7 @@ // "Change return type of called function 'AA.contains' to 'Int'" "false" // ACTION: Change return type of enclosing function 'AAA.g' to 'Boolean' // ACTION: Replace overloaded operator with function call +// ACTION: Expand boolean expression to 'if else' // ERROR: Type mismatch: inferred type is Boolean but Int was expected interface A { operator fun contains(i: Int): Boolean diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 8bf42c08c31..c0d2531a5a5 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -8641,6 +8641,79 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/expandBooleanExpression") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ExpandBooleanExpression extends AbstractIntentionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + public void testAllFilesPresentInExpandBooleanExpression() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/intentions/expandBooleanExpression"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), null, true); + } + + @TestMetadata("binary.kt") + public void testBinary() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/binary.kt"); + } + + @TestMetadata("call.kt") + public void testCall() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/call.kt"); + } + + @TestMetadata("constant.kt") + public void testConstant() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/constant.kt"); + } + + @TestMetadata("inArgument.kt") + public void testInArgument() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/inArgument.kt"); + } + + @TestMetadata("inParameter.kt") + public void testInParameter() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/inParameter.kt"); + } + + @TestMetadata("inStringTemplate.kt") + public void testInStringTemplate() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/inStringTemplate.kt"); + } + + @TestMetadata("is.kt") + public void testIs() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/is.kt"); + } + + @TestMetadata("parenthesized.kt") + public void testParenthesized() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/parenthesized.kt"); + } + + @TestMetadata("postfix.kt") + public void testPostfix() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/postfix.kt"); + } + + @TestMetadata("prefix.kt") + public void testPrefix() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/prefix.kt"); + } + + @TestMetadata("qualified.kt") + public void testQualified() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/qualified.kt"); + } + + @TestMetadata("variable.kt") + public void testVariable() throws Exception { + runTest("idea/testData/intentions/expandBooleanExpression/variable.kt"); + } + } + @TestMetadata("idea/testData/intentions/implementAbstractMember") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)