Extract Function: Allow capturing local function references as lambda parameters
This commit is contained in:
@@ -195,6 +195,10 @@ public class JetPsiFactory(private val project: Project) {
|
||||
return createProperty(name, null, false, name).getInitializer() as JetSimpleNameExpression
|
||||
}
|
||||
|
||||
public fun createOperationName(name: String): JetSimpleNameExpression {
|
||||
return (createExpression("0 $name 0") as JetBinaryExpression).getOperationReference()
|
||||
}
|
||||
|
||||
public fun createObjectDeclarationName(name: String): JetObjectDeclarationName {
|
||||
return createDeclaration<JetObjectDeclaration>("object $name").getNameAsDeclaration()!!
|
||||
}
|
||||
|
||||
+10
-2
@@ -74,8 +74,16 @@ class RenameReplacement(override val parameter: Parameter): ParameterReplacement
|
||||
|
||||
[suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")]
|
||||
override fun invoke(e: JetElement): JetElement {
|
||||
val thisExpr = e.getParent() as? JetThisExpression
|
||||
return (thisExpr ?: e).replaced(JetPsiFactory(e).createSimpleName(parameter.nameForRef))
|
||||
val expressionToReplace = e.getParent() as? JetThisExpression ?: e
|
||||
val psiFactory = JetPsiFactory(e)
|
||||
val replacement =
|
||||
if (expressionToReplace is JetOperationReferenceExpression) {
|
||||
psiFactory.createOperationName(parameter.nameForRef)
|
||||
}
|
||||
else {
|
||||
psiFactory.createSimpleName(parameter.nameForRef)
|
||||
}
|
||||
return expressionToReplace.replaced(replacement)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -47,7 +47,8 @@ data class ExtractionOptions(
|
||||
val inferUnitTypeForUnusedValues: Boolean = true,
|
||||
val enableListBoxing: Boolean = false,
|
||||
val extractAsProperty: Boolean = false,
|
||||
val allowSpecialClassNames: Boolean = false
|
||||
val allowSpecialClassNames: Boolean = false,
|
||||
val captureLocalFunctions: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT = ExtractionOptions()
|
||||
|
||||
+27
-4
@@ -36,6 +36,7 @@ import org.jetbrains.kotlin.cfg.pseudocodeTraverser.TraversalOrder
|
||||
import org.jetbrains.kotlin.cfg.pseudocodeTraverser.traverse
|
||||
import org.jetbrains.kotlin.cfg.pseudocodeTraverser.traverseFollowingInstructions
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.descriptors.annotations.Annotations
|
||||
import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor
|
||||
import org.jetbrains.kotlin.diagnostics.Errors
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully
|
||||
@@ -66,6 +67,7 @@ import org.jetbrains.kotlin.psi.psiUtil.isInsideOf
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parents
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsStatement
|
||||
import org.jetbrains.kotlin.resolve.calls.CallTransformer
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
|
||||
@@ -83,8 +85,8 @@ import org.jetbrains.kotlin.utils.DFS.VisitedWithSet
|
||||
import java.util.*
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
private val DEFAULT_RETURN_TYPE = KotlinBuiltIns.getInstance().getUnitType()
|
||||
private val DEFAULT_PARAMETER_TYPE = KotlinBuiltIns.getInstance().getNullableAnyType()
|
||||
private val DEFAULT_RETURN_TYPE = KotlinBuiltIns.getInstance().getUnitType()!!
|
||||
private val DEFAULT_PARAMETER_TYPE = KotlinBuiltIns.getInstance().getNullableAnyType()!!
|
||||
|
||||
private fun DeclarationDescriptor.renderForMessage(): String =
|
||||
IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.render(this)
|
||||
@@ -640,9 +642,16 @@ private fun ExtractionData.inferParametersInfo(
|
||||
(originalDeclaration is JetProperty && originalDeclaration.isLocal()) ||
|
||||
originalDeclaration is JetParameter
|
||||
|
||||
val extractFunctionRef =
|
||||
options.captureLocalFunctions
|
||||
&& originalRef.getReferencedName() == originalDescriptor.getName().asString() // to forbid calls by convention
|
||||
&& originalDescriptor is FunctionDescriptor
|
||||
&& DescriptorUtils.isLocal(originalDescriptor)
|
||||
&& (targetScope == null || originalDescriptor !in targetScope.getFunctions(originalDescriptor.getName()))
|
||||
|
||||
val descriptorToExtract = (if (extractThis) thisDescriptor else null) ?: originalDescriptor
|
||||
|
||||
val extractParameter = extractThis || extractLocalVar
|
||||
val extractParameter = extractThis || extractLocalVar || extractFunctionRef
|
||||
if (extractParameter) {
|
||||
val parameterExpression = when {
|
||||
receiverToExtract is ExpressionReceiver -> receiverToExtract.getExpression()
|
||||
@@ -651,6 +660,13 @@ private fun ExtractionData.inferParametersInfo(
|
||||
}
|
||||
|
||||
val parameterType = when {
|
||||
extractFunctionRef -> {
|
||||
originalDescriptor as FunctionDescriptor
|
||||
KotlinBuiltIns.getInstance().getFunctionType(Annotations.EMPTY,
|
||||
originalDescriptor.getExtensionReceiverParameter()?.getType(),
|
||||
originalDescriptor.getValueParameters().map { it.getType() },
|
||||
originalDescriptor.getReturnType() ?: DEFAULT_RETURN_TYPE)
|
||||
}
|
||||
parameterExpression != null -> bindingContext[BindingContext.SMARTCAST, parameterExpression]
|
||||
?: bindingContext[BindingContext.EXPRESSION_TYPE, parameterExpression]
|
||||
?: if (receiverToExtract.exists()) receiverToExtract.getType() else null
|
||||
@@ -666,13 +682,17 @@ private fun ExtractionData.inferParametersInfo(
|
||||
} ?: DEFAULT_PARAMETER_TYPE
|
||||
|
||||
val parameter = extractedDescriptorToParameter.getOrPut(descriptorToExtract) {
|
||||
val argumentText =
|
||||
var argumentText =
|
||||
if (hasThisReceiver && extractThis) {
|
||||
val label = if (descriptorToExtract is ClassDescriptor) "@${descriptorToExtract.getName().asString()}" else ""
|
||||
"this$label"
|
||||
}
|
||||
else
|
||||
(thisExpr ?: ref).getText() ?: throw AssertionError("'this' reference shouldn't be empty: code fragment = $codeFragmentText")
|
||||
if (extractFunctionRef) {
|
||||
val receiverTypeText = (originalDeclaration as JetCallableDeclaration).getReceiverTypeReference()?.getText() ?: ""
|
||||
argumentText = "$receiverTypeText::$argumentText"
|
||||
}
|
||||
|
||||
MutableParameter(argumentText, descriptorToExtract, extractThis, targetScope)
|
||||
}
|
||||
@@ -694,6 +714,9 @@ private fun ExtractionData.inferParametersInfo(
|
||||
parameter.addTypePredicate(getExpectedTypePredicate(receiverValue, bindingContext))
|
||||
}
|
||||
}
|
||||
else if (extractFunctionRef) {
|
||||
parameter.addTypePredicate(SingleType(parameterType))
|
||||
}
|
||||
else {
|
||||
pseudocode.getElementValuesRecursively(originalRef).forEach {
|
||||
parameter.addTypePredicate(getExpectedTypePredicate(it, bindingContext))
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// WITH_RUNTIME
|
||||
// OPTIONS: true, true, false, false
|
||||
// OPTIONS: true, true, false, false, false
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// WITH_RUNTIME
|
||||
// OPTIONS: true, true, false, false
|
||||
// OPTIONS: true, true, false, false, false
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun baz(m: kotlin.Int): Int defined in foo.bar
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: (kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
|
||||
// SIBLING:
|
||||
fun foo(n: Int): Int {
|
||||
fun bar(): Int {
|
||||
fun baz(m: Int) = m * n
|
||||
|
||||
return <selection>baz(n + 1)</selection>
|
||||
}
|
||||
|
||||
return bar()
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun baz(m: kotlin.Int): Int defined in foo.bar
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: (kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
|
||||
// SIBLING:
|
||||
fun foo(n: Int): Int {
|
||||
fun bar(): Int {
|
||||
fun baz(m: Int) = m * n
|
||||
|
||||
return i(::baz, n)
|
||||
}
|
||||
|
||||
return bar()
|
||||
}
|
||||
|
||||
private fun i(baz: (Int) -> Int, n: Int) = baz(n + 1)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun Int.bar(m: Int) = this * m * n
|
||||
|
||||
return <selection>n.bar(n + 1)</selection>
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun Int.bar(m: Int) = this * m * n
|
||||
|
||||
return i(Int::bar, n)
|
||||
}
|
||||
|
||||
private fun i(bar: Int.(Int) -> Int, n: Int) = n.bar(n + 1)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar1(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar2(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun Int.bar1(m: Int) = this + m + n
|
||||
fun Int.bar2(m: Int) = this * m * n
|
||||
|
||||
return <selection>n.bar1(n + 1).bar2(n + 2)</selection>
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar1(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar2(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun Int.bar1(m: Int) = this + m + n
|
||||
fun Int.bar2(m: Int) = this * m * n
|
||||
|
||||
return i(Int::bar1, Int::bar2, n)
|
||||
}
|
||||
|
||||
private fun i(bar1: Int.(Int) -> Int, bar2: Int.(Int) -> Int, n: Int) = n.bar1(n + 1).bar2(n + 2)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun Int.bar(m: Int) = this * m * n
|
||||
|
||||
return <selection>n bar (n + 1)</selection>
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun kotlin.Int.bar(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: kotlin.Int.(kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun Int.bar(m: Int) = this * m * n
|
||||
|
||||
return i(Int::bar, n)
|
||||
}
|
||||
|
||||
private fun i(bar: Int.(Int) -> Int, n: Int) = n bar (n + 1)
|
||||
@@ -0,0 +1,10 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun bar(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: (kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun bar(m: Int) = m * n
|
||||
|
||||
return <selection>bar(n + 1)</selection>
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// OPTIONS: true, false, false, false, true
|
||||
// PARAM_DESCRIPTOR: local final fun bar(m: kotlin.Int): Int defined in foo
|
||||
// PARAM_DESCRIPTOR: value-parameter val n: kotlin.Int defined in foo
|
||||
// PARAM_TYPES: (kotlin.Int) -> Int
|
||||
// PARAM_TYPES: kotlin.Int
|
||||
fun foo(n: Int): Int {
|
||||
fun bar(m: Int) = m * n
|
||||
|
||||
return i(::bar, n)
|
||||
}
|
||||
|
||||
private fun i(bar: (Int) -> Int, n: Int) = bar(n + 1)
|
||||
+39
@@ -1584,6 +1584,45 @@ public class JetExtractionTestGenerated extends AbstractJetExtractionTest {
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/refactoring/extractFunction/parameters/capturedFunctions")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class CapturedFunctions extends AbstractJetExtractionTest {
|
||||
public void testAllFilesPresentInCapturedFunctions() throws Exception {
|
||||
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/extractFunction/parameters/capturedFunctions"), Pattern.compile("^(.+)\\.kt$"), true);
|
||||
}
|
||||
|
||||
@TestMetadata("deeplyLocalFun.kt")
|
||||
public void testDeeplyLocalFun() throws Exception {
|
||||
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/extractFunction/parameters/capturedFunctions/deeplyLocalFun.kt");
|
||||
doExtractFunctionTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("localExtensionFun.kt")
|
||||
public void testLocalExtensionFun() throws Exception {
|
||||
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/extractFunction/parameters/capturedFunctions/localExtensionFun.kt");
|
||||
doExtractFunctionTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("localExtensionFunChainedCalls.kt")
|
||||
public void testLocalExtensionFunChainedCalls() throws Exception {
|
||||
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/extractFunction/parameters/capturedFunctions/localExtensionFunChainedCalls.kt");
|
||||
doExtractFunctionTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("localExtensionFunInfixCall.kt")
|
||||
public void testLocalExtensionFunInfixCall() throws Exception {
|
||||
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/extractFunction/parameters/capturedFunctions/localExtensionFunInfixCall.kt");
|
||||
doExtractFunctionTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("localFun.kt")
|
||||
public void testLocalFun() throws Exception {
|
||||
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/extractFunction/parameters/capturedFunctions/localFun.kt");
|
||||
doExtractFunctionTest(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/refactoring/extractFunction/parameters/extractSuper")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
Reference in New Issue
Block a user