[kotlin] KTIJ-37276 ReplaceManualRangeWithIndicesCallsInspection: take names conflict into account

GitOrigin-RevId: e9a783709d4a0286def7842f6b98bdc071a3a92c
This commit is contained in:
Olga Klisho
2026-02-16 20:13:36 +00:00
committed by intellij-monorepo-bot
parent 8417e280a3
commit 5792d62c48
12 changed files with 148 additions and 13 deletions
@@ -9702,6 +9702,26 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/complexReceiver.kt");
}
@TestMetadata("elementNameCollision.kt")
public void testElementNameCollision() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollision.kt");
}
@TestMetadata("elementNameCollisionMultiple.kt")
public void testElementNameCollisionMultiple() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollisionMultiple.kt");
}
@TestMetadata("elementNameCollisionNestedLoop.kt")
public void testElementNameCollisionNestedLoop() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollisionNestedLoop.kt");
}
@TestMetadata("elementNameCollisionParameter.kt")
public void testElementNameCollisionParameter() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollisionParameter.kt");
}
@TestMetadata("forNotTarget.kt")
public void testForNotTarget() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/forNotTarget.kt");
@@ -9,6 +9,9 @@ import com.intellij.psi.search.searches.ReferencesSearch
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.resolution.successfulVariableAccessCall
import org.jetbrains.kotlin.analysis.api.types.KaType
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinDeclarationNameValidator
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinNameSuggester
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinNameSuggestionProvider
import org.jetbrains.kotlin.idea.base.psi.replaced
import org.jetbrains.kotlin.idea.base.psi.safeDeparenthesize
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
@@ -51,6 +54,7 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
val explicitReceiver: KtExpression?,
val suggestElementLoop: Boolean,
val implicitReceiverInfo: ImplicitReceiverInfo?,
val suggestedElementName: String?,
)
override fun getProblemDescription(
@@ -79,7 +83,7 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
override fun KaSession.prepareContext(range: RangeExpression): Context? {
val (_, right) = range.arguments
// Must end with a size/length call
val sizeCall = right?.let { rightBound(range.type, it) } ?: return null
val explicitReceiver = (sizeCall as? KtQualifiedExpression)?.receiverExpression
@@ -87,7 +91,20 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
val suggestElementLoop = shouldSuggestElementLoop(range, explicitReceiver)
val implicitReceiverInfo = if (explicitReceiver == null) sizeCall.getImplicitReceiverInfo() else null
return Context(explicitReceiver, suggestElementLoop, implicitReceiverInfo)
// Pre-compute a unique element name for the loop transformation
val suggestedElementName = if (suggestElementLoop) {
val forExpression = findContainingForLoop(range)
forExpression?.loopParameter?.let { loopParameter ->
val nameValidator = KotlinDeclarationNameValidator(
loopParameter,
true,
KotlinNameSuggestionProvider.ValidatorTarget.VARIABLE,
)
KotlinNameSuggester.suggestNameByName("element") { nameValidator.validate(it) }
}
} else null
return Context(explicitReceiver, suggestElementLoop, implicitReceiverInfo, suggestedElementName)
}
private fun KaSession.rightBound(type: RangeKtExpressionType, expression: KtExpression): KtExpression? {
@@ -245,13 +262,14 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
val forExpression = element.getStrictParentOfType<KtForExpression>() ?: return
val loopParameter = forExpression.loopParameter ?: return
val loopRange = forExpression.loopRange ?: return
val elementName = context.suggestedElementName ?: return
val collection = resolveCollectionExpression(project)
// Try a single usage first
val singleUsageInfo = LoopToCollectionTransformUtils.findSingleArrayAccessUsage(loopParameter)
if (singleUsageInfo != null) {
LoopToCollectionTransformUtils.transformLoop(project, listOf(singleUsageInfo), loopParameter, loopRange, collection)
LoopToCollectionTransformUtils.transformLoop(project, listOf(singleUsageInfo), loopParameter, loopRange, collection, elementName)
return
}
@@ -267,7 +285,7 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
}
if (usageInfos.isNotEmpty()) {
LoopToCollectionTransformUtils.transformLoop(project, usageInfos, loopParameter, loopRange, collection)
LoopToCollectionTransformUtils.transformLoop(project, usageInfos, loopParameter, loopRange, collection, elementName)
}
}
@@ -30,7 +30,7 @@ object LoopToCollectionTransformUtils {
/**
* Finds the usage pattern for a loop parameter where it's used as an array index exactly once.
*
*
* @param loopParameter the loop parameter (e.g., `i` in `for (i in 0..<size)`)
* @return LoopUsageInfo if the parameter is used exactly once as an array index, null otherwise
*/
@@ -38,38 +38,41 @@ object LoopToCollectionTransformUtils {
val paramElement = loopParameter.originalElement ?: return null
val usageElement = ReferencesSearch.search(paramElement).findAll().singleOrNull()?.element ?: return null
val arrayAccessElement = usageElement.parents.match(KtContainerNode::class, last = KtArrayAccessExpression::class) ?: return null
return LoopUsageInfo(paramElement as KtParameter, usageElement, arrayAccessElement)
}
/**
* Transforms an index-based loop to a collection-based loop with multiple array access usages by:
* 1. Replacing the loop parameter with "element"
* 1. Replacing the loop parameter with the provided element name
* 2. Replacing all array access expressions with direct element references
* 3. Replacing the loop range with the collection expression
*
*
* @param project the current project
* @param usageInfos information about all loop usage patterns
* @param loopParameter the original loop parameter
* @param loopRange the original loop range
* @param newLoopRange the new collection expression to iterate over
* @param elementName the name for the new loop variable (should be pre-validated to avoid conflicts)
*/
fun transformLoop(
project: Project,
usageInfos: List<LoopUsageInfo>,
loopParameter: KtParameter,
loopRange: KtExpression,
newLoopRange: KtExpression
newLoopRange: KtExpression,
elementName: String
) {
val factory = KtPsiFactory(project)
val newParameter = factory.createLoopParameter("element")
val newReferenceExpression = factory.createExpression("element")
val newParameter = factory.createLoopParameter(elementName)
val newReferenceExpression = factory.createExpression(elementName)
// Replace all array access expressions with direct element references
usageInfos.forEach { usageInfo ->
usageInfo.arrayAccessElement.replace(newReferenceExpression.copy())
}
loopParameter.replace(newParameter)
loopRange.replace(newLoopRange)
}
@@ -14386,6 +14386,26 @@ public abstract class LocalInspectionTestGenerated extends AbstractK1LocalInspec
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/complexReceiver.kt");
}
@TestMetadata("elementNameCollision.kt")
public void testElementNameCollision() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollision.kt");
}
@TestMetadata("elementNameCollisionMultiple.kt")
public void testElementNameCollisionMultiple() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollisionMultiple.kt");
}
@TestMetadata("elementNameCollisionNestedLoop.kt")
public void testElementNameCollisionNestedLoop() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollisionNestedLoop.kt");
}
@TestMetadata("elementNameCollisionParameter.kt")
public void testElementNameCollisionParameter() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/elementNameCollisionParameter.kt");
}
@TestMetadata("forNotTarget.kt")
public void testForNotTarget() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/forNotTarget.kt");
@@ -0,0 +1,9 @@
// WITH_STDLIB
// IGNORE_K1
fun test() {
val element = mutableListOf<String>("hello", "world")
for (i in 0 unt<caret>il element.size) {
element[i].length
element.size
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// IGNORE_K1
fun test() {
val element = mutableListOf<String>("hello", "world")
for (element1 in element) {
element1.length
element.size
}
}
@@ -0,0 +1,10 @@
// WITH_STDLIB
// IGNORE_K1
fun test() {
val element = "first"
val element1 = "second"
val list = mutableListOf<String>("hello", "world")
for (i in 0 unt<caret>il list.size) {
list[i].length
}
}
@@ -0,0 +1,10 @@
// WITH_STDLIB
// IGNORE_K1
fun test() {
val element = "first"
val element1 = "second"
val list = mutableListOf<String>("hello", "world")
for (element2 in list) {
element2.length
}
}
@@ -0,0 +1,10 @@
// WITH_STDLIB
// IGNORE_K1
fun test() {
val outer = listOf(listOf("a", "b"), listOf("c", "d"))
for (element in outer) {
for (j in 0 unt<caret>il element.size) {
element[j].length
}
}
}
@@ -0,0 +1,10 @@
// WITH_STDLIB
// IGNORE_K1
fun test() {
val outer = listOf(listOf("a", "b"), listOf("c", "d"))
for (element in outer) {
for (element1 in element) {
element1.length
}
}
}
@@ -0,0 +1,8 @@
// WITH_STDLIB
// IGNORE_K1
fun test(element: String, list: List<String>) {
for (i in 0 unt<caret>il list.size) {
list[i].length
element.length
}
}
@@ -0,0 +1,8 @@
// WITH_STDLIB
// IGNORE_K1
fun test(element: String, list: List<String>) {
for (element1 in list) {
element1.length
element.length
}
}