Add "Replace with 'equals(..., ignoreCase = true)'" inspection

#KT-40016 Fixed
This commit is contained in:
Toshiaki Kameyama
2020-07-05 07:25:29 +09:00
committed by Vladimir Dolzhenko
parent a5bfa3ae63
commit d2deff4864
24 changed files with 252 additions and 0 deletions
@@ -0,0 +1,16 @@
<html>
<body>
This inspection reports case insensitive comparison that can be replaced with <b>equals(..., ignoreCase = true)</b>.
By using <b>equals()</b> you won't have to allocate any extra strings with <b>toLowerCase()/toUpperCase()</b> to do the comparison.
<br><br>
For example:
<br>
<pre>
fun main() {
val a = "KoTliN"
val b = "KOTLIN"
println(a.toLowerCase() == b.toLowerCase()) // can be replaced with "a.equals(b, ignoreCase = true)"
}
</pre>
</body>
</html>
@@ -2245,6 +2245,7 @@ title.packages.to.use.import.with=Packages to Use Import with '*'
redundant.qualifier.unnecessary.non.direct.parent.class.qualifier=Unnecessary non-direct parent classes qualifiers
fix.add.exception.to.throws=Add ''{0}''
fix.add.eq.eq.true=Add '== true'
inspection.replace.with.ignore.case.equals.display.name=Replace with 'equals(..., ignoreCase = true)'
hints.title.codevision=Code Vision
hints.title.codevision.show.hints.for=Show hints for:
+8
View File
@@ -2533,6 +2533,14 @@
level="WARNING"
language="kotlin"
key="inspection.logger.initialized.with.foreign.class.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceWithIgnoreCaseEqualsInspection"
groupPath="Kotlin"
groupName="Redundant constructs"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
key="inspection.replace.with.ignore.case.equals.display.name" bundle="messages.KotlinBundle"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,82 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.ProblemsHolder
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.intentions.callExpression
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class ReplaceWithIgnoreCaseEqualsInspection : AbstractKotlinInspection() {
companion object {
private val caseConversionFunctionFqNames =
listOf(FqName("kotlin.text.toUpperCase"), FqName("kotlin.text.toLowerCase")).associateBy { it.shortName().asString() }
private fun KtExpression.callInfo(): Pair<KtCallExpression, String>? {
val call = (this as? KtQualifiedExpression)?.callExpression ?: this as? KtCallExpression ?: return null
val calleeText = call.calleeExpression?.text ?: return null
return call to calleeText
}
private fun KtCallExpression.fqName(context: BindingContext): FqName? {
return getResolvedCall(context)?.resultingDescriptor?.fqNameOrNull()
}
}
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) =
binaryExpressionVisitor(fun(binaryExpression: KtBinaryExpression) {
if (binaryExpression.operationToken != KtTokens.EQEQ) return
val (leftCall, leftCalleeText) = binaryExpression.left?.callInfo() ?: return
val (rightCall, rightCalleeText) = binaryExpression.right?.callInfo() ?: return
if (leftCalleeText != rightCalleeText) return
val caseConversionFunctionFqName = caseConversionFunctionFqNames[leftCalleeText] ?: return
val context = binaryExpression.analyze(BodyResolveMode.PARTIAL)
val leftCallFqName = leftCall.fqName(context) ?: return
val rightCallFqName = rightCall.fqName(context) ?: return
if (leftCallFqName != rightCallFqName) return
if (leftCallFqName != caseConversionFunctionFqName) return
holder.registerProblem(
binaryExpression,
KotlinBundle.message("replace.with.0", "equals(..., ignoreCase = true)"),
ReplaceFix()
)
})
private class ReplaceFix : LocalQuickFix {
override fun getName() = KotlinBundle.message("replace.with.0", "equals(..., ignoreCase = true)")
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val binary = descriptor.psiElement as? KtBinaryExpression ?: return
val (leftCall, _) = binary.left?.callInfo() ?: return
val (rightCall, _) = binary.right?.callInfo() ?: return
val psiFactory = KtPsiFactory(binary)
val leftReceiver = leftCall.getQualifiedExpressionForSelector()?.receiverExpression
val rightReceiver = rightCall.getQualifiedExpressionForSelector()?.receiverExpression ?: psiFactory.createThisExpression()
val newExpression = if (leftReceiver != null) {
psiFactory.createExpressionByPattern("$0.equals($1, ignoreCase = true)", leftReceiver, rightReceiver)
} else {
psiFactory.createExpressionByPattern("equals($0, ignoreCase = true)", rightReceiver)
}
binary.replace(newExpression)
}
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.ReplaceWithIgnoreCaseEqualsInspection
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun String.test(s: String): Boolean {
return <caret>toLowerCase() == s.toLowerCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun String.test(s: String): Boolean {
return equals(s, ignoreCase = true)
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun String.test(s: String): Boolean {
return <caret>s.toLowerCase() == toLowerCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun String.test(s: String): Boolean {
return s.equals(this, ignoreCase = true)
}
@@ -0,0 +1,5 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return <caret>a.toLowerCase() != b.toLowerCase()
}
@@ -0,0 +1,5 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return <caret>a == b.toLowerCase()
}
@@ -0,0 +1,5 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return <caret>a.toLowerCase() == b
}
@@ -0,0 +1,5 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return <caret>a.toLowerCase() == b.toUpperCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String?, b: String): Boolean {
return <caret>a?.toLowerCase() == b.toLowerCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String?, b: String): Boolean {
return a.equals(b, ignoreCase = true)
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String, b: String?): Boolean {
return <caret>a.toLowerCase() == b?.toLowerCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String, b: String?): Boolean {
return a.equals(b, ignoreCase = true)
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String?, b: String?): Boolean {
return <caret>a?.toLowerCase() == b?.toLowerCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String?, b: String?): Boolean {
return a.equals(b, ignoreCase = true)
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return <caret>a.toLowerCase() == b.toLowerCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return a.equals(b, ignoreCase = true)
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return <caret>a.toUpperCase() == b.toUpperCase()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test(a: String, b: String): Boolean {
return a.equals(b, ignoreCase = true)
}
@@ -11721,6 +11721,74 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
}
}
@TestMetadata("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceWithIgnoreCaseEquals extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInReplaceWithIgnoreCaseEquals() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), null, true);
}
@TestMetadata("extension.kt")
public void testExtension() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension.kt");
}
@TestMetadata("extension2.kt")
public void testExtension2() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension2.kt");
}
@TestMetadata("notApplicable.kt")
public void testNotApplicable() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable.kt");
}
@TestMetadata("notApplicable2.kt")
public void testNotApplicable2() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable2.kt");
}
@TestMetadata("notApplicable3.kt")
public void testNotApplicable3() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable3.kt");
}
@TestMetadata("notApplicable4.kt")
public void testNotApplicable4() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable4.kt");
}
@TestMetadata("nullable.kt")
public void testNullable() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable.kt");
}
@TestMetadata("nullable2.kt")
public void testNullable2() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable2.kt");
}
@TestMetadata("nullable3.kt")
public void testNullable3() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable3.kt");
}
@TestMetadata("toLowerCase.kt")
public void testToLowerCase() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toLowerCase.kt");
}
@TestMetadata("toUpperCase.kt")
public void testToUpperCase() throws Exception {
runTest("idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toUpperCase.kt");
}
}
@TestMetadata("idea/testData/inspectionsLocal/replaceWithOperatorAssignment")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)