From f5c0a30c51a8a3b7c4058452d978e5116cf3878f Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Fri, 2 Aug 2019 12:29:54 +0900 Subject: [PATCH] Add intention to replace "Map.getOrDefault" #KT-21503 Fixed --- idea/resources/META-INF/plugin-common.xml | 5 ++ .../after.kt.template | 3 + .../before.kt.template | 3 + .../description.html | 5 ++ .../ReplaceMapGetOrDefaultIntention.kt | 59 +++++++++++++++++++ .../replaceMapGetOrDefault/.intention | 1 + .../replaceMapGetOrDefault/inExpression.kt | 4 ++ .../inExpression.kt.after | 4 ++ .../replaceMapGetOrDefault/nullableValue.kt | 6 ++ .../replaceMapGetOrDefault/simple.kt | 4 ++ .../replaceMapGetOrDefault/simple.kt.after | 4 ++ .../intentions/IntentionTestGenerated.java | 28 +++++++++ .../nj2k/postProcessing/J2kPostProcessor.kt | 3 +- .../postProcessing/MapGetOrDefault.java | 8 +++ .../newJ2k/postProcessing/MapGetOrDefault.kt | 5 ++ ...otlinConverterSingleFileTestGenerated.java | 5 ++ 16 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/after.kt.template create mode 100644 idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/before.kt.template create mode 100644 idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/description.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/ReplaceMapGetOrDefaultIntention.kt create mode 100644 idea/testData/intentions/replaceMapGetOrDefault/.intention create mode 100644 idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt create mode 100644 idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt.after create mode 100644 idea/testData/intentions/replaceMapGetOrDefault/nullableValue.kt create mode 100644 idea/testData/intentions/replaceMapGetOrDefault/simple.kt create mode 100644 idea/testData/intentions/replaceMapGetOrDefault/simple.kt.after create mode 100644 nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.java create mode 100644 nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.kt diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index 1d9054ca84c..4d8faa732e1 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -1654,6 +1654,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.ReplaceMapGetOrDefaultIntention + Kotlin + + org.jetbrains.kotlin.idea.intentions.RemoveUnderscoresFromNumericLiteralIntention Kotlin diff --git a/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/after.kt.template b/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/after.kt.template new file mode 100644 index 00000000000..4510859e732 --- /dev/null +++ b/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/after.kt.template @@ -0,0 +1,3 @@ +fun test(map: Map) { + map[1] ?: "foo" +} \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/before.kt.template b/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/before.kt.template new file mode 100644 index 00000000000..59979da2a52 --- /dev/null +++ b/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/before.kt.template @@ -0,0 +1,3 @@ +fun test(map: Map) { + map.getOrDefault(1, "foo") +} \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/description.html b/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/description.html new file mode 100644 index 00000000000..878e8ae94de --- /dev/null +++ b/idea/resources/intentionDescriptions/ReplaceMapGetOrDefaultIntention/description.html @@ -0,0 +1,5 @@ + + +This intention replaces map.getOrDefault(key, defaultValue) function calls with the indexing and elvis operator (map[key] ?: defaultValue). + + \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ReplaceMapGetOrDefaultIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ReplaceMapGetOrDefaultIntention.kt new file mode 100644 index 00000000000..0bf8b42edb9 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ReplaceMapGetOrDefaultIntention.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2018 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.intentions + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.core.replaced +import org.jetbrains.kotlin.idea.inspections.collections.isCalling +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType +import org.jetbrains.kotlin.psi.psiUtil.startOffset +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.types.isNullable + +class ReplaceMapGetOrDefaultIntention : SelfTargetingRangeIntention( + KtDotQualifiedExpression::class.java, "Replace with indexing and elvis operator" +) { + companion object { + private val getOrDefaultFqName = FqName("kotlin.collections.Map.getOrDefault") + } + + override fun applicabilityRange(element: KtDotQualifiedExpression): TextRange? { + val callExpression = element.callExpression ?: return null + val calleeExpression = callExpression.calleeExpression ?: return null + if (calleeExpression.text != getOrDefaultFqName.shortName().asString()) return null + val (firstArg, secondArg) = callExpression.arguments() ?: return null + val context = element.analyze(BodyResolveMode.PARTIAL) + if (callExpression.getResolvedCall(context)?.isCalling(getOrDefaultFqName) != true) return null + if (element.receiverExpression.getType(context)?.arguments?.lastOrNull()?.type?.isNullable() == true) return null + text = "Replace with ${element.receiverExpression.text}[${firstArg.text}] ?: ${secondArg.text}" + return calleeExpression.textRange + } + + override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) { + val callExpression = element.callExpression ?: return + val (firstArg, secondArg) = callExpression.arguments() ?: return + val replaced = element.replaced( + KtPsiFactory(element).createExpressionByPattern("$0[$1] ?: $2", element.receiverExpression, firstArg, secondArg) + ) + replaced.findDescendantOfType()?.leftBracket?.startOffset?.let { + editor?.caretModel?.moveToOffset(it) + } + } + + private fun KtCallExpression.arguments(): Pair? { + val args = valueArguments + if (args.size != 2) return null + val first = args[0].getArgumentExpression() ?: return null + val second = args[1].getArgumentExpression() ?: return null + return first to second + } +} \ No newline at end of file diff --git a/idea/testData/intentions/replaceMapGetOrDefault/.intention b/idea/testData/intentions/replaceMapGetOrDefault/.intention new file mode 100644 index 00000000000..9b59c5f7880 --- /dev/null +++ b/idea/testData/intentions/replaceMapGetOrDefault/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.ReplaceMapGetOrDefaultIntention diff --git a/idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt b/idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt new file mode 100644 index 00000000000..36c7004f478 --- /dev/null +++ b/idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt @@ -0,0 +1,4 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + map.getOrDefault(1, "bar") + "baz" +} diff --git a/idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt.after b/idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt.after new file mode 100644 index 00000000000..b6ffbdbe3e2 --- /dev/null +++ b/idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt.after @@ -0,0 +1,4 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + (map[1] ?: "bar") + "baz" +} \ No newline at end of file diff --git a/idea/testData/intentions/replaceMapGetOrDefault/nullableValue.kt b/idea/testData/intentions/replaceMapGetOrDefault/nullableValue.kt new file mode 100644 index 00000000000..f2fe83d7b28 --- /dev/null +++ b/idea/testData/intentions/replaceMapGetOrDefault/nullableValue.kt @@ -0,0 +1,6 @@ +// IS_APPLICABLE: false +// RUNTIME_WITH_FULL_JDK +fun main() { + val map = mapOf(1 to "", 2 to null) + val b = map.getOrDefault(2, "bar") == null +} \ No newline at end of file diff --git a/idea/testData/intentions/replaceMapGetOrDefault/simple.kt b/idea/testData/intentions/replaceMapGetOrDefault/simple.kt new file mode 100644 index 00000000000..ced6e3a52eb --- /dev/null +++ b/idea/testData/intentions/replaceMapGetOrDefault/simple.kt @@ -0,0 +1,4 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + map.getOrDefault(1, "bar") +} \ No newline at end of file diff --git a/idea/testData/intentions/replaceMapGetOrDefault/simple.kt.after b/idea/testData/intentions/replaceMapGetOrDefault/simple.kt.after new file mode 100644 index 00000000000..96f836d871a --- /dev/null +++ b/idea/testData/intentions/replaceMapGetOrDefault/simple.kt.after @@ -0,0 +1,4 @@ +// RUNTIME_WITH_FULL_JDK +fun test(map: Map) { + map[1] ?: "bar" +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index b3035594b1b..2acf8514902 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -14177,6 +14177,34 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/replaceMapGetOrDefault") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ReplaceMapGetOrDefault extends AbstractIntentionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInReplaceMapGetOrDefault() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/replaceMapGetOrDefault"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); + } + + @TestMetadata("inExpression.kt") + public void testInExpression() throws Exception { + runTest("idea/testData/intentions/replaceMapGetOrDefault/inExpression.kt"); + } + + @TestMetadata("nullableValue.kt") + public void testNullableValue() throws Exception { + runTest("idea/testData/intentions/replaceMapGetOrDefault/nullableValue.kt"); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("idea/testData/intentions/replaceMapGetOrDefault/simple.kt"); + } + } + @TestMetadata("idea/testData/intentions/replaceSizeCheckWithIsNotEmpty") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/J2kPostProcessor.kt b/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/J2kPostProcessor.kt index caf4122e223..d41c1e912dc 100644 --- a/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/J2kPostProcessor.kt +++ b/nj2k/nj2k-services/src/org/jetbrains/kotlin/nj2k/postProcessing/J2kPostProcessor.kt @@ -220,7 +220,8 @@ private val inspectionLikePostProcessingGroup = generalInspectionBasedProcessing(LiftReturnOrAssignmentInspection(skipLongExpressions = false)), intentionBasedProcessing(RemoveEmptyPrimaryConstructorIntention()), generalInspectionBasedProcessing(MayBeConstantInspection()), - RemoveForExpressionLoopParameterTypeProcessing() + RemoveForExpressionLoopParameterTypeProcessing(), + intentionBasedProcessing(ReplaceMapGetOrDefaultIntention()) ) diff --git a/nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.java b/nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.java new file mode 100644 index 00000000000..b86251a931b --- /dev/null +++ b/nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.java @@ -0,0 +1,8 @@ +// RUNTIME_WITH_FULL_JDK +import java.util.Map; + +class C { + String foo(Map map) { + return map.getOrDefault(1, "bar"); + } +} \ No newline at end of file diff --git a/nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.kt b/nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.kt new file mode 100644 index 00000000000..587f0164258 --- /dev/null +++ b/nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.kt @@ -0,0 +1,5 @@ +internal class C { + fun foo(map: Map): String { + return map[1] ?: "bar" + } +} \ No newline at end of file diff --git a/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java b/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java index a48a2684171..41db92b79a5 100644 --- a/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java +++ b/nj2k/tests/org/jetbrains/kotlin/nj2k/NewJavaToKotlinConverterSingleFileTestGenerated.java @@ -4102,6 +4102,11 @@ public class NewJavaToKotlinConverterSingleFileTestGenerated extends AbstractNew runTest("nj2k/testData/newJ2k/postProcessing/java8MapForEachWithFullJdk.java"); } + @TestMetadata("MapGetOrDefault.java") + public void testMapGetOrDefault() throws Exception { + runTest("nj2k/testData/newJ2k/postProcessing/MapGetOrDefault.java"); + } + @TestMetadata("NotIs.java") public void testNotIs() throws Exception { runTest("nj2k/testData/newJ2k/postProcessing/NotIs.java");