FIR IDE: Move ReplaceCallFix to idea-frontend-independent.
This commit is contained in:
committed by
Ilya Kirillov
parent
482c677274
commit
05deecaafa
+116
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user