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)
UNNECESSARY_SAFE_CALL.registerFactory(ReplaceWithDotCallFix)
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallFix)
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallFixFactory)
UNSAFE_CALL.registerFactory(SurroundWithNullCheckFix)
UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(SurroundWithNullCheckFix)
@@ -264,7 +264,7 @@ class QuickFixRegistrar : QuickFixContributor {
UNSAFE_OPERATOR_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
UNSAFE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) // [] only
UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallForScopeFunctionFix)
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallForScopeFunctionFixFactory)
AMBIGUOUS_ANONYMOUS_TYPE_INFERRED.registerActions(SpecifyTypeExplicitlyFix())
PROPERTY_WITH_NO_TYPE_NO_INITIALIZER.registerActions(SpecifyTypeExplicitlyFix())
@@ -17,12 +17,7 @@
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.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
@@ -32,150 +27,67 @@ import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
abstract class ReplaceCallFix(
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
object ReplaceWithSafeCallFixFactory : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val psiElement = diagnostic.psiElement
val qualifiedExpression = psiElement.parent as? KtDotQualifiedExpression
if (qualifiedExpression != null) {
return ReplaceWithSafeCallFix(qualifiedExpression, qualifiedExpression.shouldHaveNotNullType())
} else {
if (psiElement !is KtNameReferenceExpression) return null
if (psiElement.getResolvedCall(psiElement.analyze())?.getImplicitReceiverValue() != null) {
val expressionToReplace: KtExpression = psiElement.parent as? KtCallExpression ?: psiElement
return ReplaceImplicitReceiverCallFix(expressionToReplace, expressionToReplace.shouldHaveNotNullType())
}
return 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!!
object ReplaceWithSafeCallForScopeFunctionFixFactory : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val element = diagnostic.psiElement
val scopeFunctionLiteral = element.getStrictParentOfType<KtFunctionLiteral>() ?: return null
val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType<KtCallExpression>() ?: return null
val scopeDotQualifiedExpression = scopeCallExpression.getStrictParentOfType<KtDotQualifiedExpression>() ?: return null
val context = scopeCallExpression.analyze()
val scopeFunctionLiteralDescriptor = context[BindingContext.FUNCTION, scopeFunctionLiteral] ?: return null
val scopeFunctionKind = scopeCallExpression.scopeFunctionKind(context) ?: return null
val internalReceiver = (element.parent as? KtDotQualifiedExpression)?.receiverExpression
val internalReceiverDescriptor = internalReceiver.getResolvedCall(context)?.candidateDescriptor
val internalResolvedCall = (element.getParentOfType<KtElement>(strict = false))?.getResolvedCall(context)
?: return null
when (scopeFunctionKind) {
ScopeFunctionKind.WITH_PARAMETER -> {
if (internalReceiverDescriptor != scopeFunctionLiteralDescriptor.valueParameters.singleOrNull()) {
return null
}
}
ScopeFunctionKind.WITH_RECEIVER -> {
if (internalReceiverDescriptor != scopeFunctionLiteralDescriptor.extensionReceiverParameter &&
internalResolvedCall.getImplicitReceiverValue() == null
) {
return null
}
}
}
return ReplaceWithSafeCallForScopeFunctionFix(
scopeDotQualifiedExpression, scopeDotQualifiedExpression.shouldHaveNotNullType()
)
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? {
val psiElement = diagnostic.psiElement
val qualifiedExpression = psiElement.parent as? KtDotQualifiedExpression
if (qualifiedExpression != null) {
return ReplaceWithSafeCallFix(qualifiedExpression, qualifiedExpression.shouldHaveNotNullType())
} else {
if (psiElement !is KtNameReferenceExpression) return null
if (psiElement.getResolvedCall(psiElement.analyze())?.getImplicitReceiverValue() != null) {
val expressionToReplace: KtExpression = psiElement.parent as? KtCallExpression ?: psiElement
return ReplaceImplicitReceiverCallFix(expressionToReplace, expressionToReplace.shouldHaveNotNullType())
}
return null
}
}
}
}
class ReplaceWithSafeCallForScopeFunctionFix(
expression: KtDotQualifiedExpression,
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 scopeFunctionLiteral = element.getStrictParentOfType<KtFunctionLiteral>() ?: return null
val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType<KtCallExpression>() ?: return null
val scopeDotQualifiedExpression = scopeCallExpression.getStrictParentOfType<KtDotQualifiedExpression>() ?: return null
val context = scopeCallExpression.analyze()
val scopeFunctionLiteralDescriptor = context[BindingContext.FUNCTION, scopeFunctionLiteral] ?: return null
val scopeFunctionKind = scopeCallExpression.scopeFunctionKind(context) ?: return null
val internalReceiver = (element.parent as? KtDotQualifiedExpression)?.receiverExpression
val internalReceiverDescriptor = internalReceiver.getResolvedCall(context)?.candidateDescriptor
val internalResolvedCall = (element.getParentOfType<KtElement>(strict = false))?.getResolvedCall(context)
?: return null
when (scopeFunctionKind) {
ScopeFunctionKind.WITH_PARAMETER -> {
if (internalReceiverDescriptor != scopeFunctionLiteralDescriptor.valueParameters.singleOrNull()) {
return null
}
}
ScopeFunctionKind.WITH_RECEIVER -> {
if (internalReceiverDescriptor != scopeFunctionLiteralDescriptor.extensionReceiverParameter &&
internalResolvedCall.getImplicitReceiverValue() == null
) {
return null
}
}
}
return ReplaceWithSafeCallForScopeFunctionFix(
scopeDotQualifiedExpression, scopeDotQualifiedExpression.shouldHaveNotNullType()
)
}
private fun KtCallExpression.scopeFunctionKind(context: BindingContext): ScopeFunctionKind? {
val methodName = getResolvedCall(context)?.resultingDescriptor?.fqNameUnsafe?.asString()
return ScopeFunctionKind.values().firstOrNull { kind -> kind.names.contains(methodName) }
}
private enum class ScopeFunctionKind(vararg val names: String) {
WITH_PARAMETER("kotlin.let", "kotlin.also"),
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)
}
private fun KtCallExpression.scopeFunctionKind(context: BindingContext): ScopeFunctionKind? {
val methodName = getResolvedCall(context)?.resultingDescriptor?.fqNameUnsafe?.asString()
return ScopeFunctionKind.values().firstOrNull { kind -> kind.names.contains(methodName) }
}
private enum class ScopeFunctionKind(vararg val names: String) {
WITH_PARAMETER("kotlin.let", "kotlin.also"),
WITH_RECEIVER("kotlin.apply", "kotlin.run")
}
}
@@ -5,26 +5,13 @@
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.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtExpression
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.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 {
val type = when (val parent = parent) {
is KtBinaryExpression -> parent.left?.let { it.getType(it.analyze()) }
@@ -33,12 +20,3 @@ fun KtExpression.shouldHaveNotNullType(): Boolean {
} ?: return false
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)
}
}