diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantLambdaArrowInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantLambdaArrowInspection.kt index 07cdde274c8..0f000ed168e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantLambdaArrowInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantLambdaArrowInspection.kt @@ -16,6 +16,7 @@ import com.intellij.psi.PsiElementVisitor import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall +import org.jetbrains.kotlin.idea.refactoring.replaceWithCopyWithResolveCheck import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType @@ -23,9 +24,8 @@ import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.resolve.BindingContext -import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.resolve.calls.util.isSingleUnderscore -import org.jetbrains.kotlin.types.typeUtil.isTypeParameter class RedundantLambdaArrowInspection : AbstractKotlinInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { @@ -49,17 +49,12 @@ class RedundantLambdaArrowInspection : AbstractKotlinInspection() { if (callee != null && callee.getReferencedName() == "forEach" && singleParameter?.name != "it") return } - if (parameters.isNotEmpty()) { - val context = lambdaExpression.analyze() - if (context[BindingContext.EXPECTED_EXPRESSION_TYPE, lambdaExpression] == null) return - } + val lambdaContext = lambdaExpression.analyze() + if (parameters.isNotEmpty() && lambdaContext[BindingContext.EXPECTED_EXPRESSION_TYPE, lambdaExpression] == null) return val valueArgument = lambdaExpression.parent as? KtValueArgument val valueArgumentCall = valueArgument?.getStrictParentOfType() - if (valueArgumentCall != null) { - val argumentMatch = valueArgumentCall.resolveToCall()?.getArgumentMapping(valueArgument) as? ArgumentMatch - if (argumentMatch?.valueParameter?.original?.type?.isTypeParameter() == true) return - } + if (valueArgumentCall?.isApplicableCall(lambdaExpression, lambdaContext) == false) return val functionLiteralDescriptor = functionLiteral.descriptor if (functionLiteralDescriptor != null) { @@ -88,8 +83,44 @@ class RedundantLambdaArrowInspection : AbstractKotlinInspection() { override fun applyFix(project: Project, descriptor: ProblemDescriptor) { val element = descriptor.psiElement as? KtFunctionLiteral ?: return FileModificationService.getInstance().preparePsiElementForWrite(element) - element.valueParameterList?.delete() - element.arrow?.delete() + element.removeArrow() } } -} \ No newline at end of file +} + +private fun KtCallExpression.isApplicableCall(lambdaExpression: KtLambdaExpression, lambdaContext: BindingContext): Boolean { + val offset = lambdaExpression.textOffset - textOffset + val dotQualifiedExpression = parent as? KtDotQualifiedExpression + return if (dotQualifiedExpression == null) { + replaceWithCopyWithResolveCheck( + resolveStrategy = { expr, context -> + expr.getResolvedCall(context)?.resultingDescriptor + }, + context = lambdaContext, + preHook = { + findLambdaExpressionByOffset(offset)?.functionLiteral?.removeArrow() + } + ) + } else { + dotQualifiedExpression.replaceWithCopyWithResolveCheck( + resolveStrategy = { expr, context -> + expr.selectorExpression.getResolvedCall(context)?.resultingDescriptor + }, + context = lambdaContext, + preHook = { + val call = selectorExpression as? KtCallExpression + call?.findLambdaExpressionByOffset(offset)?.functionLiteral?.removeArrow() + } + ) + } != null +} + +private fun KtFunctionLiteral.removeArrow() { + valueParameterList?.delete() + arrow?.delete() +} + +private fun KtCallExpression.findLambdaExpressionByOffset(offset: Int): KtLambdaExpression? = + lambdaArguments.asSequence().mapNotNull(KtLambdaArgument::getLambdaExpression).firstOrNull { it.textOffset == offset } + ?: valueArguments.asSequence().mapNotNull(KtValueArgument::getArgumentExpression).firstOrNull { it.textOffset == offset } as? KtLambdaExpression + diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt index 100c1f72761..a71855a4275 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt @@ -65,6 +65,7 @@ import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.idea.KotlinBundle import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.idea.analysis.analyzeAsReplacement import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny @@ -78,6 +79,7 @@ import org.jetbrains.kotlin.idea.project.languageVersionSettings import org.jetbrains.kotlin.idea.refactoring.changeSignature.KotlinValVar import org.jetbrains.kotlin.idea.refactoring.changeSignature.toValVar import org.jetbrains.kotlin.idea.refactoring.memberInfo.KtPsiClassWrapper +import org.jetbrains.kotlin.idea.refactoring.rename.canonicalRender import org.jetbrains.kotlin.idea.util.* import org.jetbrains.kotlin.idea.util.string.collapseSpaces import org.jetbrains.kotlin.j2k.ConverterSettings @@ -1031,4 +1033,19 @@ fun PsiMethod.checkDeclarationConflict(name: String, conflicts: MultiMap "$s already exists" } } -} \ No newline at end of file +} + +fun T.replaceWithCopyWithResolveCheck( + resolveStrategy: (T, BindingContext) -> DeclarationDescriptor?, + context: BindingContext = analyze(), + preHook: T.() -> Unit = {}, + postHook: T.() -> T? = { this } +): T? { + val originDescriptor = resolveStrategy(this, context) ?: return null + @Suppress("UNCHECKED_CAST") val elementCopy = copy() as T + elementCopy.preHook() + val newContext = elementCopy.analyzeAsReplacement(this, context) + val newDescriptor = resolveStrategy(elementCopy, newContext) ?: return null + + return if (originDescriptor.canonicalRender() == newDescriptor.canonicalRender()) elementCopy.postHook() else null +} diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/it2.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/it2.kt new file mode 100644 index 00000000000..b649747a03f --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/it2.kt @@ -0,0 +1,12 @@ +class A + +fun A.foo(f: (String) -> Unit) {} + +fun print(s: String) {} + +fun bar() { + A().foo { it -> + print(it) + print(it) + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/it2.kt.after b/idea/testData/inspectionsLocal/redundantLambdaArrow/it2.kt.after new file mode 100644 index 00000000000..76597564009 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/it2.kt.after @@ -0,0 +1,12 @@ +class A + +fun A.foo(f: (String) -> Unit) {} + +fun print(s: String) {} + +fun bar() { + A().foo { + print(it) + print(it) + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor.kt new file mode 100644 index 00000000000..d7cb9e90b6a --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor.kt @@ -0,0 +1,10 @@ +// PROBLEM: none +// WITH_RUNTIME + +fun main() { + MyClass { _: String -> 0 } +} + +class MyClass( + private val selector: (K) -> V +) \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor2.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor2.kt new file mode 100644 index 00000000000..5ac13534732 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor2.kt @@ -0,0 +1,10 @@ +// PROBLEM: none +// WITH_RUNTIME + +fun main() { + MyClass { it: String -> 0 } +} + +class MyClass( + private val selector: (K) -> V +) \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload.kt new file mode 100644 index 00000000000..37811bdd422 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload.kt @@ -0,0 +1,9 @@ +// PROBLEM: none +// WITH_RUNTIME + +fun bar(f: () -> Unit) {} +fun bar(f: (Int) -> Unit) {} + +fun test() { + bar { -> } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload2.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload2.kt new file mode 100644 index 00000000000..a31f50d2335 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload2.kt @@ -0,0 +1,9 @@ +// PROBLEM: none +// WITH_RUNTIME + +fun bar(f: () -> Unit) {} +fun bar(f: (Int) -> Unit) {} + +fun test() { + bar({ -> }) +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload3.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload3.kt new file mode 100644 index 00000000000..f91676c5ffb --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload3.kt @@ -0,0 +1,9 @@ +// PROBLEM: none +// WITH_RUNTIME + +fun bar(f: () -> Unit) {} +fun bar(f: (Int) -> Unit) {} + +fun test() { + bar(f = { -> }) +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload5.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload5.kt new file mode 100644 index 00000000000..9a624677c97 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload5.kt @@ -0,0 +1,9 @@ +// WITH_RUNTIME +// PROBLEM: none + +fun bar(f: () -> Unit) {} +fun bar(f: (Int) -> Unit) {} + +fun test() { + bar { it -> } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg.kt new file mode 100644 index 00000000000..b7330abedd8 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg.kt @@ -0,0 +1,13 @@ +// PROBLEM: none +// WITH_RUNTIME + +fun main() { + registerHandler(handlers = *arrayOf( + { _ -> }, + { it -> } + )) +} + +fun registerHandler(vararg handlers: (String) -> Unit) { + handlers.forEach { it.invoke("hello") } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg2.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg2.kt new file mode 100644 index 00000000000..40408ac6e59 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg2.kt @@ -0,0 +1,13 @@ +// PROBLEM: none +// WITH_RUNTIME + +fun main() { + registerHandler(handlers = *arrayOf( + { _ -> }, + { it -> } + )) +} + +fun registerHandler(vararg handlers: (String) -> Unit) { + handlers.forEach { it.invoke("hello") } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/overload4.kt b/idea/testData/inspectionsLocal/redundantLambdaArrow/overload4.kt new file mode 100644 index 00000000000..320acb570af --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/overload4.kt @@ -0,0 +1,8 @@ +// WITH_RUNTIME + +fun bar(f: () -> Unit) {} +fun bar(g: (Int, Int) -> Unit) {} + +fun test() { + bar(f = { -> }) +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/redundantLambdaArrow/overload4.kt.after b/idea/testData/inspectionsLocal/redundantLambdaArrow/overload4.kt.after new file mode 100644 index 00000000000..7bfa946b559 --- /dev/null +++ b/idea/testData/inspectionsLocal/redundantLambdaArrow/overload4.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME + +fun bar(f: () -> Unit) {} +fun bar(g: (Int, Int) -> Unit) {} + +fun test() { + bar(f = { }) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index 5fc3c135d75..da12575ef06 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -5140,6 +5140,11 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/it.kt"); } + @TestMetadata("it2.kt") + public void testIt2() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/it2.kt"); + } + @TestMetadata("nestedLambda.kt") public void testNestedLambda() throws Exception { runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/nestedLambda.kt"); @@ -5160,6 +5165,51 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/noExpectedType.kt"); } + @TestMetadata("notApplicableGenericConstructor.kt") + public void testNotApplicableGenericConstructor() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor.kt"); + } + + @TestMetadata("notApplicableGenericConstructor2.kt") + public void testNotApplicableGenericConstructor2() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableGenericConstructor2.kt"); + } + + @TestMetadata("notApplicableOverload.kt") + public void testNotApplicableOverload() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload.kt"); + } + + @TestMetadata("notApplicableOverload2.kt") + public void testNotApplicableOverload2() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload2.kt"); + } + + @TestMetadata("notApplicableOverload3.kt") + public void testNotApplicableOverload3() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload3.kt"); + } + + @TestMetadata("notApplicableOverload5.kt") + public void testNotApplicableOverload5() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableOverload5.kt"); + } + + @TestMetadata("notApplicableVararg.kt") + public void testNotApplicableVararg() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg.kt"); + } + + @TestMetadata("notApplicableVararg2.kt") + public void testNotApplicableVararg2() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/notApplicableVararg2.kt"); + } + + @TestMetadata("overload4.kt") + public void testOverload4() throws Exception { + runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/overload4.kt"); + } + @TestMetadata("simple.kt") public void testSimple() throws Exception { runTest("idea/testData/inspectionsLocal/redundantLambdaArrow/simple.kt"); @@ -6569,39 +6619,6 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { } } - @TestMetadata("idea/testData/inspectionsLocal/replaceCollectionCountWithSize") - @TestDataPath("$PROJECT_ROOT") - @RunWith(JUnit3RunnerWithInners.class) - public static class ReplaceCollectionCountWithSize extends AbstractLocalInspectionTest { - private void runTest(String testDataFilePath) throws Exception { - KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); - } - - public void testAllFilesPresentInReplaceCollectionCountWithSize() throws Exception { - KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/replaceCollectionCountWithSize"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); - } - - @TestMetadata("countOfArray.kt") - public void testCountOfArray() throws Exception { - runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfArray.kt"); - } - - @TestMetadata("countOfArrayWithPredicate.kt") - public void testCountOfArrayWithPredicate() throws Exception { - runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfArrayWithPredicate.kt"); - } - - @TestMetadata("countOfCollection.kt") - public void testCountOfCollection() throws Exception { - runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfCollection.kt"); - } - - @TestMetadata("countOfMap.kt") - public void testCountOfMap() throws Exception { - runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfMap.kt"); - } - } - @TestMetadata("idea/testData/inspectionsLocal/replaceAssociateFunction") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) @@ -6868,6 +6885,39 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { } } + @TestMetadata("idea/testData/inspectionsLocal/replaceCollectionCountWithSize") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ReplaceCollectionCountWithSize extends AbstractLocalInspectionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInReplaceCollectionCountWithSize() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/replaceCollectionCountWithSize"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); + } + + @TestMetadata("countOfArray.kt") + public void testCountOfArray() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfArray.kt"); + } + + @TestMetadata("countOfArrayWithPredicate.kt") + public void testCountOfArrayWithPredicate() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfArrayWithPredicate.kt"); + } + + @TestMetadata("countOfCollection.kt") + public void testCountOfCollection() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfCollection.kt"); + } + + @TestMetadata("countOfMap.kt") + public void testCountOfMap() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceCollectionCountWithSize/countOfMap.kt"); + } + } + @TestMetadata("idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)