mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[kotlin] KTIJ-37274 ReplaceManualRangeWithIndicesCallsInspection support withIndex replacement
GitOrigin-RevId: bbb044e2db9897009cce15cb083e618666f23baf
This commit is contained in:
committed by
intellij-monorepo-bot
parent
5792d62c48
commit
c8b48a46eb
@@ -1688,6 +1688,8 @@ 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()'
|
||||
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}''
|
||||
replace.with.return=Replace '!!' with '?: return'
|
||||
|
||||
+20
@@ -9861,6 +9861,26 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp
|
||||
public void testUsedForOtherArray() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/usedForOtherArray.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("withIndexBasic.kt")
|
||||
public void testWithIndexBasic() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexBasic.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("withIndexElementNameCollision.kt")
|
||||
public void testWithIndexElementNameCollision() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexElementNameCollision.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("withIndexMultipleArrayAccess.kt")
|
||||
public void testWithIndexMultipleArrayAccess() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexMultipleArrayAccess.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("withIndexRangeTo.kt")
|
||||
public void testWithIndexRangeTo() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/replaceManualRangeWithIndicesCalls/withIndexRangeTo.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
+199
-52
@@ -1,12 +1,20 @@
|
||||
// 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.codeInsight.inspections.shared
|
||||
|
||||
import com.intellij.codeInspection.InspectionManager
|
||||
import com.intellij.codeInspection.ProblemDescriptor
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.codeInspection.util.InspectionMessage
|
||||
import com.intellij.codeInspection.util.IntentionFamilyName
|
||||
import com.intellij.modcommand.ModPsiUpdater
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.SmartPsiElementPointer
|
||||
import com.intellij.psi.createSmartPointer
|
||||
import com.intellij.psi.search.searches.ReferencesSearch
|
||||
import org.jetbrains.kotlin.analysis.api.KaSession
|
||||
import org.jetbrains.kotlin.analysis.api.resolution.singleFunctionCallOrNull
|
||||
import org.jetbrains.kotlin.analysis.api.resolution.symbol
|
||||
import org.jetbrains.kotlin.analysis.api.resolution.successfulVariableAccessCall
|
||||
import org.jetbrains.kotlin.analysis.api.types.KaType
|
||||
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinDeclarationNameValidator
|
||||
@@ -15,6 +23,9 @@ 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
|
||||
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.api.applicable.inspections.KotlinApplicableInspectionBase
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix
|
||||
import org.jetbrains.kotlin.idea.codeinsight.utils.ImplicitReceiverInfo
|
||||
import org.jetbrains.kotlin.idea.codeinsight.utils.LoopToCollectionTransformUtils
|
||||
@@ -25,6 +36,7 @@ import org.jetbrains.kotlin.idea.codeinsight.utils.RangeKtExpressionType.RANGE_U
|
||||
import org.jetbrains.kotlin.idea.codeinsight.utils.RangeKtExpressionType.UNTIL
|
||||
import org.jetbrains.kotlin.idea.codeinsight.utils.getImplicitReceiverInfo
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.name.StandardClassIds
|
||||
import org.jetbrains.kotlin.psi.KtArrayAccessExpression
|
||||
import org.jetbrains.kotlin.psi.KtBinaryExpression
|
||||
@@ -39,6 +51,8 @@ import org.jetbrains.kotlin.psi.KtPsiUtil
|
||||
import org.jetbrains.kotlin.psi.KtQualifiedExpression
|
||||
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
|
||||
import org.jetbrains.kotlin.psi.KtThisExpression
|
||||
import org.jetbrains.kotlin.psi.KtVisitor
|
||||
import org.jetbrains.kotlin.psi.KtVisitorVoid
|
||||
import org.jetbrains.kotlin.psi.createExpressionByPattern
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parents
|
||||
@@ -47,26 +61,56 @@ import org.jetbrains.kotlin.util.match
|
||||
|
||||
/**
|
||||
* Inspection that suggests replacing manual ranges like `0..<arr.size` with `arr.indices` calls.
|
||||
* Extends KotlinApplicableInspectionBase directly (not Simple) to support multiple quick fixes.
|
||||
*/
|
||||
class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<ReplaceManualRangeWithIndicesCallsInspection.Context>() {
|
||||
class ReplaceManualRangeWithIndicesCallsInspection : KotlinApplicableInspectionBase<KtExpression, ReplaceManualRangeWithIndicesCallsInspection.Context>() {
|
||||
|
||||
/**
|
||||
* Describes how the loop index variable is used in the loop body,
|
||||
* determining which quick fixes are applicable.
|
||||
*/
|
||||
enum class IndexUsagePattern {
|
||||
/**
|
||||
* Index is only used for array access: `arr[i]`.
|
||||
* Can transform to: `for (element in arr)`
|
||||
*/
|
||||
ELEMENT_LOOP,
|
||||
|
||||
/**
|
||||
* Index is used both for array access and other purposes: `println("$i: ${arr[i]}")`.
|
||||
* Can transform to: `for ((i, element) in arr.withIndex())`
|
||||
*/
|
||||
WITH_INDEX,
|
||||
|
||||
/**
|
||||
* Index is not used for array access, or range is not inside a for loop.
|
||||
* Can only replace range with `.indices`: `for (i in arr.indices)`
|
||||
*/
|
||||
INDICES_ONLY
|
||||
}
|
||||
|
||||
data class Context(
|
||||
val explicitReceiver: KtExpression?,
|
||||
val suggestElementLoop: Boolean,
|
||||
val indexUsagePattern: IndexUsagePattern,
|
||||
val implicitReceiverInfo: ImplicitReceiverInfo?,
|
||||
val suggestedElementName: String?,
|
||||
val arrayAccessUsages: List<SmartPsiElementPointer<KtArrayAccessExpression>>,
|
||||
)
|
||||
|
||||
override fun getProblemDescription(
|
||||
range: RangeExpression,
|
||||
context: Context
|
||||
): @InspectionMessage String {
|
||||
return if (context.suggestElementLoop) {
|
||||
KotlinBundle.message("for.loop.over.indices.could.be.replaced.with.loop.over.elements")
|
||||
} else KotlinBundle.message("range.could.be.replaced.with.indices.call")
|
||||
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")
|
||||
IndexUsagePattern.WITH_INDEX -> KotlinBundle.message("for.loop.could.use.withIndex")
|
||||
IndexUsagePattern.INDICES_ONLY -> KotlinBundle.message("range.could.be.replaced.with.indices.call")
|
||||
}
|
||||
}
|
||||
|
||||
override fun isApplicableByPsi(range: RangeExpression): Boolean {
|
||||
override fun isApplicableByPsi(element: KtExpression): Boolean {
|
||||
val range = rangeExpressionByPsi(element) ?: return false
|
||||
return isApplicableByPsi(range)
|
||||
}
|
||||
|
||||
private fun isApplicableByPsi(range: RangeExpression): Boolean {
|
||||
// Only ascending ranges are candidates for indices replacement
|
||||
if (range.type == DOWN_TO) return false
|
||||
|
||||
@@ -81,18 +125,34 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
|
||||
return extractTargetExpression(range.type, right) != null
|
||||
}
|
||||
|
||||
override fun KaSession.prepareContext(range: RangeExpression): Context? {
|
||||
override fun KaSession.prepareContext(element: KtExpression): Context? {
|
||||
val range = rangeExpressionByAnalyze(element) ?: return null
|
||||
return prepareContextForRange(range)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [RangeExpression] only if it resolves to a Kotlin stdlib range function.
|
||||
* This prevents false positives when custom `until`/`rangeTo` functions are defined.
|
||||
*/
|
||||
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"))
|
||||
}
|
||||
|
||||
private fun KaSession.prepareContextForRange(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
|
||||
|
||||
val suggestElementLoop = shouldSuggestElementLoop(range, explicitReceiver)
|
||||
val indexUsageAnalysis = analyzeIndexUsagePattern(range, explicitReceiver)
|
||||
val implicitReceiverInfo = if (explicitReceiver == null) sizeCall.getImplicitReceiverInfo() else null
|
||||
|
||||
// Pre-compute a unique element name for the loop transformation
|
||||
val suggestedElementName = if (suggestElementLoop) {
|
||||
// Pre-compute a unique element name for the loop transformation (for ELEMENT_LOOP and WITH_INDEX patterns)
|
||||
val suggestedElementName = if (indexUsageAnalysis.pattern != IndexUsagePattern.INDICES_ONLY) {
|
||||
val forExpression = findContainingForLoop(range)
|
||||
forExpression?.loopParameter?.let { loopParameter ->
|
||||
val nameValidator = KotlinDeclarationNameValidator(
|
||||
@@ -104,7 +164,16 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
|
||||
}
|
||||
} else null
|
||||
|
||||
return Context(explicitReceiver, suggestElementLoop, implicitReceiverInfo, suggestedElementName)
|
||||
// Store array access usages as smart pointers for use in quick fixes
|
||||
val arrayAccessPointers = indexUsageAnalysis.arrayAccessUsages.map { it.createSmartPointer() }
|
||||
|
||||
return Context(
|
||||
explicitReceiver,
|
||||
indexUsageAnalysis.pattern,
|
||||
implicitReceiverInfo,
|
||||
suggestedElementName,
|
||||
arrayAccessPointers
|
||||
)
|
||||
}
|
||||
|
||||
private fun KaSession.rightBound(type: RangeKtExpressionType, expression: KtExpression): KtExpression? {
|
||||
@@ -161,22 +230,50 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if we should suggest converting the index loop to an element loop.
|
||||
* This checks if the loop parameter is used only as array index(es) with the same receiver.
|
||||
* Result of analyzing how the loop index variable is used in the loop body.
|
||||
*/
|
||||
private fun shouldSuggestElementLoop(range: RangeExpression, explicitReceiver: KtExpression?): Boolean {
|
||||
val forExpression = findContainingForLoop(range) ?: return false
|
||||
val loopParam = forExpression.loopParameter?.originalElement ?: return false
|
||||
private data class IndexUsageAnalysis(
|
||||
val pattern: IndexUsagePattern,
|
||||
val arrayAccessUsages: List<KtArrayAccessExpression>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Analyzes how the loop index variable is used in the loop body.
|
||||
*
|
||||
* @return IndexUsageAnalysis containing:
|
||||
* - pattern: ELEMENT_LOOP (all array accesses), WITH_INDEX (mixed), or INDICES_ONLY (no array accesses)
|
||||
* - arrayAccessUsages: list of valid array access expressions for transformation
|
||||
*/
|
||||
private fun analyzeIndexUsagePattern(range: RangeExpression, explicitReceiver: KtExpression?): IndexUsageAnalysis {
|
||||
val forExpression = findContainingForLoop(range)
|
||||
?: return IndexUsageAnalysis(IndexUsagePattern.INDICES_ONLY, emptyList())
|
||||
val loopParam = forExpression.loopParameter?.originalElement
|
||||
?: return IndexUsageAnalysis(IndexUsagePattern.INDICES_ONLY, emptyList())
|
||||
|
||||
val usages = ReferencesSearch.search(loopParam).findAll()
|
||||
if (usages.isEmpty()) return false
|
||||
if (usages.isEmpty()) return IndexUsageAnalysis(IndexUsagePattern.INDICES_ONLY, emptyList())
|
||||
|
||||
// Check that all usages are valid array accesses with the same receiver
|
||||
return usages.all { reference ->
|
||||
val validArrayAccesses = mutableListOf<KtArrayAccessExpression>()
|
||||
var otherUsageCount = 0
|
||||
|
||||
for (reference in usages) {
|
||||
val usage = reference.element
|
||||
val arrayAccess = usage.parents.match(KtContainerNode::class, last = KtArrayAccessExpression::class)
|
||||
arrayAccess != null && isValidElementLoopCandidate(arrayAccess, explicitReceiver)
|
||||
|
||||
if (arrayAccess != null && isValidElementLoopCandidate(arrayAccess, explicitReceiver)) {
|
||||
validArrayAccesses.add(arrayAccess)
|
||||
} else {
|
||||
otherUsageCount++
|
||||
}
|
||||
}
|
||||
|
||||
val pattern = when {
|
||||
validArrayAccesses.isNotEmpty() && otherUsageCount == 0 -> IndexUsagePattern.ELEMENT_LOOP
|
||||
validArrayAccesses.isNotEmpty() && otherUsageCount > 0 -> IndexUsagePattern.WITH_INDEX
|
||||
else -> IndexUsagePattern.INDICES_ONLY
|
||||
}
|
||||
|
||||
return IndexUsageAnalysis(pattern, validArrayAccesses)
|
||||
}
|
||||
|
||||
private fun findContainingForLoop(range: RangeExpression): KtForExpression? =
|
||||
@@ -203,12 +300,41 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
|
||||
return parent !is KtBinaryExpression || parent.left != arrayAccess || parent.operationToken !in KtTokens.ALL_ASSIGNMENTS
|
||||
}
|
||||
|
||||
override fun createQuickFix(
|
||||
range: RangeExpression,
|
||||
context: Context
|
||||
): KotlinModCommandQuickFix<KtExpression> {
|
||||
return if (context.suggestElementLoop) ReplaceIndexLoopWithCollectionLoopQuickFix(context)
|
||||
else ReplaceManualRangeWithIndicesCallQuickFix(context)
|
||||
private fun createQuickFixes(context: Context): List<KotlinModCommandQuickFix<KtExpression>> {
|
||||
val indicesFix = ReplaceManualRangeWithIndicesCallQuickFix(context)
|
||||
return when (context.indexUsagePattern) {
|
||||
IndexUsagePattern.ELEMENT_LOOP -> listOf(ReplaceIndexLoopWithCollectionLoopQuickFix(context), indicesFix)
|
||||
IndexUsagePattern.WITH_INDEX -> listOf(ReplaceWithWithIndexLoopQuickFix(context), indicesFix)
|
||||
IndexUsagePattern.INDICES_ONLY -> listOf(indicesFix)
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitor<*, *> =
|
||||
object : KtVisitorVoid() {
|
||||
override fun visitBinaryExpression(expression: KtBinaryExpression) {
|
||||
visitTargetElement(expression, holder, isOnTheFly)
|
||||
}
|
||||
|
||||
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
|
||||
visitTargetElement(expression, holder, isOnTheFly)
|
||||
}
|
||||
}
|
||||
|
||||
override fun InspectionManager.createProblemDescriptor(
|
||||
element: KtExpression,
|
||||
context: Context,
|
||||
rangeInElement: com.intellij.openapi.util.TextRange?,
|
||||
onTheFly: Boolean,
|
||||
): ProblemDescriptor {
|
||||
val fixes = createQuickFixes(context)
|
||||
return createProblemDescriptor(
|
||||
element,
|
||||
rangeInElement,
|
||||
getProblemDescription(context),
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
onTheFly,
|
||||
*fixes.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,29 +390,14 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
|
||||
val loopRange = forExpression.loopRange ?: return
|
||||
val elementName = context.suggestedElementName ?: return
|
||||
|
||||
// Get writable array accesses from smart pointers
|
||||
val arrayAccesses = context.arrayAccessUsages.mapNotNull { pointer ->
|
||||
pointer.element?.let { updater.getWritable(it) }
|
||||
}
|
||||
if (arrayAccesses.isEmpty()) 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, elementName)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle multiple usages
|
||||
val loopParam = loopParameter.originalElement ?: return
|
||||
val usages = ReferencesSearch.search(loopParam).findAll()
|
||||
val usageInfos = usages.mapNotNull { reference ->
|
||||
val usage = reference.element
|
||||
val arrayAccess = usage.parents.match(KtContainerNode::class, last = KtArrayAccessExpression::class)
|
||||
if (arrayAccess != null) {
|
||||
LoopToCollectionTransformUtils.LoopUsageInfo(loopParameter, usage, arrayAccess)
|
||||
} else null
|
||||
}
|
||||
|
||||
if (usageInfos.isNotEmpty()) {
|
||||
LoopToCollectionTransformUtils.transformLoop(project, usageInfos, loopParameter, loopRange, collection, elementName)
|
||||
}
|
||||
LoopToCollectionTransformUtils.transformLoop(project, arrayAccesses, loopParameter, loopRange, collection, elementName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,4 +417,40 @@ class ReplaceManualRangeWithIndicesCallsInspection : AbstractRangeInspection<Rep
|
||||
return KtPsiFactory(project).createThisExpression()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class ReplaceWithWithIndexLoopQuickFix(
|
||||
private val context: Context,
|
||||
) : KotlinModCommandQuickFix<KtExpression>() {
|
||||
override fun getFamilyName(): @IntentionFamilyName String =
|
||||
KotlinBundle.message("replace.index.loop.with.withIndex.quick.fix.text")
|
||||
|
||||
override fun applyFix(project: Project, element: KtExpression, updater: ModPsiUpdater) {
|
||||
val forExpression = element.getStrictParentOfType<KtForExpression>() ?: return
|
||||
val loopParameter = forExpression.loopParameter ?: return
|
||||
val loopRange = forExpression.loopRange ?: return
|
||||
val elementName = context.suggestedElementName ?: return
|
||||
|
||||
// Get writable array accesses from smart pointers
|
||||
val arrayAccesses = context.arrayAccessUsages.mapNotNull { pointer ->
|
||||
pointer.element?.let { updater.getWritable(it) }
|
||||
}
|
||||
if (arrayAccesses.isEmpty()) return
|
||||
|
||||
val collection = resolveCollectionExpression(project)
|
||||
LoopToCollectionTransformUtils.transformLoopWithIndex(
|
||||
project, arrayAccesses, loopParameter, loopRange, collection, elementName
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveCollectionExpression(project: Project): KtExpression {
|
||||
context.explicitReceiver?.let { return it }
|
||||
|
||||
context.implicitReceiverInfo?.takeUnless { it.isUnambiguousLabel }?.let {
|
||||
return createImplicitThis(project, it)
|
||||
}
|
||||
|
||||
return KtPsiFactory(project).createThisExpression()
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-34
@@ -2,16 +2,14 @@
|
||||
package org.jetbrains.kotlin.idea.codeinsight.utils
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.search.searches.ReferencesSearch
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.kotlin.psi.KtArrayAccessExpression
|
||||
import org.jetbrains.kotlin.psi.KtContainerNode
|
||||
import org.jetbrains.kotlin.psi.KtBlockStringTemplateEntry
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtParameter
|
||||
import org.jetbrains.kotlin.psi.KtPsiFactory
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parents
|
||||
import org.jetbrains.kotlin.util.match
|
||||
import org.jetbrains.kotlin.psi.createExpressionByPattern
|
||||
import org.jetbrains.kotlin.psi.psiUtil.canPlaceAfterSimpleNameEntry
|
||||
|
||||
/**
|
||||
* Utility functions for transforming index-based loops to collection loops.
|
||||
@@ -20,36 +18,13 @@ import org.jetbrains.kotlin.util.match
|
||||
object LoopToCollectionTransformUtils {
|
||||
|
||||
/**
|
||||
* Data class representing the result of finding loop usage patterns.
|
||||
*/
|
||||
data class LoopUsageInfo(
|
||||
val paramElement: KtParameter,
|
||||
val usageElement: PsiElement,
|
||||
val arrayAccessElement: KtArrayAccessExpression
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun findSingleArrayAccessUsage(loopParameter: KtParameter): LoopUsageInfo? {
|
||||
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:
|
||||
* Transforms an index-based loop to a collection-based loop by:
|
||||
* 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 arrayAccesses array access expressions to replace with element references
|
||||
* @param loopParameter the original loop parameter
|
||||
* @param loopRange the original loop range
|
||||
* @param newLoopRange the new collection expression to iterate over
|
||||
@@ -57,7 +32,7 @@ object LoopToCollectionTransformUtils {
|
||||
*/
|
||||
fun transformLoop(
|
||||
project: Project,
|
||||
usageInfos: List<LoopUsageInfo>,
|
||||
arrayAccesses: List<KtArrayAccessExpression>,
|
||||
loopParameter: KtParameter,
|
||||
loopRange: KtExpression,
|
||||
newLoopRange: KtExpression,
|
||||
@@ -66,11 +41,58 @@ object LoopToCollectionTransformUtils {
|
||||
val factory = KtPsiFactory(project)
|
||||
|
||||
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())
|
||||
arrayAccesses.forEach { arrayAccess ->
|
||||
replaceArrayAccessWithElement(factory, arrayAccess, elementName)
|
||||
}
|
||||
|
||||
loopParameter.replace(newParameter)
|
||||
loopRange.replace(newLoopRange)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an array access expression with a simple element reference.
|
||||
* When inside a string template ${...}, creates the simplified $name form directly if possible.
|
||||
*/
|
||||
private fun replaceArrayAccessWithElement(factory: KtPsiFactory, arrayAccess: KtArrayAccessExpression, elementName: String) {
|
||||
val blockEntry = arrayAccess.parent as? KtBlockStringTemplateEntry
|
||||
if (blockEntry != null && canPlaceAfterSimpleNameEntry(blockEntry.nextSibling)) {
|
||||
blockEntry.replace(factory.createSimpleNameStringTemplateEntry(elementName))
|
||||
} else {
|
||||
arrayAccess.replace(factory.createExpression(elementName))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an index-based loop to a withIndex() loop by:
|
||||
* 1. Replacing the loop parameter with a destructuring declaration (indexName, elementName)
|
||||
* 2. Replacing array access expressions with direct element references
|
||||
* 3. Replacing the loop range with collection.withIndex()
|
||||
* @param project the current project
|
||||
* @param arrayAccesses array access expressions to replace with element references
|
||||
* @param loopParameter the original loop parameter (the index variable)
|
||||
* @param loopRange the original loop range
|
||||
* @param collectionExpression the collection to iterate with withIndex()
|
||||
* @param elementName the name for the new element variable
|
||||
*/
|
||||
fun transformLoopWithIndex(
|
||||
project: Project,
|
||||
arrayAccesses: List<KtArrayAccessExpression>,
|
||||
loopParameter: KtParameter,
|
||||
loopRange: KtExpression,
|
||||
collectionExpression: KtExpression,
|
||||
elementName: String
|
||||
) {
|
||||
val factory = KtPsiFactory(project)
|
||||
|
||||
val indexName = loopParameter.name ?: "index"
|
||||
val newParameter = factory.createDestructuringParameter("($indexName, $elementName)")
|
||||
val newLoopRange = factory.createExpressionByPattern("$0.withIndex()", collectionExpression)
|
||||
|
||||
// Replace all array access expressions with direct element references
|
||||
arrayAccesses.forEach { arrayAccess ->
|
||||
replaceArrayAccessWithElement(factory, arrayAccess, elementName)
|
||||
}
|
||||
|
||||
loopParameter.replace(newParameter)
|
||||
|
||||
+20
@@ -14545,6 +14545,26 @@ public abstract class LocalInspectionTestGenerated extends AbstractK1LocalInspec
|
||||
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)
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
class Person(val name: String)
|
||||
fun getName(person: Person) {
|
||||
for (i in 0 until<caret> person.name.length) {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
class Person(val name: String)
|
||||
fun getName(person: Person) {
|
||||
for (element in person.name) {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val element = mutableListOf<String>("hello", "world")
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val element = mutableListOf<String>("hello", "world")
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val element = "first"
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val element = "first"
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val outer = listOf(listOf("a", "b"), listOf("c", "d"))
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val outer = listOf(listOf("a", "b"), listOf("c", "d"))
|
||||
|
||||
+1
@@ -1,4 +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) {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
// IGNORE_K1
|
||||
fun test(element: String, list: List<String>) {
|
||||
for (element1 in list) {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun Array<String>.test() {
|
||||
for (index in <caret>0..size - 1) {
|
||||
val out = this[index]
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun Array<String>.test() {
|
||||
for (element in this) {
|
||||
val out = element
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun foo() {
|
||||
val intArray = intArrayOf(1, 2, 3, 4, 5)
|
||||
for (i in 0.<caret>.intArray.lastIndex) {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun foo() {
|
||||
val intArray = intArrayOf(1, 2, 3, 4, 5)
|
||||
for (element in intArray) {
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun IntArray.arrayToString(): String = buildString {
|
||||
for (i in <caret>0 until size) {
|
||||
append(this@arrayToString[i])
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun IntArray.arrayToString(): String = buildString {
|
||||
for (element in this@arrayToString) {
|
||||
append(element)
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun LongArray.arrayToString(): String = buildString {
|
||||
for (i in <caret>0 until size) {
|
||||
if (i > 0) append(", ")
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun LongArray.arrayToString(): String = buildString {
|
||||
for (i in this@arrayToString.indices) {
|
||||
for ((i, element) in this@arrayToString.withIndex()) {
|
||||
if (i > 0) append(", ")
|
||||
append(this@arrayToString[i])
|
||||
append(element)
|
||||
}
|
||||
}
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun test(args: Array<String>) {
|
||||
for (index in 0..ar<caret>gs.size - 1) {
|
||||
val out = args[index]
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun test(args: Array<String>) {
|
||||
for (element in args) {
|
||||
val out = element
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun main() {
|
||||
println(listOf(1, 2, 3).doubled())
|
||||
}
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun main() {
|
||||
println(listOf(1, 2, 3).doubled())
|
||||
}
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun Array<String>.test() {
|
||||
for (index in <caret>0..this.size - 1) {
|
||||
val out = this[index]
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with loop over elements
|
||||
fun Array<String>.test() {
|
||||
for (element in this) {
|
||||
val out = element
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun test() {
|
||||
val list = listOf("a", "b", "c")
|
||||
for (i in 0 unt<caret>il list.size) {
|
||||
println("Index $i: ${list[i]}")
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun test() {
|
||||
val list = listOf("a", "b", "c")
|
||||
for ((i, element) in list.withIndex()) {
|
||||
println("Index $i: $element")
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val element = "existing"
|
||||
val list = listOf("a", "b", "c")
|
||||
for (i in 0 unt<caret>il list.size) {
|
||||
println("Index $i: ${list[i]}, also $element")
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
// IGNORE_K1
|
||||
fun test() {
|
||||
val element = "existing"
|
||||
val list = listOf("a", "b", "c")
|
||||
for ((i, element1) in list.withIndex()) {
|
||||
println("Index $i: $element1, also $element")
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun test() {
|
||||
val list = listOf("a", "b", "c")
|
||||
for (i in 0..<caret><list.size) {
|
||||
println("Processing index $i")
|
||||
val item = list[i]
|
||||
println("Item: $item, also ${list[i].length}")
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun test() {
|
||||
val list = listOf("a", "b", "c")
|
||||
for ((i, element) in list.withIndex()) {
|
||||
println("Processing index $i")
|
||||
val item = element
|
||||
println("Item: $item, also ${element.length}")
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun test() {
|
||||
val arr = arrayOf(1, 2, 3)
|
||||
for (idx in 0.<caret>.arr.size - 1) {
|
||||
println("arr[$idx] = ${arr[idx]}")
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// WITH_STDLIB
|
||||
// FIX: Replace with 'withIndex()'
|
||||
fun test() {
|
||||
val arr = arrayOf(1, 2, 3)
|
||||
for ((idx, element) in arr.withIndex()) {
|
||||
println("arr[$idx] = $element")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user