Fix false positive "Redundant lambda arrow" inspection
#KT-29590 Fixed #KT-19462 Fixed #KT-29124 Fixed
This commit is contained in:
@@ -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<KtCallExpression>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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<PsiElem
|
||||
// as is necessary here: see KT-10386
|
||||
?.firstOrNull { it.parameterList.parametersCount == 0 && !callables.contains(it.namedUnwrappedElement as PsiElement?) }
|
||||
?.let { reportDeclarationConflict(conflicts, it) { s -> "$s already exists" } }
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : KtExpression> 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
class A
|
||||
|
||||
fun A.foo(f: (String) -> Unit) {}
|
||||
|
||||
fun print(s: String) {}
|
||||
|
||||
fun bar() {
|
||||
A().foo { <caret>it ->
|
||||
print(it)
|
||||
print(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
class A
|
||||
|
||||
fun A.foo(f: (String) -> Unit) {}
|
||||
|
||||
fun print(s: String) {}
|
||||
|
||||
fun bar() {
|
||||
A().foo {
|
||||
print(it)
|
||||
print(it)
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun main() {
|
||||
MyClass { _<caret>: String -> 0 }
|
||||
}
|
||||
|
||||
class MyClass<K, V>(
|
||||
private val selector: (K) -> V
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun main() {
|
||||
MyClass { it<caret>: String -> 0 }
|
||||
}
|
||||
|
||||
class MyClass<K, V>(
|
||||
private val selector: (K) -> V
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun bar(f: () -> Unit) {}
|
||||
fun bar(f: (Int) -> Unit) {}
|
||||
|
||||
fun test() {
|
||||
bar { -><caret> }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun bar(f: () -> Unit) {}
|
||||
fun bar(f: (Int) -> Unit) {}
|
||||
|
||||
fun test() {
|
||||
bar({ -><caret> })
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun bar(f: () -> Unit) {}
|
||||
fun bar(f: (Int) -> Unit) {}
|
||||
|
||||
fun test() {
|
||||
bar(f = { -><caret> })
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// WITH_RUNTIME
|
||||
// PROBLEM: none
|
||||
|
||||
fun bar(f: () -> Unit) {}
|
||||
fun bar(f: (Int) -> Unit) {}
|
||||
|
||||
fun test() {
|
||||
bar { it<caret> -> }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun main() {
|
||||
registerHandler(handlers = *arrayOf(
|
||||
{ _<caret> -> },
|
||||
{ it -> }
|
||||
))
|
||||
}
|
||||
|
||||
fun registerHandler(vararg handlers: (String) -> Unit) {
|
||||
handlers.forEach { it.invoke("hello") }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun main() {
|
||||
registerHandler(handlers = *arrayOf(
|
||||
{ _ -> },
|
||||
{ it<caret> -> }
|
||||
))
|
||||
}
|
||||
|
||||
fun registerHandler(vararg handlers: (String) -> Unit) {
|
||||
handlers.forEach { it.invoke("hello") }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun bar(f: () -> Unit) {}
|
||||
fun bar(g: (Int, Int) -> Unit) {}
|
||||
|
||||
fun test() {
|
||||
bar(f = { -><caret> })
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun bar(f: () -> Unit) {}
|
||||
fun bar(g: (Int, Int) -> Unit) {}
|
||||
|
||||
fun test() {
|
||||
bar(f = { })
|
||||
}
|
||||
+83
-33
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user