Change Signature: Support implicit calls of 'invoke' and 'get'

Also drop 'operator' keyword when necessary

 #KT-22718 Fixed
This commit is contained in:
Alexey Sedunov
2018-02-06 21:17:32 +03:00
parent e38d5395df
commit fddfbf225f
35 changed files with 437 additions and 32 deletions

View File

@@ -47,6 +47,8 @@ import org.jetbrains.kotlin.idea.core.compareDescriptors
import org.jetbrains.kotlin.idea.refactoring.*
import org.jetbrains.kotlin.idea.refactoring.changeSignature.usages.*
import org.jetbrains.kotlin.idea.refactoring.rename.noReceivers
import org.jetbrains.kotlin.idea.references.KtArrayAccessReference
import org.jetbrains.kotlin.idea.references.KtInvokeFunctionReference
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinReferencesSearchOptions
@@ -227,24 +229,30 @@ class KotlinChangeSignatureUsageProcessor : ChangeSignatureUsageProcessor {
val functionPsi = functionUsageInfo.element ?: return
for (reference in findReferences(functionPsi)) {
val element = reference.element
val element = reference.element ?: continue
if (element is KtReferenceExpression) {
var parent = element.parent
when {
reference is KtInvokeFunctionReference || reference is KtArrayAccessReference -> {
result.add(KotlinByConventionCallUsage(element as KtExpression, functionUsageInfo))
}
when {
parent is KtCallExpression ->
result.add(KotlinFunctionCallUsage(parent, functionUsageInfo))
element is KtReferenceExpression -> {
var parent = element.parent
parent is KtUserType && parent.parent is KtTypeReference -> {
parent = parent.parent.parent
when {
parent is KtCallExpression ->
result.add(KotlinFunctionCallUsage(parent, functionUsageInfo))
if (parent is KtConstructorCalleeExpression && parent.parent is KtSuperTypeCallEntry)
result.add(KotlinFunctionCallUsage(parent.parent as KtSuperTypeCallEntry, functionUsageInfo))
parent is KtUserType && parent.parent is KtTypeReference -> {
parent = parent.parent.parent
if (parent is KtConstructorCalleeExpression && parent.parent is KtSuperTypeCallEntry)
result.add(KotlinFunctionCallUsage(parent.parent as KtSuperTypeCallEntry, functionUsageInfo))
}
element is KtSimpleNameExpression && (functionPsi is KtProperty || functionPsi is KtParameter) ->
result.add(KotlinPropertyCallUsage(element))
}
element is KtSimpleNameExpression && (functionPsi is KtProperty || functionPsi is KtParameter) ->
result.add(KotlinPropertyCallUsage(element))
}
}
}
@@ -660,16 +668,20 @@ class KotlinChangeSignatureUsageProcessor : ChangeSignatureUsageProcessor {
) {
if (originalReceiverInfo != null) return
for (usageInfo in usages) {
if (!(usageInfo is KotlinFunctionCallUsage || usageInfo is KotlinPropertyCallUsage)) continue
loop@ for (usageInfo in usages) {
if (!(usageInfo is KotlinFunctionCallUsage || usageInfo is KotlinPropertyCallUsage || usageInfo is KotlinByConventionCallUsage)) continue
val callElement = usageInfo.element as? KtElement ?: continue
val parent = callElement.parent
if (parent is KtQualifiedExpression && parent.selectorExpression === callElement) {
val message = "Explicit receiver is already present in call element: " + CommonRefactoringUtil.htmlEmphasize(parent.text)
result.putValue(callElement, message)
val elementToReport = when {
usageInfo is KotlinByConventionCallUsage -> callElement
parent is KtQualifiedExpression && parent.selectorExpression === callElement -> parent
else -> continue@loop
}
val message = "Explicit receiver is already present in call element: " + CommonRefactoringUtil.htmlEmphasize(elementToReport.text)
result.putValue(callElement, message)
}
}
@@ -851,6 +863,10 @@ class KotlinChangeSignatureUsageProcessor : ChangeSignatureUsageProcessor {
}
if (beforeMethodChange) {
if (usageInfo is KotlinUsageInfo<*>) {
usageInfo.preprocessUsage()
}
if (method !is PsiMethod || initializedOriginalDescriptor) return false
val descriptorWrapper = usages.firstIsInstanceOrNull<OriginalJavaMethodDescriptorWrapper>()

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. 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.refactoring.changeSignature.usages
import com.intellij.usageView.UsageInfo
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.inspections.conventionNameCalls.ReplaceGetOrSetInspection
import org.jetbrains.kotlin.idea.intentions.OperatorToFunctionIntention
import org.jetbrains.kotlin.idea.intentions.conventionNameCalls.ReplaceInvokeIntention
import org.jetbrains.kotlin.idea.refactoring.changeSignature.KotlinChangeInfo
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.psiUtil.getPossiblyQualifiedCallExpression
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelectorOrThis
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.util.OperatorNameConventions
class KotlinByConventionCallUsage(
expression: KtExpression,
private val callee: KotlinCallableDefinitionUsage<*>
) : KotlinUsageInfo<KtExpression>(expression) {
private var resolvedCall: ResolvedCall<*>? = null
private var convertedCallExpression: KtCallExpression? = null
private fun foldExpression(expression: KtDotQualifiedExpression, changeInfo: KotlinChangeInfo) {
when (changeInfo.newName) {
OperatorNameConventions.INVOKE.asString() -> {
with(ReplaceInvokeIntention()) {
if (applicabilityRange(expression) != null) {
applyTo(expression, null)
}
}
}
OperatorNameConventions.GET.asString() -> {
with(ReplaceGetOrSetInspection()) {
if (isApplicable(expression)) {
applyTo(expression)
}
}
}
}
}
override fun getElement(): KtExpression? {
return convertedCallExpression ?: super.getElement()
}
override fun preprocessUsage() {
val element = element ?: return
val convertedExpression = OperatorToFunctionIntention.convert(element).first
val callExpression = convertedExpression.getPossiblyQualifiedCallExpression() ?: return
resolvedCall = callExpression.getResolvedCall(callExpression.analyze(BodyResolveMode.PARTIAL))
convertedCallExpression = callExpression
}
override fun processUsage(changeInfo: KotlinChangeInfo, element: KtExpression, allUsages: Array<out UsageInfo>): Boolean {
val resolvedCall = resolvedCall ?: return true
val callExpression = convertedCallExpression ?: return true
val callProcessor = KotlinFunctionCallUsage(callExpression, callee, resolvedCall)
val newExpression = callProcessor.processUsageAndGetResult(changeInfo, callExpression, allUsages) as? KtExpression
val qualifiedCall = newExpression?.getQualifiedExpressionForSelectorOrThis() as? KtDotQualifiedExpression
if (qualifiedCall != null) {
foldExpression(qualifiedCall, changeInfo)
}
return true
}
}

View File

@@ -33,6 +33,7 @@ import org.jetbrains.kotlin.idea.refactoring.replaceListPsiAndKeepDelimiters
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.ShortenReferences.Options
import org.jetbrains.kotlin.idea.refactoring.changeSignature.*
import org.jetbrains.kotlin.idea.refactoring.dropOperatorKeywordIfNecessary
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
@@ -140,6 +141,7 @@ class KotlinCallableDefinitionUsage<T : PsiElement>(
if (canDropOverride) {
dropOverrideKeywordIfNecessary(element)
}
dropOperatorKeywordIfNecessary(element)
return true
}

View File

@@ -56,19 +56,28 @@ import java.util.*
class KotlinFunctionCallUsage(
element: KtCallElement,
private val callee: KotlinCallableDefinitionUsage<*>
private val callee: KotlinCallableDefinitionUsage<*>,
forcedResolvedCall: ResolvedCall<*>? = null
) : KotlinUsageInfo<KtCallElement>(element) {
private val context = element.analyze(BodyResolveMode.FULL)
private val resolvedCall = element.getResolvedCall(context)
private val resolvedCall = forcedResolvedCall ?: element.getResolvedCall(context)
private val skipUnmatchedArgumentsCheck = forcedResolvedCall != null
override fun processUsage(changeInfo: KotlinChangeInfo, element: KtCallElement, allUsages: Array<out UsageInfo>): Boolean {
if (shouldSkipUsage(element)) return true
processUsageAndGetResult(changeInfo, element, allUsages)
return true
}
fun processUsageAndGetResult(changeInfo: KotlinChangeInfo, element: KtCallElement, allUsages: Array<out UsageInfo>): KtElement {
if (shouldSkipUsage(element)) return element
var result: KtElement = element
changeNameIfNeeded(changeInfo, element)
if (element.valueArgumentList != null) {
if (changeInfo.isParameterSetOrOrderChanged) {
updateArgumentsAndReceiver(changeInfo, element, allUsages)
result = updateArgumentsAndReceiver(changeInfo, element, allUsages)
}
else {
changeArgumentNames(changeInfo, element)
@@ -83,7 +92,7 @@ class KotlinFunctionCallUsage(
}
}
return true
return result
}
private fun shouldSkipUsage(element: KtCallElement): Boolean {
@@ -94,6 +103,8 @@ class KotlinFunctionCallUsage(
// TODO: investigate why arguments are not recorded for enum constructor call
if (element is KtSuperTypeCallEntry && element.parent.parent is KtEnumEntry) return false
if (skipUnmatchedArgumentsCheck) return false
if (!resolvedCall.call.valueArguments.all{ resolvedCall.getArgumentMapping(it) is ArgumentMatch }) return true
val arguments = resolvedCall.valueArguments
@@ -305,7 +316,7 @@ class KotlinFunctionCallUsage(
}
}
private fun updateArgumentsAndReceiver(changeInfo: KotlinChangeInfo, element: KtCallElement, allUsages: Array<out UsageInfo>) {
private fun updateArgumentsAndReceiver(changeInfo: KotlinChangeInfo, element: KtCallElement, allUsages: Array<out UsageInfo>): KtElement {
if (isPropertyJavaUsage) return updateJavaPropertyCall(changeInfo, element)
val fullCallElement = element.getQualifiedExpressionForSelector() ?: element
@@ -322,7 +333,7 @@ class KotlinFunctionCallUsage(
val dispatchReceiver = resolvedCall?.dispatchReceiver
// Do not add extension receiver to calls with explicit dispatch receiver
if (newReceiverInfo != null && fullCallElement is KtQualifiedExpression && dispatchReceiver is ExpressionReceiver) return
if (newReceiverInfo != null && fullCallElement is KtQualifiedExpression && dispatchReceiver is ExpressionReceiver) return element
val newArgumentInfos = newParameters.withIndex().map {
val (index, param) = it
@@ -386,7 +397,7 @@ class KotlinFunctionCallUsage(
if (it is KtValueArgument) addArgument(it)
}
else -> return
else -> return element
}
}
}
@@ -456,6 +467,8 @@ class KotlinFunctionCallUsage(
val newCallExpression = ((newElement as? KtQualifiedExpression)?.selectorExpression ?: newElement) as KtCallExpression
newCallExpression.moveFunctionLiteralOutsideParentheses()
}
return newElement
}
private fun changeArgumentNames(changeInfo: KotlinChangeInfo, element: KtCallElement) {
@@ -488,10 +501,10 @@ class KotlinFunctionCallUsage(
private val SHORTEN_ARGUMENTS_OPTIONS = ShortenReferences.Options(true, true)
private fun updateJavaPropertyCall(changeInfo: KotlinChangeInfo, element: KtCallElement) {
private fun updateJavaPropertyCall(changeInfo: KotlinChangeInfo, element: KtCallElement): KtElement {
val newReceiverInfo = changeInfo.receiverParameterInfo
val originalReceiverInfo = changeInfo.methodDescriptor.receiver
if (newReceiverInfo == originalReceiverInfo) return
if (newReceiverInfo == originalReceiverInfo) return element
val arguments = element.valueArgumentList.sure { "Argument list is expected: " + element.text }
val oldArguments = element.valueArguments
@@ -515,6 +528,8 @@ class KotlinFunctionCallUsage(
firstArgument != null -> arguments.removeArgument(firstArgument)
}
return element
}
private fun getReceiverExpression(receiver: ReceiverValue, psiFactory: KtPsiFactory): KtExpression? {

View File

@@ -28,5 +28,7 @@ abstract class KotlinUsageInfo<T : PsiElement> : UsageInfo {
@Suppress("UNCHECKED_CAST")
override fun getElement() = super.getElement() as T?
open fun preprocessUsage() {}
abstract fun processUsage(changeInfo: KotlinChangeInfo, element: T, allUsages: Array<out UsageInfo>): Boolean
}

View File

@@ -70,6 +70,7 @@ import org.jetbrains.kotlin.idea.highlighter.markers.actualsForExpected
import org.jetbrains.kotlin.idea.highlighter.markers.liftToExpected
import org.jetbrains.kotlin.idea.intentions.RemoveCurlyBracesFromTemplateIntention
import org.jetbrains.kotlin.idea.j2k.IdeaJavaToKotlinServices
import org.jetbrains.kotlin.idea.project.languageVersionSettings
import org.jetbrains.kotlin.idea.refactoring.changeSignature.KotlinValVar
import org.jetbrains.kotlin.idea.refactoring.changeSignature.toValVar
import org.jetbrains.kotlin.idea.refactoring.memberInfo.KtPsiClassWrapper
@@ -85,9 +86,7 @@ import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.codeFragmentUtil.suppressDiagnosticsInDebugMode
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.resolve.AnalyzingUtils
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.calls.callUtil.getCallWithAssert
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
@@ -797,6 +796,15 @@ fun dropOverrideKeywordIfNecessary(element: KtNamedDeclaration) {
}
}
fun dropOperatorKeywordIfNecessary(element: KtNamedDeclaration) {
val callableDescriptor = element.resolveToDescriptorIfAny() as? CallableDescriptor ?: return
val diagnosticHolder = BindingTraceContext()
OperatorModifierChecker.check(element, callableDescriptor, diagnosticHolder, element.languageVersionSettings)
if (diagnosticHolder.bindingContext.diagnostics.any { it.factory == Errors.INAPPLICABLE_OPERATOR_MODIFIER }) {
element.removeModifier(KtTokens.OPERATOR_KEYWORD)
}
}
fun getQualifiedTypeArgumentList(
initializer: KtExpression,
context: BindingContext = initializer.analyze(BodyResolveMode.PARTIAL)

View File

@@ -0,0 +1,9 @@
class A {
operator fun get(s: String, i: Int, b: Boolean) {}
}
fun usage(a: A) {
a["", 42, false]
A()["", 42, false]
a.get("", 42, false)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>get(s: String, i: Int) {}
}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>get(s: String, i: Int) {}
}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,3 @@
Explicit receiver is already present in call element: A()[&quot;&quot;, 42]
Explicit receiver is already present in call element: a.get(&quot;&quot;, 42)
Explicit receiver is already present in call element: a[&quot;&quot;, 42]

View File

@@ -0,0 +1,9 @@
class A
fun get(a: A, s: String, i: Int) {}
fun usage(a: A) {
get(a, "", 42)
get(A(), "", 42)
get(a, "", 42)
}

View File

@@ -0,0 +1,9 @@
class A
operator fun A.<caret>get(s: String, i: Int) {}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun get(i: Int) {}
}
fun usage(a: A) {
a[42]
A()[42]
a.get(42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>get(s: String, i: Int) {}
}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
fun foo(s: String, i: Int) {}
}
fun usage(a: A) {
a.foo("", 42)
A().foo("", 42)
a.foo("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>get(s: String, i: Int) {}
}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun invoke(s: String, i: Int) {}
}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>get(s: String, i: Int) {}
}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun get(i: Int, s: String) {}
}
fun usage(a: A) {
a[42, ""]
A()[42, ""]
a.get(42, "")
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>get(s: String, i: Int) {}
}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun invoke(s: String, i: Int, b: Boolean) {}
}
fun usage(a: A) {
a("", 42, false)
A()("", 42, false)
a.invoke("", 42, false)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>invoke(s: String, i: Int) {}
}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>invoke(s: String, i: Int) {}
}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -0,0 +1,3 @@
Explicit receiver is already present in call element: A()(&quot;&quot;, 42)
Explicit receiver is already present in call element: a(&quot;&quot;, 42)
Explicit receiver is already present in call element: a.invoke(&quot;&quot;, 42)

View File

@@ -0,0 +1,9 @@
class A
fun invoke(a: A, s: String, i: Int) {}
fun usage(a: A) {
invoke(a, "", 42)
invoke(A(), "", 42)
invoke(a, "", 42)
}

View File

@@ -0,0 +1,9 @@
class A
operator fun A.<caret>invoke(s: String, i: Int) {}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun invoke(i: Int) {}
}
fun usage(a: A) {
a(42)
A()(42)
a.invoke(42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>invoke(s: String, i: Int) {}
}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
fun foo(s: String, i: Int) {}
}
fun usage(a: A) {
a.foo("", 42)
A().foo("", 42)
a.foo("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>invoke(s: String, i: Int) {}
}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun get(s: String, i: Int) {}
}
fun usage(a: A) {
a["", 42]
A()["", 42]
a.get("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>invoke(s: String, i: Int) {}
}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun invoke(i: Int, s: String) {}
}
fun usage(a: A) {
a(42, "")
A()(42, "")
a.invoke(42, "")
}

View File

@@ -0,0 +1,9 @@
class A {
operator fun <caret>invoke(s: String, i: Int) {}
}
fun usage(a: A) {
a("", 42)
A()("", 42)
a.invoke("", 42)
}

View File

@@ -960,4 +960,52 @@ class KotlinChangeSignatureTest : KotlinLightCodeInsightFixtureTestCase() {
fun testChangeReturnTypeToNonUnit() = doTest {
newReturnTypeInfo = KotlinTypeInfo(true, BUILT_INS.intType)
}
fun testInvokeConventionRemoveParameter() = doTest { removeParameter(0) }
fun testInvokeConventionAddParameter() = doTest {
addParameter(
KotlinParameterInfo(
originalBaseFunctionDescriptor,
-1,
"b",
KotlinTypeInfo(false, BUILT_INS.booleanType),
defaultValueForCall = KtPsiFactory(project).createExpression("false")
)
)
}
fun testInvokeConventionSwapParameters() = doTest { swapParameters(0, 1) }
fun testInvokeConventionParameterToReceiver() = doTestConflict { receiverParameterInfo = newParameters[0] }
fun testInvokeConventionReceiverToParameter() = doTest { receiverParameterInfo = null }
fun testInvokeConventionRenameToFoo() = doTest { newName = "foo" }
fun testInvokeConventionRenameToGet() = doTest { newName = "get" }
fun testGetConventionRemoveParameter() = doTest { removeParameter(0) }
fun testGetConventionAddParameter() = doTest {
addParameter(
KotlinParameterInfo(
originalBaseFunctionDescriptor,
-1,
"b",
KotlinTypeInfo(false, BUILT_INS.booleanType),
defaultValueForCall = KtPsiFactory(project).createExpression("false")
)
)
}
fun testGetConventionSwapParameters() = doTest { swapParameters(0, 1) }
fun testGetConventionParameterToReceiver() = doTestConflict { receiverParameterInfo = newParameters[0] }
fun testGetConventionReceiverToParameter() = doTest { receiverParameterInfo = null }
fun testGetConventionRenameToFoo() = doTest { newName = "foo" }
fun testGetConventionRenameToInvoke() = doTest { newName = "invoke" }
}