diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetPsiUtil.java b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetPsiUtil.java index 5534a08893e..624c74440c2 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetPsiUtil.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetPsiUtil.java @@ -551,6 +551,11 @@ public class JetPsiUtil { return true; } + // 'x ?: ...' case + if (parentExpression instanceof JetBinaryExpression && parentOperation == JetTokens.ELVIS && currentInner == ((JetBinaryExpression) parentExpression).getRight()) { + return false; + } + int innerPriority = getPriority(innerExpression); int parentPriority = getPriority(parentExpression); diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 564652f7729..9e6f4a48cd7 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -498,6 +498,11 @@ language="jet" implementationClass="org.jetbrains.kotlin.idea.search.ideaExtensions.JetTargetElementEvaluator" /> + + org.jetbrains.kotlin.idea.intentions.IfNullToElvisIntention + Kotlin + + org.jetbrains.kotlin.idea.intentions.SpecifyTypeExplicitlyAction Kotlin diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/IfNullToElvisIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/IfNullToElvisIntention.kt new file mode 100644 index 00000000000..5f4f576b94a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/IfNullToElvisIntention.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.intentions + +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiRecursiveElementVisitor +import org.jetbrains.kotlin.JetNodeTypes +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.util.isNothing +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.replaced +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import java.util.ArrayList + +public class IfNullToElvisIntention : JetSelfTargetingIntention(javaClass(), "Replace 'if' with elvis operator"){ + override fun isApplicableTo(element: JetIfExpression, caretOffset: Int): Boolean { + //TODO: range! + val data = calcData(element) ?: return false + + val type = data.ifNullExpression.analyze().getType(data.ifNullExpression) ?: return false + if (!type.isNothing()) return false + + return true + } + + override fun applyTo(element: JetIfExpression, editor: Editor) { + val (initializer, statement, ifNullExpr) = calcData(element)!! + val factory = JetPsiFactory(element) + + // do not loose any comments! + val comments = element.extractComments(ifNullExpr) + + for (comment in comments) { + statement.add(factory.createWhiteSpace()) + statement.add(comment) + } + + val elvis = factory.createExpression("a ?: b") as JetBinaryExpression + elvis.getLeft()!!.replace(initializer) + elvis.getRight()!!.replace(ifNullExpr) + val newElvis = initializer.replaced(elvis) + element.delete() + + editor.getCaretModel().moveToOffset(newElvis.getRight()!!.getTextOffset()) + } + + private data class Data( + val initializer: JetExpression, + val statement: JetExpression, + val ifNullExpression: JetExpression + ) + + private fun calcData(ifExpression: JetIfExpression): Data? { + if (ifExpression.getElse() != null) return null + + val binaryExpression = ifExpression.getCondition() as? JetBinaryExpression ?: return null + if (binaryExpression.getRight()?.getNode()?.getElementType() != JetNodeTypes.NULL) return null + val left = binaryExpression.getLeft() as? JetSimpleNameExpression ?: return null + + if (ifExpression.getParent() !is JetBlockExpression) return null + val prevStatement = ifExpression.siblings(forward = false, withItself = false) + .firstIsInstanceOrNull() ?: return null + if (prevStatement !is JetVariableDeclaration) return null + if (prevStatement.getNameAsName() != left.getReferencedNameAsName()) return null + val initializer = prevStatement.getInitializer() ?: return null + val then = ifExpression.getThen() ?: return null + + if (then is JetBlockExpression) { + val statement = then.getStatements().singleOrNull() as? JetExpression ?: return null + return Data(initializer, prevStatement, statement) + } + else { + return Data(initializer, prevStatement, then) + } + } + + private fun PsiElement.extractComments(skipElement: PsiElement): List { + val comments = ArrayList() + accept(object : PsiRecursiveElementVisitor() { + override fun visitElement(element: PsiElement) { + if (element == skipElement) return + super.visitElement(element) + } + + override fun visitComment(comment: PsiComment) { + comments.add(comment) + } + }) + return comments + } +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/.intention b/idea/testData/intentions/ifNullToElvis/.intention new file mode 100644 index 00000000000..f7463a9e35f --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.IfNullToElvisIntention diff --git a/idea/testData/intentions/ifNullToElvis/Break.kt b/idea/testData/intentions/ifNullToElvis/Break.kt new file mode 100644 index 00000000000..7bfc3082fd4 --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Break.kt @@ -0,0 +1,6 @@ +fun foo(p: List) { + for (i in 1..10) { + val v = p[i] + if (v == null) break + } +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Break.kt.after b/idea/testData/intentions/ifNullToElvis/Break.kt.after new file mode 100644 index 00000000000..00a52987e9b --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Break.kt.after @@ -0,0 +1,5 @@ +fun foo(p: List) { + for (i in 1..10) { + val v = p[i] ?: break + } +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/CommentInBlock.kt b/idea/testData/intentions/ifNullToElvis/CommentInBlock.kt new file mode 100644 index 00000000000..1d4f323f9ce --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/CommentInBlock.kt @@ -0,0 +1,8 @@ +fun foo(p: List): Int { + val v = p[0] + if (v == null) { + // return -1 if null + return -1 + } + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/CommentInBlock.kt.after b/idea/testData/intentions/ifNullToElvis/CommentInBlock.kt.after new file mode 100644 index 00000000000..d8079d811ef --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/CommentInBlock.kt.after @@ -0,0 +1,4 @@ +fun foo(p: List): Int { + val v = p[0] ?: return -1 // return -1 if null + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Comments.kt b/idea/testData/intentions/ifNullToElvis/Comments.kt new file mode 100644 index 00000000000..2aa1c6bdaef --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Comments.kt @@ -0,0 +1,6 @@ +fun foo(p: List): Int { + val v = p[0] + // now check if v is null + if (v == null/* null */) return -1 // return -1 + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Comments.kt.after b/idea/testData/intentions/ifNullToElvis/Comments.kt.after new file mode 100644 index 00000000000..690207a2def --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Comments.kt.after @@ -0,0 +1,6 @@ +fun foo(p: List): Int { + val v = p[0] ?: return -1 /* null */ + // now check if v is null + // return -1 + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Comments2.kt b/idea/testData/intentions/ifNullToElvis/Comments2.kt new file mode 100644 index 00000000000..ce462b54c9a --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Comments2.kt @@ -0,0 +1,8 @@ +fun foo(p: List): Int { + val v = p[0] + if (v == null) { // v is null + // we should do something with it + return -1 // let's return -1 + } // end of if + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Comments2.kt.after b/idea/testData/intentions/ifNullToElvis/Comments2.kt.after new file mode 100644 index 00000000000..55cc5df7eba --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Comments2.kt.after @@ -0,0 +1,5 @@ +fun foo(p: List): Int { + val v = p[0] ?: return -1 // v is null // we should do something with it // let's return -1 + // end of if + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Continue.kt b/idea/testData/intentions/ifNullToElvis/Continue.kt new file mode 100644 index 00000000000..42ee0bdaec9 --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Continue.kt @@ -0,0 +1,6 @@ +fun foo(p: List) { + for (i in 1..10) { + val v = p[i] + if (v == null) continue + } +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Continue.kt.after b/idea/testData/intentions/ifNullToElvis/Continue.kt.after new file mode 100644 index 00000000000..c87fb239232 --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Continue.kt.after @@ -0,0 +1,5 @@ +fun foo(p: List) { + for (i in 1..10) { + val v = p[i] ?: continue + } +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/MultiStatementBlock.kt b/idea/testData/intentions/ifNullToElvis/MultiStatementBlock.kt new file mode 100644 index 00000000000..45e0b97ac0c --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/MultiStatementBlock.kt @@ -0,0 +1,10 @@ +// IS_APPLICABLE: false +fun foo(p: List) { + val v = p[0] + if (v == null) { + bar() + return + } +} + +fun bar(){} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/NotExit.kt b/idea/testData/intentions/ifNullToElvis/NotExit.kt new file mode 100644 index 00000000000..ce91eaaefdf --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/NotExit.kt @@ -0,0 +1,8 @@ +// IS_APPLICABLE: false +fun foo(p: List): Int? { + val v = p[0] + if (v == null) bar() + return v?.length() +} + +fun bar(){} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/OtherVar1.kt b/idea/testData/intentions/ifNullToElvis/OtherVar1.kt new file mode 100644 index 00000000000..f956f4365dc --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/OtherVar1.kt @@ -0,0 +1,11 @@ +// IS_APPLICABLE: false +class C { + var v: String? = null + + fun foo(p: List): Int { + val v = p[0] + if (this.v == null) return -1 + return v!!.length() + } +} + diff --git a/idea/testData/intentions/ifNullToElvis/OtherVar2.kt b/idea/testData/intentions/ifNullToElvis/OtherVar2.kt new file mode 100644 index 00000000000..f0764475964 --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/OtherVar2.kt @@ -0,0 +1,11 @@ +// IS_APPLICABLE: false +class C { + var x: String? = null + + fun foo(p: List): Int { + val v = p[0] + if (x == null) return -1 + return v!!.length() + } +} + diff --git a/idea/testData/intentions/ifNullToElvis/Return.kt b/idea/testData/intentions/ifNullToElvis/Return.kt new file mode 100644 index 00000000000..a2eb900e9d3 --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Return.kt @@ -0,0 +1,5 @@ +fun foo(p: List): Int { + val v = p[0] + if (v == null) return -1 + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/Return.kt.after b/idea/testData/intentions/ifNullToElvis/Return.kt.after new file mode 100644 index 00000000000..d0c49ad031c --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/Return.kt.after @@ -0,0 +1,4 @@ +fun foo(p: List): Int { + val v = p[0] ?: return -1 + return v.length() +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/ThrowInBlock.kt b/idea/testData/intentions/ifNullToElvis/ThrowInBlock.kt new file mode 100644 index 00000000000..8b1a17d6be8 --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/ThrowInBlock.kt @@ -0,0 +1,8 @@ +// WITH_RUNTIME + +fun foo(p: List) { + val v = p[0] + if (v == null) { + throw RuntimeException() + } +} \ No newline at end of file diff --git a/idea/testData/intentions/ifNullToElvis/ThrowInBlock.kt.after b/idea/testData/intentions/ifNullToElvis/ThrowInBlock.kt.after new file mode 100644 index 00000000000..20a03b81934 --- /dev/null +++ b/idea/testData/intentions/ifNullToElvis/ThrowInBlock.kt.after @@ -0,0 +1,5 @@ +// WITH_RUNTIME + +fun foo(p: List) { + val v = p[0] ?: throw RuntimeException() +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 538013f9420..67441fd1573 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -4094,6 +4094,81 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/ifNullToElvis") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class IfNullToElvis extends AbstractIntentionTest { + public void testAllFilesPresentInIfNullToElvis() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/ifNullToElvis"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("Break.kt") + public void testBreak() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/Break.kt"); + doTest(fileName); + } + + @TestMetadata("CommentInBlock.kt") + public void testCommentInBlock() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/CommentInBlock.kt"); + doTest(fileName); + } + + @TestMetadata("Comments.kt") + public void testComments() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/Comments.kt"); + doTest(fileName); + } + + @TestMetadata("Comments2.kt") + public void testComments2() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/Comments2.kt"); + doTest(fileName); + } + + @TestMetadata("Continue.kt") + public void testContinue() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/Continue.kt"); + doTest(fileName); + } + + @TestMetadata("MultiStatementBlock.kt") + public void testMultiStatementBlock() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/MultiStatementBlock.kt"); + doTest(fileName); + } + + @TestMetadata("NotExit.kt") + public void testNotExit() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/NotExit.kt"); + doTest(fileName); + } + + @TestMetadata("OtherVar1.kt") + public void testOtherVar1() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/OtherVar1.kt"); + doTest(fileName); + } + + @TestMetadata("OtherVar2.kt") + public void testOtherVar2() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/OtherVar2.kt"); + doTest(fileName); + } + + @TestMetadata("Return.kt") + public void testReturn() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/Return.kt"); + doTest(fileName); + } + + @TestMetadata("ThrowInBlock.kt") + public void testThrowInBlock() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/ifNullToElvis/ThrowInBlock.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/intentions/insertCurlyBracesToTemplate") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)