From 1a818970c30c78f22207b901daef68d40320eff7 Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Fri, 1 Feb 2019 06:37:54 +0300 Subject: [PATCH] Add inspection to replace Java Map.forEach with Kotlin's forEach #KT-17278 Fixed --- idea/resources/META-INF/plugin-common.xml | 9 +++ .../JavaMapForEach.html | 5 ++ .../inspections/JavaMapForEachInspection.kt | 63 +++++++++++++++++++ .../kotlin/idea/j2k/J2kPostProcessings.kt | 1 + .../javaMapForEach/.inspection | 1 + .../inspectionsLocal/javaMapForEach/java.kt | 8 +++ .../javaMapForEach/java.kt.after | 8 +++ .../inspectionsLocal/javaMapForEach/java2.kt | 8 +++ .../javaMapForEach/java2.kt.after | 8 +++ .../inspectionsLocal/javaMapForEach/java3.kt | 6 ++ .../javaMapForEach/java3.kt.after | 6 ++ .../inspectionsLocal/javaMapForEach/kotlin.kt | 9 +++ .../javaMapForEach/kotlin2.kt | 8 +++ .../LocalInspectionTestGenerated.java | 38 +++++++++++ .../java8MapForEachWithFullJdk.java | 10 +++ .../java8MapForEachWithFullJdk.kt | 9 +++ ...ractJavaToKotlinConverterSingleFileTest.kt | 9 ++- ...otlinConverterForWebDemoTestGenerated.java | 5 ++ ...otlinConverterSingleFileTestGenerated.java | 5 ++ 19 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 idea/resources/inspectionDescriptions/JavaMapForEach.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/JavaMapForEachInspection.kt create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/.inspection create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/java.kt create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/java.kt.after create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/java2.kt create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/java2.kt.after create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/java3.kt create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/java3.kt.after create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/kotlin.kt create mode 100644 idea/testData/inspectionsLocal/javaMapForEach/kotlin2.kt create mode 100644 j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.java create mode 100644 j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.kt diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index ca1191cd119..9b6d21b7680 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -3288,6 +3288,15 @@ language="kotlin" /> + + diff --git a/idea/resources/inspectionDescriptions/JavaMapForEach.html b/idea/resources/inspectionDescriptions/JavaMapForEach.html new file mode 100644 index 00000000000..bc51452fc59 --- /dev/null +++ b/idea/resources/inspectionDescriptions/JavaMapForEach.html @@ -0,0 +1,5 @@ + + +This inspection reports a Java Map.forEach method call replaceable by Kotlin's forEach. + + \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/JavaMapForEachInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/JavaMapForEachInspection.kt new file mode 100644 index 00000000000..2ba2f5e26df --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/JavaMapForEachInspection.kt @@ -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::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() ?: 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() + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/j2k/J2kPostProcessings.kt b/idea/src/org/jetbrains/kotlin/idea/j2k/J2kPostProcessings.kt index d5d8394332f..31b2dd6f526 100644 --- a/idea/src/org/jetbrains/kotlin/idea/j2k/J2kPostProcessings.kt +++ b/idea/src/org/jetbrains/kotlin/idea/j2k/J2kPostProcessings.kt @@ -98,6 +98,7 @@ object J2KPostProcessingRegistrar { registerIntentionBasedProcessing(DestructureIntention()) registerInspectionBasedProcessing(SimplifyAssertNotNullInspection()) registerIntentionBasedProcessing(RemoveRedundantCallsOfConversionMethodsIntention()) + registerInspectionBasedProcessing(JavaMapForEachInspection()) registerDiagnosticBasedProcessing(Errors.USELESS_CAST) { element, _ -> val expression = RemoveUselessCastFix.invoke(element) diff --git a/idea/testData/inspectionsLocal/javaMapForEach/.inspection b/idea/testData/inspectionsLocal/javaMapForEach/.inspection new file mode 100644 index 00000000000..a0b115dc28b --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.JavaMapForEachInspection diff --git a/idea/testData/inspectionsLocal/javaMapForEach/java.kt b/idea/testData/inspectionsLocal/javaMapForEach/java.kt new file mode 100644 index 00000000000..d475456f981 --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/java.kt @@ -0,0 +1,8 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + map.forEach { key, value -> + foo(key, value) + } +} + +fun foo(i: Int, s: String) {} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/javaMapForEach/java.kt.after b/idea/testData/inspectionsLocal/javaMapForEach/java.kt.after new file mode 100644 index 00000000000..068a6895086 --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/java.kt.after @@ -0,0 +1,8 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + map.forEach { (key, value) -> + foo(key, value) + } +} + +fun foo(i: Int, s: String) {} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/javaMapForEach/java2.kt b/idea/testData/inspectionsLocal/javaMapForEach/java2.kt new file mode 100644 index 00000000000..eb37529d710 --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/java2.kt @@ -0,0 +1,8 @@ +// RUNTIME_WITH_FULL_JDK +fun test(hashMap: HashMap) { + hashMap.forEach { key, value -> + foo(key, value) + } +} + +fun foo(i: Int, s: String) {} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/javaMapForEach/java2.kt.after b/idea/testData/inspectionsLocal/javaMapForEach/java2.kt.after new file mode 100644 index 00000000000..89075b71e8f --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/java2.kt.after @@ -0,0 +1,8 @@ +// RUNTIME_WITH_FULL_JDK +fun test(hashMap: HashMap) { + hashMap.forEach { (key, value) -> + foo(key, value) + } +} + +fun foo(i: Int, s: String) {} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/javaMapForEach/java3.kt b/idea/testData/inspectionsLocal/javaMapForEach/java3.kt new file mode 100644 index 00000000000..7900901eca5 --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/java3.kt @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + map.forEach({ key, value -> foo(key, value) }) +} + +fun foo(i: Int, s: String) {} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/javaMapForEach/java3.kt.after b/idea/testData/inspectionsLocal/javaMapForEach/java3.kt.after new file mode 100644 index 00000000000..8d33d9505e4 --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/java3.kt.after @@ -0,0 +1,6 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + map.forEach({ (key, value) -> foo(key, value) }) +} + +fun foo(i: Int, s: String) {} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/javaMapForEach/kotlin.kt b/idea/testData/inspectionsLocal/javaMapForEach/kotlin.kt new file mode 100644 index 00000000000..f9e5b3c8834 --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/kotlin.kt @@ -0,0 +1,9 @@ +// PROBLEM: none +// WITH_RUNTIME +fun test(map: Map) { + map.forEach { (key, value) -> + foo(key, value) + } +} + +fun foo(i: Int, s: String) {} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/javaMapForEach/kotlin2.kt b/idea/testData/inspectionsLocal/javaMapForEach/kotlin2.kt new file mode 100644 index 00000000000..7c0f5905687 --- /dev/null +++ b/idea/testData/inspectionsLocal/javaMapForEach/kotlin2.kt @@ -0,0 +1,8 @@ +// PROBLEM: none +// WITH_RUNTIME +fun test(map: Map) { + map.forEach { + foo(it) + } +} +fun foo(it: Map.Entry) {} \ 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 e0c2452223e..c417d569bf9 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -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) diff --git a/j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.java b/j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.java new file mode 100644 index 00000000000..da0ca262fdd --- /dev/null +++ b/j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.java @@ -0,0 +1,10 @@ +import java.util.HashMap; + +class Test { + void test(HashMap map) { + map.forEach((key, value) -> foo(key, value)); + } + + void foo(String key, String value) { + } +} \ No newline at end of file diff --git a/j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.kt b/j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.kt new file mode 100644 index 00000000000..bcc85a05e83 --- /dev/null +++ b/j2k/testData/fileOrElement/postProcessing/java8MapForEachWithFullJdk.kt @@ -0,0 +1,9 @@ +import java.util.HashMap + +internal class Test { + fun test(map: HashMap) { + map.forEach { (key, value) -> foo(key, value) } + } + + fun foo(key: String, value: String) {} +} \ No newline at end of file diff --git a/j2k/tests/org/jetbrains/kotlin/j2k/AbstractJavaToKotlinConverterSingleFileTest.kt b/j2k/tests/org/jetbrains/kotlin/j2k/AbstractJavaToKotlinConverterSingleFileTest.kt index 6dc919c261b..d56d7a61373 100644 --- a/j2k/tests/org/jetbrains/kotlin/j2k/AbstractJavaToKotlinConverterSingleFileTest.kt +++ b/j2k/tests/org/jetbrains/kotlin/j2k/AbstractJavaToKotlinConverterSingleFileTest.kt @@ -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', "") diff --git a/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterForWebDemoTestGenerated.java b/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterForWebDemoTestGenerated.java index 603ed4c78f4..8874d638826 100644 --- a/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterForWebDemoTestGenerated.java +++ b/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterForWebDemoTestGenerated.java @@ -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"); diff --git a/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterSingleFileTestGenerated.java b/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterSingleFileTestGenerated.java index dab358e8a13..a744748c21f 100644 --- a/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterSingleFileTestGenerated.java +++ b/j2k/tests/org/jetbrains/kotlin/j2k/JavaToKotlinConverterSingleFileTestGenerated.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");