From 15ed569d36fd78b09f922305dac5110f82ff8bda Mon Sep 17 00:00:00 2001 From: Nikita Bobko Date: Mon, 1 Aug 2022 11:59:13 +0200 Subject: [PATCH] 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 --- .../messages/KotlinBundle.properties | 4 +- .../ReplaceRangeToWithRangeUntil.html | 24 +++++++ ...ertTwoComparisonsToRangeCheckInspection.kt | 2 +- .../ReplaceRangeToWithUntilInspection.kt | 63 +++++++++++++------ .../LocalInspectionTestGenerated.java | 48 ++++++++++++++ .../replaceRangeToWithRangeUntil/.inspection | 1 + .../closedRange.kt | 7 +++ .../closedRange.kt.after | 7 +++ .../replaceRangeToWithRangeUntil/minusTwo.kt | 10 +++ .../notMinusOne.kt | 10 +++ .../replaceRangeToWithRangeUntil/operator.kt | 9 +++ .../operator.kt.after | 9 +++ .../operatorLong.kt | 9 +++ .../operatorLong.kt.after | 9 +++ .../parentheses.kt | 9 +++ .../parentheses.kt.after | 9 +++ .../replaceRangeToWithRangeUntil/plusOne.kt | 10 +++ .../replaceRangeToWithRangeUntil/rangeTo.kt | 9 +++ .../rangeTo.kt.after | 9 +++ .../resources/META-INF/inspections-fe10.xml | 8 +++ 20 files changed, 246 insertions(+), 20 deletions(-) create mode 100644 plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/ReplaceRangeToWithRangeUntil.html create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/.inspection create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/minusTwo.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/notMinusOne.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operatorLong.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operatorLong.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/parentheses.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/parentheses.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/plusOne.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt create mode 100644 plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt.after diff --git a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties index 12b9c3d30486..e1770dc41e58 100644 --- a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties +++ b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties @@ -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 diff --git a/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/ReplaceRangeToWithRangeUntil.html b/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/ReplaceRangeToWithRangeUntil.html new file mode 100644 index 000000000000..edb138cea076 --- /dev/null +++ b/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/ReplaceRangeToWithRangeUntil.html @@ -0,0 +1,24 @@ + + +Reports calls to rangeTo or the .. operator instead of calls to ..<. +

Using corresponding functions makes your code simpler.

+

The quick-fix replaces rangeTo or the .. call with ..<.

+

Example:

+

+  fun foo(a: Int) {
+      for (i in 0..a - 1) {
+
+      }
+  }
+
+

After the quick-fix is applied:

+

+  fun foo(a: Int) {
+      for (i in 0..
+ + + \ No newline at end of file diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ConvertTwoComparisonsToRangeCheckInspection.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ConvertTwoComparisonsToRangeCheckInspection.kt index 711af1410f35..c0ea65da1fd8 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ConvertTwoComparisonsToRangeCheckInspection.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ConvertTwoComparisonsToRangeCheckInspection.kt @@ -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) } } diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceRangeToWithUntilInspection.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceRangeToWithUntilInspection.kt index 8a6fca20638d..1d0fa9fa7d42 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceRangeToWithUntilInspection.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceRangeToWithUntilInspection.kt @@ -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) diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index be66efd38b38..57d4545bdfbd 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -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 { diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/.inspection b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/.inspection new file mode 100644 index 000000000000..8db066829073 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.ReplaceRangeToWithRangeUntilInspection \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt new file mode 100644 index 000000000000..ff036631c323 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt @@ -0,0 +1,7 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Float) { + 1f..a - 1 +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt.after new file mode 100644 index 000000000000..fe8a5760b8ee --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/closedRange.kt.after @@ -0,0 +1,7 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Float) { + 1f..) { + + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/notMinusOne.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/notMinusOne.kt new file mode 100644 index 000000000000..146931fb1f72 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/notMinusOne.kt @@ -0,0 +1,10 @@ +// WITH_STDLIB +// PROBLEM: none +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Int) { + for (i in 0..a) { + + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt new file mode 100644 index 000000000000..d1c27f599828 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt @@ -0,0 +1,9 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Int) { + for (i in 0..a - 1) { + + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt.after new file mode 100644 index 000000000000..1cc2bfad6c70 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operator.kt.after @@ -0,0 +1,9 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Int) { + for (i in 0....a - 1L) { + + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operatorLong.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operatorLong.kt.after new file mode 100644 index 000000000000..4d7ff20dc825 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/operatorLong.kt.after @@ -0,0 +1,9 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Long) { + for (i in 1L..(max - 1)) { + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/parentheses.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/parentheses.kt.after new file mode 100644 index 000000000000..11df9b1fc85a --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/parentheses.kt.after @@ -0,0 +1,9 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun example() { + val max = 5 + for (i in 0..) { + + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt new file mode 100644 index 000000000000..716903d70496 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt @@ -0,0 +1,9 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Int) { + for (i in 0.rangeTo(a - 1)) { + + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt.after new file mode 100644 index 000000000000..1cc2bfad6c70 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceRangeToWithRangeUntil/rangeTo.kt.after @@ -0,0 +1,9 @@ +// WITH_STDLIB +// LANGUAGE_VERSION: 1.8 + +@OptIn(ExperimentalStdlibApi::class) +fun foo(a: Int) { + for (i in 0.. + +