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,150 +27,67 @@ 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, override fun createAction(diagnostic: Diagnostic): IntentionAction? {
private val operation: String, val psiElement = diagnostic.psiElement
private val notNullNeeded: Boolean = false val qualifiedExpression = psiElement.parent as? KtDotQualifiedExpression
) : KotlinQuickFixAction<KtQualifiedExpression>(expression) { if (qualifiedExpression != null) {
return ReplaceWithSafeCallFix(qualifiedExpression, qualifiedExpression.shouldHaveNotNullType())
override fun getFamilyName() = text } else {
if (psiElement !is KtNameReferenceExpression) return null
override fun isAvailable(project: Project, editor: Editor?, file: KtFile): Boolean { if (psiElement.getResolvedCall(psiElement.analyze())?.getImplicitReceiverValue() != null) {
val element = element ?: return false val expressionToReplace: KtExpression = psiElement.parent as? KtCallExpression ?: psiElement
return element.selectorExpression != null return ReplaceImplicitReceiverCallFix(expressionToReplace, expressionToReplace.shouldHaveNotNullType())
}
return null
}
} }
}
override fun invoke(project: Project, editor: Editor?, file: KtFile) { object ReplaceWithSafeCallForScopeFunctionFixFactory : KotlinSingleIntentionActionFactory() {
val element = element ?: return override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val elvis = element.elvisOrEmpty(notNullNeeded) val element = diagnostic.psiElement
val betweenReceiverAndOperation = element.elementsBetweenReceiverAndOperation().joinToString(separator = "") { it.text } val scopeFunctionLiteral = element.getStrictParentOfType<KtFunctionLiteral>() ?: return null
val newExpression = KtPsiFactory(element).createExpressionByPattern( val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType<KtCallExpression>() ?: return null
"$0$betweenReceiverAndOperation$operation$1$elvis", val scopeDotQualifiedExpression = scopeCallExpression.getStrictParentOfType<KtDotQualifiedExpression>() ?: return null
element.receiverExpression,
element.selectorExpression!! 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> { private fun KtCallExpression.scopeFunctionKind(context: BindingContext): ScopeFunctionKind? {
val receiver = receiverExpression val methodName = getResolvedCall(context)?.resultingDescriptor?.fqNameUnsafe?.asString()
val operation = operationTokenNode as? PsiElement ?: return emptyList() return ScopeFunctionKind.values().firstOrNull { kind -> kind.names.contains(methodName) }
val start = receiver.nextSibling?.takeIf { it != operation } ?: return emptyList() }
val end = operation.prevSibling?.takeIf { it != receiver } ?: return emptyList()
return PsiTreeUtil.getElementsOfRange(start, end) private enum class ScopeFunctionKind(vararg val names: String) {
} WITH_PARAMETER("kotlin.let", "kotlin.also"),
} WITH_RECEIVER("kotlin.apply", "kotlin.run")
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)
}
} }
} }
@@ -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)
}
}