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)