diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/MoveSuspiciousCallableReferenceIntoParenthesesInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/MoveSuspiciousCallableReferenceIntoParenthesesInspection.kt index 5e828f65ac1..5e7fdc3baaa 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/MoveSuspiciousCallableReferenceIntoParenthesesInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/MoveSuspiciousCallableReferenceIntoParenthesesInspection.kt @@ -13,20 +13,23 @@ import com.intellij.psi.PsiElementVisitor import org.jetbrains.kotlin.builtins.getReturnTypeFromFunctionType import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType import org.jetbrains.kotlin.builtins.isBuiltinFunctionalTypeOrSubtype +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.descriptors.VariableDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny import org.jetbrains.kotlin.idea.intentions.ConvertLambdaToReferenceIntention import org.jetbrains.kotlin.idea.intentions.getCallableDescriptor -import org.jetbrains.kotlin.psi.KtCallableReferenceExpression -import org.jetbrains.kotlin.psi.KtLambdaExpression -import org.jetbrains.kotlin.psi.ValueArgument -import org.jetbrains.kotlin.psi.lambdaExpressionVisitor +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.calls.callUtil.getParameterForArgument import org.jetbrains.kotlin.resolve.calls.callUtil.getParentCall import org.jetbrains.kotlin.resolve.calls.callUtil.getParentResolvedCall import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf class MoveSuspiciousCallableReferenceIntoParenthesesInspection : AbstractKotlinInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { @@ -47,17 +50,41 @@ class MoveSuspiciousCallableReferenceIntoParenthesesInspection : AbstractKotlinI } } } + val quickFix = if (canMove(lambdaExpression, callableReference, context)) + IntentionWrapper(MoveIntoParenthesesIntention(), lambdaExpression.containingFile) + else + null holder.registerProblem( lambdaExpression, "Suspicious callable reference as the only lambda element", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, - IntentionWrapper(MoveIntoParenthesesIntention(), lambdaExpression.containingFile) + quickFix ) - } }) } + private fun canMove( + lambdaExpression: KtLambdaExpression, + callableReference: KtCallableReferenceExpression, + context: BindingContext + ): Boolean { + val lambdaDescriptor = context[BindingContext.FUNCTION, lambdaExpression.functionLiteral] ?: return false + val lambdaParameter = lambdaDescriptor.extensionReceiverParameter ?: lambdaDescriptor.valueParameters.singleOrNull() + + val functionReceiver = callableReference.receiverExpression?.mainReference?.resolveToDescriptors(context)?.firstOrNull() + if (functionReceiver == lambdaParameter) return true + val lambdaParameterType = lambdaParameter?.type + if (functionReceiver is VariableDescriptor && functionReceiver.type == lambdaParameterType) return true + if (functionReceiver is ClassDescriptor && functionReceiver == lambdaParameterType?.constructor?.declarationDescriptor) return true + + if (lambdaParameterType == null) return false + val functionDescriptor = + callableReference.callableReference.mainReference.resolveToDescriptors(context).firstOrNull() as? FunctionDescriptor + val functionParameterType = functionDescriptor?.valueParameters?.firstOrNull()?.type ?: return false + return functionParameterType == lambdaParameterType + } + class MoveIntoParenthesesIntention : ConvertLambdaToReferenceIntention( "Move suspicious callable reference into parentheses '()'" ) { diff --git a/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference.kt b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference.kt new file mode 100644 index 00000000000..cbd79c9e091 --- /dev/null +++ b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +// FIX: none +fun test(x: Int) { + fun p(s: String): Boolean = true + x.let { ::p } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference2.kt b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference2.kt new file mode 100644 index 00000000000..facc7092780 --- /dev/null +++ b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference2.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +// FIX: none +fun test(x: Int) { + val p = { _: String -> true } + x.run { p::invoke } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference3.kt b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference3.kt new file mode 100644 index 00000000000..15f3b549a15 --- /dev/null +++ b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference3.kt @@ -0,0 +1,5 @@ +// WITH_RUNTIME +// FIX: none +fun test(x: Int, y: String) { + x.run { y::length } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference4.kt b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference4.kt new file mode 100644 index 00000000000..8f56178b340 --- /dev/null +++ b/idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference4.kt @@ -0,0 +1,5 @@ +// WITH_RUNTIME +// FIX: none +fun test(x: Int) { + x.run { String::length } +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/AbstractLocalInspectionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/inspections/AbstractLocalInspectionTest.kt index f9b5867c4c5..bfd6df9d95e 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/AbstractLocalInspectionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/AbstractLocalInspectionTest.kt @@ -190,7 +190,12 @@ abstract class AbstractLocalInspectionTest : KotlinLightCodeInsightFixtureTestCa val allLocalFixActions = highlightInfos.flatMap { it.quickFixActionMarkers ?: emptyList() }.map { it.first.action } - val localFixActions = allLocalFixActions.filter { fix -> localFixTextString == null || fix.text == localFixTextString } + val localFixActions = if (localFixTextString == null || localFixTextString == "none") { + allLocalFixActions + } else { + allLocalFixActions.filter { fix -> fix.text == localFixTextString } + } + val availableDescription = allLocalFixActions.joinToString { it.text } val fixDescription = localFixTextString?.let { "with specified text '$localFixTextString'" } ?: "" @@ -201,6 +206,10 @@ abstract class AbstractLocalInspectionTest : KotlinLightCodeInsightFixtureTestCa ) val localFixAction = localFixActions.singleOrNull { it !is EmptyIntentionAction } + if (localFixTextString == "none") { + Assert.assertTrue("Expected no fix action", localFixAction == null) + return false + } TestCase.assertTrue( "More than one fix action $fixDescription\n" + "Available actions: $availableDescription", diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index e15a0476d2e..2c0f6123aea 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -3252,6 +3252,26 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { runTest("idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/implicitThisReceiver.kt"); } + @TestMetadata("invalidFunctionReference.kt") + public void testInvalidFunctionReference() throws Exception { + runTest("idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference.kt"); + } + + @TestMetadata("invalidFunctionReference2.kt") + public void testInvalidFunctionReference2() throws Exception { + runTest("idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference2.kt"); + } + + @TestMetadata("invalidFunctionReference3.kt") + public void testInvalidFunctionReference3() throws Exception { + runTest("idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference3.kt"); + } + + @TestMetadata("invalidFunctionReference4.kt") + public void testInvalidFunctionReference4() throws Exception { + runTest("idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/invalidFunctionReference4.kt"); + } + @TestMetadata("it.kt") public void testIt() throws Exception { runTest("idea/testData/inspectionsLocal/moveSuspiciousCallableReferenceIntoParentheses/it.kt");