KT-14779 Inspection to replace String.format with string templates (#1645)
* Add inspection to replace String.format with string templates #KT-14779 Fixed * KT-14779 Fixed
This commit is contained in:
committed by
Vyacheslav Gerasimov
parent
e41a34af88
commit
6675d7814c
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
This inspection reports <b>String.format</b> call can be replaced with string templates.
|
||||
</body>
|
||||
</html>
|
||||
@@ -2882,6 +2882,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceStringFormatWithLiteralInspection"
|
||||
displayName="Replace with string templates"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFORMATION"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.NestedLambdaShadowedImplicitParameterInspection"
|
||||
displayName="Nested lambda has shadowed implicit parameter"
|
||||
groupPath="Kotlin"
|
||||
|
||||
@@ -2880,6 +2880,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceStringFormatWithLiteralInspection"
|
||||
displayName="Replace with string templates"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFORMATION"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.NestedLambdaShadowedImplicitParameterInspection"
|
||||
displayName="Nested lambda has shadowed implicit parameter"
|
||||
groupPath="Kotlin"
|
||||
|
||||
@@ -2880,6 +2880,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceStringFormatWithLiteralInspection"
|
||||
displayName="Replace with string templates"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFORMATION"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.NestedLambdaShadowedImplicitParameterInspection"
|
||||
displayName="Nested lambda has shadowed implicit parameter"
|
||||
groupPath="Kotlin"
|
||||
|
||||
@@ -2881,6 +2881,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceStringFormatWithLiteralInspection"
|
||||
displayName="Replace with string templates"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFORMATION"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.NestedLambdaShadowedImplicitParameterInspection"
|
||||
displayName="Nested lambda has shadowed implicit parameter"
|
||||
groupPath="Kotlin"
|
||||
|
||||
@@ -2880,6 +2880,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceStringFormatWithLiteralInspection"
|
||||
displayName="Replace with string templates"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFORMATION"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.NestedLambdaShadowedImplicitParameterInspection"
|
||||
displayName="Nested lambda has shadowed implicit parameter"
|
||||
groupPath="Kotlin"
|
||||
|
||||
@@ -2880,6 +2880,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceStringFormatWithLiteralInspection"
|
||||
displayName="Replace with string templates"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFORMATION"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.NestedLambdaShadowedImplicitParameterInspection"
|
||||
displayName="Nested lambda has shadowed implicit parameter"
|
||||
groupPath="Kotlin"
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2010-2018 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.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.imports.importableFqName
|
||||
import org.jetbrains.kotlin.idea.intentions.ConvertToStringTemplateIntention
|
||||
import org.jetbrains.kotlin.idea.intentions.callExpression
|
||||
import org.jetbrains.kotlin.idea.intentions.getCallableDescriptor
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import java.util.*
|
||||
|
||||
class ReplaceStringFormatWithLiteralInspection : AbstractKotlinInspection() {
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
|
||||
return callExpressionVisitor(fun(callExpression) {
|
||||
if (callExpression.calleeExpression?.text != "format") return
|
||||
val qualifiedExpression = callExpression.parent as? KtQualifiedExpression
|
||||
if (qualifiedExpression != null && !qualifiedExpression.receiverExpression.text.endsWith("String")) return
|
||||
|
||||
val args = callExpression.valueArguments.mapNotNull { it.getArgumentExpression() }
|
||||
if (args.size <= 1) return
|
||||
|
||||
val format = args[0].text
|
||||
if (format.startsWith("\"\"\"")) return
|
||||
|
||||
val fqName = callExpression.getCallableDescriptor()?.importableFqName?.asString() ?: return
|
||||
if (fqName != "kotlin.text.format" && fqName != "java.lang.String.format") return
|
||||
|
||||
val placeHolders = placeHolder.findAll(format).toList()
|
||||
if (placeHolders.size != args.size - 1) return
|
||||
placeHolders.forEach {
|
||||
val next = it.range.last + 1
|
||||
val nextStr = if (next < format.length) format.substring(next, next + 1) else null
|
||||
if (nextStr != "s") return
|
||||
}
|
||||
|
||||
val context = callExpression.analyze(BodyResolveMode.PARTIAL)
|
||||
if (args.drop(1).any { it.isSubtypeOfFormattable(context) }) return
|
||||
|
||||
holder.registerProblem(
|
||||
qualifiedExpression ?: callExpression,
|
||||
"String.format call can be replaced with string templates",
|
||||
ProblemHighlightType.INFORMATION,
|
||||
ReplaceWithStringLiteralFix()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val placeHolder: Regex by lazy { "%".toRegex() }
|
||||
private val stringPlaceHolder: Regex by lazy { "%s".toRegex() }
|
||||
}
|
||||
|
||||
private class ReplaceWithStringLiteralFix : LocalQuickFix {
|
||||
override fun getFamilyName() = "Replace with string templates"
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val element = descriptor.psiElement
|
||||
val qualifiedExpression = element as? KtQualifiedExpression
|
||||
val callExpression = qualifiedExpression?.callExpression ?: element as? KtCallExpression ?: return
|
||||
|
||||
val args = callExpression.valueArguments.mapNotNull { it.getArgumentExpression() }
|
||||
val format = args[0].text.removePrefix("\"").removeSuffix("\"")
|
||||
val replaceArgs = args.drop(1).mapTo(LinkedList()) { ConvertToStringTemplateIntention.buildText(it, false) }
|
||||
val stringLiteral = stringPlaceHolder.replace(format) { replaceArgs.pop() }
|
||||
|
||||
(qualifiedExpression ?: callExpression).also { it.replace(KtPsiFactory(it).createStringTemplate(stringLiteral)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtExpression.isSubtypeOfFormattable(context: BindingContext): Boolean {
|
||||
return getType(context)?.constructor?.supertypes?.reversed()?.any {
|
||||
it.constructor.declarationDescriptor?.fqNameSafe?.asString() == "java.util.Formattable"
|
||||
} == true
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.jetbrains.kotlin.idea.inspections.ReplaceStringFormatWithLiteralInspection
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// PROBLEM: none
|
||||
// RUNTIME_WITH_FULL_JDK
|
||||
|
||||
import java.util.*
|
||||
|
||||
fun test() {
|
||||
val foo = Foo(1)
|
||||
val bar = 2
|
||||
|
||||
<caret>String.format("foo is %s, bar is %s.", foo, bar)
|
||||
}
|
||||
|
||||
class Foo(private val value: Int) : Formattable {
|
||||
override fun formatTo(formatter: Formatter?, flags: Int, width: Int, precision: Int) {
|
||||
formatter?.out()?.append("[$value]")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
|
||||
<caret>String.format("foo is %s, bar is %s.", foo)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// RUNTIME_WITH_FULL_JDK
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
val bar = 2
|
||||
|
||||
<caret>java.lang.String.format("foo is %s, bar is %s.", foo, bar)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// RUNTIME_WITH_FULL_JDK
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
val bar = 2
|
||||
|
||||
"foo is $foo, bar is $bar."
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// RUNTIME_WITH_FULL_JDK
|
||||
|
||||
import java.lang.String.format
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
val bar = 2
|
||||
|
||||
<caret>format("foo is %s, bar is %s.", foo, bar)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// RUNTIME_WITH_FULL_JDK
|
||||
|
||||
import java.lang.String.format
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
val bar = 2
|
||||
|
||||
"foo is $foo, bar is $bar."
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test() {
|
||||
<caret>String.format("%%")
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
val bar = 2
|
||||
|
||||
<caret>String.format("foo is %s, bar is %d.", foo, bar)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
val bar = 2
|
||||
|
||||
<caret>String.format("foo is %s, bar is %s.%n", foo, bar)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
val bar = 2
|
||||
|
||||
<caret>String.format("""foo is %s, bar is %s.%n""", foo, bar)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// WITH_RUNTIME
|
||||
// HIGHLIGHT: INFORMATION
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
|
||||
<caret>String.format("foo is %s, bar is %s.", foo, Bar().value)
|
||||
}
|
||||
|
||||
class Bar {
|
||||
val value = 2
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// WITH_RUNTIME
|
||||
// HIGHLIGHT: INFORMATION
|
||||
|
||||
fun test() {
|
||||
val foo = 1
|
||||
|
||||
"foo is $foo, bar is ${Bar().value}."
|
||||
}
|
||||
|
||||
class Bar {
|
||||
val value = 2
|
||||
}
|
||||
+58
@@ -4148,6 +4148,64 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class ReplaceStringFormatWithLiteral extends AbstractLocalInspectionTest {
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
|
||||
}
|
||||
|
||||
public void testAllFilesPresentInReplaceStringFormatWithLiteral() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
|
||||
}
|
||||
|
||||
@TestMetadata("formattableArgs.kt")
|
||||
public void testFormattableArgs() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/formattableArgs.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("invalidArgs.kt")
|
||||
public void testInvalidArgs() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/invalidArgs.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("javaStringFormat.kt")
|
||||
public void testJavaStringFormat() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/javaStringFormat.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("javaStringFormat2.kt")
|
||||
public void testJavaStringFormat2() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/javaStringFormat2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("noArgs.kt")
|
||||
public void testNoArgs() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/noArgs.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("notStringPlaceFolder.kt")
|
||||
public void testNotStringPlaceFolder() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/notStringPlaceFolder.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("notStringPlaceFolder2.kt")
|
||||
public void testNotStringPlaceFolder2() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/notStringPlaceFolder2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("rawStringFormat.kt")
|
||||
public void testRawStringFormat() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/rawStringFormat.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simple.kt")
|
||||
public void testSimple() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/replaceStringFormatWithLiteral/simple.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/inspectionsLocal/replaceToWithInfixForm")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
Reference in New Issue
Block a user