Suggest to replace manual range with explicit indices or iterable
#KT-14344 Fixed
This commit is contained in:
committed by
Mikhail Glukhikh
parent
8cda5cb849
commit
3451c60f93
@@ -357,6 +357,10 @@ class KtPsiFactory @JvmOverloads constructor(private val project: Project, val m
|
||||
return createClass("class A($text)").primaryConstructorParameters.first()
|
||||
}
|
||||
|
||||
fun createLoopParameter(text: String): KtParameter {
|
||||
return (createExpression("for ($text in list) {}") as KtForExpression).loopParameter!!
|
||||
}
|
||||
|
||||
fun createParameterList(text: String): KtParameterList {
|
||||
return createFunction("fun foo$text{}").valueParameterList!!
|
||||
}
|
||||
|
||||
@@ -1820,6 +1820,15 @@
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceManualRangeWithIndicesCallsInspection"
|
||||
displayName="Convert manual ranges to indices or iteration over collection"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFO"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions.IfThenToElvisInspection"
|
||||
displayName="If-Then foldable to '?:'"
|
||||
groupPath="Kotlin"
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
This inspection reports <b>until</b> and <b>..</b> operator usages that are replaceable with <b>Collection.indices</b>
|
||||
or iteration over collection inside <b>for</b> loop.</body>
|
||||
</html>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.inspections
|
||||
|
||||
import com.intellij.codeInspection.LocalQuickFix
|
||||
import com.intellij.codeInspection.ProblemDescriptor
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.search.searches.ReferencesSearch
|
||||
import org.jetbrains.kotlin.idea.intentions.getArguments
|
||||
import org.jetbrains.kotlin.idea.intentions.isSizeOrLength
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
|
||||
|
||||
class ReplaceManualRangeWithIndicesCallsInspection : AbstractKotlinInspection() {
|
||||
val rangeFunctions = setOf("until", "rangeTo")
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : KtVisitorVoid() {
|
||||
override fun visitBinaryExpression(binaryExpression: KtBinaryExpression) {
|
||||
val find = rangeFunctions.find { it == binaryExpression.operationReference.text }
|
||||
if (binaryExpression.operationToken == KtTokens.RANGE || find != null) {
|
||||
val operator = find ?: "rangeTo"
|
||||
visitRange(holder, binaryExpression, binaryExpression.left ?: return, binaryExpression.right ?: return, operator)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
|
||||
val find = rangeFunctions.find { it == expression.selectorExpression?.text }
|
||||
if (find != null) {
|
||||
val call = (expression.parent as? KtCallExpression) ?: return
|
||||
val firstArg = call.valueArguments.first().getArgumentExpression() ?: return
|
||||
visitRange(holder, expression, expression.receiverExpression, firstArg, find)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun visitRange(holder: ProblemsHolder, expression: KtExpression, left: KtExpression, right: KtExpression, method: String) {
|
||||
if ((method == "until" && left.toIntConstant() == 0 && right.receiverIfIsSizeOrLengthCall() != null) ||
|
||||
(method == "rangeTo" && left.toIntConstant() == 0 && right.receiverIfIsSizeOrLengthMinusOneCall() != null)
|
||||
) {
|
||||
visitIndicesRange(holder, expression)
|
||||
}
|
||||
}
|
||||
|
||||
private fun visitIndicesRange(holder: ProblemsHolder, range: KtExpression) {
|
||||
val parent = range.parent.parent
|
||||
if (parent is KtForExpression) {
|
||||
val paramElement = parent.loopParameter?.originalElement ?: return
|
||||
val usageElement = ReferencesSearch.search(paramElement).singleOrNull()?.element
|
||||
val arrayAccess = usageElement?.parent?.parent as? KtArrayAccessExpression
|
||||
if (arrayAccess != null && arrayAccess.indexExpressions.singleOrNull() == usageElement) {
|
||||
val arrayAccessParent = arrayAccess.parent
|
||||
if (arrayAccessParent !is KtBinaryExpression ||
|
||||
arrayAccessParent.left != arrayAccess ||
|
||||
arrayAccessParent.operationToken !in KtTokens.ALL_ASSIGNMENTS
|
||||
) {
|
||||
holder.registerProblem(
|
||||
range,
|
||||
"For loop over indices could be replaced with loop over elements",
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
ReplaceIndexLoopWithCollectionLoopQuickFix()
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.registerProblem(
|
||||
range,
|
||||
"Range could be replaced with '.indices' call",
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
ReplaceManualRangeWithIndicesCallQuickFix()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ReplaceManualRangeWithIndicesCallQuickFix : LocalQuickFix {
|
||||
override fun getName() = "Replace with indices"
|
||||
|
||||
override fun getFamilyName() = name
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val element = descriptor.psiElement as KtExpression
|
||||
val args = element.getArguments() ?: return
|
||||
if (args.second is KtBinaryExpression) {
|
||||
val second = (args.second as? KtBinaryExpression) ?: return
|
||||
replaceWithIndices(element, (second.left as? KtDotQualifiedExpression)?.receiverExpression ?: return)
|
||||
} else {
|
||||
replaceWithIndices(element, (args.second as? KtDotQualifiedExpression)?.receiverExpression ?: return)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceWithIndices(toReplace: KtExpression, receiver: KtExpression) {
|
||||
toReplace.replace(KtPsiFactory(toReplace).createExpressionByPattern("$0.indices", receiver))
|
||||
}
|
||||
}
|
||||
|
||||
class ReplaceIndexLoopWithCollectionLoopQuickFix : LocalQuickFix {
|
||||
override fun getName() = "Replace with loop over elements"
|
||||
|
||||
override fun getFamilyName() = name
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val element = descriptor.psiElement.getStrictParentOfType<KtForExpression>() ?: return
|
||||
val loopParameter = element.loopParameter ?: return
|
||||
val loopRange = element.loopRange ?: return
|
||||
val collectionParent = when (loopRange) {
|
||||
is KtDotQualifiedExpression -> (loopRange.parent as? KtCallExpression)?.valueArguments?.firstOrNull()?.getArgumentExpression()
|
||||
is KtBinaryExpression -> loopRange.right
|
||||
else -> null
|
||||
} ?: return
|
||||
val collection =
|
||||
collectionParent.receiverIfIsSizeOrLengthCall() ?: collectionParent.receiverIfIsSizeOrLengthMinusOneCall() ?: return
|
||||
val paramElement = loopParameter.originalElement ?: return
|
||||
val usageElement = ReferencesSearch.search(paramElement).singleOrNull()?.element ?: return
|
||||
val arrayAccessElement = usageElement.parent.parent as? KtArrayAccessExpression ?: return
|
||||
val factory = KtPsiFactory(project)
|
||||
val newParameter = factory.createLoopParameter("element")
|
||||
val newReferenceExpression = factory.createExpression("element")
|
||||
arrayAccessElement.replace(newReferenceExpression)
|
||||
loopParameter.replace(newParameter)
|
||||
loopRange.replace(collection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtExpression.toIntConstant(): Int? {
|
||||
return (this as? KtConstantExpression)?.text?.toIntOrNull()
|
||||
}
|
||||
|
||||
fun KtExpression.receiverIfIsSizeOrLengthCall(): KtExpression? {
|
||||
if (this.isSizeOrLength()) {
|
||||
return (this as? KtDotQualifiedExpression)?.receiverExpression ?: return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun KtExpression.receiverIfIsSizeOrLengthMinusOneCall(): KtExpression? {
|
||||
if (this !is KtBinaryExpression) return null
|
||||
if (this.operationToken != KtTokens.MINUS) return null
|
||||
val collection = this.left?.receiverIfIsSizeOrLengthCall() ?: return null
|
||||
val constant = this.right?.toIntConstant() ?: return null
|
||||
if (constant == 1) return collection
|
||||
return null
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.jetbrains.kotlin.idea.inspections.ReplaceManualRangeWithIndicesCallsInspection
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
// WITH_RUNTIME
|
||||
// PROBLEM: none
|
||||
fun test(args: Array<Int>) {
|
||||
val x = arrayOf<String>()
|
||||
for (index in ar<caret>gs) {
|
||||
val out = x[index]
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
for (index in 0..ar<caret>gs.size - 1) {
|
||||
args[index] = "Hello"
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
for (index in args.indices) {
|
||||
args[index] = "Hello"
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
for (index in 0..ar<caret>gs.size - 1) {
|
||||
println(index)
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
for (index in args.indices) {
|
||||
println(index)
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
val ind = 0..<caret>args.size-1
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
val ind = args.indices
|
||||
}
|
||||
idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection/simpleExpressionUntil.kt
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
val ind = 0 <caret>until args.size
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
val ind = args.indices
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
for (index in 0..ar<caret>gs.size - 1) {
|
||||
val out = args[index]
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(args: Array<String>) {
|
||||
for (element in args) {
|
||||
val out = element
|
||||
}
|
||||
}
|
||||
+43
@@ -7757,6 +7757,49 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class ReplaceManualRangeWithIndicesCallsInspection extends AbstractLocalInspectionTest {
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
|
||||
}
|
||||
|
||||
public void testAllFilesPresentInReplaceManualRangeWithIndicesCallsInspection() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
|
||||
}
|
||||
|
||||
@TestMetadata("forNotTarget.kt")
|
||||
public void testForNotTarget() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection/forNotTarget.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("indexInLvalue.kt")
|
||||
public void testIndexInLvalue() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection/indexInLvalue.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("notUsedAsIndex.kt")
|
||||
public void testNotUsedAsIndex() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection/notUsedAsIndex.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleExpression.kt")
|
||||
public void testSimpleExpression() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection/simpleExpression.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleExpressionUntil.kt")
|
||||
public void testSimpleExpressionUntil() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection/simpleExpressionUntil.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleFor.kt")
|
||||
public void testSimpleFor() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceManualRangeWithIndicesCallsInspection/simpleFor.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
Reference in New Issue
Block a user