Join Lines: join initializer + if null check to elvis (KT-22388)
#KT-22388 Fixed
This commit is contained in:
committed by
Nikolay Krasko
parent
0708872f62
commit
e409007749
@@ -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"/>
|
||||
|
||||
+110
-95
@@ -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
|
||||
}
|
||||
+18
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user