Make "ReplaceRangeToWithUntil" suggest using rangeUntil operator

instead of `until`

Make it suggest only when it's possible (language level is 1.8 at least
and OptIn is allowed)

Tests are copied from ReplaceRangeToWithUntilInspection. (only
closedRange.kt test is changed)

^KTIJ-22372 Fixed

GitOrigin-RevId: dded3ea9395ffa9269be94c47545c18048fbe504
This commit is contained in:
Nikita Bobko
2022-08-02 22:07:13 +00:00
committed by intellij-monorepo-bot
parent e1de80a85b
commit 15ed569d36
20 changed files with 246 additions and 20 deletions
@@ -1496,7 +1496,8 @@ replace.int.range.end.inclusive.with.last.quick.fix.text=Replace 'endInclusive'
replace.int.range.start.with.first.quick.fix.text=Replace 'start' with 'first'
could.be.replaced.with.unboxed.last=Could be replaced with unboxed 'last'
could.be.replaced.with.unboxed.first=Could be replaced with unboxed 'first'
replace.with.until.quick.fix.text=Replace with until
replace.with.until.quick.fix.text=Replace with 'until'
replace.with.rangeUntil.quick.fix.text=Replace with '..<'
replace.with.string.literal.fix.family.name=Replace with string templates
replace.tostring.with.string.template=Replace 'toString' with string template
replace.to.with.infix.form.quickfix.text=Replace 'to' with infix form
@@ -2120,6 +2121,7 @@ inspection.wrap.unary.operator.display.name=Ambiguous unary operator use with nu
inspection.nullable.boolean.elvis.display.name=Equality check can be used instead of elvis for nullable boolean check
inspection.member.visibility.can.be.private.display.name=Class member can have 'private' visibility
inspection.replace.range.to.with.until.display.name='rangeTo' or the '..' call should be replaced with 'until'
inspection.replace.range.to.with.rangeUntil.display.name='rangeTo' or the '..' call should be replaced with '..<'
inspection.recursive.property.accessor.display.name=Recursive property accessor
inspection.replace.array.of.with.literal.display.name='arrayOf' call can be replaced with array literal [...]
inspection.copy.without.named.arguments.display.name='copy' method of data class is called without named arguments
@@ -0,0 +1,24 @@
<html>
<body>
Reports calls to <code>rangeTo</code> or the <code>..</code> operator instead of calls to <code>..<</code>.
<p>Using corresponding functions makes your code simpler.</p>
<p>The quick-fix replaces <code>rangeTo</code> or the <code>..</code> call with <code>..<</code>.</p>
<p><b>Example:</b></p>
<pre><code>
fun foo(a: Int) {
for (i in 0..a - 1) {
}
}
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
fun foo(a: Int) {
for (i in 0..<a) {
}
}
</code></pre>
<!-- tooltip end -->
</body>
</html>
@@ -48,7 +48,7 @@ class ConvertTwoComparisonsToRangeCheckInspection :
val rangeData = generateRangeExpressionData(element) ?: return
val replaced = element.replace(rangeData.createExpression())
(replaced as? KtBinaryExpression)?.right?.let {
ReplaceRangeToWithUntilInspection.applyFixIfApplicable(it)
AbstractReplaceRangeToWithRangeUntilInspection.applyFixIfApplicable(it)
}
}
@@ -7,26 +7,32 @@ import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.idea.base.projectStructure.languageVersionSettings
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.intentions.getArguments
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.typeUtil.isDouble
import org.jetbrains.kotlin.types.typeUtil.isFloat
import org.jetbrains.kotlin.types.typeUtil.isPrimitiveNumberType
import org.jetbrains.kotlin.resolve.checkers.OptInUsageChecker.Companion.isOptInAllowed
class ReplaceRangeToWithUntilInspection : AbstractRangeInspection() {
sealed class AbstractReplaceRangeToWithRangeUntilInspection : AbstractRangeInspection() {
override fun visitRangeTo(expression: KtExpression, context: BindingContext, holder: ProblemsHolder) {
if (!isApplicable(expression, context)) return
holder.registerProblem(
expression,
KotlinBundle.message("inspection.replace.range.to.with.until.display.name"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
ReplaceWithUntilQuickFix()
)
val useRangeUntil = expression.possibleToUseRangeUntil()
if (!isApplicable(expression, context, useRangeUntil)) return
if (useRangeUntil xor (this is ReplaceRangeToWithRangeUntilInspection)) {
return
}
val desc =
if (useRangeUntil) KotlinBundle.message("inspection.replace.range.to.with.rangeUntil.display.name")
else KotlinBundle.message("inspection.replace.range.to.with.until.display.name")
holder.registerProblem(expression, desc, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, ReplaceWithUntilQuickFix(useRangeUntil))
}
override fun visitUntilOrRangeUntil(expression: KtExpression, context: BindingContext, holder: ProblemsHolder) {
@@ -35,35 +41,50 @@ class ReplaceRangeToWithUntilInspection : AbstractRangeInspection() {
override fun visitDownTo(expression: KtExpression, context: BindingContext, holder: ProblemsHolder) {
}
class ReplaceWithUntilQuickFix : LocalQuickFix {
override fun getName() = KotlinBundle.message("replace.with.until.quick.fix.text")
private class ReplaceWithUntilQuickFix(private val useRangeUntil: Boolean) : LocalQuickFix {
override fun getName(): String =
if (useRangeUntil) KotlinBundle.message("replace.with.rangeUntil.quick.fix.text")
else KotlinBundle.message("replace.with.until.quick.fix.text")
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement as KtExpression
applyFix(element)
applyFix(element, useRangeUntil)
}
}
companion object {
fun applyFixIfApplicable(expression: KtExpression) {
if (isApplicable(expression, expression.analyze(BodyResolveMode.PARTIAL_NO_ADDITIONAL))) applyFix(expression)
val useRangeUntil = expression.possibleToUseRangeUntil()
if (isApplicable(expression, expression.analyze(BodyResolveMode.PARTIAL_NO_ADDITIONAL), useRangeUntil)) {
applyFix(expression, useRangeUntil)
}
}
private fun isApplicable(expression: KtExpression, context: BindingContext): Boolean {
private fun KtElement.possibleToUseRangeUntil(): Boolean =
languageVersionSettings.supportsFeature(LanguageFeature.RangeUntilOperator) &&
isOptInAllowed(
FqName("kotlin.ExperimentalStdlibApi"),
languageVersionSettings,
analyze(BodyResolveMode.PARTIAL_NO_ADDITIONAL)
)
private fun isApplicable(expression: KtExpression, context: BindingContext, useRangeUntil: Boolean): Boolean {
val (left, right) = expression.getArguments() ?: return false
// `until` isn't available for floating point numbers
fun KtExpression.isIntegerType() = context.getType(this)
?.let { it.isPrimitiveNumberType() && !it.isDouble() && !it.isFloat() }
return right?.deparenthesize()?.isMinusOne() == true && left?.isIntegerType() == true && right.isIntegerType() == true
fun KtExpression.isRangeUntilOrUntilApplicable() = context.getType(this)
?.let { it.isPrimitiveNumberType() && (useRangeUntil || !it.isDouble() && !it.isFloat()) }
return right?.deparenthesize()?.isMinusOne() == true &&
left?.isRangeUntilOrUntilApplicable() == true && right.isRangeUntilOrUntilApplicable() == true
}
private fun applyFix(element: KtExpression) {
private fun applyFix(element: KtExpression, useRangeUntil: Boolean) {
val args = element.getArguments() ?: return
val operator = if (useRangeUntil) "..<" else " until "
element.replace(
KtPsiFactory(element).createExpressionByPattern(
"$0 until $1",
"$0$operator$1",
args.first ?: return,
(args.second?.deparenthesize() as? KtBinaryExpression)?.left ?: return
)
@@ -81,4 +102,10 @@ class ReplaceRangeToWithUntilInspection : AbstractRangeInspection() {
}
}
/**
* Tests: [org.jetbrains.kotlin.idea.inspections.LocalInspectionTestGenerated.ReplaceRangeToWithUntil]
*/
class ReplaceRangeToWithUntilInspection : AbstractReplaceRangeToWithRangeUntilInspection()
class ReplaceRangeToWithRangeUntilInspection : AbstractReplaceRangeToWithRangeUntilInspection()
private fun KtExpression.deparenthesize() = KtPsiUtil.safeDeparenthesize(this)
@@ -13003,6 +13003,54 @@ public abstract class LocalInspectionTestGenerated extends AbstractLocalInspecti
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceRangeToWithRangeUntil")
public static class ReplaceRangeToWithRangeUntil extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("closedRange.kt")
public void testClosedRange() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt");
}
@TestMetadata("minusTwo.kt")
public void testMinusTwo() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/minusTwo.kt");
}
@TestMetadata("notMinusOne.kt")
public void testNotMinusOne() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/notMinusOne.kt");
}
@TestMetadata("operator.kt")
public void testOperator() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt");
}
@TestMetadata("operatorLong.kt")
public void testOperatorLong() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/operatorLong.kt");
}
@TestMetadata("parentheses.kt")
public void testParentheses() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/parentheses.kt");
}
@TestMetadata("plusOne.kt")
public void testPlusOne() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/plusOne.kt");
}
@TestMetadata("rangeTo.kt")
public void testRangeTo() throws Exception {
runTest("testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceRangeToWithUntil")
public static class ReplaceRangeToWithUntil extends AbstractLocalInspectionTest {
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.ReplaceRangeToWithRangeUntilInspection
@@ -0,0 +1,7 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Float) {
1f<caret>..a - 1
}
@@ -0,0 +1,7 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Float) {
1f..<a
}
@@ -0,0 +1,10 @@
// WITH_STDLIB
// PROBLEM: none
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Int) {
for (i in 0..a - 2<caret>) {
}
}
@@ -0,0 +1,10 @@
// WITH_STDLIB
// PROBLEM: none
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Int) {
for (i in 0..a<caret>) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Int) {
for (i in 0..<caret>a - 1) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Int) {
for (i in 0..<a) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Long) {
for (i in 1L<caret>..a - 1L) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Long) {
for (i in 1L..<a) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun example() {
val max = 5
for (i in 0..<caret>(max - 1)) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun example() {
val max = 5
for (i in 0..<max) {
}
}
@@ -0,0 +1,10 @@
// WITH_STDLIB
// PROBLEM: none
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Int) {
for (i in 0..a + 1<caret>) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Int) {
for (i in <caret>0.rangeTo(a - 1)) {
}
}
@@ -0,0 +1,9 @@
// WITH_STDLIB
// LANGUAGE_VERSION: 1.8
@OptIn(ExperimentalStdlibApi::class)
fun foo(a: Int) {
for (i in 0..<a) {
}
}
@@ -1651,6 +1651,14 @@
language="kotlin"
key="inspection.replace.range.to.with.until.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceRangeToWithRangeUntilInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.style.issues"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
key="inspection.replace.range.to.with.rangeUntil.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.MemberVisibilityCanBePrivateInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.style.issues"