From e409007749114edc2cec8c06e400acf827305cdc Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Tue, 4 Sep 2018 07:12:01 +0300 Subject: [PATCH] Join Lines: join initializer + if null check to elvis (KT-22388) #KT-22388 Fixed --- idea/src/META-INF/plugin-common.xml | 1 + .../FoldInitializerAndIfToElvisIntention.kt | 205 ++++++++++-------- .../JoinInitializerAndIfToElvisHandler.kt | 31 +++ .../initializerAndIfToElvis/simple.kt | 5 + .../initializerAndIfToElvis/simple.kt.after | 4 + .../declarations/JoinLinesTestGenerated.java | 18 ++ 6 files changed, 169 insertions(+), 95 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/joinLines/JoinInitializerAndIfToElvisHandler.kt create mode 100644 idea/testData/joinLines/initializerAndIfToElvis/simple.kt create mode 100644 idea/testData/joinLines/initializerAndIfToElvis/simple.kt.after diff --git a/idea/src/META-INF/plugin-common.xml b/idea/src/META-INF/plugin-common.xml index cd984da64e0..c7b1ddaa2ad 100644 --- a/idea/src/META-INF/plugin-common.xml +++ b/idea/src/META-INF/plugin-common.xml @@ -761,6 +761,7 @@ + diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/FoldInitializerAndIfToElvisIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/FoldInitializerAndIfToElvisIntention.kt index 8bec7c36609..53ce7b632b9 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/FoldInitializerAndIfToElvisIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/FoldInitializerAndIfToElvisIntention.kt @@ -43,111 +43,126 @@ import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull class FoldInitializerAndIfToElvisInspection : IntentionBasedInspection(FoldInitializerAndIfToElvisIntention::class) -class FoldInitializerAndIfToElvisIntention : SelfTargetingRangeIntention(KtIfExpression::class.java, "Replace 'if' with elvis operator"){ +class FoldInitializerAndIfToElvisIntention : + SelfTargetingRangeIntention(KtIfExpression::class.java, "Replace 'if' with elvis operator") { + override fun applicabilityRange(element: KtIfExpression): TextRange? { - val data = calcData(element) ?: return null - - val type = data.ifNullExpression.analyze().getType(data.ifNullExpression) ?: return null - if (!type.isNothing()) return null - - val rParen = element.rightParenthesis ?: return null - return TextRange(element.startOffset, rParen.endOffset) + return FoldInitializerAndIfToElvisIntention.applicabilityRange(element) } override fun applyTo(element: KtIfExpression, editor: Editor?) { - val (initializer, declaration, ifNullExpr, typeReference) = calcData(element)!! - val factory = KtPsiFactory(element) - - val explicitTypeToSet = when { - // for var with no explicit type, add it so that the actual change won't change - declaration.isVar && declaration.typeReference == null -> initializer.analyze(BodyResolveMode.PARTIAL).getType(initializer) - - // for val with explicit type, change it to non-nullable - !declaration.isVar && declaration.typeReference != null -> initializer.analyze(BodyResolveMode.PARTIAL).getType(initializer)?.makeNotNullable() - - else -> null - } - - val childRangeBefore = PsiChildRange(declaration, element) - val commentSaver = CommentSaver(childRangeBefore) - val childRangeAfter = childRangeBefore.withoutLastStatement() - - val pattern = if (element.then?.hasComments() == true) - "$0\n?: $1" - else - "$0 ?: $1" - - val elvis = factory.createExpressionByPattern(pattern, initializer, ifNullExpr) as KtBinaryExpression - if (typeReference != null) { - elvis.left!!.replace(factory.createExpressionByPattern("$0 as? $1", initializer, typeReference)) - } - val newElvis = initializer.replaced(elvis) - element.delete() - - if (explicitTypeToSet != null && !explicitTypeToSet.isError) { - declaration.setType(explicitTypeToSet) - } - - commentSaver.restore(childRangeAfter) - + val newElvis = FoldInitializerAndIfToElvisIntention.applyTo(element) editor?.caretModel?.moveToOffset(newElvis.right!!.textOffset) } - private fun PsiElement.hasComments(): Boolean { - var result = false - accept(object : KtTreeVisitorVoid() { - override fun visitComment(comment: PsiComment?) { - result = true + companion object { + private fun applicabilityRange(element: KtIfExpression): TextRange? { + val data = calcData(element) ?: return null + + val type = data.ifNullExpression.analyze().getType(data.ifNullExpression) ?: return null + if (!type.isNothing()) return null + + val rParen = element.rightParenthesis ?: return null + return TextRange(element.startOffset, rParen.endOffset) + } + + fun isApplicable(element: KtIfExpression): Boolean = applicabilityRange(element) != null + + fun applyTo(element: KtIfExpression): KtBinaryExpression { + val (initializer, declaration, ifNullExpr, typeReference) = calcData(element)!! + val factory = KtPsiFactory(element) + + val explicitTypeToSet = when { + // for var with no explicit type, add it so that the actual change won't change + declaration.isVar && declaration.typeReference == null -> initializer.analyze(BodyResolveMode.PARTIAL).getType(initializer) + + // for val with explicit type, change it to non-nullable + !declaration.isVar && declaration.typeReference != null -> initializer.analyze(BodyResolveMode.PARTIAL).getType(initializer)?.makeNotNullable() + + else -> null } - }) - return result + + val childRangeBefore = PsiChildRange(declaration, element) + val commentSaver = CommentSaver(childRangeBefore) + val childRangeAfter = childRangeBefore.withoutLastStatement() + + val pattern = if (element.then?.hasComments() == true) + "$0\n?: $1" + else + "$0 ?: $1" + + val elvis = factory.createExpressionByPattern(pattern, initializer, ifNullExpr) as KtBinaryExpression + if (typeReference != null) { + elvis.left!!.replace(factory.createExpressionByPattern("$0 as? $1", initializer, typeReference)) + } + val newElvis = initializer.replaced(elvis) + element.delete() + + if (explicitTypeToSet != null && !explicitTypeToSet.isError) { + declaration.setType(explicitTypeToSet) + } + + commentSaver.restore(childRangeAfter) + + return newElvis + } + + private fun PsiElement.hasComments(): Boolean { + var result = false + accept(object : KtTreeVisitorVoid() { + override fun visitComment(comment: PsiComment?) { + result = true + } + }) + return result + } + + private fun calcData(ifExpression: KtIfExpression): Data? { + if (ifExpression.`else` != null) return null + + val operationExpression = ifExpression.condition as? KtOperationExpression ?: return null + val value = when (operationExpression) { + is KtBinaryExpression -> { + if (operationExpression.operationToken != KtTokens.EQEQ) return null + operationExpression.expressionComparedToNull() + } + is KtIsExpression -> { + if (!operationExpression.isNegated) return null + if (operationExpression.typeReference?.typeElement is KtNullableType) return null + operationExpression.leftHandSide + } + else -> return null + } as? KtNameReferenceExpression ?: return null + + if (ifExpression.parent !is KtBlockExpression) return null + val prevStatement = (ifExpression.siblings(forward = false, withItself = false) + .firstIsInstanceOrNull() ?: return null) as? KtVariableDeclaration + prevStatement ?: return null + if (prevStatement.nameAsName != value.getReferencedNameAsName()) return null + val initializer = prevStatement.initializer ?: return null + val then = ifExpression.then ?: return null + val typeReference = (operationExpression as? KtIsExpression)?.typeReference + + val statement = if (then is KtBlockExpression) then.statements.singleOrNull() else then + statement ?: return null + + if (ReferencesSearch.search(prevStatement, LocalSearchScope(statement)).findFirst() != null) { + return null + } + + return Data(initializer, prevStatement, statement, typeReference) + } + + private fun PsiChildRange.withoutLastStatement(): PsiChildRange { + val newLast = last!!.siblings(forward = false, withItself = false).first { it !is PsiWhiteSpace } + return PsiChildRange(first, newLast) + } } private data class Data( - val initializer: KtExpression, - val declaration: KtVariableDeclaration, - val ifNullExpression: KtExpression, - val typeChecked: KtTypeReference? = null + val initializer: KtExpression, + val declaration: KtVariableDeclaration, + val ifNullExpression: KtExpression, + val typeChecked: KtTypeReference? = null ) - - private fun calcData(ifExpression: KtIfExpression): Data? { - if (ifExpression.`else` != null) return null - - val operationExpression = ifExpression.condition as? KtOperationExpression ?: return null - val value = when (operationExpression) { - is KtBinaryExpression -> { - if (operationExpression.operationToken != KtTokens.EQEQ) return null - operationExpression.expressionComparedToNull() - } - is KtIsExpression -> { - if (!operationExpression.isNegated) return null - if (operationExpression.typeReference?.typeElement is KtNullableType) return null - operationExpression.leftHandSide - } - else -> return null - } as? KtNameReferenceExpression ?: return null - - if (ifExpression.parent !is KtBlockExpression) return null - val prevStatement = (ifExpression.siblings(forward = false, withItself = false) - .firstIsInstanceOrNull() ?: return null) as? KtVariableDeclaration - prevStatement ?: return null - if (prevStatement.nameAsName != value.getReferencedNameAsName()) return null - val initializer = prevStatement.initializer ?: return null - val then = ifExpression.then ?: return null - val typeReference = (operationExpression as? KtIsExpression)?.typeReference - - val statement = if (then is KtBlockExpression) then.statements.singleOrNull() else then - statement ?: return null - - if (ReferencesSearch.search(prevStatement, LocalSearchScope(statement)).findFirst() != null) { - return null - } - - return Data(initializer, prevStatement, statement, typeReference) - } - - private fun PsiChildRange.withoutLastStatement(): PsiChildRange { - val newLast = last!!.siblings(forward = false, withItself = false).first { it !is PsiWhiteSpace } - return PsiChildRange(first, newLast) - } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/joinLines/JoinInitializerAndIfToElvisHandler.kt b/idea/src/org/jetbrains/kotlin/idea/joinLines/JoinInitializerAndIfToElvisHandler.kt new file mode 100644 index 00000000000..675dd462a04 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/joinLines/JoinInitializerAndIfToElvisHandler.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. 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.joinLines + +import com.intellij.codeInsight.editorActions.JoinRawLinesHandlerDelegate +import com.intellij.openapi.editor.Document +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.idea.intentions.FoldInitializerAndIfToElvisIntention +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtIfExpression +import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments +import org.jetbrains.kotlin.psi.psiUtil.siblings + +class JoinInitializerAndIfToElvisHandler : JoinRawLinesHandlerDelegate { + + override fun tryJoinRawLines(document: Document, file: PsiFile, start: Int, end: Int): Int { + if (file !is KtFile) return -1 + val lineBreak = file.findElementAt(start) + ?.siblings(forward = true, withItself = true) + ?.firstOrNull { it.textContains('\n') } + ?: return -1 + val ifExpression = lineBreak.getNextSiblingIgnoringWhitespaceAndComments() as? KtIfExpression ?: return -1 + if (!FoldInitializerAndIfToElvisIntention.isApplicable(ifExpression)) return -1 + return FoldInitializerAndIfToElvisIntention.applyTo(ifExpression).textRange.startOffset + } + + override fun tryJoinLines(document: Document, file: PsiFile, start: Int, end: Int) = -1 +} \ No newline at end of file diff --git a/idea/testData/joinLines/initializerAndIfToElvis/simple.kt b/idea/testData/joinLines/initializerAndIfToElvis/simple.kt new file mode 100644 index 00000000000..5f2bac9c9c2 --- /dev/null +++ b/idea/testData/joinLines/initializerAndIfToElvis/simple.kt @@ -0,0 +1,5 @@ +fun foo(list: List): Int { + val first = list.firstOrNull() + if (first == null) return -1 + return first.length +} \ No newline at end of file diff --git a/idea/testData/joinLines/initializerAndIfToElvis/simple.kt.after b/idea/testData/joinLines/initializerAndIfToElvis/simple.kt.after new file mode 100644 index 00000000000..086246c21c7 --- /dev/null +++ b/idea/testData/joinLines/initializerAndIfToElvis/simple.kt.after @@ -0,0 +1,4 @@ +fun foo(list: List): Int { + val first = list.firstOrNull() ?: return -1 + return first.length +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/declarations/JoinLinesTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/declarations/JoinLinesTestGenerated.java index c3f2aed70df..1e08e527810 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/declarations/JoinLinesTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/declarations/JoinLinesTestGenerated.java @@ -225,6 +225,24 @@ public class JoinLinesTestGenerated extends AbstractJoinLinesTest { } } + @TestMetadata("idea/testData/joinLines/initializerAndIfToElvis") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class InitializerAndIfToElvis extends AbstractJoinLinesTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInInitializerAndIfToElvis() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/joinLines/initializerAndIfToElvis"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("idea/testData/joinLines/initializerAndIfToElvis/simple.kt"); + } + } + @TestMetadata("idea/testData/joinLines/nestedIfs") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)