Fix false positive "Redundant lambda arrow" inspection

#KT-29590 Fixed
 #KT-19462 Fixed
 #KT-29124 Fixed
This commit is contained in:
Dmitry Gridin
2019-03-13 17:33:21 +07:00
parent 49ee197578
commit 479e812bbc
15 changed files with 267 additions and 47 deletions
@@ -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)
}
}
@@ -0,0 +1,10 @@
// PROBLEM: none
// WITH_RUNTIME
fun main() {
MyClass { _<caret>: String -> 0 }
}
class MyClass<K, V>(
private val selector: (K) -> V
)
@@ -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 = { })
}
@@ -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)