Add "Replace with 'equals(..., ignoreCase = true)'" inspection
#KT-40016 Fixed
This commit is contained in:
committed by
Vladimir Dolzhenko
parent
a5bfa3ae63
commit
d2deff4864
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
+82
@@ -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()
|
||||
}
|
||||
+4
@@ -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()
|
||||
}
|
||||
+4
@@ -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()
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test(a: String, b: String): Boolean {
|
||||
return a.equals(b, ignoreCase = true)
|
||||
}
|
||||
+68
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user