Fix convert unary operator to function call

#KT-25501 Fixed
This commit is contained in:
Dmitry Gridin
2019-02-11 23:36:29 +03:00
parent e341286fb1
commit 5089df2441
17 changed files with 188 additions and 58 deletions
@@ -21,8 +21,7 @@ import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.*
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.project.languageVersionSettings import org.jetbrains.kotlin.idea.project.languageVersionSettings
import org.jetbrains.kotlin.idea.references.ReferenceAccess import org.jetbrains.kotlin.idea.references.ReferenceAccess
import org.jetbrains.kotlin.idea.references.readWriteAccess import org.jetbrains.kotlin.idea.references.readWriteAccess
@@ -30,30 +29,37 @@ import org.jetbrains.kotlin.idea.util.CommentSaver
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector
import org.jetbrains.kotlin.psi.psiUtil.lastBlockStatementOrThis
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.util.OperatorNameConventions import org.jetbrains.kotlin.util.OperatorNameConventions
class OperatorToFunctionIntention : SelfTargetingIntention<KtExpression>(KtExpression::class.java, "Replace overloaded operator with function call") { class OperatorToFunctionIntention :
SelfTargetingIntention<KtExpression>(KtExpression::class.java, "Replace overloaded operator with function call") {
companion object { companion object {
private fun isApplicablePrefix(element: KtPrefixExpression, caretOffset: Int): Boolean { private fun isApplicableUnary(element: KtUnaryExpression, caretOffset: Int): Boolean {
val opRef = element.operationReference val opRef = element.operationReference
if (!opRef.textRange.containsOffset(caretOffset)) return false if (!opRef.textRange.containsOffset(caretOffset)) return false
return when (opRef.getReferencedNameElementType()) { return when (opRef.getReferencedNameElementType()) {
KtTokens.PLUS, KtTokens.MINUS, KtTokens.PLUSPLUS, KtTokens.MINUSMINUS, KtTokens.EXCL -> true KtTokens.PLUS, KtTokens.MINUS, KtTokens.EXCL -> true
KtTokens.PLUSPLUS, KtTokens.MINUSMINUS -> !isUsedAsExpression(element)
else -> false else -> false
} }
} }
private fun isApplicablePostfix(element: KtPostfixExpression, caretOffset: Int): Boolean { // TODO: replace to `element.isUsedAsExpression(element.analyze(BodyResolveMode.PARTIAL_WITH_CFA))` after fix KT-25682
val opRef = element.operationReference private fun isUsedAsExpression(element: KtExpression): Boolean {
if (!opRef.textRange.containsOffset(caretOffset)) return false val parent = element.parent
if (element.baseExpression == null) return false return if (parent is KtBlockExpression) parent.lastBlockStatementOrThis() == element && parentIsUsedAsExpression(parent.parent)
return when (opRef.getReferencedNameElementType()) { else parentIsUsedAsExpression(parent)
KtTokens.PLUSPLUS, KtTokens.MINUSMINUS -> true }
else -> false
} private fun parentIsUsedAsExpression(element: PsiElement): Boolean = when (val parent = element.parent) {
is KtLoopExpression, is KtFile -> false
is KtIfExpression, is KtWhenExpression -> (parent as KtExpression).isUsedAsExpression(parent.analyze(BodyResolveMode.PARTIAL_WITH_CFA))
else -> true
} }
private fun isApplicableBinary(element: KtBinaryExpression, caretOffset: Int): Boolean { private fun isApplicableBinary(element: KtBinaryExpression, caretOffset: Int): Boolean {
@@ -95,13 +101,12 @@ class OperatorToFunctionIntention : SelfTargetingIntention<KtExpression>(KtExpre
return false return false
} }
private fun convertPrefix(element: KtPrefixExpression): KtExpression { private fun convertUnary(element: KtUnaryExpression): KtExpression {
val op = element.operationReference.getReferencedNameElementType() val op = element.operationReference.getReferencedNameElementType()
val operatorName = when (op) { val operatorName = when (op) {
KtTokens.PLUSPLUS, KtTokens.MINUSMINUS -> return convertUnaryWithAssignFix(element)
KtTokens.PLUS -> OperatorNameConventions.UNARY_PLUS KtTokens.PLUS -> OperatorNameConventions.UNARY_PLUS
KtTokens.MINUS -> OperatorNameConventions.UNARY_MINUS KtTokens.MINUS -> OperatorNameConventions.UNARY_MINUS
KtTokens.PLUSPLUS -> OperatorNameConventions.INC
KtTokens.MINUSMINUS -> OperatorNameConventions.DEC
KtTokens.EXCL -> OperatorNameConventions.NOT KtTokens.EXCL -> OperatorNameConventions.NOT
else -> return element else -> return element
} }
@@ -110,7 +115,7 @@ class OperatorToFunctionIntention : SelfTargetingIntention<KtExpression>(KtExpre
return element.replace(transformed) as KtExpression return element.replace(transformed) as KtExpression
} }
private fun convertPostFix(element: KtPostfixExpression): KtExpression { private fun convertUnaryWithAssignFix(element: KtUnaryExpression): KtExpression {
val op = element.operationReference.getReferencedNameElementType() val op = element.operationReference.getReferencedNameElementType()
val operatorName = when (op) { val operatorName = when (op) {
KtTokens.PLUSPLUS -> OperatorNameConventions.INC KtTokens.PLUSPLUS -> OperatorNameConventions.INC
@@ -118,7 +123,7 @@ class OperatorToFunctionIntention : SelfTargetingIntention<KtExpression>(KtExpre
else -> return element else -> return element
} }
val transformed = KtPsiFactory(element).createExpressionByPattern("$0.$1()", element.baseExpression!!, operatorName) val transformed = KtPsiFactory(element).createExpressionByPattern("$0 = $0.$1()", element.baseExpression!!, operatorName)
return element.replace(transformed) as KtExpression return element.replace(transformed) as KtExpression
} }
@@ -188,8 +193,7 @@ class OperatorToFunctionIntention : SelfTargetingIntention<KtExpression>(KtExpre
appendExpressions(element.indexExpressions) appendExpressions(element.indexExpressions)
appendFixedText(",") appendFixedText(",")
appendExpression(parent.right) appendExpression(parent.right)
} } else {
else {
appendFixedText("get(") appendFixedText("get(")
appendExpressions(element.indexExpressions) appendExpressions(element.indexExpressions)
} }
@@ -234,8 +238,7 @@ class OperatorToFunctionIntention : SelfTargetingIntention<KtExpression>(KtExpre
val commentSaver = CommentSaver(elementToBeReplaced, saveLineBreaks = true) val commentSaver = CommentSaver(elementToBeReplaced, saveLineBreaks = true)
val result = when (element) { val result = when (element) {
is KtPrefixExpression -> convertPrefix(element) is KtUnaryExpression -> convertUnary(element)
is KtPostfixExpression -> convertPostFix(element)
is KtBinaryExpression -> convertBinary(element) is KtBinaryExpression -> convertBinary(element)
is KtArrayAccessExpression -> convertArrayAccess(element) is KtArrayAccessExpression -> convertArrayAccess(element)
is KtCallExpression -> convertCall(element) is KtCallExpression -> convertCall(element)
@@ -244,40 +247,29 @@ class OperatorToFunctionIntention : SelfTargetingIntention<KtExpression>(KtExpre
commentSaver.restore(result) commentSaver.restore(result)
val callName = findCallName(result) val callName = findCallName(result) ?: error("No call name found in ${result.text}")
?: error("No call name found in ${result.text}")
return result to callName return result to callName
} }
private fun findCallName(result: KtExpression): KtSimpleNameExpression? { private fun findCallName(result: KtExpression): KtSimpleNameExpression? = when (result) {
return when (result) { is KtBinaryExpression -> {
is KtBinaryExpression -> { if (KtPsiUtil.isAssignment(result))
if (KtPsiUtil.isAssignment(result)) findCallName(result.right!!)
findCallName(result.right!!) else
else findCallName(result.left!!)
findCallName(result.left!!)
}
is KtUnaryExpression -> {
result.baseExpression?.let { findCallName(it) }
}
is KtParenthesizedExpression -> result.expression?.let { findCallName(it) }
else -> result.getQualifiedElementSelector() as KtSimpleNameExpression?
} }
is KtUnaryExpression -> result.baseExpression?.let { findCallName(it) }
is KtParenthesizedExpression -> result.expression?.let { findCallName(it) }
else -> result.getQualifiedElementSelector() as KtSimpleNameExpression?
} }
} }
override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean { override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean = when (element) {
return when (element) { is KtUnaryExpression -> isApplicableUnary(element, caretOffset)
is KtPrefixExpression -> isApplicablePrefix(element, caretOffset) is KtBinaryExpression -> isApplicableBinary(element, caretOffset)
is KtPostfixExpression -> isApplicablePostfix(element, caretOffset) is KtArrayAccessExpression -> isApplicableArrayAccess(element, caretOffset)
is KtBinaryExpression -> isApplicableBinary(element, caretOffset) is KtCallExpression -> isApplicableCall(element, caretOffset)
is KtArrayAccessExpression -> isApplicableArrayAccess(element, caretOffset) else -> false
is KtCallExpression -> isApplicableCall(element, caretOffset)
else -> false
}
} }
override fun applyTo(element: KtExpression, editor: Editor?) { override fun applyTo(element: KtExpression, editor: Editor?) {
@@ -23,15 +23,19 @@ import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.intentions.callExpression import org.jetbrains.kotlin.idea.intentions.callExpression
import org.jetbrains.kotlin.idea.intentions.calleeName import org.jetbrains.kotlin.idea.intentions.calleeName
import org.jetbrains.kotlin.idea.intentions.isReceiverExpressionWithValue import org.jetbrains.kotlin.idea.intentions.isReceiverExpressionWithValue
import org.jetbrains.kotlin.lexer.KtSingleValueToken
import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtPsiFactory import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.createExpressionByPattern import org.jetbrains.kotlin.psi.createExpressionByPattern
import org.jetbrains.kotlin.types.expressions.OperatorConventions import org.jetbrains.kotlin.types.expressions.OperatorConventions
class ReplaceCallWithUnaryOperatorIntention : SelfTargetingRangeIntention<KtDotQualifiedExpression>(KtDotQualifiedExpression::class.java, "Replace call with unary operator"), HighPriorityAction { class ReplaceCallWithUnaryOperatorIntention :
SelfTargetingRangeIntention<KtDotQualifiedExpression>(KtDotQualifiedExpression::class.java, "Replace call with unary operator"),
HighPriorityAction {
override fun applicabilityRange(element: KtDotQualifiedExpression): TextRange? { override fun applicabilityRange(element: KtDotQualifiedExpression): TextRange? {
val operation = operation(element.calleeName) ?: return null val operation = operation(element.calleeName) ?: return null
if (!isApplicableOperation(operation)) return null
val call = element.callExpression ?: return null val call = element.callExpression ?: return null
if (call.typeArgumentList != null) return null if (call.typeArgumentList != null) return null
@@ -39,18 +43,18 @@ class ReplaceCallWithUnaryOperatorIntention : SelfTargetingRangeIntention<KtDotQ
if (!element.isReceiverExpressionWithValue()) return null if (!element.isReceiverExpressionWithValue()) return null
text = "Replace with '$operation' operator" text = "Replace with '${operation.value}' operator"
return call.calleeExpression!!.textRange return call.calleeExpression!!.textRange
} }
override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) { override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) {
val operation = operation(element.calleeName)!! val operation = operation(element.calleeName)?.value ?: return
val receiver = element.receiverExpression val receiver = element.receiverExpression
element.replace(KtPsiFactory(element).createExpressionByPattern("$0$1", operation, receiver)) element.replace(KtPsiFactory(element).createExpressionByPattern("$0$1", operation, receiver))
} }
private fun operation(functionName: String?) : String? { private fun isApplicableOperation(operation: KtSingleValueToken): Boolean = operation !in OperatorConventions.INCREMENT_OPERATIONS
if (functionName == null) return null
return OperatorConventions.UNARY_OPERATION_NAMES.inverse()[Name.identifier(functionName)]?.value private fun operation(functionName: String?): KtSingleValueToken? =
} functionName?.let { OperatorConventions.UNARY_OPERATION_NAMES.inverse()[Name.identifier(it)] }
} }
@@ -0,0 +1,8 @@
// IS_APPLICABLE: false
fun test() {
class Test {
operator fun dec(): Test = Test()
}
val test = Test()
test.dec<caret>()
}
@@ -0,0 +1,8 @@
// IS_APPLICABLE: false
fun test() {
class Test {
operator fun inc(): Test = Test()
}
val test = Test()
test.inc<caret>()
}
@@ -0,0 +1,9 @@
// IS_APPLICABLE: false
fun foo() {
var a = 0
val b = if (true) {
a++<caret>
} else {
a--
}
}
@@ -0,0 +1,9 @@
// IS_APPLICABLE: false
fun foo() {
var a = 0
val b = if (false) {
if (true) {
a++<caret>
} else a
} else a
}
@@ -0,0 +1,5 @@
// IS_APPLICABLE: false
fun foo() {
var a = 0
val b = a++<caret>
}
@@ -0,0 +1,8 @@
// IS_APPLICABLE: false
fun foo() {
var a = 0
val b = when {
true -> a++<caret>
else -> a--
}
}
@@ -1,4 +1,4 @@
fun foo() { fun foo() {
var a = 5 var a = 5
a.dec() a = a.dec()
} }
@@ -0,0 +1,8 @@
fun foo() {
var a = 0
if (true) {
a++<caret>
} else {
a--
}
}
@@ -0,0 +1,8 @@
fun foo() {
var a = 0
if (true) {
a = a.inc()
} else {
a--
}
}
@@ -0,0 +1,6 @@
fun foo() {
var a = 0
for (i in 0..42)
a++<caret>
}
}
@@ -0,0 +1,6 @@
fun foo() {
var a = 0
for (i in 0..42)
a = a.inc()
}
}
@@ -0,0 +1,7 @@
fun foo() {
var a = 0
when {
true -> a++<caret>
else -> a--
}
}
@@ -0,0 +1,7 @@
fun foo() {
var a = 0
when {
true -> a = a.inc()
else -> a--
}
}
@@ -1,4 +1,4 @@
fun foo() { fun foo() {
var a = 0 var a = 0
a.inc() a = a.inc()
} }
@@ -4187,6 +4187,11 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/complexPlus.kt"); runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/complexPlus.kt");
} }
@TestMetadata("dec.kt")
public void testDec() throws Exception {
runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/dec.kt");
}
@TestMetadata("extensionFunction.kt") @TestMetadata("extensionFunction.kt")
public void testExtensionFunction() throws Exception { public void testExtensionFunction() throws Exception {
runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/extensionFunction.kt"); runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/extensionFunction.kt");
@@ -4197,6 +4202,11 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/functionLiteralArgument.kt"); runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/functionLiteralArgument.kt");
} }
@TestMetadata("inc.kt")
public void testInc() throws Exception {
runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/inc.kt");
}
@TestMetadata("minusSanityTest.kt") @TestMetadata("minusSanityTest.kt")
public void testMinusSanityTest() throws Exception { public void testMinusSanityTest() throws Exception {
runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/minusSanityTest.kt"); runTest("idea/testData/intentions/conventionNameCalls/replaceCallWithUnaryOperator/minusSanityTest.kt");
@@ -13138,6 +13148,26 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
runTest("idea/testData/intentions/operatorToFunction/notApplicableNewClassObject.kt"); runTest("idea/testData/intentions/operatorToFunction/notApplicableNewClassObject.kt");
} }
@TestMetadata("notApplicablePostfixPlusPlusInIfExpression.kt")
public void testNotApplicablePostfixPlusPlusInIfExpression() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/notApplicablePostfixPlusPlusInIfExpression.kt");
}
@TestMetadata("notApplicablePostfixPlusPlusInNestedIfExpression.kt")
public void testNotApplicablePostfixPlusPlusInNestedIfExpression() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/notApplicablePostfixPlusPlusInNestedIfExpression.kt");
}
@TestMetadata("notApplicablePostfixPlusPlusInProperty.kt")
public void testNotApplicablePostfixPlusPlusInProperty() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/notApplicablePostfixPlusPlusInProperty.kt");
}
@TestMetadata("notApplicablePostfixPlusPlusInWhenExpression.kt")
public void testNotApplicablePostfixPlusPlusInWhenExpression() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/notApplicablePostfixPlusPlusInWhenExpression.kt");
}
@TestMetadata("pluPlus.kt") @TestMetadata("pluPlus.kt")
public void testPluPlus() throws Exception { public void testPluPlus() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/pluPlus.kt"); runTest("idea/testData/intentions/operatorToFunction/pluPlus.kt");
@@ -13148,6 +13178,21 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
runTest("idea/testData/intentions/operatorToFunction/postfixMinusMinus.kt"); runTest("idea/testData/intentions/operatorToFunction/postfixMinusMinus.kt");
} }
@TestMetadata("postfixPlusPlusInIfExpression.kt")
public void testPostfixPlusPlusInIfExpression() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/postfixPlusPlusInIfExpression.kt");
}
@TestMetadata("postfixPlusPlusInLoop.kt")
public void testPostfixPlusPlusInLoop() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/postfixPlusPlusInLoop.kt");
}
@TestMetadata("postfixPlusPlusInWhenExpression.kt")
public void testPostfixPlusPlusInWhenExpression() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/postfixPlusPlusInWhenExpression.kt");
}
@TestMetadata("prefixPlus.kt") @TestMetadata("prefixPlus.kt")
public void testPrefixPlus() throws Exception { public void testPrefixPlus() throws Exception {
runTest("idea/testData/intentions/operatorToFunction/prefixPlus.kt"); runTest("idea/testData/intentions/operatorToFunction/prefixPlus.kt");