(KTIJ-31946) RemoveForLoopIndicesIntention ported to k2, description added.

GitOrigin-RevId: ffd9b687c2e846ee25f37558f5386c7c5c90a8e1
This commit is contained in:
Olga Klisho
2025-05-14 20:17:50 +00:00
committed by intellij-monorepo-bot
parent 3fbc56ed97
commit 6a444c675a
8 changed files with 148 additions and 0 deletions
@@ -0,0 +1,5 @@
fun foo(bar: List<String>) {
for (<spot>value: String in bar</spot>) {
println(value)
}
}
@@ -0,0 +1,5 @@
fun foo(bar: List<String>) {
for (<spot>(index : Int, value: String) in bar.withIndex()</spot>) {
println(value)
}
}
@@ -0,0 +1,5 @@
<html>
<body>
Reports <code>for</code> loops iterating over a collection using the <code>withIndex()</code> function and not using the index variable.
</body>
</html>
@@ -248,6 +248,13 @@
<categoryKey>group.names.kotlin</categoryKey>
</intentionAction>
<intentionAction>
<language>kotlin</language>
<className>org.jetbrains.kotlin.idea.k2.codeinsight.intentions.RemoveForLoopIndicesIntention</className>
<bundleName>messages.KotlinBundle</bundleName>
<categoryKey>group.names.kotlin</categoryKey>
</intentionAction>
<intentionAction>
<language>kotlin</language>
<className>org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ConvertPropertyGetterToInitializerIntention</className>
@@ -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, RemoveForLoopIndicesIntention.Context>(KtForExpression::class) {
private val WITH_INDEX_NAME = "withIndex"
private val WITH_INDEX_FQ_NAMES: Set<FqName> 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<TextRange> {
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]
}
}
@@ -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 {
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.k2.codeinsight.intentions.RemoveForLoopIndicesIntention
@@ -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)