FIR IDE: Move ReplaceCallFix to idea-frontend-independent.

This commit is contained in:
Mark Punzalan
2021-02-23 08:08:34 +00:00
committed by Ilya Kirillov
parent 482c677274
commit 05deecaafa
5 changed files with 174 additions and 168 deletions
@@ -0,0 +1,116 @@
/*
* Copyright 2010-2021 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.quickfix
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
abstract class ReplaceCallFix(
expression: KtQualifiedExpression,
private val operation: String,
private val notNullNeeded: Boolean = false
) : KotlinPsiOnlyQuickFixAction<KtQualifiedExpression>(expression) {
override fun getFamilyName() = text
override fun isAvailable(project: Project, editor: Editor?, file: KtFile): Boolean {
val element = element ?: return false
return element.selectorExpression != null
}
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return
val elvis = element.elvisOrEmpty(notNullNeeded)
val betweenReceiverAndOperation = element.elementsBetweenReceiverAndOperation().joinToString(separator = "") { it.text }
val newExpression = KtPsiFactory(element).createExpressionByPattern(
"$0$betweenReceiverAndOperation$operation$1$elvis",
element.receiverExpression,
element.selectorExpression!!
)
val replacement = element.replace(newExpression)
if (elvis.isNotEmpty()) {
replacement.moveCaretToEnd(editor, project)
}
}
private fun KtQualifiedExpression.elementsBetweenReceiverAndOperation(): List<PsiElement> {
val receiver = receiverExpression
val operation = operationTokenNode as? PsiElement ?: return emptyList()
val start = receiver.nextSibling?.takeIf { it != operation } ?: return emptyList()
val end = operation.prevSibling?.takeIf { it != receiver } ?: return emptyList()
return PsiTreeUtil.getElementsOfRange(start, end)
}
}
class ReplaceImplicitReceiverCallFix(
expression: KtExpression,
private val notNullNeeded: Boolean
) : KotlinPsiOnlyQuickFixAction<KtExpression>(expression) {
override fun getFamilyName() = text
override fun getText() = KotlinBundle.message("replace.with.safe.this.call")
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return
val elvis = element.elvisOrEmpty(notNullNeeded)
val newExpression = KtPsiFactory(element).createExpressionByPattern("this?.$0$elvis", element)
val replacement = element.replace(newExpression)
if (elvis.isNotEmpty()) {
replacement.moveCaretToEnd(editor, project)
}
}
}
class ReplaceWithSafeCallFix(
expression: KtDotQualifiedExpression,
notNullNeeded: Boolean
) : ReplaceCallFix(expression, "?.", notNullNeeded) {
override fun getText() = KotlinBundle.message("replace.with.safe.call")
}
class ReplaceWithSafeCallForScopeFunctionFix(
expression: KtDotQualifiedExpression,
notNullNeeded: Boolean
) : ReplaceCallFix(expression, "?.", notNullNeeded) {
override fun getText() = KotlinBundle.message("replace.scope.function.with.safe.call")
}
class ReplaceWithDotCallFix(expression: KtSafeQualifiedExpression) : ReplaceCallFix(expression, "."), CleanupFix {
override fun getText() = KotlinBundle.message("replace.with.dot.call")
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val qualifiedExpression = diagnostic.psiElement.getParentOfType<KtSafeQualifiedExpression>(strict = false) ?: return null
return ReplaceWithDotCallFix(qualifiedExpression)
}
}
}
fun KtExpression.elvisOrEmpty(notNullNeeded: Boolean): String {
if (!notNullNeeded) return ""
val binaryExpression = getStrictParentOfType<KtBinaryExpression>()
return if (binaryExpression?.left == this && binaryExpression.operationToken == KtTokens.ELVIS) "" else "?:"
}
fun PsiElement.moveCaretToEnd(editor: Editor?, project: Project) {
editor?.run {
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document)
val endOffset = if (text.endsWith(")")) endOffset - 1 else endOffset
document.insertString(endOffset, " ")
caretModel.moveToOffset(endOffset + 1)
}
}
@@ -241,7 +241,7 @@ class QuickFixRegistrar : QuickFixContributor {
SENSELESS_COMPARISON.registerFactory(SimplifyComparisonFix) SENSELESS_COMPARISON.registerFactory(SimplifyComparisonFix)
UNNECESSARY_SAFE_CALL.registerFactory(ReplaceWithDotCallFix) UNNECESSARY_SAFE_CALL.registerFactory(ReplaceWithDotCallFix)
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallFix) UNSAFE_CALL.registerFactory(ReplaceWithSafeCallFixFactory)
UNSAFE_CALL.registerFactory(SurroundWithNullCheckFix) UNSAFE_CALL.registerFactory(SurroundWithNullCheckFix)
UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(SurroundWithNullCheckFix) UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(SurroundWithNullCheckFix)
@@ -264,7 +264,7 @@ class QuickFixRegistrar : QuickFixContributor {
UNSAFE_OPERATOR_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) UNSAFE_OPERATOR_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
UNSAFE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) // [] only UNSAFE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) // [] only
UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallForScopeFunctionFix) UNSAFE_CALL.registerFactory(ReplaceWithSafeCallForScopeFunctionFixFactory)
AMBIGUOUS_ANONYMOUS_TYPE_INFERRED.registerActions(SpecifyTypeExplicitlyFix()) AMBIGUOUS_ANONYMOUS_TYPE_INFERRED.registerActions(SpecifyTypeExplicitlyFix())
PROPERTY_WITH_NO_TYPE_NO_INITIALIZER.registerActions(SpecifyTypeExplicitlyFix()) PROPERTY_WITH_NO_TYPE_NO_INITIALIZER.registerActions(SpecifyTypeExplicitlyFix())
@@ -17,12 +17,7 @@
package org.jetbrains.kotlin.idea.quickfix package org.jetbrains.kotlin.idea.quickfix
import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
@@ -32,70 +27,7 @@ import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
abstract class ReplaceCallFix( object ReplaceWithSafeCallFixFactory : KotlinSingleIntentionActionFactory() {
expression: KtQualifiedExpression,
private val operation: String,
private val notNullNeeded: Boolean = false
) : KotlinQuickFixAction<KtQualifiedExpression>(expression) {
override fun getFamilyName() = text
override fun isAvailable(project: Project, editor: Editor?, file: KtFile): Boolean {
val element = element ?: return false
return element.selectorExpression != null
}
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return
val elvis = element.elvisOrEmpty(notNullNeeded)
val betweenReceiverAndOperation = element.elementsBetweenReceiverAndOperation().joinToString(separator = "") { it.text }
val newExpression = KtPsiFactory(element).createExpressionByPattern(
"$0$betweenReceiverAndOperation$operation$1$elvis",
element.receiverExpression,
element.selectorExpression!!
)
val replacement = element.replace(newExpression)
if (elvis.isNotEmpty()) {
replacement.moveCaretToEnd(editor, project)
}
}
private fun KtQualifiedExpression.elementsBetweenReceiverAndOperation(): List<PsiElement> {
val receiver = receiverExpression
val operation = operationTokenNode as? PsiElement ?: return emptyList()
val start = receiver.nextSibling?.takeIf { it != operation } ?: return emptyList()
val end = operation.prevSibling?.takeIf { it != receiver } ?: return emptyList()
return PsiTreeUtil.getElementsOfRange(start, end)
}
}
class ReplaceImplicitReceiverCallFix(
expression: KtExpression,
private val notNullNeeded: Boolean
) : KotlinQuickFixAction<KtExpression>(expression) {
override fun getFamilyName() = text
override fun getText() = KotlinBundle.message("replace.with.safe.this.call")
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return
val elvis = element.elvisOrEmpty(notNullNeeded)
val newExpression = KtPsiFactory(element).createExpressionByPattern("this?.$0$elvis", element)
val replacement = element.replace(newExpression)
if (elvis.isNotEmpty()) {
replacement.moveCaretToEnd(editor, project)
}
}
}
class ReplaceWithSafeCallFix(
expression: KtDotQualifiedExpression,
notNullNeeded: Boolean
) : ReplaceCallFix(expression, "?.", notNullNeeded) {
override fun getText() = KotlinBundle.message("replace.with.safe.call")
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? { override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val psiElement = diagnostic.psiElement val psiElement = diagnostic.psiElement
val qualifiedExpression = psiElement.parent as? KtDotQualifiedExpression val qualifiedExpression = psiElement.parent as? KtDotQualifiedExpression
@@ -111,17 +43,9 @@ class ReplaceWithSafeCallFix(
} }
} }
} }
}
class ReplaceWithSafeCallForScopeFunctionFix( object ReplaceWithSafeCallForScopeFunctionFixFactory : KotlinSingleIntentionActionFactory() {
expression: KtDotQualifiedExpression, override fun createAction(diagnostic: Diagnostic): IntentionAction? {
notNullNeeded: Boolean
) : ReplaceCallFix(expression, "?.", notNullNeeded) {
override fun getText() = KotlinBundle.message("replace.scope.function.with.safe.call")
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): KotlinQuickFixAction<KtExpression>? {
val element = diagnostic.psiElement val element = diagnostic.psiElement
val scopeFunctionLiteral = element.getStrictParentOfType<KtFunctionLiteral>() ?: return null val scopeFunctionLiteral = element.getStrictParentOfType<KtFunctionLiteral>() ?: return null
val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType<KtCallExpression>() ?: return null val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType<KtCallExpression>() ?: return null
@@ -166,16 +90,4 @@ class ReplaceWithSafeCallForScopeFunctionFix(
WITH_RECEIVER("kotlin.apply", "kotlin.run") WITH_RECEIVER("kotlin.apply", "kotlin.run")
} }
} }
}
class ReplaceWithDotCallFix(expression: KtSafeQualifiedExpression) : ReplaceCallFix(expression, "."), CleanupFix {
override fun getText() = KotlinBundle.message("replace.with.dot.call")
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val qualifiedExpression = diagnostic.psiElement.getParentOfType<KtSafeQualifiedExpression>(strict = false) ?: return null
return ReplaceWithDotCallFix(qualifiedExpression)
}
}
}
@@ -5,26 +5,13 @@
package org.jetbrains.kotlin.idea.quickfix package org.jetbrains.kotlin.idea.quickfix
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getType import org.jetbrains.kotlin.resolve.calls.callUtil.getType
fun KtExpression.elvisOrEmpty(notNullNeeded: Boolean): String {
if (!notNullNeeded) return ""
val binaryExpression = getStrictParentOfType<KtBinaryExpression>()
return if (binaryExpression?.left == this && binaryExpression.operationToken == KtTokens.ELVIS) "" else "?:"
}
fun KtExpression.shouldHaveNotNullType(): Boolean { fun KtExpression.shouldHaveNotNullType(): Boolean {
val type = when (val parent = parent) { val type = when (val parent = parent) {
is KtBinaryExpression -> parent.left?.let { it.getType(it.analyze()) } is KtBinaryExpression -> parent.left?.let { it.getType(it.analyze()) }
@@ -33,12 +20,3 @@ fun KtExpression.shouldHaveNotNullType(): Boolean {
} ?: return false } ?: return false
return !type.isMarkedNullable return !type.isMarkedNullable
} }
fun PsiElement.moveCaretToEnd(editor: Editor?, project: Project) {
editor?.run {
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document)
val endOffset = if (text.endsWith(")")) endOffset - 1 else endOffset
document.insertString(endOffset, " ")
caretModel.moveToOffset(endOffset + 1)
}
}