Add inspection to replace Java Map.forEach with Kotlin's forEach

#KT-17278 Fixed
This commit is contained in:
Toshiaki Kameyama
2019-02-01 06:37:54 +03:00
committed by Mikhail Glukhikh
parent 9c674cb7c3
commit 1a818970c3
19 changed files with 214 additions and 2 deletions
@@ -3288,6 +3288,15 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.JavaMapForEachInspection"
displayName="Java Map.forEach method call should be replaced with Kotlin's forEach"
groupPath="Kotlin"
groupName="Style issues"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
/>
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports a Java Map.<b>forEach</b> method call replaceable by Kotlin's <b>forEach</b>.
</body>
</html>
@@ -0,0 +1,63 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.inspections
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.core.getLastLambdaExpression
import org.jetbrains.kotlin.idea.inspections.collections.isMap
import org.jetbrains.kotlin.idea.intentions.callExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.synthetic.SamAdapterExtensionFunctionDescriptor
class JavaMapForEachInspection : AbstractApplicabilityBasedInspection<KtDotQualifiedExpression>(
KtDotQualifiedExpression::class.java
) {
override fun isApplicable(element: KtDotQualifiedExpression): Boolean {
val callExpression = element.callExpression ?: return false
val calleeExpression = callExpression.calleeExpression ?: return false
if (calleeExpression.text != "forEach") return false
val lambda = callExpression.lambda() ?: return false
if (lambda.valueParameters.size != 2) return false
val context = element.analyze(BodyResolveMode.PARTIAL)
if (!element.receiverExpression.getType(context).isMap(DefaultBuiltIns.Instance)) return false
return callExpression.getResolvedCall(context)?.resultingDescriptor is SamAdapterExtensionFunctionDescriptor
}
override fun inspectionTarget(element: KtDotQualifiedExpression) = element.callExpression?.calleeExpression ?: element
override fun inspectionText(element: KtDotQualifiedExpression) = "Java Map.forEach method call should be replaced with Kotlin's forEach"
override fun inspectionHighlightType(element: KtDotQualifiedExpression) = ProblemHighlightType.GENERIC_ERROR_OR_WARNING
override val defaultFixText = "Replace with Kotlin's forEach"
override fun applyTo(element: PsiElement, project: Project, editor: Editor?) {
val call = element.getStrictParentOfType<KtCallExpression>() ?: return
val lambda = call.lambda() ?: return
val valueParameters = lambda.valueParameters
lambda.functionLiteral.valueParameterList?.replace(
KtPsiFactory(call).createLambdaParameterList("(${valueParameters[0].text}, ${valueParameters[1].text})")
)
}
private fun KtCallExpression.lambda(): KtLambdaExpression? {
return lambdaArguments.singleOrNull()?.getArgumentExpression() as? KtLambdaExpression ?: getLastLambdaExpression()
}
}
@@ -98,6 +98,7 @@ object J2KPostProcessingRegistrar {
registerIntentionBasedProcessing(DestructureIntention())
registerInspectionBasedProcessing(SimplifyAssertNotNullInspection())
registerIntentionBasedProcessing(RemoveRedundantCallsOfConversionMethodsIntention())
registerInspectionBasedProcessing(JavaMapForEachInspection())
registerDiagnosticBasedProcessing<KtBinaryExpressionWithTypeRHS>(Errors.USELESS_CAST) { element, _ ->
val expression = RemoveUselessCastFix.invoke(element)
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.JavaMapForEachInspection
+8
View File
@@ -0,0 +1,8 @@
// RUNTIME_WITH_FULL_JDK
fun test(map: Map<Int, String>) {
map.<caret>forEach { key, value ->
foo(key, value)
}
}
fun foo(i: Int, s: String) {}
@@ -0,0 +1,8 @@
// RUNTIME_WITH_FULL_JDK
fun test(map: Map<Int, String>) {
map.forEach { (key, value) ->
foo(key, value)
}
}
fun foo(i: Int, s: String) {}
@@ -0,0 +1,8 @@
// RUNTIME_WITH_FULL_JDK
fun test(hashMap: HashMap<Int, String>) {
hashMap.<caret>forEach { key, value ->
foo(key, value)
}
}
fun foo(i: Int, s: String) {}
@@ -0,0 +1,8 @@
// RUNTIME_WITH_FULL_JDK
fun test(hashMap: HashMap<Int, String>) {
hashMap.forEach { (key, value) ->
foo(key, value)
}
}
fun foo(i: Int, s: String) {}
@@ -0,0 +1,6 @@
// RUNTIME_WITH_FULL_JDK
fun test(map: Map<Int, String>) {
map.forEach<caret>({ key, value -> foo(key, value) })
}
fun foo(i: Int, s: String) {}
@@ -0,0 +1,6 @@
// RUNTIME_WITH_FULL_JDK
fun test(map: Map<Int, String>) {
map.forEach({ (key, value) -> foo(key, value) })
}
fun foo(i: Int, s: String) {}
@@ -0,0 +1,9 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(map: Map<Int, String>) {
map.<caret>forEach { (key, value) ->
foo(key, value)
}
}
fun foo(i: Int, s: String) {}
@@ -0,0 +1,8 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(map: Map<Int, String>) {
map.<caret>forEach {
foo(it)
}
}
fun foo(it: Map.Entry<Int, String>) {}
@@ -3189,6 +3189,44 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
}
}
@TestMetadata("idea/testData/inspectionsLocal/javaMapForEach")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class JavaMapForEach extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInJavaMapForEach() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/javaMapForEach"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("java.kt")
public void testJava() throws Exception {
runTest("idea/testData/inspectionsLocal/javaMapForEach/java.kt");
}
@TestMetadata("java2.kt")
public void testJava2() throws Exception {
runTest("idea/testData/inspectionsLocal/javaMapForEach/java2.kt");
}
@TestMetadata("java3.kt")
public void testJava3() throws Exception {
runTest("idea/testData/inspectionsLocal/javaMapForEach/java3.kt");
}
@TestMetadata("kotlin.kt")
public void testKotlin() throws Exception {
runTest("idea/testData/inspectionsLocal/javaMapForEach/kotlin.kt");
}
@TestMetadata("kotlin2.kt")
public void testKotlin2() throws Exception {
runTest("idea/testData/inspectionsLocal/javaMapForEach/kotlin2.kt");
}
}
@TestMetadata("idea/testData/inspectionsLocal/kdocMissingDocumentation")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -0,0 +1,10 @@
import java.util.HashMap;
class Test {
void test(HashMap<String, String> map) {
map.forEach((key, value) -> foo(key, value));
}
void foo(String key, String value) {
}
}
@@ -0,0 +1,9 @@
import java.util.HashMap
internal class Test {
fun test(map: HashMap<String, String>) {
map.forEach { (key, value) -> foo(key, value) }
}
fun foo(key: String, value: String) {}
}
@@ -139,8 +139,13 @@ abstract class AbstractJavaToKotlinConverterSingleFileTest : AbstractJavaToKotli
.trim()
}
override fun getProjectDescriptor()
= KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
override fun getProjectDescriptor(): KotlinWithJdkAndRuntimeLightProjectDescriptor {
val testName = getTestName(false)
return if (testName.contains("WithFullJdk") || testName.contains("withFullJdk"))
KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE_FULL_JDK
else
KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
}
private fun String.removeFirstLine() = substringAfter('\n', "")
@@ -3794,6 +3794,11 @@ public class JavaToKotlinConverterForWebDemoTestGenerated extends AbstractJavaTo
runTest("j2k/testData/fileOrElement/postProcessing/IfToSafeCall.java");
}
@TestMetadata("java8MapForEachWithFullJdk.java")
public void testJava8MapForEachWithFullJdk() throws Exception {
runTest("j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.java");
}
@TestMetadata("NotIs.java")
public void testNotIs() throws Exception {
runTest("j2k/testData/fileOrElement/postProcessing/NotIs.java");
@@ -3794,6 +3794,11 @@ public class JavaToKotlinConverterSingleFileTestGenerated extends AbstractJavaTo
runTest("j2k/testData/fileOrElement/postProcessing/IfToSafeCall.java");
}
@TestMetadata("java8MapForEachWithFullJdk.java")
public void testJava8MapForEachWithFullJdk() throws Exception {
runTest("j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.java");
}
@TestMetadata("NotIs.java")
public void testNotIs() throws Exception {
runTest("j2k/testData/fileOrElement/postProcessing/NotIs.java");