From 05deecaafafe3ccf730f39ee73d5063998a26c0e Mon Sep 17 00:00:00 2001 From: Mark Punzalan Date: Tue, 23 Feb 2021 08:08:34 +0000 Subject: [PATCH] FIR IDE: Move ReplaceCallFix to idea-frontend-independent. --- .../kotlin/idea/quickfix/CleanupFix.kt | 0 .../kotlin/idea/quickfix/ReplaceCallFix.kt | 116 ++++++++++ .../kotlin/idea/quickfix/QuickFixRegistrar.kt | 4 +- .../kotlin/idea/quickfix/ReplaceCallFix.kt | 200 +++++------------- .../idea/quickfix/ReplaceCallFixUtils.kt | 22 -- 5 files changed, 174 insertions(+), 168 deletions(-) rename idea/{ => idea-frontend-independent}/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt (100%) create mode 100644 idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt similarity index 100% rename from idea/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt rename to idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/quickfix/CleanupFix.kt diff --git a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt new file mode 100644 index 00000000000..6d47295aaf4 --- /dev/null +++ b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt @@ -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(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 { + 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(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(strict = false) ?: return null + return ReplaceWithDotCallFix(qualifiedExpression) + } + } +} + +fun KtExpression.elvisOrEmpty(notNullNeeded: Boolean): String { + if (!notNullNeeded) return "" + val binaryExpression = getStrictParentOfType() + 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) + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index a82b72e91ad..afcd63f6892 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -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()) diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt index e59e22e11c0..77daf006f8a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFix.kt @@ -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(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() ?: return null + val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType() ?: return null + val scopeDotQualifiedExpression = scopeCallExpression.getStrictParentOfType() ?: 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(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 { - 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(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? { - val element = diagnostic.psiElement - val scopeFunctionLiteral = element.getStrictParentOfType() ?: return null - val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType() ?: return null - val scopeDotQualifiedExpression = scopeCallExpression.getStrictParentOfType() ?: 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(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(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") } } diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFixUtils.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFixUtils.kt index 98bfbc85b51..69eacaab82f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFixUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceCallFixUtils.kt @@ -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() - 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) - } -}