[kotlin] KTIJ-37276 ReplaceManualRangeWithIndicesCallsInspection: refactoring

GitOrigin-RevId: 00562c51071dedbf72f12bcc78628c3fef6ecac2
This commit is contained in:
Olga Klisho
2026-02-16 20:13:37 +00:00
committed by intellij-monorepo-bot
parent cad4e7a412
commit 73600a25ea
18 changed files with 95 additions and 232 deletions
@@ -1688,7 +1688,7 @@ replace.index.loop.with.collection.loop.quick.fix.text=Replace with loop over el
replace.manual.range.with.indices.call.quick.fix.text=Replace with indices
range.could.be.replaced.with.indices.call=Range could be replaced with '.indices' call
for.loop.over.indices.could.be.replaced.with.loop.over.elements=For loop over indices could be replaced with loop over elements
for.loop.could.use.withIndex=For loop could use 'withIndex()'
for.loop.could.use.withIndex=Range can be replaced with withIndex()
replace.index.loop.with.withIndex.quick.fix.text=Replace with 'withIndex()'
replace.negated.0.with.1=Replace negated ''{0}'' with ''{1}''
replace.with.elvis.return.fix.text=Replace with ''?: return{0}''
@@ -9802,6 +9802,11 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/notUsedAsIndex.kt");
}
@TestMetadata("qualifiedReceiverDifferentCollection.kt")
public void testQualifiedReceiverDifferentCollection() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/qualifiedReceiverDifferentCollection.kt");
}
@TestMetadata("rangeTo.kt")
public void testRangeTo() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/rangeTo.kt");
@@ -20,13 +20,14 @@ 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.references.mainReference
import org.jetbrains.kotlin.idea.base.psi.isAssignmentLHS
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.base.psi.replaced
import org.jetbrains.kotlin.idea.base.psi.safeDeparenthesize
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.AbstractRangeInspection.Companion.rangeExpressionByPsi
import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.AbstractRangeInspection.RangeExpression
import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.utils.isPrimitiveRangeType
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix
import org.jetbrains.kotlin.idea.codeinsight.utils.ImplicitReceiverInfo
@@ -40,7 +41,7 @@ import org.jetbrains.kotlin.idea.codeinsight.utils.RangeKtExpressionType.RANGE_T
import org.jetbrains.kotlin.idea.codeinsight.utils.RangeKtExpressionType.RANGE_UNTIL
import org.jetbrains.kotlin.idea.codeinsight.utils.RangeKtExpressionType.UNTIL
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.psi.KtArrayAccessExpression
import org.jetbrains.kotlin.psi.KtBinaryExpression
@@ -101,6 +102,8 @@ class ReplaceManualRangeWithIndicesCallsInspection : KotlinApplicableInspectionB
val indexedAccessUsages: List<SmartPsiElementPointer<KtExpression>>, // Either KtArrayAccessExpression or KtDotQualifiedExpression
)
private val RANGE_CALLABLE_NAMES = setOf("until", "rangeTo", "rangeUntil", "downTo")
private fun getProblemDescription(context: Context): @InspectionMessage String {
return when (context.indexUsagePattern) {
IndexUsagePattern.ELEMENT_LOOP -> KotlinBundle.message("for.loop.over.indices.could.be.replaced.with.loop.over.elements")
@@ -140,11 +143,25 @@ class ReplaceManualRangeWithIndicesCallsInspection : KotlinApplicableInspectionB
*/
private fun KaSession.rangeExpressionByAnalyze(expression: KtExpression): RangeExpression? =
rangeExpressionByPsi(expression)?.takeIf {
val call = expression.resolveToCall()?.singleFunctionCallOrNull()
val packageName = call?.symbol?.callableId?.packageName
packageName != null && packageName.startsWith(Name.identifier("kotlin"))
val callableId = expression.resolveToCall()?.singleFunctionCallOrNull()?.symbol?.callableId
callableId != null && isStdlibRangeFunction(callableId)
}
private fun isStdlibRangeFunction(callableId: CallableId): Boolean {
val callableName = callableId.callableName.asString()
if (callableName !in RANGE_CALLABLE_NAMES) return false
// Extension functions in kotlin.ranges (until, rangeUntil, downTo, and some rangeTo overloads)
if (callableId.packageName == StandardClassIds.BASE_RANGES_PACKAGE) return true
// Member functions on primitive types (Int.rangeTo, Int.rangeUntil, etc.)
if (callableName == "rangeTo" || callableName == "rangeUntil") {
return callableId.classId?.isPrimitiveRangeType() == true
}
return false
}
private fun KaSession.prepareContextForRange(range: RangeExpression): Context? {
val (_, right) = range.arguments
@@ -305,8 +322,26 @@ class ReplaceManualRangeWithIndicesCallsInspection : KotlinApplicableInspectionB
private fun receiversMatch(accessReceiver: KtExpression?, explicitReceiver: KtExpression?): Boolean {
if (explicitReceiver == null) return accessReceiver?.safeDeparenthesize() is KtThisExpression
if (accessReceiver == null) return false
return (accessReceiver as? KtNameReferenceExpression)?.mainReference?.resolve() ==
(explicitReceiver as? KtNameReferenceExpression)?.mainReference?.resolve()
val access = accessReceiver.safeDeparenthesize()
val explicit = explicitReceiver.safeDeparenthesize()
return when {
access is KtNameReferenceExpression && explicit is KtNameReferenceExpression -> {
access.mainReference.resolve() == explicit.mainReference.resolve()
}
access is KtDotQualifiedExpression && explicit is KtDotQualifiedExpression -> {
access.selectorExpression?.text == explicit.selectorExpression?.text &&
receiversMatch(access.receiverExpression, explicit.receiverExpression)
}
access is KtThisExpression && explicit is KtThisExpression -> {
access.labelQualifier?.text == explicit.labelQualifier?.text
}
else -> false
}
}
private fun createQuickFixes(context: Context): List<KotlinModCommandQuickFix<KtExpression>> {
@@ -22,6 +22,7 @@ import org.jetbrains.kotlin.idea.codeinsights.impl.base.isOptInSatisfied
import org.jetbrains.kotlin.idea.compiler.configuration.IdeKotlinVersion
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinJpsPluginSettings
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.psi.KtElement
context(_: KaSession)
@@ -63,3 +64,24 @@ val KaType.isFloatingPointType: Boolean
private val OPEN_END_RANGE_CLASS_ID = ClassId.fromString("kotlin/ranges/OpenEndRange")
private val EXPERIMENTAL_STDLIB_API_CLASS_ID = ClassId.fromString("kotlin/ExperimentalStdlibApi")
/**
* ClassIds of primitive types that support rangeTo/rangeUntil operations.
* These are the types that have member functions like Int.rangeTo(Int).
*/
private val PRIMITIVE_RANGE_TYPE_CLASS_IDS: Set<ClassId> = setOf(
StandardClassIds.Int,
StandardClassIds.Long,
StandardClassIds.Short,
StandardClassIds.Byte,
StandardClassIds.Char,
StandardClassIds.UInt,
StandardClassIds.ULong,
StandardClassIds.UShort,
StandardClassIds.UByte,
)
/**
* Checks if this ClassId represents a primitive type that supports range operations (rangeTo/rangeUntil).
*/
fun ClassId.isPrimitiveRangeType(): Boolean = this in PRIMITIVE_RANGE_TYPE_CLASS_IDS
@@ -14363,220 +14363,6 @@ public abstract class LocalInspectionTestGenerated extends AbstractK1LocalInspec
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls")
public static class ReplaceManualRangeWithIndicesCalls extends AbstractK1LocalInspectionTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K1;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("asReceiver.kt")
public void testAsReceiver() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/asReceiver.kt");
}
@TestMetadata("complexReceiver.kt")
public void testComplexReceiver() throws Exception {
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");
}
@TestMetadata("getMethod.kt")
public void testGetMethod() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/getMethod.kt");
}
@TestMetadata("getMethodDifferentArray.kt")
public void testGetMethodDifferentArray() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/getMethodDifferentArray.kt");
}
@TestMetadata("implicitReceiver.kt")
public void testImplicitReceiver() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/implicitReceiver.kt");
}
@TestMetadata("implicitReceiverFor.kt")
public void testImplicitReceiverFor() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/implicitReceiverFor.kt");
}
@TestMetadata("implicitReceiverRangeUntil.kt")
public void testImplicitReceiverRangeUntil() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/implicitReceiverRangeUntil.kt");
}
@TestMetadata("implicitReceiverUntil.kt")
public void testImplicitReceiverUntil() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/implicitReceiverUntil.kt");
}
@TestMetadata("indexInLvalue.kt")
public void testIndexInLvalue() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/indexInLvalue.kt");
}
@TestMetadata("lastIndex.kt")
public void testLastIndex() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/lastIndex.kt");
}
@TestMetadata("lengthLiteralConstant.kt")
public void testLengthLiteralConstant() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/lengthLiteralConstant.kt");
}
@TestMetadata("lengthVariable.kt")
public void testLengthVariable() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/lengthVariable.kt");
}
@TestMetadata("map.kt")
public void testMap() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/map.kt");
}
@TestMetadata("multidimensionalArray2.kt")
public void testMultidimensionalArray2() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/multidimensionalArray2.kt");
}
@TestMetadata("multipleImplicitReceivers.kt")
public void testMultipleImplicitReceivers() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/multipleImplicitReceivers.kt");
}
@TestMetadata("multipleImplicitReceivers2.kt")
public void testMultipleImplicitReceivers2() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/multipleImplicitReceivers2.kt");
}
@TestMetadata("notUsedAsIndex.kt")
public void testNotUsedAsIndex() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/notUsedAsIndex.kt");
}
@TestMetadata("rangeTo.kt")
public void testRangeTo() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/rangeTo.kt");
}
@TestMetadata("simpleExpression.kt")
public void testSimpleExpression() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/simpleExpression.kt");
}
@TestMetadata("simpleExpressionRangeUntil.kt")
public void testSimpleExpressionRangeUntil() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/simpleExpressionRangeUntil.kt");
}
@TestMetadata("simpleExpressionUntil.kt")
public void testSimpleExpressionUntil() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/simpleExpressionUntil.kt");
}
@TestMetadata("simpleFor.kt")
public void testSimpleFor() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/simpleFor.kt");
}
@TestMetadata("sizeLabeled.kt")
public void testSizeLabeled() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/sizeLabeled.kt");
}
@TestMetadata("sizeLiteralConstant.kt")
public void testSizeLiteralConstant() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/sizeLiteralConstant.kt");
}
@TestMetadata("sizeVariable.kt")
public void testSizeVariable() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/sizeVariable.kt");
}
@TestMetadata("thisReceiver.kt")
public void testThisReceiver() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/thisReceiver.kt");
}
@TestMetadata("thisReceiverFor.kt")
public void testThisReceiverFor() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/thisReceiverFor.kt");
}
@TestMetadata("thisReceiverRangeUntil.kt")
public void testThisReceiverRangeUntil() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/thisReceiverRangeUntil.kt");
}
@TestMetadata("thisReceiverUntil.kt")
public void testThisReceiverUntil() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/thisReceiverUntil.kt");
}
@TestMetadata("typeMismatch.kt")
public void testTypeMismatch() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/typeMismatch.kt");
}
@TestMetadata("usedForOtherArray.kt")
public void testUsedForOtherArray() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/usedForOtherArray.kt");
}
@TestMetadata("withIndexBasic.kt")
public void testWithIndexBasic() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexBasic.kt");
}
@TestMetadata("withIndexElementNameCollision.kt")
public void testWithIndexElementNameCollision() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexElementNameCollision.kt");
}
@TestMetadata("withIndexMultipleArrayAccess.kt")
public void testWithIndexMultipleArrayAccess() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexMultipleArrayAccess.kt");
}
@TestMetadata("withIndexRangeTo.kt")
public void testWithIndexRangeTo() throws Exception {
runTest("testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexRangeTo.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty")
public static class ReplaceNegatedIsEmptyWithIsNotEmpty extends AbstractK1LocalInspectionTest {
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test() {
val element = mutableListOf<String>("hello", "world")
for (i in 0 unt<caret>il element.size) {
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test() {
val element = mutableListOf<String>("hello", "world")
for (element1 in element) {
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test() {
val element = "first"
val element1 = "second"
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test() {
val element = "first"
val element1 = "second"
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test() {
val outer = listOf(listOf("a", "b"), listOf("c", "d"))
for (element in outer) {
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test() {
val outer = listOf(listOf("a", "b"), listOf("c", "d"))
for (element in outer) {
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test(element: String, list: List<String>) {
for (i in 0 unt<caret>il list.size) {
list[i].length
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with loop over elements
// IGNORE_K1
fun test(element: String, list: List<String>) {
for (element1 in list) {
element1.length
@@ -0,0 +1,12 @@
// WITH_STDLIB
// FIX: Replace with indices
// NO_FIX: Replace index loop with collection loop
private class RC(val a: List<Int>, val b: List<Int>)
fun test() {
val rc = RC(a = listOf(1), b = listOf(2))
for (i in <caret>0 until rc.a.size) {
println(rc.a[i] != rc.b[i])
}
}
@@ -0,0 +1,12 @@
// WITH_STDLIB
// FIX: Replace with indices
// NO_FIX: Replace index loop with collection loop
private class RC(val a: List<Int>, val b: List<Int>)
fun test() {
val rc = RC(a = listOf(1), b = listOf(2))
for (i in rc.a.indices) {
println(rc.a[i] != rc.b[i])
}
}
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with 'withIndex()'
// IGNORE_K1
fun test() {
val element = "existing"
val list = listOf("a", "b", "c")
@@ -1,6 +1,5 @@
// WITH_STDLIB
// FIX: Replace with 'withIndex()'
// IGNORE_K1
fun test() {
val element = "existing"
val list = listOf("a", "b", "c")
@@ -836,6 +836,7 @@ private fun assembleWorkspace(): TWorkspace = workspace(KotlinPluginMode.K1) {
"convertFromMultiDollarToRegularString", // K2-only
"javaCollectionsWithNullableTypes", // K2-only
"kdocResolutionResultHasChanged", // K2-only
"replaceManualRangeWithIndicesCalls", // K2-only
"convertLongToDuration", // K2-only
"customComponentDestructuringMigration", // K2-only
)