diff --git a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties index 43b5fd04e0d2..cb15e31b873d 100644 --- a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties +++ b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties @@ -2496,3 +2496,5 @@ inspection.message.sealed.object.can.be.converted.to.data.object='sealed' sub-ob serializable.object.must.be.marked.with.data=Serializable 'object' must be marked with 'data' to properly support serialization convert.to.data.object=Convert to 'data object' (may change semantic) inspection.convert.object.to.data.object.display.name=Convert 'object' to 'data object' + +inspection.replace.with.string.builder.append.range.display.name='StringBuilder.append(CharArray, offset, len)' call on the JVM diff --git a/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/ReplaceWithStringBuilderAppendRange.html b/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/ReplaceWithStringBuilderAppendRange.html new file mode 100644 index 000000000000..d27d094919bc --- /dev/null +++ b/plugins/kotlin/code-insight/descriptions/resources-en/inspectionDescriptions/ReplaceWithStringBuilderAppendRange.html @@ -0,0 +1,22 @@ + + +Reports a StringBuilder.append(CharArray, offset, len) function call on the JVM platform that should be replaced with a StringBuilder.appendRange(CharArray, startIndex, endIndex) function call. +

The append function behaves differently on the JVM, JS and Native platforms, so using the appendRange function is recommended.

+

Example:

+

+  fun f(charArray: CharArray, offset: Int, len: Int): String {
+      return buildString {
+          append(charArray, offset, len)
+      }
+  }
+
+

After the quick-fix is applied:

+

+  fun f(charArray: CharArray, offset: Int, len: Int): String {
+      return buildString {
+          appendRange(charArray, offset, offset + len)
+      }
+  }
+
+ + diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceWithStringBuilderAppendRangeInspection.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceWithStringBuilderAppendRangeInspection.kt new file mode 100644 index 000000000000..43707f4a83bd --- /dev/null +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceWithStringBuilderAppendRangeInspection.kt @@ -0,0 +1,77 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +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 org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.idea.base.facet.platform.platform +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall +import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection +import org.jetbrains.kotlin.idea.refactoring.fqName.fqName +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.platform.jvm.isJvm +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.types.typeUtil.isInt + +class ReplaceWithStringBuilderAppendRangeInspection : AbstractKotlinInspection() { + companion object { + private const val appendFunctionName = "append" + private const val appendRangeFunctionName = "appendRange" + private val appendFunctionFqName = FqName("java.lang.StringBuilder.append") + private val charArrayFqName = FqName("kotlin.CharArray") + } + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid = + callExpressionVisitor(fun(callExpression: KtCallExpression) { + if (!callExpression.platform.isJvm()) return + val calleeExpression = callExpression.calleeExpression ?: return + if (calleeExpression.text != appendFunctionName) return + if (callExpression.valueArguments.size != 3) return + + val descriptor = callExpression.resolveToCall()?.resultingDescriptor ?: return + if (descriptor.fqNameSafe != appendFunctionFqName) return + + val params = descriptor.valueParameters + if (params.size != 3 || !params[0].isCharArray() || !params[1].isInt() || !params[2].isInt()) return + + holder.registerProblem( + calleeExpression, + KotlinBundle.message("replace.with.0", appendRangeFunctionName), + ReplaceFix(appendRangeFunctionName) + ) + }) + + private fun ValueParameterDescriptor.isCharArray(): Boolean = type.fqName == charArrayFqName + + private fun ValueParameterDescriptor.isInt(): Boolean = type.isInt() + + private class ReplaceFix(private val functionName: String) : LocalQuickFix { + override fun getName(): String = KotlinBundle.message("replace.with.0", functionName) + + override fun getFamilyName(): String = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val calleeExpression = descriptor.psiElement as? KtExpression ?: return + val callExpression = calleeExpression.parent as? KtCallExpression ?: return + + val args = callExpression.valueArguments + val secondArg = args.getOrNull(1)?.getArgumentExpression() ?: return + val thirdArg = args.getOrNull(2)?.getArgumentExpression() ?: return + + val psiFactory = KtPsiFactory(callExpression) + calleeExpression.replace(psiFactory.createCalleeExpression(functionName)) + if (secondArg is KtConstantExpression && thirdArg is KtConstantExpression) { + thirdArg.replace(psiFactory.createExpression(secondArg.text.toInt().plus(thirdArg.text.toInt()).toString())) + } else { + thirdArg.replace(psiFactory.createExpressionByPattern("$0 + $1", secondArg, thirdArg)) + } + } + + private fun KtPsiFactory.createCalleeExpression(functionName: String): KtExpression = + (createExpression("$functionName()") as KtCallExpression).calleeExpression!! + } +} 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 ed4ae6ea2ca1..9a64d3c3e113 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 @@ -13277,6 +13277,34 @@ public abstract class LocalInspectionTestGenerated extends AbstractLocalInspecti } } + @RunWith(JUnit3RunnerWithInners.class) + @TestMetadata("testData/inspectionsLocal/replaceWithStringBuilderAppendRange") + public static class ReplaceWithStringBuilderAppendRange extends AbstractLocalInspectionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + @TestMetadata("constantArgument.kt") + public void testConstantArgument() throws Exception { + runTest("testData/inspectionsLocal/replaceWithStringBuilderAppendRange/constantArgument.kt"); + } + + @TestMetadata("hasReceiver.kt") + public void testHasReceiver() throws Exception { + runTest("testData/inspectionsLocal/replaceWithStringBuilderAppendRange/hasReceiver.kt"); + } + + @TestMetadata("precedence.kt") + public void testPrecedence() throws Exception { + runTest("testData/inspectionsLocal/replaceWithStringBuilderAppendRange/precedence.kt"); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("testData/inspectionsLocal/replaceWithStringBuilderAppendRange/simple.kt"); + } + } + @RunWith(JUnit3RunnerWithInners.class) @TestMetadata("testData/inspectionsLocal/restrictReturnStatementTargetMigration") public static class RestrictReturnStatementTargetMigration extends AbstractLocalInspectionTest { diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/.inspection b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/.inspection new file mode 100644 index 000000000000..89a746c548ff --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.ReplaceWithStringBuilderAppendRangeInspection diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/constantArgument.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/constantArgument.kt new file mode 100644 index 000000000000..7f74b419cf4c --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/constantArgument.kt @@ -0,0 +1,6 @@ +// WITH_STDLIB +fun test(charArray: CharArray): String { + return buildString { + append(charArray, 2, 4) + } +} diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/constantArgument.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/constantArgument.kt.after new file mode 100644 index 000000000000..58b0cfac0605 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/constantArgument.kt.after @@ -0,0 +1,6 @@ +// WITH_STDLIB +fun test(charArray: CharArray): String { + return buildString { + appendRange(charArray, 2, 6) + } +} diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/hasReceiver.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/hasReceiver.kt new file mode 100644 index 000000000000..6f0a8f9661be --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/hasReceiver.kt @@ -0,0 +1,6 @@ +// WITH_STDLIB +fun test(charArray: CharArray, offset: Int, len: Int): String { + return buildString { + this.append(charArray, offset, len) + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/hasReceiver.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/hasReceiver.kt.after new file mode 100644 index 000000000000..d64a93ec5c0c --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/hasReceiver.kt.after @@ -0,0 +1,6 @@ +// WITH_STDLIB +fun test(charArray: CharArray, offset: Int, len: Int): String { + return buildString { + this.appendRange(charArray, offset, offset + len) + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/precedence.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/precedence.kt new file mode 100644 index 000000000000..d65a96edb371 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/precedence.kt @@ -0,0 +1,6 @@ +// WITH_STDLIB +fun test(charArray: CharArray, a: Int, b: Int, c: Int, d: Int): String { + return buildString { + append(charArray, a - b, c - d) + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/precedence.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/precedence.kt.after new file mode 100644 index 000000000000..3c6ea37a0473 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/precedence.kt.after @@ -0,0 +1,6 @@ +// WITH_STDLIB +fun test(charArray: CharArray, a: Int, b: Int, c: Int, d: Int): String { + return buildString { + appendRange(charArray, a - b, a - b + (c - d)) + } +} diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/simple.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/simple.kt new file mode 100644 index 000000000000..8743c10f5d3c --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/simple.kt @@ -0,0 +1,7 @@ +// FIX: Replace with 'appendRange' +// WITH_STDLIB +fun test(charArray: CharArray, offset: Int, len: Int): String { + return buildString { + append(charArray, offset, len) + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/simple.kt.after b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/simple.kt.after new file mode 100644 index 000000000000..42760294f310 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceWithStringBuilderAppendRange/simple.kt.after @@ -0,0 +1,7 @@ +// FIX: Replace with 'appendRange' +// WITH_STDLIB +fun test(charArray: CharArray, offset: Int, len: Int): String { + return buildString { + appendRange(charArray, offset, offset + len) + } +} \ No newline at end of file diff --git a/plugins/kotlin/plugin/k1/resources/META-INF/inspections-fe10.xml b/plugins/kotlin/plugin/k1/resources/META-INF/inspections-fe10.xml index d7d0d0d2eb5c..07537b0b6241 100644 --- a/plugins/kotlin/plugin/k1/resources/META-INF/inspections-fe10.xml +++ b/plugins/kotlin/plugin/k1/resources/META-INF/inspections-fe10.xml @@ -2914,6 +2914,15 @@ level="WEAK WARNING" language="kotlin" key="inspection.replace.readline.with.readln.display.name" bundle="messages.KotlinBundle"/> + + + \ No newline at end of file