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 { companion object {
fun replaceSamConstructorCall(callExpression: KtCallExpression): KtExpression { fun replaceSamConstructorCall(callExpression: KtCallExpression): KtLambdaExpression {
val functionalArgument = callExpression.samConstructorValueArgument()?.getArgumentExpression() val functionalArgument = callExpression.samConstructorValueArgument()?.getArgumentExpression()
?: throw AssertionError("SAM-constructor should have a FunctionLiteralExpression as single argument: ${callExpression.getElementTextWithContext()}") ?: 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 { 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.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.core.moveFunctionLiteralOutsideParentheses import org.jetbrains.kotlin.idea.core.moveFunctionLiteralOutsideParentheses
import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.containsInside 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.psiUtil.startOffset
import org.jetbrains.kotlin.psi.unpackFunctionLiteral
import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class MoveLambdaOutsideParenthesesIntention : SelfTargetingIntention<KtCallExpression>(KtCallExpression::class.java, "Move lambda argument out of parentheses") { class MoveLambdaOutsideParenthesesIntention : SelfTargetingIntention<KtCallExpression>(KtCallExpression::class.java, "Move lambda argument out of parentheses") {
override fun isApplicableTo(element: KtCallExpression, caretOffset: Int): Boolean { companion object {
if (element.lambdaArguments.isNotEmpty()) return false private fun getLambdaExpression(callExpression: KtCallExpression): KtLambdaExpression? {
val argument = element.valueArguments.lastOrNull() ?: return false if (callExpression.lambdaArguments.isNotEmpty()) return null
val expression = argument.getArgumentExpression() ?: return false return callExpression.valueArguments.lastOrNull()?.getArgumentExpression()?.unpackFunctionLiteral()
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
}
} }
if (caretOffset < argument.asElement().startOffset) return false fun canMove(element: KtCallExpression): Boolean {
val bodyRange = functionLiteral.bodyExpression?.textRange ?: return true 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) return !bodyRange.containsInside(caretOffset)
} }
@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor 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.core.replaced
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
import org.jetbrains.kotlin.idea.inspections.RedundantSamConstructorInspection import org.jetbrains.kotlin.idea.inspections.RedundantSamConstructorInspection
@@ -135,6 +136,9 @@ class ObjectLiteralToLambdaIntention : SelfTargetingRangeIntention<KtObjectLiter
?.parent as? KtCallExpression ?.parent as? KtCallExpression
if (parentCall != null && RedundantSamConstructorInspection.samConstructorCallsToBeConverted(parentCall).singleOrNull() == callExpression) { if (parentCall != null && RedundantSamConstructorInspection.samConstructorCallsToBeConverted(parentCall).singleOrNull() == callExpression) {
RedundantSamConstructorInspection.replaceSamConstructorCall(callExpression) RedundantSamConstructorInspection.replaceSamConstructorCall(callExpression)
if (MoveLambdaOutsideParenthesesIntention.canMove(parentCall)) {
parentCall.moveFunctionLiteralOutsideParentheses()
}
} }
else { else {
ShortenReferences.DEFAULT.process(replaced.getContainingKtFile(), replaced.startOffset, callee.endOffset) ShortenReferences.DEFAULT.process(replaced.getContainingKtFile(), replaced.startOffset, callee.endOffset)
@@ -3,5 +3,5 @@
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
fun bar() { 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); 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") @TestMetadata("reconcilePackageWithDirectory/changeToDefaultPackage/changeToDefaultPackage.test")
public void testReconcilePackageWithDirectory_changeToDefaultPackage_ChangeToDefaultPackage() throws Exception { public void testReconcilePackageWithDirectory_changeToDefaultPackage_ChangeToDefaultPackage() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/reconcilePackageWithDirectory/changeToDefaultPackage/changeToDefaultPackage.test"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/reconcilePackageWithDirectory/changeToDefaultPackage/changeToDefaultPackage.test");