From 6a444c675a367dc17c49214992f00896b22ba2b8 Mon Sep 17 00:00:00 2001 From: Olga Klisho Date: Sun, 11 May 2025 17:29:39 +0200 Subject: [PATCH] (KTIJ-31946) RemoveForLoopIndicesIntention ported to k2, description added. GitOrigin-RevId: ffd9b687c2e846ee25f37558f5386c7c5c90a8e1 --- .../after.kt.template | 5 ++ .../before.kt.template | 5 ++ .../description.html | 5 ++ .../kotlin.code-insight.intentions.k2.xml | 7 ++ .../RemoveForLoopIndicesIntention.kt | 75 +++++++++++++++++++ .../tests/K2IntentionTestGenerated.java | 49 ++++++++++++ .../removeForLoopIndices/.k2Intention | 1 + .../codeinsight/GenerateK2IntentionTests.kt | 1 + 8 files changed, 148 insertions(+) create mode 100644 plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/after.kt.template create mode 100644 plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/before.kt.template create mode 100644 plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/description.html create mode 100644 plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/RemoveForLoopIndicesIntention.kt create mode 100644 plugins/kotlin/idea/tests/testData/intentions/removeForLoopIndices/.k2Intention diff --git a/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/after.kt.template b/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/after.kt.template new file mode 100644 index 000000000000..b76eb9d7b7bd --- /dev/null +++ b/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/after.kt.template @@ -0,0 +1,5 @@ +fun foo(bar: List) { + for (value: String in bar) { + println(value) + } + } \ No newline at end of file diff --git a/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/before.kt.template b/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/before.kt.template new file mode 100644 index 000000000000..ad40547e1f70 --- /dev/null +++ b/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/before.kt.template @@ -0,0 +1,5 @@ +fun foo(bar: List) { + for ((index : Int, value: String) in bar.withIndex()) { + println(value) + } + } \ No newline at end of file diff --git a/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/description.html b/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/description.html new file mode 100644 index 000000000000..4379d0689c6e --- /dev/null +++ b/plugins/kotlin/code-insight/descriptions/resources-en/intentionDescriptions/RemoveForLoopIndicesIntention/description.html @@ -0,0 +1,5 @@ + + +Reports for loops iterating over a collection using the withIndex() function and not using the index variable. + + \ No newline at end of file diff --git a/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml b/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml index 17fb8ebc4542..1b334b9abd01 100644 --- a/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml +++ b/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml @@ -248,6 +248,13 @@ group.names.kotlin + + kotlin + org.jetbrains.kotlin.idea.k2.codeinsight.intentions.RemoveForLoopIndicesIntention + messages.KotlinBundle + group.names.kotlin + + kotlin org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ConvertPropertyGetterToInitializerIntention diff --git a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/RemoveForLoopIndicesIntention.kt b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/RemoveForLoopIndicesIntention.kt new file mode 100644 index 000000000000..9d9f319ca58a --- /dev/null +++ b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/RemoveForLoopIndicesIntention.kt @@ -0,0 +1,75 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.k2.codeinsight.intentions + +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.modcommand.ActionContext +import com.intellij.modcommand.ModPsiUpdater +import com.intellij.openapi.util.TextRange +import com.intellij.psi.search.searches.ReferencesSearch +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull +import org.jetbrains.kotlin.analysis.api.resolution.symbol +import org.jetbrains.kotlin.idea.base.codeInsight.handlers.fixers.range +import org.jetbrains.kotlin.idea.base.psi.relativeTo +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.KotlinApplicableModCommandAction +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* + + +class RemoveForLoopIndicesIntention : + KotlinApplicableModCommandAction(KtForExpression::class) { + private val WITH_INDEX_NAME = "withIndex" + private val WITH_INDEX_FQ_NAMES: Set by lazy { + sequenceOf("collections", "sequences", "text", "ranges").map { FqName("kotlin.$it.$WITH_INDEX_NAME") }.toSet() + } + + data class Context(val elementVar: KtDestructuringDeclarationEntry) + + override fun getFamilyName(): @IntentionFamilyName String = + KotlinBundle.message("remove.indices.in.for.loop") + + override fun getApplicableRanges(element: KtForExpression): List { + val indexVar = getIndexVar(element) ?: return emptyList() + return listOfNotNull(indexVar.nameIdentifier?.range?.relativeTo(element)) + } + + override fun isApplicableByPsi(element: KtForExpression): Boolean { + val multiParameter = element.destructuringDeclaration ?: return false + return multiParameter.entries.size == 2 + } + + override fun KaSession.prepareContext(element: KtForExpression): Context? { + val loopRange = element.loopRange as? KtDotQualifiedExpression ?: return null + val multiParameter = element.destructuringDeclaration ?: return null + + val functionCall = loopRange.resolveToCall()?.successfulFunctionCallOrNull()?.symbol?.callableId?.asSingleFqName() ?: return null + if (functionCall !in WITH_INDEX_FQ_NAMES) return null + val indexVar = multiParameter.entries[0] + if (ReferencesSearch.search(indexVar).any()) return null + + val elementVar = multiParameter.entries[1] + return Context(elementVar) + } + + override fun invoke( + actionContext: ActionContext, + element: KtForExpression, + elementContext: Context, + updater: ModPsiUpdater + ) { + val loopRange = element.loopRange as KtDotQualifiedExpression + + val psiFactory = KtPsiFactory(element.project) + val loop = psiFactory.createExpressionByPattern("for ($0 in _) {}", elementContext.elementVar.text) as KtForExpression + + loop.loopParameter?.let { element.loopParameter?.replace(it) } + loopRange.replace(loopRange.receiverExpression) + } + + private fun getIndexVar(element: KtForExpression): KtDestructuringDeclarationEntry? { + val multiParameter = element.destructuringDeclaration ?: return null + if (multiParameter.entries.size != 2) return null + return multiParameter.entries[0] + } +} diff --git a/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java b/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java index 6bac45a9c50d..fa9b130f66e6 100644 --- a/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java +++ b/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java @@ -3330,6 +3330,55 @@ public abstract class K2IntentionTestGenerated extends AbstractK2IntentionTest { } } + @RunWith(JUnit3RunnerWithInners.class) + @TestMetadata("../../../idea/tests/testData/intentions/removeForLoopIndices") + public static class RemoveForLoopIndices extends AbstractK2IntentionTest { + @java.lang.Override + @org.jetbrains.annotations.NotNull + public final KotlinPluginMode getPluginMode() { + return KotlinPluginMode.K2; + } + + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + @TestMetadata("inapplicableForLoop.kt") + public void testInapplicableForLoop() throws Exception { + runTest("../../../idea/tests/testData/intentions/removeForLoopIndices/inapplicableForLoop.kt"); + } + + @TestMetadata("inapplicableIndexUse.kt") + public void testInapplicableIndexUse() throws Exception { + runTest("../../../idea/tests/testData/intentions/removeForLoopIndices/inapplicableIndexUse.kt"); + } + + @TestMetadata("inapplicableOverridenFunction.kt") + public void testInapplicableOverridenFunction() throws Exception { + runTest("../../../idea/tests/testData/intentions/removeForLoopIndices/inapplicableOverridenFunction.kt"); + } + + @TestMetadata("loopWithType.kt") + public void testLoopWithType() throws Exception { + runTest("../../../idea/tests/testData/intentions/removeForLoopIndices/loopWithType.kt"); + } + + @TestMetadata("simpleLoopWithIndices.kt") + public void testSimpleLoopWithIndices() throws Exception { + runTest("../../../idea/tests/testData/intentions/removeForLoopIndices/simpleLoopWithIndices.kt"); + } + + @TestMetadata("simpleLoopWithIndicesOverCharSequence.kt") + public void testSimpleLoopWithIndicesOverCharSequence() throws Exception { + runTest("../../../idea/tests/testData/intentions/removeForLoopIndices/simpleLoopWithIndicesOverCharSequence.kt"); + } + + @TestMetadata("simpleLoopWithIndicesOverSequence.kt") + public void testSimpleLoopWithIndicesOverSequence() throws Exception { + runTest("../../../idea/tests/testData/intentions/removeForLoopIndices/simpleLoopWithIndicesOverSequence.kt"); + } + } + @RunWith(JUnit3RunnerWithInners.class) @TestMetadata("../../../idea/tests/testData/intentions/addWhenRemainingBranches") public static class AddWhenRemainingBranches extends AbstractK2IntentionTest { diff --git a/plugins/kotlin/idea/tests/testData/intentions/removeForLoopIndices/.k2Intention b/plugins/kotlin/idea/tests/testData/intentions/removeForLoopIndices/.k2Intention new file mode 100644 index 000000000000..a3bcdd5803db --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/intentions/removeForLoopIndices/.k2Intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.k2.codeinsight.intentions.RemoveForLoopIndicesIntention \ No newline at end of file diff --git a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt index a34164b1cafe..7f905d3c7efd 100644 --- a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt +++ b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2IntentionTests.kt @@ -34,6 +34,7 @@ internal fun MutableTWorkspace.generateK2IntentionTests() { model("${idea}intentions/convertStringTemplateToBuildStringMultiDollarPrefix", pattern = pattern) model("${idea}intentions/convertFilteringFunctionWithDemorgansLaw", pattern = pattern) model("${idea}intentions/convertToBlockBody", pattern = pattern) + model("${idea}intentions/removeForLoopIndices", pattern = pattern) model("${idea}intentions/addWhenRemainingBranches", pattern = pattern) model("${idea}intentions/convertToConcatenatedString", pattern = pattern) model("${idea}intentions/convertToStringTemplate", pattern = pattern)