Extract Function: Allow capturing local function references as lambda parameters

This commit is contained in:
Alexey Sedunov
2015-04-13 14:37:18 +03:00
parent 86bb0d5bc5
commit daebf569ab
17 changed files with 212 additions and 9 deletions
@@ -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()!!
}
@@ -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)
}
}
@@ -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()
@@ -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,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,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
@@ -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()
}
@@ -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)
@@ -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>
}
@@ -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,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>
}
@@ -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)
@@ -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>
}
@@ -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>
}
@@ -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)
@@ -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)