Intention to convert anonymous function to lambda

This commit is contained in:
Natalia Ukhorskaya
2015-12-15 10:22:53 +03:00
parent c79ffbac5c
commit 693e158759
27 changed files with 451 additions and 57 deletions
@@ -0,0 +1,7 @@
fun foo(f: () -> Int) {
f()
}
fun main(args: String) {
foo { 1 }
}
@@ -0,0 +1,9 @@
fun foo(f: () -> Int) {
f()
}
fun main(args: String) {
foo(fun(): Int {
return 1
})
}
@@ -0,0 +1,5 @@
<html>
<body>
Converts an anonymous function in call to lambda expression.
</body>
</html>
+5
View File
@@ -1105,6 +1105,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.AnonymousFunctionToLambdaIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.ObjectLiteralToLambdaInspection"
displayName="Convert object literal to lambda"
groupName="Kotlin"
@@ -0,0 +1,102 @@
/*
* Copyright 2010-2015 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.util.TextRange
import com.intellij.psi.search.LocalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.inspections.RedundantSamConstructorInspection
import org.jetbrains.kotlin.idea.util.CommentSaver
import org.jetbrains.kotlin.idea.util.ShortenReferences
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
class AnonymousFunctionToLambdaIntention : SelfTargetingRangeIntention<KtNamedFunction>(
KtNamedFunction::class.java,
"Convert to lambda expression",
"Convert anonymous function to lambda expression"
) {
override fun applicabilityRange(element: KtNamedFunction): TextRange? {
if (element.name != null) return null
if (!element.hasBody()) return null
val callElement = element.getParentOfTypeAndBranch<KtCallElement> { valueArgumentList } ?: return null
if (callElement.getCalleeExpressionIfAny() !is KtNameReferenceExpression) {
return null
}
return element.funKeyword!!.textRange
}
override fun applyTo(element: KtNamedFunction, editor: Editor) {
applyTo(element)
}
fun applyTo(element: KtNamedFunction) {
val commentSaver = CommentSaver(element)
val returnSaver = ReturnSaver(element)
val body = element.bodyExpression!!
val newExpression = KtPsiFactory(element).buildExpression {
appendFixedText("{")
val parameters = element.valueParameters
val needParameters = parameters.count() > 1 || parameters.any { parameter -> ReferencesSearch.search(parameter, LocalSearchScope(body)).any() }
if (needParameters) {
parameters.forEachIndexed { index, parameter ->
if (index > 0) {
appendFixedText(",")
}
appendName(parameter.nameAsSafeName)
}
appendFixedText("->")
}
if (element.hasBlockBody()) {
appendChildRange((body as KtBlockExpression).contentRange())
}
else {
appendExpression(body)
}
appendFixedText("}")
}
val replaced = element.replaced(newExpression) as KtLambdaExpression
commentSaver.restore(replaced, forceAdjustIndent = true/* by some reason lambda body is sometimes not properly indented */)
val callExpression = replaced.parents.firstIsInstance<KtCallExpression>()
val callee = callExpression.getCalleeExpressionIfAny()!! as KtNameReferenceExpression
val returnLabel = callee.getReferencedNameAsName()
returnSaver.restore(replaced, returnLabel)
val moveLambdaOutsideParenthesesIntention = MoveLambdaOutsideParenthesesIntention()
if (moveLambdaOutsideParenthesesIntention.isApplicableTo(callExpression, replaced.textOffset)) {
moveLambdaOutsideParenthesesIntention.applyTo(callExpression)
}
}
}
@@ -17,7 +17,6 @@
package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.psi.search.LocalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
@@ -34,9 +33,10 @@ import org.jetbrains.kotlin.idea.util.ShortenReferences
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.load.java.sam.SingleAbstractMethodUtils
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.psi.psiUtil.contentRange
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.bindingContextUtil.getTargetFunction
import org.jetbrains.kotlin.resolve.calls.callUtil.getCalleeExpressionIfAny
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.KotlinType
@@ -72,14 +72,9 @@ class ObjectLiteralToLambdaIntention : SelfTargetingRangeIntention<KtObjectLiter
val (@Suppress("UNUSED_VARIABLE") baseTypeRef, baseType, singleFunction) = extractData(element)!!
val RETURN_KEY = Key<Unit>("RETURN_KEY")
val returnSaver = ReturnSaver(singleFunction)
val body = singleFunction.bodyExpression!!
body.forEachDescendantOfType<KtReturnExpression> {
if (it.getTargetFunction(it.analyze(BodyResolveMode.PARTIAL)) == singleFunction) {
it.putCopyableUserData(RETURN_KEY, Unit)
}
}
val factory = KtPsiFactory(element)
val newExpression = factory.buildExpression {
@@ -111,8 +106,6 @@ class ObjectLiteralToLambdaIntention : SelfTargetingRangeIntention<KtObjectLiter
appendFixedText("}")
}
body.forEachDescendantOfType<KtReturnExpression> { it.putCopyableUserData(RETURN_KEY, null) }
val replaced = element.replaced(newExpression)
commentSaver.restore(replaced, forceAdjustIndent = true/* by some reason lambda body is sometimes not properly indented */)
@@ -120,26 +113,8 @@ class ObjectLiteralToLambdaIntention : SelfTargetingRangeIntention<KtObjectLiter
val callExpression = callee.parent as KtCallExpression
val functionLiteral = callExpression.lambdaArguments.single().getLambdaExpression()
val lambdaBody = functionLiteral.bodyExpression!!
val returnToReplace = functionLiteral.collectDescendantsOfType<KtReturnExpression>() { it.getCopyableUserData(RETURN_KEY) != null }
val returnLabel = callee.getReferencedNameAsName()
for (returnExpression in returnToReplace) {
val value = returnExpression.returnedExpression
val replaceWith = if (value != null && returnExpression.isValueOfBlock(lambdaBody)) {
value
}
else if (value != null) {
factory.createExpressionByPattern("return@$0 $1", returnLabel, value)
}
else {
factory.createExpressionByPattern("return@$0", returnLabel)
}
returnExpression.replace(replaceWith)
}
returnSaver.restore(functionLiteral, returnLabel)
val parentCall = ((replaced.parent as? KtValueArgument)
?.parent as? KtValueArgumentList)
@@ -152,32 +127,6 @@ class ObjectLiteralToLambdaIntention : SelfTargetingRangeIntention<KtObjectLiter
}
}
private fun KtExpression.isValueOfBlock(inBlock: KtBlockExpression): Boolean {
val parent = parent
when (parent) {
inBlock -> {
return this == inBlock.statements.last()
}
is KtBlockExpression -> {
return isValueOfBlock(parent) && parent.isValueOfBlock(inBlock)
}
is KtContainerNode -> {
val owner = parent.parent
if (owner is KtIfExpression) {
return (this == owner.then || this == owner.`else`) && owner.isValueOfBlock(inBlock)
}
}
is KtWhenEntry -> {
return this == parent.expression && (parent.parent as KtWhenExpression).isValueOfBlock(inBlock)
}
}
return false
}
private data class Data(
val baseTypeRef: KtTypeReference,
val baseType: KotlinType,
@@ -197,4 +146,4 @@ class ObjectLiteralToLambdaIntention : SelfTargetingRangeIntention<KtObjectLiter
return Data(typeRef, baseType, singleFunction)
}
}
}
@@ -0,0 +1,101 @@
/*
* Copyright 2010-2015 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.util.Key
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
import org.jetbrains.kotlin.resolve.bindingContextUtil.getTargetFunction
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class ReturnSaver(val function: KtNamedFunction) {
val RETURN_KEY = Key<Unit>("RETURN_KEY")
init {
save()
}
private fun save() {
val body = function.bodyExpression!!
body.forEachDescendantOfType<KtReturnExpression> {
if (it.getTargetFunction(it.analyze(BodyResolveMode.PARTIAL)) == function) {
it.putCopyableUserData(RETURN_KEY, Unit)
}
}
}
private fun clear() {
val body = function.bodyExpression!!
body.forEachDescendantOfType<KtReturnExpression> { it.putCopyableUserData(RETURN_KEY, null) }
}
fun restore(lambda: KtLambdaExpression, label: Name) {
clear()
val factory = KtPsiFactory(lambda)
val lambdaBody = lambda.bodyExpression!!
val returnToReplace = lambda.collectDescendantsOfType<KtReturnExpression>() { it.getCopyableUserData(RETURN_KEY) != null }
for (returnExpression in returnToReplace) {
val value = returnExpression.returnedExpression
val replaceWith = if (value != null && returnExpression.isValueOfBlock(lambdaBody)) {
value
}
else if (value != null) {
factory.createExpressionByPattern("return@$0 $1", label, value)
}
else {
factory.createExpressionByPattern("return@$0", label)
}
returnExpression.replace(replaceWith)
}
}
private fun KtExpression.isValueOfBlock(inBlock: KtBlockExpression): Boolean {
val parent = parent
when (parent) {
inBlock -> {
return this == inBlock.statements.last()
}
is KtBlockExpression -> {
return isValueOfBlock(parent) && parent.isValueOfBlock(inBlock)
}
is KtContainerNode -> {
val owner = parent.parent
if (owner is KtIfExpression) {
return (this == owner.then || this == owner.`else`) && owner.isValueOfBlock(inBlock)
}
}
is KtWhenEntry -> {
return this == parent.expression && (parent.parent as KtWhenExpression).isValueOfBlock(inBlock)
}
}
return false
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.AnonymousFunctionToLambdaIntention
@@ -0,0 +1,10 @@
fun foo(f: () -> Unit) {
f()
}
fun main(args: String) {
foo(<caret>fun() {
val p1 = 1
val p2 = 1
})
}
@@ -0,0 +1,10 @@
fun foo(f: () -> Unit) {
f()
}
fun main(args: String) {
foo {
val p1 = 1
val p2 = 1
}
}
@@ -0,0 +1,7 @@
class Foo(f: () -> Unit)
fun main(args: String) {
Foo(fun<caret>() {
val p = 1
})
}
@@ -0,0 +1,5 @@
class Foo(f: () -> Unit)
fun main(args: String) {
Foo { val p = 1 }
}
@@ -0,0 +1,7 @@
fun foo3(f: () -> Int) {
f()
}
fun main(args: String) {
foo3(<caret>fun () = 1)
}
@@ -0,0 +1,7 @@
fun foo3(f: () -> Int) {
f()
}
fun main(args: String) {
foo3 { 1 }
}
@@ -0,0 +1,9 @@
fun foo2(f: (Int) -> Unit) {
f(1)
}
fun main(args: String) {
foo2(<caret>fun(i: Int) {
val p = i
})
}
@@ -0,0 +1,7 @@
fun foo2(f: (Int) -> Unit) {
f(1)
}
fun main(args: String) {
foo2 { i -> val p = i }
}
@@ -0,0 +1,9 @@
fun foo(f: () -> Unit, i: Int) {
f()
}
fun main(args: String) {
foo(<caret>fun() {
val p = 1
}, 1)
}
@@ -0,0 +1,7 @@
fun foo(f: () -> Unit, i: Int) {
f()
}
fun main(args: String) {
foo({ val p = 1 }, 1)
}
@@ -0,0 +1,9 @@
fun foo2(f: (Int) -> Unit) {
f(1)
}
fun main(args: String) {
foo2(<caret>fun(i) {
val p = i
})
}
@@ -0,0 +1,7 @@
fun foo2(f: (Int) -> Unit) {
f(1)
}
fun main(args: String) {
foo2 { i -> val p = i }
}
@@ -0,0 +1,9 @@
fun foo(f: () -> Int) {
f()
}
fun main(args: String) {
foo(<caret>fun(): Int {
return 1
})
}
@@ -0,0 +1,7 @@
fun foo(f: () -> Int) {
f()
}
fun main(args: String) {
foo { 1 }
}
@@ -0,0 +1,13 @@
fun foo(f: () -> Int) {
f()
}
fun main(args: String) {
foo(<caret>fun(): Int {
val a = 1
if (a > 1) {
return 1
}
return 2
})
}
@@ -0,0 +1,13 @@
fun foo(f: () -> Int) {
f()
}
fun main(args: String) {
foo {
val a = 1
if (a > 1) {
return@foo 1
}
2
}
}
@@ -0,0 +1,9 @@
fun foo(f: () -> Unit) {
f()
}
fun main(args: String) {
foo(fun<caret>() {
val p = 1
})
}
@@ -0,0 +1,7 @@
fun foo(f: () -> Unit) {
f()
}
fun main(args: String) {
foo { val p = 1 }
}
@@ -362,6 +362,69 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/anonymousFunctionToLambda")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AnonymousFunctionToLambda extends AbstractIntentionTest {
public void testAllFilesPresentInAnonymousFunctionToLambda() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/anonymousFunctionToLambda"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("callMultiline.kt")
public void testCallMultiline() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/callMultiline.kt");
doTest(fileName);
}
@TestMetadata("constructor.kt")
public void testConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/constructor.kt");
doTest(fileName);
}
@TestMetadata("expressionBody.kt")
public void testExpressionBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/expressionBody.kt");
doTest(fileName);
}
@TestMetadata("fullParam.kt")
public void testFullParam() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/fullParam.kt");
doTest(fileName);
}
@TestMetadata("moveOut.kt")
public void testMoveOut() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/moveOut.kt");
doTest(fileName);
}
@TestMetadata("paramName.kt")
public void testParamName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/paramName.kt");
doTest(fileName);
}
@TestMetadata("replaceReturnWithExpression.kt")
public void testReplaceReturnWithExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/replaceReturnWithExpression.kt");
doTest(fileName);
}
@TestMetadata("replaceReturnWithLabel.kt")
public void testReplaceReturnWithLabel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/replaceReturnWithLabel.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/anonymousFunctionToLambda/simple.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/branched")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)