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:
Toshiaki Kameyama
2018-06-20 22:23:04 +09:00
committed by Vyacheslav Gerasimov
parent e41a34af88
commit 6675d7814c
22 changed files with 326 additions and 0 deletions
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports <b>String.format</b> call can be replaced with string templates.
</body>
</html>
+9
View File
@@ -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"
+9
View File
@@ -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"
+9
View File
@@ -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"
+9
View File
@@ -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"
+9
View File
@@ -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"
+9
View File
@@ -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"
@@ -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
@@ -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)
}
@@ -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)
}
@@ -0,0 +1,8 @@
// RUNTIME_WITH_FULL_JDK
fun test() {
val foo = 1
val bar = 2
"foo is $foo, bar is $bar."
}
@@ -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)
}
@@ -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("%%")
}
@@ -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)
}
@@ -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,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
}
@@ -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
}
@@ -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)