Convert Object Literal to Lambda Intention: Move lambda out of parentheses when possible. Fix after-template

#KT-10778 Fixed
This commit is contained in:
Alexey Sedunov
2016-01-27 16:06:38 +03:00
parent 1c65797950
commit 26c47cda47
11 changed files with 84 additions and 29 deletions
@@ -1 +1 @@
SwingUtilities { println("a") }
SwingUtilities.invokeLater { println("a") }
@@ -106,10 +106,10 @@ class RedundantSamConstructorInspection : AbstractKotlinInspection() {
}
companion object {
fun replaceSamConstructorCall(callExpression: KtCallExpression): KtExpression {
fun replaceSamConstructorCall(callExpression: KtCallExpression): KtLambdaExpression {
val functionalArgument = callExpression.samConstructorValueArgument()?.getArgumentExpression()
?: throw AssertionError("SAM-constructor should have a FunctionLiteralExpression as single argument: ${callExpression.getElementTextWithContext()}")
return callExpression.getQualifiedExpressionForSelectorOrThis().replace(functionalArgument) as KtExpression
return callExpression.getQualifiedExpressionForSelectorOrThis().replace(functionalArgument) as KtLambdaExpression
}
private fun canBeReplaced(parentCall: KtCallExpression, samConstructorArguments: List<KtValueArgument>): Boolean {
@@ -21,39 +21,50 @@ import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.core.moveFunctionLiteralOutsideParentheses
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containsInside
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.unpackFunctionLiteral
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class MoveLambdaOutsideParenthesesIntention : SelfTargetingIntention<KtCallExpression>(KtCallExpression::class.java, "Move lambda argument out of parentheses") {
override fun isApplicableTo(element: KtCallExpression, caretOffset: Int): Boolean {
if (element.lambdaArguments.isNotEmpty()) return false
val argument = element.valueArguments.lastOrNull() ?: return false
val expression = argument.getArgumentExpression() ?: return false
val functionLiteral = expression.unpackFunctionLiteral() ?: return false
val callee = element.calleeExpression
if (callee is KtNameReferenceExpression) {
val bindingContext = element.analyze(BodyResolveMode.PARTIAL)
val targets = bindingContext[BindingContext.REFERENCE_TARGET, callee]?.let { listOf(it) }
?: bindingContext[BindingContext.AMBIGUOUS_REFERENCE_TARGET, callee]
?: listOf()
val candidates = targets.filterIsInstance<FunctionDescriptor>()
// if there are functions among candidates but none of them have last function parameter then not show the intention
if (candidates.isNotEmpty() && candidates.none {
val lastParameter = it.valueParameters.lastOrNull()
lastParameter != null && KotlinBuiltIns.isExactFunctionOrExtensionFunctionType(lastParameter.type)
}) {
return false
}
companion object {
private fun getLambdaExpression(callExpression: KtCallExpression): KtLambdaExpression? {
if (callExpression.lambdaArguments.isNotEmpty()) return null
return callExpression.valueArguments.lastOrNull()?.getArgumentExpression()?.unpackFunctionLiteral()
}
if (caretOffset < argument.asElement().startOffset) return false
val bodyRange = functionLiteral.bodyExpression?.textRange ?: return true
fun canMove(element: KtCallExpression): Boolean {
if (getLambdaExpression(element) == null) return false
val callee = element.calleeExpression
if (callee is KtNameReferenceExpression) {
val bindingContext = element.analyze(BodyResolveMode.PARTIAL)
val targets = bindingContext[BindingContext.REFERENCE_TARGET, callee]?.let { listOf(it) }
?: bindingContext[BindingContext.AMBIGUOUS_REFERENCE_TARGET, callee]
?: listOf()
val candidates = targets.filterIsInstance<FunctionDescriptor>()
// if there are functions among candidates but none of them have last function parameter then not show the intention
if (candidates.isNotEmpty() && candidates.none {
val lastParameter = it.valueParameters.lastOrNull()
lastParameter != null && KotlinBuiltIns.isExactFunctionOrExtensionFunctionType(lastParameter.type)
}) {
return false
}
}
return true
}
}
override fun isApplicableTo(element: KtCallExpression, caretOffset: Int): Boolean {
if (!canMove(element)) return false
val lambdaExpression = getLambdaExpression(element) ?: return false
val argument = lambdaExpression.getStrictParentOfType<KtValueArgument>() ?: return false
if (caretOffset < argument.startOffset) return false
val bodyRange = lambdaExpression.bodyExpression?.textRange ?: return true
return !bodyRange.containsInside(caretOffset)
}
@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.core.moveFunctionLiteralOutsideParentheses
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
import org.jetbrains.kotlin.idea.inspections.RedundantSamConstructorInspection
@@ -135,6 +136,9 @@ class ObjectLiteralToLambdaIntention : SelfTargetingRangeIntention<KtObjectLiter
?.parent as? KtCallExpression
if (parentCall != null && RedundantSamConstructorInspection.samConstructorCallsToBeConverted(parentCall).singleOrNull() == callExpression) {
RedundantSamConstructorInspection.replaceSamConstructorCall(callExpression)
if (MoveLambdaOutsideParenthesesIntention.canMove(parentCall)) {
parentCall.moveFunctionLiteralOutsideParentheses()
}
}
else {
ShortenReferences.DEFAULT.process(replaced.getContainingKtFile(), replaced.startOffset, callee.endOffset)
@@ -3,5 +3,5 @@
import javax.swing.SwingUtilities
fun bar() {
SwingUtilities.invokeLater({ throw UnsupportedOperationException() })
SwingUtilities.invokeLater { throw UnsupportedOperationException() }
}
@@ -0,0 +1,7 @@
package source;
public class J {
static void foo(Runnable r, int n) {
}
}
@@ -0,0 +1,5 @@
package source
fun bar() {
J.foo({ println("a") }, 1)
}
@@ -0,0 +1,7 @@
package source;
public class J {
static void foo(Runnable r, int n) {
}
}
@@ -0,0 +1,9 @@
package source
fun bar() {
J.foo(<caret>object : Runnable {
override fun run() {
println("a")
}
}, 1)
}
@@ -0,0 +1,6 @@
{
"mainFile": "source/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.ObjectLiteralToLambdaIntention",
"intentionText": "Convert to lambda",
"withRuntime": "true"
}
@@ -95,6 +95,12 @@ public class MultiFileIntentionTestGenerated extends AbstractMultiFileIntentionT
doTest(fileName);
}
@TestMetadata("objectLiteralToLambda/objectLiteralToLambda.test")
public void testObjectLiteralToLambda_ObjectLiteralToLambda() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/objectLiteralToLambda/objectLiteralToLambda.test");
doTest(fileName);
}
@TestMetadata("reconcilePackageWithDirectory/changeToDefaultPackage/changeToDefaultPackage.test")
public void testReconcilePackageWithDirectory_changeToDefaultPackage_ChangeToDefaultPackage() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/reconcilePackageWithDirectory/changeToDefaultPackage/changeToDefaultPackage.test");