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 aStringBuilder.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 {
+