From 215d8b1e1a31f25f2967bab2d89a14b3437cfd6c Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Wed, 7 Dec 2016 14:39:19 +0300 Subject: [PATCH] Intentions: Convert function type receiver to parameter #KT-14246 Fixed --- ChangeLog.md | 1 + .../psi/EditCommaSeparatedListHelper.kt | 2 +- .../org/jetbrains/kotlin/psi/KtPsiFactory.kt | 4 + .../jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt | 23 +- .../after.kt.template | 3 + .../before.kt.template | 3 + .../description.html | 5 + idea/src/META-INF/plugin.xml | 5 + .../AbstractProcessableUsageInfo.kt | 28 +++ ...unctionTypeParameterToReceiverIntention.kt | 26 +- ...unctionTypeReceiverToParameterIntention.kt | 224 ++++++++++++++++++ .../.intention | 1 + .../function.kt | 22 ++ .../function.kt.after | 22 ++ .../notInFunctionParameter.kt | 2 + .../notOnReceiver.kt | 4 + .../onlyLambdaArgument.kt | 3 + .../onlyLambdaArgument.kt.after | 3 + .../parameterlessFunction.kt | 22 ++ .../parameterlessFunction.kt.after | 22 ++ .../primaryConstructor.kt | 41 ++++ .../primaryConstructor.kt.after | 41 ++++ .../secondaryConstructor.kt | 41 ++++ .../secondaryConstructor.kt.after | 41 ++++ .../intentions/IntentionTestGenerated.java | 51 ++++ 25 files changed, 623 insertions(+), 17 deletions(-) create mode 100644 idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/after.kt.template create mode 100644 idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/before.kt.template create mode 100644 idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/description.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/AbstractProcessableUsageInfo.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeReceiverToParameterIntention.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/.intention create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt.after create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/notInFunctionParameter.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/notOnReceiver.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt.after create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt.after create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt.after create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt create mode 100644 idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt.after diff --git a/ChangeLog.md b/ChangeLog.md index ea1362bc013..5247d1fd84e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -338,6 +338,7 @@ These artifacts include extensions for the types available in the latter JDKs, s - [`KT-14044`](https://youtrack.jetbrains.com/issue/KT-14044) Fix exception on deleting unused declaration in IDEA 2016.3 - [`KT-14019`](https://youtrack.jetbrains.com/issue/KT-14019) Create from Usage: Support generation of abstract members for superclasses - [`KT-14246`](https://youtrack.jetbrains.com/issue/KT-14246) Intentions: Convert function type parameter to receiver +- [`KT-14246`](https://youtrack.jetbrains.com/issue/KT-14246) Intentions: Convert function type receiver to parameter ##### New features diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/EditCommaSeparatedListHelper.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/EditCommaSeparatedListHelper.kt index bd30fa92f11..848b598a15f 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/EditCommaSeparatedListHelper.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/EditCommaSeparatedListHelper.kt @@ -32,7 +32,7 @@ object EditCommaSeparatedListHelper { fun addItemAfter(list: KtElement, allItems: List, item: TItem, anchor: TItem?, prefix: KtToken = KtTokens.LPAR): TItem { assert(anchor == null || anchor.parent == list) if (allItems.isEmpty()) { - if (list.firstChild.node.elementType == prefix) { + if (list.firstChild?.node?.elementType == prefix) { return list.addAfter(item, list.firstChild) as TItem } else { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt index 9ec89b04e11..d53b62bc28d 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt @@ -112,6 +112,10 @@ class KtPsiFactory(private val project: Project) { return (createType("A.() -> B").typeElement as KtFunctionType).receiver!!.apply { this.typeReference.replace(typeReference) } } + fun createFunctionTypeParameter(typeReference: KtTypeReference): KtParameter { + return (createType("(A) -> B").typeElement as KtFunctionType).parameters.first().apply { this.typeReference!!.replace(typeReference) } + } + fun createTypeAlias(name: String, typeParameters: List, typeElement: KtTypeElement): KtTypeAlias { return createTypeAlias(name, typeParameters, "X").apply { getTypeReference()!!.replace(createType(typeElement)) } } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt index d80a95ccf24..1fa807c111f 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt @@ -475,4 +475,25 @@ fun isTypeConstructorReference(e: PsiElement): Boolean { fun KtParameter.isPropertyParameter() = ownerFunction is KtPrimaryConstructor && hasValOrVar() -fun isDoubleColonReceiver(expression: KtExpression) = expression.getParentOfTypeAndBranch { this.receiverExpression } != null \ No newline at end of file +fun isDoubleColonReceiver(expression: KtExpression) = expression.getParentOfTypeAndBranch { this.receiverExpression } != null + +fun KtFunctionLiteral.getOrCreateParameterList(): KtParameterList { + valueParameterList?.let { return it } + + val psiFactory = KtPsiFactory(this) + + val anchor = lBrace + val newParameterList = addAfter(psiFactory.createLambdaParameterList("x"), anchor) as KtParameterList + newParameterList.removeParameter(0) + if (arrow == null) { + val whitespaceAndArrow = psiFactory.createWhitespaceAndArrow() + addRangeAfter(whitespaceAndArrow.first, whitespaceAndArrow.second, newParameterList) + } + return newParameterList +} + +fun KtCallExpression.getOrCreateValueArgumentList(): KtValueArgumentList { + valueArgumentList?.let { return it } + return addAfter(KtPsiFactory(this).createCallArguments("()"), + typeArgumentList ?: calleeExpression) as KtValueArgumentList +} \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/after.kt.template b/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/after.kt.template new file mode 100644 index 00000000000..9ca70895207 --- /dev/null +++ b/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/after.kt.template @@ -0,0 +1,3 @@ +fun foo(f: (i: Int, s: String) -> Boolean) = f(1, "2") + +fun bar = foo { i, s -> s.length() > n } \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/before.kt.template b/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/before.kt.template new file mode 100644 index 00000000000..24e22d6cd68 --- /dev/null +++ b/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/before.kt.template @@ -0,0 +1,3 @@ +fun foo(f: Int.(s: String) -> Boolean) = 1.f("2") + +fun bar = foo { s -> s.length() > this } \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/description.html b/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/description.html new file mode 100644 index 00000000000..b4a73a4b0d7 --- /dev/null +++ b/idea/resources/intentionDescriptions/ConvertFunctionTypeReceiverToParameterIntention/description.html @@ -0,0 +1,5 @@ + + +This intention converts receiver of a function type used in a function parameter to its first parameter + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 0f9b58e2d69..119bae4d53f 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1461,6 +1461,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.ConvertFunctionTypeReceiverToParameterIntention + Kotlin + + (element: T) : UsageInfo(element) { + @Suppress("UNCHECKED_CAST") + override fun getElement() = super.getElement() as T? + + abstract fun process(data: D, elementsToShorten: MutableList) +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeParameterToReceiverIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeParameterToReceiverIntention.kt index 996b8c7c198..4310bbd5f5d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeParameterToReceiverIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeParameterToReceiverIntention.kt @@ -26,7 +26,6 @@ import com.intellij.psi.search.LocalSearchScope import com.intellij.psi.search.searches.MethodReferencesSearch import com.intellij.psi.search.searches.ReferencesSearch import com.intellij.refactoring.util.RefactoringUIUtil -import com.intellij.usageView.UsageInfo import com.intellij.util.containers.MultiMap import org.jetbrains.kotlin.asJava.toLightMethods import org.jetbrains.kotlin.descriptors.CallableDescriptor @@ -60,14 +59,7 @@ class ConvertFunctionTypeParameterToReceiverIntention : SelfTargetingRangeIntent KtTypeReference::class.java, "Convert function type parameter to receiver" ) { - abstract class AbstractUsageInfo(element: T) : UsageInfo(element) { - @Suppress("UNCHECKED_CAST") - override fun getElement() = super.getElement() as T? - - abstract fun process(data: ConversionData, elementsToShorten: MutableList) - } - - class FunctionDefinitionInfo(element: KtFunction) : AbstractUsageInfo(element) { + class FunctionDefinitionInfo(element: KtFunction) : AbstractProcessableUsageInfo(element) { override fun process(data: ConversionData, elementsToShorten: MutableList) { val function = element ?: return val functionParameter = function.valueParameters.getOrNull(data.functionParameterIndex) ?: return @@ -80,7 +72,7 @@ class ConvertFunctionTypeParameterToReceiverIntention : SelfTargetingRangeIntent } } - class ParameterCallInfo(element: KtCallExpression) : AbstractUsageInfo(element) { + class ParameterCallInfo(element: KtCallExpression) : AbstractProcessableUsageInfo(element) { override fun process(data: ConversionData, elementsToShorten: MutableList) { val callExpression = element ?: return val argumentList = callExpression.valueArgumentList ?: return @@ -91,7 +83,7 @@ class ConvertFunctionTypeParameterToReceiverIntention : SelfTargetingRangeIntent } } - class InternalReferencePassInfo(element: KtSimpleNameExpression) : AbstractUsageInfo(element) { + class InternalReferencePassInfo(element: KtSimpleNameExpression) : AbstractProcessableUsageInfo(element) { override fun process(data: ConversionData, elementsToShorten: MutableList) { val expression = element ?: return val lambdaType = data.lambdaType @@ -111,7 +103,7 @@ class ConvertFunctionTypeParameterToReceiverIntention : SelfTargetingRangeIntent } } - class LambdaInfo(element: KtExpression) : AbstractUsageInfo(element) { + class LambdaInfo(element: KtExpression) : AbstractProcessableUsageInfo(element) { override fun process(data: ConversionData, elementsToShorten: MutableList) { val expression = element ?: return val context = expression.analyze(BodyResolveMode.PARTIAL) @@ -185,7 +177,7 @@ class ConvertFunctionTypeParameterToReceiverIntention : SelfTargetingRangeIntent val conflicts = MultiMap() - val usages = ArrayList>() + val usages = ArrayList>() project.runSynchronouslyWithProgress("Looking for usages and conflicts...", true) { runReadAction { @@ -233,7 +225,11 @@ class ConvertFunctionTypeParameterToReceiverIntention : SelfTargetingRangeIntent } } - private fun processExternalUsage(conflicts: MultiMap, refElement: PsiElement, usages: java.util.ArrayList>) { + private fun processExternalUsage( + conflicts: MultiMap, + refElement: PsiElement, + usages: ArrayList> + ) { val callElement = refElement.getParentOfTypeAndBranch { calleeExpression } if (callElement != null) { val context = callElement.analyze(BodyResolveMode.PARTIAL) @@ -294,7 +290,7 @@ class ConvertFunctionTypeParameterToReceiverIntention : SelfTargetingRangeIntent return true } - private fun processInternalUsages(callable: KtFunction, usages: ArrayList>) { + private fun processInternalUsages(callable: KtFunction, usages: ArrayList>) { val body = when (callable) { is KtConstructor<*> -> callable.containingClassOrObject?.getBody() else -> callable.bodyExpression diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeReceiverToParameterIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeReceiverToParameterIntention.kt new file mode 100644 index 00000000000..f6eb601a5ef --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertFunctionTypeReceiverToParameterIntention.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.intentions + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.search.LocalSearchScope +import com.intellij.psi.search.searches.MethodReferencesSearch +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.refactoring.util.RefactoringUIUtil +import com.intellij.util.containers.MultiMap +import org.jetbrains.kotlin.asJava.toLightMethods +import org.jetbrains.kotlin.builtins.getReceiverTypeFromFunctionType +import org.jetbrains.kotlin.descriptors.CallableDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde +import org.jetbrains.kotlin.idea.core.* +import org.jetbrains.kotlin.idea.refactoring.CallableRefactoring +import org.jetbrains.kotlin.idea.refactoring.checkConflictsInteractively +import org.jetbrains.kotlin.idea.refactoring.getAffectedCallables +import org.jetbrains.kotlin.idea.references.KtSimpleReference +import org.jetbrains.kotlin.idea.runSynchronouslyWithProgress +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.psi.typeRefHelpers.setReceiverTypeReference +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.bindingContextUtil.getAbbreviatedTypeOrType +import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getArgumentByParameterIndex +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.KotlinType + +class ConvertFunctionTypeReceiverToParameterIntention : SelfTargetingRangeIntention( + KtTypeReference::class.java, + "Convert function type receiver to parameter" +) { + class ConversionData( + val functionParameterIndex: Int, + val lambdaReceiverType: KotlinType, + val function: KtFunction + ) { + val functionDescriptor by lazy { function.resolveToDescriptor() as FunctionDescriptor } + } + + class FunctionDefinitionInfo(element: KtFunction) : AbstractProcessableUsageInfo(element) { + override fun process(data: ConversionData, elementsToShorten: MutableList) { + val function = element as? KtFunction ?: return + val functionParameter = function.valueParameters.getOrNull(data.functionParameterIndex) ?: return + val functionType = functionParameter.typeReference?.typeElement as? KtFunctionType ?: return + val functionTypeParameterList = functionType.parameterList ?: return + val functionTypeReceiver = functionType.receiverTypeReference ?: return + val parameterToAdd = KtPsiFactory(project).createFunctionTypeParameter(functionTypeReceiver) + functionTypeParameterList.addParameterBefore(parameterToAdd, functionTypeParameterList.parameters.firstOrNull()) + functionType.setReceiverTypeReference(null) + } + } + + class ParameterCallInfo(element: KtCallExpression) : AbstractProcessableUsageInfo(element) { + override fun process(data: ConversionData, elementsToShorten: MutableList) { + val callExpression = element ?: return + val qualifiedExpression = callExpression.getQualifiedExpressionForSelector() ?: return + val receiverExpression = qualifiedExpression.receiverExpression + val argumentList = callExpression.getOrCreateValueArgumentList() + argumentList.addArgumentBefore(KtPsiFactory(project).createArgument(receiverExpression), argumentList.arguments.firstOrNull()) + qualifiedExpression.replace(callExpression) + } + } + + class LambdaInfo(element: KtLambdaExpression) : AbstractProcessableUsageInfo(element) { + override fun process(data: ConversionData, elementsToShorten: MutableList) { + val lambda = element?.functionLiteral ?: return + val context = lambda.analyze(BodyResolveMode.PARTIAL) + + val psiFactory = KtPsiFactory(project) + + val validator = CollectingNameValidator( + lambda.valueParameters.mapNotNull { it.name }, + NewDeclarationNameValidator(lambda.bodyExpression!!, null, NewDeclarationNameValidator.Target.VARIABLES) + ) + val newParameterName = KotlinNameSuggester.suggestNamesByType(data.lambdaReceiverType, validator, "p").first() + val newParameterRefExpression = psiFactory.createExpression(newParameterName) + + lambda.forEachDescendantOfType { + val thisTarget = context[BindingContext.REFERENCE_TARGET, it.instanceReference] ?: return@forEachDescendantOfType + if (DescriptorToSourceUtilsIde.getAnyDeclaration(project, thisTarget) == lambda) { + it.replace(newParameterRefExpression) + } + } + + val lambdaParameterList = lambda.getOrCreateParameterList() + val parameterToAdd = psiFactory.createLambdaParameterList(newParameterName).parameters.first() + lambdaParameterList.addParameterBefore(parameterToAdd, lambdaParameterList.parameters.firstOrNull()) + } + } + + private inner class Converter( + private val data: ConversionData + ) : CallableRefactoring(data.function.project, data.functionDescriptor, text) { + override fun performRefactoring(descriptorsForChange: Collection) { + val callables = getAffectedCallables(project, descriptorsForChange) + + val conflicts = MultiMap() + + val usages = ArrayList>() + + project.runSynchronouslyWithProgress("Looking for usages and conflicts...", true) { + runReadAction { + val progressStep = 1.0/callables.size + for ((i, callable) in callables.withIndex()) { + ProgressManager.getInstance().progressIndicator.fraction = (i + 1) * progressStep + + if (callable !is KtFunction) continue + + if (!checkModifiable(callable)) { + val renderedCallable = RefactoringUIUtil.getDescription(callable, true).capitalize() + conflicts.putValue(callable, "Can't modify $renderedCallable") + } + + val references = callable.toLightMethods().flatMapTo(LinkedHashSet()) { MethodReferencesSearch.search(it) } + for (ref in references) { + if (ref !is KtSimpleReference<*>) continue + processExternalUsage(ref, usages) + } + + usages += FunctionDefinitionInfo(callable) + + processInternalUsages(callable, usages) + } + } + } + + project.checkConflictsInteractively(conflicts) { + project.executeWriteCommand(text) { + val elementsToShorten = ArrayList() + usages.forEach { it.process(data, elementsToShorten) } + ShortenReferences.DEFAULT.process(elementsToShorten) + } + } + } + + private fun processExternalUsage(ref: KtSimpleReference<*>, usages: java.util.ArrayList>) { + val callElement = ref.element.getParentOfTypeAndBranch { calleeExpression } ?: return + val context = callElement.analyze(BodyResolveMode.PARTIAL) + val expressionToProcess = callElement + .getArgumentByParameterIndex(data.functionParameterIndex, context) + .singleOrNull() + ?.getArgumentExpression() + ?.let { KtPsiUtil.safeDeparenthesize(it) } + ?: return + if (expressionToProcess is KtLambdaExpression) { + usages += LambdaInfo(expressionToProcess) + } + } + + private fun processInternalUsages(callable: KtFunction, usages: java.util.ArrayList>) { + val body = when (callable) { + is KtConstructor<*> -> callable.containingClassOrObject?.getBody() + else -> callable.bodyExpression + } + if (body != null) { + val functionParameter = callable.valueParameters.getOrNull(data.functionParameterIndex) ?: return + for (ref in ReferencesSearch.search(functionParameter, LocalSearchScope(body))) { + val element = ref.element as? KtSimpleNameExpression ?: continue + val callExpression = element.getParentOfTypeAndBranch { calleeExpression } ?: continue + usages += ParameterCallInfo(callExpression) + } + } + } + } + + private fun KtTypeReference.getConversionData(): ConversionData? { + val functionTypeReceiver = parent as? KtFunctionTypeReceiver ?: return null + val functionType = functionTypeReceiver.parent as? KtFunctionType ?: return null + val lambdaReceiverType = functionType + .getAbbreviatedTypeOrType(functionType.analyze(BodyResolveMode.PARTIAL)) + ?.getReceiverTypeFromFunctionType() + ?: return null + val containingParameter = (functionType.parent as? KtTypeReference)?.parent as? KtParameter ?: return null + val ownerFunction = containingParameter.ownerFunction ?: return null + val functionParameterIndex = ownerFunction.valueParameters.indexOf(containingParameter) + return ConversionData(functionParameterIndex, lambdaReceiverType, ownerFunction) + } + + override fun startInWriteAction(): Boolean = false + + override fun applicabilityRange(element: KtTypeReference): TextRange? { + val data = element.getConversionData() ?: return null + + val elementBefore = data.function.valueParameters[data.functionParameterIndex].typeReference!!.typeElement as KtFunctionType + val elementAfter = elementBefore.copied().apply { + parameterList!!.addParameterBefore( + KtPsiFactory(element).createFunctionTypeParameter(element), + parameterList!!.parameters.firstOrNull() + ) + setReceiverTypeReference(null) + } + text = "Convert '${elementBefore.text}' to '${elementAfter.text}'" + + return element.textRange + } + + override fun applyTo(element: KtTypeReference, editor: Editor?) { + element.getConversionData()?.let { Converter(it).run() } + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/.intention b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/.intention new file mode 100644 index 00000000000..96a90eac317 --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.ConvertFunctionTypeReceiverToParameterIntention diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt new file mode 100644 index 00000000000..dc48ede6f7f --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt @@ -0,0 +1,22 @@ +fun foo(f: Int.(Boolean) -> String) { + 1.f(false) + bar(f) +} + +fun bar(f: (Int, Boolean) -> String) { + +} + +fun lambda(): (Int, Boolean) -> String = { i, b -> "$i $b"} + +fun baz(f: (Int, Boolean) -> String) { + fun g(i: Int, b: Boolean) = "" + + foo(f) + + foo(::g) + + foo(lambda()) + + foo { b -> "${this + 1} $b" } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt.after b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt.after new file mode 100644 index 00000000000..4fc77cab79c --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt.after @@ -0,0 +1,22 @@ +fun foo(f: (Int, Boolean) -> String) { + f(1, false) + bar(f) +} + +fun bar(f: (Int, Boolean) -> String) { + +} + +fun lambda(): (Int, Boolean) -> String = { i, b -> "$i $b"} + +fun baz(f: (Int, Boolean) -> String) { + fun g(i: Int, b: Boolean) = "" + + foo(f) + + foo(::g) + + foo(lambda()) + + foo { i, b -> "${i + 1} $b" } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/notInFunctionParameter.kt b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/notInFunctionParameter.kt new file mode 100644 index 00000000000..f1d01ea06e6 --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/notInFunctionParameter.kt @@ -0,0 +1,2 @@ +// IS_APPLICABLE: false +fun foo(f: Int.(Boolean) -> String): Int.(Boolean) -> String = f \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/notOnReceiver.kt b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/notOnReceiver.kt new file mode 100644 index 00000000000..7f55dcd68cd --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/notOnReceiver.kt @@ -0,0 +1,4 @@ +// IS_APPLICABLE: false +fun foo(f: Int.(Boolean) -> String) { + +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt new file mode 100644 index 00000000000..0c4cd7b5c25 --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt @@ -0,0 +1,3 @@ +fun foo(f: Int.(() -> Unit) -> String) { + 1.f {} +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt.after b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt.after new file mode 100644 index 00000000000..b7fd10e93e7 --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt.after @@ -0,0 +1,3 @@ +fun foo(f: (Int, () -> Unit) -> String) { + f(1) {} +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt new file mode 100644 index 00000000000..7c838f901a8 --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt @@ -0,0 +1,22 @@ +fun foo(f: Int.() -> String) { + 1.f() + bar(f) +} + +fun bar(f: (Int) -> String) { + +} + +fun lambda(): (Int) -> String = { i -> "$i"} + +fun baz(f: (Int) -> String) { + fun g(i: Int) = "" + + foo(f) + + foo(::g) + + foo(lambda()) + + foo { "${this + 1}" } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt.after b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt.after new file mode 100644 index 00000000000..779b07b332b --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt.after @@ -0,0 +1,22 @@ +fun foo(f: (Int) -> String) { + f(1) + bar(f) +} + +fun bar(f: (Int) -> String) { + +} + +fun lambda(): (Int) -> String = { i -> "$i"} + +fun baz(f: (Int) -> String) { + fun g(i: Int) = "" + + foo(f) + + foo(::g) + + foo(lambda()) + + foo { i -> "${i + 1}" } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt new file mode 100644 index 00000000000..afedaf21b7b --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt @@ -0,0 +1,41 @@ +open class Foo(f: Int.(Boolean) -> String) { + constructor(a: Int, f: (Int, Boolean) -> String) : this(f) + constructor(a: Int) : this(::g) + constructor(a: Int, b: Int) : this(lambda()) + constructor(a: Int, b: Int, c: Int) : this({ b -> "${this + 1} $b" }) + + init { + 1.f(false) + bar(f) + } +} + +fun bar(f: (Int, Boolean) -> String) { + +} + +fun lambda(): (Int, Boolean) -> String = { i, b -> "$i $b"} + +fun g(i: Int, b: Boolean) = "" + +fun baz(f: (Int, Boolean) -> String) { + Foo(f) + + Foo(::g) + + Foo(lambda()) + + Foo { b -> "${this + 1} $b" } +} + +class Baz1(f: (Int, Boolean) -> String) : Foo(f) +class Baz2 : Foo(::g) +class Baz3 : Foo(lambda()) +class Baz4 : Foo({ b -> "${this + 1} $b" }) + +class Baz5 : Foo { + constructor(f: (Int, Boolean) -> String) : super(f) + constructor(a: Int) : super(::g) + constructor(a: Int, b: Int) : super(lambda()) + constructor(a: Int, b: Int, c: Int) : super({ b -> "${this + 1} $b" }) +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt.after b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt.after new file mode 100644 index 00000000000..5296e5e5ac7 --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt.after @@ -0,0 +1,41 @@ +open class Foo(f: (Int, Boolean) -> String) { + constructor(a: Int, f: (Int, Boolean) -> String) : this(f) + constructor(a: Int) : this(::g) + constructor(a: Int, b: Int) : this(lambda()) + constructor(a: Int, b: Int, c: Int) : this({ i, b -> "${i + 1} $b" }) + + init { + f(1, false) + bar(f) + } +} + +fun bar(f: (Int, Boolean) -> String) { + +} + +fun lambda(): (Int, Boolean) -> String = { i, b -> "$i $b"} + +fun g(i: Int, b: Boolean) = "" + +fun baz(f: (Int, Boolean) -> String) { + Foo(f) + + Foo(::g) + + Foo(lambda()) + + Foo { i, b -> "${i + 1} $b" } +} + +class Baz1(f: (Int, Boolean) -> String) : Foo(f) +class Baz2 : Foo(::g) +class Baz3 : Foo(lambda()) +class Baz4 : Foo({ i, b -> "${i + 1} $b" }) + +class Baz5 : Foo { + constructor(f: (Int, Boolean) -> String) : super(f) + constructor(a: Int) : super(::g) + constructor(a: Int, b: Int) : super(lambda()) + constructor(a: Int, b: Int, c: Int) : super({ i, b -> "${i + 1} $b" }) +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt new file mode 100644 index 00000000000..ff2bf77c8be --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt @@ -0,0 +1,41 @@ +open class Foo { + constructor(f: Int.(Boolean) -> String) { + 1.f(false) + bar(f) + } + + constructor(a: Int, f: (Int, Boolean) -> String) : this(f) + constructor(a: Int) : this(::g) + constructor(a: Int, b: Int) : this(lambda()) + constructor(a: Int, b: Int, c: Int) : this({ b -> "${this + 1} $b" }) +} + +fun bar(f: (Int, Boolean) -> String) { + +} + +fun lambda(): (Int, Boolean) -> String = { i, b -> "$i $b"} + +fun g(i: Int, b: Boolean) = "" + +fun baz(f: (Int, Boolean) -> String) { + Foo(f) + + Foo(::g) + + Foo(lambda()) + + Foo { b -> "${this + 1} $b" } +} + +class Baz1(f: (Int, Boolean) -> String) : Foo(f) +class Baz2 : Foo(::g) +class Baz3 : Foo(lambda()) +class Baz4 : Foo({ b -> "${this + 1} $b" }) + +class Baz5 : Foo { + constructor(f: (Int, Boolean) -> String) : super(f) + constructor(a: Int) : super(::g) + constructor(a: Int, b: Int) : super(lambda()) + constructor(a: Int, b: Int, c: Int) : super({ b -> "${this + 1} $b" }) +} \ No newline at end of file diff --git a/idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt.after b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt.after new file mode 100644 index 00000000000..6fbc7a9cdf0 --- /dev/null +++ b/idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt.after @@ -0,0 +1,41 @@ +open class Foo { + constructor(f: (Int, Boolean) -> String) { + f(1, false) + bar(f) + } + + constructor(a: Int, f: (Int, Boolean) -> String) : this(f) + constructor(a: Int) : this(::g) + constructor(a: Int, b: Int) : this(lambda()) + constructor(a: Int, b: Int, c: Int) : this({ i, b -> "${i + 1} $b" }) +} + +fun bar(f: (Int, Boolean) -> String) { + +} + +fun lambda(): (Int, Boolean) -> String = { i, b -> "$i $b"} + +fun g(i: Int, b: Boolean) = "" + +fun baz(f: (Int, Boolean) -> String) { + Foo(f) + + Foo(::g) + + Foo(lambda()) + + Foo { i, b -> "${i + 1} $b" } +} + +class Baz1(f: (Int, Boolean) -> String) : Foo(f) +class Baz2 : Foo(::g) +class Baz3 : Foo(lambda()) +class Baz4 : Foo({ i, b -> "${i + 1} $b" }) + +class Baz5 : Foo { + constructor(f: (Int, Boolean) -> String) : super(f) + constructor(a: Int) : super(::g) + constructor(a: Int, b: Int) : super(lambda()) + constructor(a: Int, b: Int, c: Int) : super({ i, b -> "${i + 1} $b" }) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 1b3240efc46..00e8419a34c 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -4101,6 +4101,57 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ConvertFunctionTypeReceiverToParameter extends AbstractIntentionTest { + public void testAllFilesPresentInConvertFunctionTypeReceiverToParameter() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertFunctionTypeReceiverToParameter"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("function.kt") + public void testFunction() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter/function.kt"); + doTest(fileName); + } + + @TestMetadata("notInFunctionParameter.kt") + public void testNotInFunctionParameter() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter/notInFunctionParameter.kt"); + doTest(fileName); + } + + @TestMetadata("notOnReceiver.kt") + public void testNotOnReceiver() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter/notOnReceiver.kt"); + doTest(fileName); + } + + @TestMetadata("onlyLambdaArgument.kt") + public void testOnlyLambdaArgument() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter/onlyLambdaArgument.kt"); + doTest(fileName); + } + + @TestMetadata("parameterlessFunction.kt") + public void testParameterlessFunction() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter/parameterlessFunction.kt"); + doTest(fileName); + } + + @TestMetadata("primaryConstructor.kt") + public void testPrimaryConstructor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter/primaryConstructor.kt"); + doTest(fileName); + } + + @TestMetadata("secondaryConstructor.kt") + public void testSecondaryConstructor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertFunctionTypeReceiverToParameter/secondaryConstructor.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/intentions/convertIfWithThrowToAssert") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)