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");