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
|
redundant.qualifier.unnecessary.non.direct.parent.class.qualifier=Unnecessary non-direct parent classes qualifiers
|
||||||
fix.add.exception.to.throws=Add ''{0}''
|
fix.add.exception.to.throws=Add ''{0}''
|
||||||
fix.add.eq.eq.true=Add '== true'
|
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=Code Vision
|
||||||
hints.title.codevision.show.hints.for=Show hints for:
|
hints.title.codevision.show.hints.for=Show hints for:
|
||||||
|
|||||||
@@ -2533,6 +2533,14 @@
|
|||||||
level="WARNING"
|
level="WARNING"
|
||||||
language="kotlin"
|
language="kotlin"
|
||||||
key="inspection.logger.initialized.with.foreign.class.display.name" bundle="messages.KotlinBundle"/>
|
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>
|
</extensions>
|
||||||
|
|
||||||
</idea-plugin>
|
</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")
|
@TestMetadata("idea/testData/inspectionsLocal/replaceWithOperatorAssignment")
|
||||||
@TestDataPath("$PROJECT_ROOT")
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
@RunWith(JUnit3RunnerWithInners.class)
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
|||||||
Reference in New Issue
Block a user