diff --git a/idea/resources-en/inspectionDescriptions/ReplaceWithIgnoreCaseEquals.html b/idea/resources-en/inspectionDescriptions/ReplaceWithIgnoreCaseEquals.html
new file mode 100644
index 00000000000..bef88ce7815
--- /dev/null
+++ b/idea/resources-en/inspectionDescriptions/ReplaceWithIgnoreCaseEquals.html
@@ -0,0 +1,16 @@
+
+
+This inspection reports case insensitive comparison that can be replaced with equals(..., ignoreCase = true).
+By using equals() you won't have to allocate any extra strings with toLowerCase()/toUpperCase() to do the comparison.
+
+For example:
+
+
+fun main() {
+ val a = "KoTliN"
+ val b = "KOTLIN"
+ println(a.toLowerCase() == b.toLowerCase()) // can be replaced with "a.equals(b, ignoreCase = true)"
+}
+
+
+
diff --git a/idea/resources-en/messages/KotlinBundle.properties b/idea/resources-en/messages/KotlinBundle.properties
index 980c4f8a34d..3ba44828fa8 100644
--- a/idea/resources-en/messages/KotlinBundle.properties
+++ b/idea/resources-en/messages/KotlinBundle.properties
@@ -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:
diff --git a/idea/resources/META-INF/inspections.xml b/idea/resources/META-INF/inspections.xml
index 125d7ea792d..275db50cb5c 100644
--- a/idea/resources/META-INF/inspections.xml
+++ b/idea/resources/META-INF/inspections.xml
@@ -2533,6 +2533,14 @@
level="WARNING"
language="kotlin"
key="inspection.logger.initialized.with.foreign.class.display.name" bundle="messages.KotlinBundle"/>
+
+
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceWithIgnoreCaseEqualsInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceWithIgnoreCaseEqualsInspection.kt
new file mode 100644
index 00000000000..c498330a885
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceWithIgnoreCaseEqualsInspection.kt
@@ -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? {
+ 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)
+ }
+ }
+}
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/.inspection b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/.inspection
new file mode 100644
index 00000000000..af6554e8a2a
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/.inspection
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.inspections.ReplaceWithIgnoreCaseEqualsInspection
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension.kt
new file mode 100644
index 00000000000..ab9c62de4f9
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension.kt
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun String.test(s: String): Boolean {
+ return toLowerCase() == s.toLowerCase()
+}
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension.kt.after b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension.kt.after
new file mode 100644
index 00000000000..ef9feeab62c
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun String.test(s: String): Boolean {
+ return equals(s, ignoreCase = true)
+}
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension2.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension2.kt
new file mode 100644
index 00000000000..3282085437b
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension2.kt
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun String.test(s: String): Boolean {
+ return s.toLowerCase() == toLowerCase()
+}
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension2.kt.after b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension2.kt.after
new file mode 100644
index 00000000000..cbb52c8bfc2
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/extension2.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun String.test(s: String): Boolean {
+ return s.equals(this, ignoreCase = true)
+}
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable.kt
new file mode 100644
index 00000000000..dcdfcdb4dd9
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable.kt
@@ -0,0 +1,5 @@
+// PROBLEM: none
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a.toLowerCase() != b.toLowerCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable2.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable2.kt
new file mode 100644
index 00000000000..0e500fc0ea4
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable2.kt
@@ -0,0 +1,5 @@
+// PROBLEM: none
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a == b.toLowerCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable3.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable3.kt
new file mode 100644
index 00000000000..caf2dcdf1d6
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable3.kt
@@ -0,0 +1,5 @@
+// PROBLEM: none
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a.toLowerCase() == b
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable4.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable4.kt
new file mode 100644
index 00000000000..266453876f3
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/notApplicable4.kt
@@ -0,0 +1,5 @@
+// PROBLEM: none
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a.toLowerCase() == b.toUpperCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable.kt
new file mode 100644
index 00000000000..ab201ece046
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable.kt
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String?, b: String): Boolean {
+ return a?.toLowerCase() == b.toLowerCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable.kt.after b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable.kt.after
new file mode 100644
index 00000000000..b2ae79c9eb9
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String?, b: String): Boolean {
+ return a.equals(b, ignoreCase = true)
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable2.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable2.kt
new file mode 100644
index 00000000000..2e849fcd236
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable2.kt
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String, b: String?): Boolean {
+ return a.toLowerCase() == b?.toLowerCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable2.kt.after b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable2.kt.after
new file mode 100644
index 00000000000..299445bb175
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable2.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String, b: String?): Boolean {
+ return a.equals(b, ignoreCase = true)
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable3.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable3.kt
new file mode 100644
index 00000000000..d79ba6f20dc
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable3.kt
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String?, b: String?): Boolean {
+ return a?.toLowerCase() == b?.toLowerCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable3.kt.after b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable3.kt.after
new file mode 100644
index 00000000000..b061b9281ad
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/nullable3.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String?, b: String?): Boolean {
+ return a.equals(b, ignoreCase = true)
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toLowerCase.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toLowerCase.kt
new file mode 100644
index 00000000000..d1b63f9b92d
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toLowerCase.kt
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a.toLowerCase() == b.toLowerCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toLowerCase.kt.after b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toLowerCase.kt.after
new file mode 100644
index 00000000000..e8f41bfef31
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toLowerCase.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a.equals(b, ignoreCase = true)
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toUpperCase.kt b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toUpperCase.kt
new file mode 100644
index 00000000000..8fbd03e5c8d
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toUpperCase.kt
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a.toUpperCase() == b.toUpperCase()
+}
\ No newline at end of file
diff --git a/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toUpperCase.kt.after b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toUpperCase.kt.after
new file mode 100644
index 00000000000..e8f41bfef31
--- /dev/null
+++ b/idea/testData/inspectionsLocal/replaceWithIgnoreCaseEquals/toUpperCase.kt.after
@@ -0,0 +1,4 @@
+// WITH_RUNTIME
+fun test(a: String, b: String): Boolean {
+ return a.equals(b, ignoreCase = true)
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java
index 8d28c1bd1c7..ed0d8c4a360 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java
@@ -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)