Add inspection to detect boxes *Range.start & endInclusive

These properties can be replaced with equivalent collections first & last.
#KT-15537 Fixed
This commit is contained in:
Jan Gerling
2019-03-01 17:23:40 +03:00
committed by Mikhail Glukhikh
parent 401d8c2d70
commit 162de77b71
18 changed files with 229 additions and 0 deletions
@@ -3306,6 +3306,15 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceRangeStartEndInclusiveWithFirstLastInspection"
displayName="Replace Range 'start' or 'endInclusive' with 'first' or 'last'"
groupPath="Kotlin"
groupName="Style issues"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.RedundantRequireNotNullCallInspection"
displayName="Redundant 'requireNotNull' or 'checkNotNull' call"
groupPath="Kotlin"
@@ -0,0 +1,7 @@
<html>
<body>
This inspection reports usages of <b>boxed</b> <code>Range.start</code> and <code>Range.endInclusive</code> properties.
These properties can be replaced with <b>unboxed</b> <code>first</code> and <code>last</code> properties.
Example: <b>range.start</b> can be replaced with <b>range.first</b>.
</body>
</html>
@@ -0,0 +1,81 @@
/*
* 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.ProblemsHolder
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.dotQualifiedExpressionVisitor
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
class ReplaceRangeStartEndInclusiveWithFirstLastInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return dotQualifiedExpressionVisitor(fun(expression: KtDotQualifiedExpression) {
val selectorExpression = expression.selectorExpression ?: return
if (selectorExpression.text != "start" && selectorExpression.text != "endInclusive") return
val resolvedCall = expression.resolveToCall() ?: return
val containing = resolvedCall.resultingDescriptor.containingDeclaration as? ClassDescriptor ?: return
if (!containing.isRange()) return
if (selectorExpression.text == "start") {
holder.registerProblem(
expression,
"Could be replaced with unboxed `first`",
ReplaceIntRangeStartWithFirstQuickFix()
)
} else if (selectorExpression.text == "endInclusive") {
holder.registerProblem(
expression,
"Could be replaced with unboxed `last`",
ReplaceIntRangeEndInclusiveWithLastQuickFix()
)
}
})
}
}
private val rangeTypes = setOf(
"kotlin.ranges.IntRange",
"kotlin.ranges.CharRange",
"kotlin.ranges.LongRange",
"kotlin.ranges.UIntRange",
"kotlin.ranges.ULongRange"
)
private fun ClassDescriptor.isRange(): Boolean {
return rangeTypes.any { this.fqNameUnsafe.asString() == it }
}
class ReplaceIntRangeStartWithFirstQuickFix : LocalQuickFix {
override fun getName() = "Replace 'start' with 'first'"
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement as KtDotQualifiedExpression
val selector = element.selectorExpression ?: return
selector.replace(KtPsiFactory(element).createExpression("first"))
}
}
class ReplaceIntRangeEndInclusiveWithLastQuickFix : LocalQuickFix {
override fun getName() = "Replace 'endInclusive' with 'last'"
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement as KtDotQualifiedExpression
val selector = element.selectorExpression ?: return
selector.replace(KtPsiFactory(element).createExpression("last"))
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.ReplaceRangeStartEndInclusiveWithFirstLastInspection
@@ -0,0 +1,11 @@
// PROBLEM: none
class MyRange : ClosedRange<String> {
override val start: String get() = "a"
override val endInclusive: String = "z"
}
fun main() {
val range = MyRange()
val start = range.<caret>endInclusive
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : CharRange = 'a' .. 'z'
range.<caret>endInclusive
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : CharRange = 'a' .. 'z'
range.last
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : IntRange = 1..2
range.<caret>endInclusive
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : IntRange = 1..2
range.last
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : LongRange = 1L..2L
range.<caret>endInclusive
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : LongRange = 1L..2L
range.last
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : CharRange = 'a' .. 'z'
range.<caret>start
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : CharRange = 'a' .. 'z'
range.first
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : IntRange = 1..2
range.<caret>start
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : IntRange = 1..2
range.first
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : LongRange = 1L..2L
range.<caret>start
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun foo() {
var range : LongRange = 1L..2L
range.first
}
@@ -7388,6 +7388,54 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
}
}
@TestMetadata("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceRangeStartEndInclusiveWithFirstLast extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInReplaceRangeStartEndInclusiveWithFirstLast() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("customRange.kt")
public void testCustomRange() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast/customRange.kt");
}
@TestMetadata("endInclusiveCharRange.kt")
public void testEndInclusiveCharRange() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast/endInclusiveCharRange.kt");
}
@TestMetadata("endInclusiveIntRange.kt")
public void testEndInclusiveIntRange() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast/endInclusiveIntRange.kt");
}
@TestMetadata("endInclusiveLongRange.kt")
public void testEndInclusiveLongRange() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast/endInclusiveLongRange.kt");
}
@TestMetadata("startCharRange.kt")
public void testStartCharRange() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast/startCharRange.kt");
}
@TestMetadata("startIntRange.kt")
public void testStartIntRange() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast/startIntRange.kt");
}
@TestMetadata("startLongRange.kt")
public void testStartLongRange() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceRangeStartEndInclusiveWithFirstLast/startLongRange.kt");
}
}
@TestMetadata("idea/testData/inspectionsLocal/replaceRangeToWithUntil")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)