Join Lines: join initializer + if null check to elvis (KT-22388)

#KT-22388 Fixed
This commit is contained in:
Toshiaki Kameyama
2018-09-04 07:12:01 +03:00
committed by Nikolay Krasko
parent 0708872f62
commit e409007749
6 changed files with 169 additions and 95 deletions
+1
View File
@@ -761,6 +761,7 @@
<moveLeftRightHandler language="kotlin" implementationClass="org.jetbrains.kotlin.idea.codeInsight.KotlinMoveLeftRightHandler"/>
<joinLinesHandler implementation="org.jetbrains.kotlin.idea.joinLines.JoinDeclarationAndAssignmentHandler"/>
<joinLinesHandler implementation="org.jetbrains.kotlin.idea.joinLines.JoinInitializerAndIfToElvisHandler"/>
<joinLinesHandler implementation="org.jetbrains.kotlin.idea.joinLines.JoinBlockIntoSingleStatementHandler"/>
<joinLinesHandler implementation="org.jetbrains.kotlin.idea.joinLines.JoinStatementsAddSemicolonHandler"/>
<joinLinesHandler implementation="org.jetbrains.kotlin.idea.joinLines.JoinToStringTemplateHandler"/>
@@ -43,111 +43,126 @@ import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
class FoldInitializerAndIfToElvisInspection : IntentionBasedInspection<KtIfExpression>(FoldInitializerAndIfToElvisIntention::class)
class FoldInitializerAndIfToElvisIntention : SelfTargetingRangeIntention<KtIfExpression>(KtIfExpression::class.java, "Replace 'if' with elvis operator"){
class FoldInitializerAndIfToElvisIntention :
SelfTargetingRangeIntention<KtIfExpression>(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<KtExpression>() ?: 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<KtExpression>() ?: 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)
}
}
@@ -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
}
@@ -0,0 +1,5 @@
fun foo(list: List<String>): Int {
<caret>val first = list.firstOrNull()
if (first == null) return -1
return first.length
}
@@ -0,0 +1,4 @@
fun foo(list: List<String>): Int {
val first = <caret>list.firstOrNull() ?: return -1
return first.length
}
@@ -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)