Introduce "Replace 'isEmpty' with 'ifEmpty'" inspection

#KT-37527 Fixed

GitOrigin-RevId: 8367d3affb8d2daa085d2e618262e17b357b4d3e
This commit is contained in:
Toshiaki Kameyama
2021-01-14 16:50:40 +03:00
committed by intellij-monorepo-bot
parent 92bfb839ea
commit 86fddc6ae4
45 changed files with 611 additions and 0 deletions
@@ -0,0 +1,6 @@
<html>
<body>
This inspection reports <b>isEmpty</b> or <b>isBlank</b> calls to assign a default value.
They can be replaced with <b>ifEmpty</b> or <b>ifBlank</b> calls.
</body>
</html>
@@ -2293,6 +2293,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.isempty.with.ifempty.display.name=Replace 'isEmpty' with 'ifEmpty'
inspection.replace.with.ignore.case.equals.display.name=Replace with 'equals(..., ignoreCase = true)'
inspection.redundant.nullable.return.type.display.name=Redundant nullable return type
0.always.returns.non.null.type=''{0}'' always returns non-null type
@@ -2599,6 +2599,14 @@
language="kotlin"
key="inspection.unused.result.of.data.class.copy" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ReplaceIsEmptyWithIfEmptyInspection"
groupPath="Kotlin"
groupName="Style issues"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
key="inspection.replace.isempty.with.ifempty.display.name" bundle="messages.KotlinBundle"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,109 @@
/*
* 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.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isElseIf
import org.jetbrains.kotlin.idea.project.languageVersionSettings
import org.jetbrains.kotlin.idea.util.textRangeIn
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.blockExpressionsOrSingle
import org.jetbrains.kotlin.psi.psiUtil.getPossiblyQualifiedCallExpression
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class ReplaceIsEmptyWithIfEmptyInspection : AbstractKotlinInspection() {
private data class Replacement(
val conditionFunctionFqName: FqName,
val replacementFunctionName: String,
val negativeCondition: Boolean = false
)
companion object {
private val replacements = listOf(
Replacement(FqName("kotlin.collections.Collection.isEmpty"), "ifEmpty"),
Replacement(FqName("kotlin.collections.List.isEmpty"), "ifEmpty"),
Replacement(FqName("kotlin.collections.Set.isEmpty"), "ifEmpty"),
Replacement(FqName("kotlin.collections.Map.isEmpty"), "ifEmpty"),
Replacement(FqName("kotlin.text.isEmpty"), "ifEmpty"),
Replacement(FqName("kotlin.text.isBlank"), "ifBlank"),
Replacement(FqName("kotlin.collections.isNotEmpty"), "ifEmpty", negativeCondition = true),
Replacement(FqName("kotlin.text.isNotEmpty"), "ifEmpty", negativeCondition = true),
Replacement(FqName("kotlin.text.isNotBlank"), "ifBlank", negativeCondition = true),
).associateBy { it.conditionFunctionFqName }
private val conditionFunctionShortNames = replacements.keys.map { it.shortName().asString() }.toSet()
}
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = ifExpressionVisitor(fun(ifExpression: KtIfExpression) {
if (ifExpression.languageVersionSettings.languageVersion < LanguageVersion.KOTLIN_1_3) return
if (ifExpression.isElseIf()) return
val thenExpression = ifExpression.then ?: return
val elseExpression = ifExpression.`else` ?: return
if (elseExpression is KtIfExpression) return
val condition = ifExpression.condition ?: return
val conditionCallExpression = condition.getPossiblyQualifiedCallExpression() ?: return
val conditionCalleeExpression = conditionCallExpression.calleeExpression ?: return
if (conditionCalleeExpression.text !in conditionFunctionShortNames) return
val context = ifExpression.analyze(BodyResolveMode.PARTIAL)
val resultingDescriptor = conditionCallExpression.getResolvedCall(context)?.resultingDescriptor ?: return
val receiverParameter = resultingDescriptor.dispatchReceiverParameter ?: resultingDescriptor.extensionReceiverParameter
val receiverType = receiverParameter?.type ?: return
if (KotlinBuiltIns.isArrayOrPrimitiveArray(receiverType)) return
val conditionCallFqName = resultingDescriptor.fqNameOrNull() ?: return
val replacement = replacements[conditionCallFqName] ?: return
val selfBranch = if (replacement.negativeCondition) thenExpression else elseExpression
val selfValueExpression = selfBranch.blockExpressionsOrSingle().singleOrNull() ?: return
if (condition is KtDotQualifiedExpression) {
if (selfValueExpression.text != condition.receiverExpression.text) return
} else {
if (selfValueExpression !is KtThisExpression) return
}
holder.registerProblem(
ifExpression,
conditionCalleeExpression.textRangeIn(ifExpression),
KotlinBundle.message("replace.with.0", "${replacement.replacementFunctionName} {...}"),
ReplaceFix(replacement)
)
})
private class ReplaceFix(private val replacement: Replacement) : LocalQuickFix {
override fun getName() = KotlinBundle.message("replace.with.0", "${replacement.replacementFunctionName} {...}")
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val ifExpression = descriptor.psiElement as? KtIfExpression ?: return
val condition = ifExpression.condition ?: return
val thenExpression = ifExpression.then ?: return
val elseExpression = ifExpression.`else` ?: return
val defaultValueExpression = (if (replacement.negativeCondition) elseExpression else thenExpression)
val psiFactory = KtPsiFactory(ifExpression)
val receiverText = (condition as? KtDotQualifiedExpression)?.receiverExpression?.text?.let { "$it." } ?: ""
val replacementFunctionName = replacement.replacementFunctionName
val newExpression = if (defaultValueExpression is KtBlockExpression) {
psiFactory.createExpression("${receiverText}$replacementFunctionName ${defaultValueExpression.text}")
} else {
psiFactory.createExpressionByPattern("${receiverText}$replacementFunctionName { $0 }", defaultValueExpression)
}
ifExpression.replace(newExpression)
}
}
}
@@ -10514,6 +10514,190 @@ public abstract class LocalInspectionTestGenerated extends AbstractLocalInspecti
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty")
public static class ReplaceIsEmptyWithIfEmpty extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("defaultValueBlockHasMultiStatement.kt")
public void testDefaultValueBlockHasMultiStatement() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/defaultValueBlockHasMultiStatement.kt");
}
@TestMetadata("defaultValueBlockHasMultiStatement2.kt")
public void testDefaultValueBlockHasMultiStatement2() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/defaultValueBlockHasMultiStatement2.kt");
}
@TestMetadata("elseIf.kt")
public void testElseIf() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/elseIf.kt");
}
@TestMetadata("implicitReceiver.kt")
public void testImplicitReceiver() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/implicitReceiver.kt");
}
@TestMetadata("negated.kt")
public void testNegated() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/negated.kt");
}
@TestMetadata("noElse.kt")
public void testNoElse() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/noElse.kt");
}
@TestMetadata("notSelfValue.kt")
public void testNotSelfValue() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/notSelfValue.kt");
}
@TestMetadata("notSelfValue2.kt")
public void testNotSelfValue2() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/notSelfValue2.kt");
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/array")
public static class Array extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("isEmpty.kt")
public void testIsEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/array/isEmpty.kt");
}
@TestMetadata("isNotEmpty.kt")
public void testIsNotEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/array/isNotEmpty.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/collection")
public static class Collection extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("isEmpty.kt")
public void testIsEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/collection/isEmpty.kt");
}
@TestMetadata("isNotEmpty.kt")
public void testIsNotEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/collection/isNotEmpty.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/intArray")
public static class IntArray extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("isEmpty.kt")
public void testIsEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/intArray/isEmpty.kt");
}
@TestMetadata("isNotEmpty.kt")
public void testIsNotEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/intArray/isNotEmpty.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/list")
public static class List extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("isEmpty.kt")
public void testIsEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/list/isEmpty.kt");
}
@TestMetadata("isNotEmpty.kt")
public void testIsNotEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/list/isNotEmpty.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/map")
public static class Map extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("isEmpty.kt")
public void testIsEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/map/isEmpty.kt");
}
@TestMetadata("isNotEmpty.kt")
public void testIsNotEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/map/isNotEmpty.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/set")
public static class Set extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("isEmpty.kt")
public void testIsEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/set/isEmpty.kt");
}
@TestMetadata("isNotEmpty.kt")
public void testIsNotEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/set/isNotEmpty.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/str")
public static class Str extends AbstractLocalInspectionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("isBlank.kt")
public void testIsBlank() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/str/isBlank.kt");
}
@TestMetadata("isEmpty.kt")
public void testIsEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/str/isEmpty.kt");
}
@TestMetadata("isNotBlank.kt")
public void testIsNotBlank() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/str/isNotBlank.kt");
}
@TestMetadata("isNotEmpty.kt")
public void testIsNotEmpty() throws Exception {
runTest("testData/inspectionsLocal/replaceIsEmptyWithIfEmpty/str/isNotEmpty.kt");
}
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/inspectionsLocal/replaceJavaStaticMethodWithKotlinAnalog")
public abstract static class ReplaceJavaStaticMethodWithKotlinAnalog extends AbstractLocalInspectionTest {
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.ReplaceIsEmptyWithIfEmptyInspection
@@ -0,0 +1,9 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(arr: Array<String>): Array<String> {
return if (arr.isEmpty<caret>()) {
arrayOf("a")
} else {
arr
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(arr: Array<String>): Array<String> {
return if (arr.isNotEmpty<caret>()) {
arr
} else {
arrayOf("a")
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(collection: Collection<Int>): Collection<Int> {
return if (collection.isEmpty<caret>()) {
listOf(1)
} else {
collection
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(collection: Collection<Int>): Collection<Int> {
return collection.ifEmpty {
listOf(1)
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(collection: Collection<Int>): Collection<Int> {
return if (collection.isNotEmpty<caret>()) {
collection
} else {
listOf(1)
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(collection: Collection<Int>): Collection<Int> {
return collection.ifEmpty {
listOf(1)
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
fun foo() = listOf(1)
fun test(list: List<Int>): List<Int> {
return if (list.isEmpty<caret>()) {
println()
foo()
} else {
list
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo() = listOf(1)
fun test(list: List<Int>): List<Int> {
return list.ifEmpty {
println()
foo()
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
fun foo() = listOf(1)
fun test(list: List<Int>): List<Int> {
return if (list.isNotEmpty<caret>()) {
list
} else {
println()
foo()
}
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo() = listOf(1)
fun test(list: List<Int>): List<Int> {
return list.ifEmpty {
println()
foo()
}
}
@@ -0,0 +1,11 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(list: List<Int>, b: Boolean): List<Int> {
return if (list.isEmpty<caret>()) {
listOf(1)
} else if (b) {
listOf(2)
} else {
list
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun String.test(): String {
return if (isBlank<caret>()) {
"foo"
} else {
this
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun String.test(): String {
return ifBlank {
"foo"
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(intArr: IntArray): IntArray {
return if (intArr.isEmpty<caret>()) {
intArrayOf(1)
} else {
intArr
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(intArr: IntArray): IntArray {
return if (intArr.isNotEmpty<caret>()) {
intArr
} else {
intArrayOf(1)
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(list: List<Int>): List<Int> {
return if (list.isEmpty<caret>()) {
listOf(1)
} else {
list
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(list: List<Int>): List<Int> {
return list.ifEmpty {
listOf(1)
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(list: List<Int>): List<Int> {
return if (list.isNotEmpty<caret>()) {
list
} else {
listOf(1)
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(list: List<Int>): List<Int> {
return list.ifEmpty {
listOf(1)
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(map: Map<Int, Int>): Map<Int, Int> {
return if (map.isEmpty<caret>()) {
mapOf(1 to 2)
} else {
map
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(map: Map<Int, Int>): Map<Int, Int> {
return map.ifEmpty {
mapOf(1 to 2)
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(map: Map<Int, Int>): Map<Int, Int> {
return if (map.isNotEmpty<caret>()) {
map
} else {
mapOf(1 to 2)
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(map: Map<Int, Int>): Map<Int, Int> {
return map.ifEmpty {
mapOf(1 to 2)
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(list: List<Int>): List<Int> {
return if (!list.isEmpty<caret>()) {
list
} else {
listOf(1)
}
}
@@ -0,0 +1,7 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(list: List<Int>) {
if (list.isEmpty<caret>()) {
listOf(1)
}
}
@@ -0,0 +1,9 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(list: List<Int>): List<Int> {
return if (list.isEmpty<caret>()) {
listOf(1)
} else {
list + list
}
}
@@ -0,0 +1,10 @@
// PROBLEM: none
// WITH_RUNTIME
fun test(list: List<Int>): List<Int> {
return if (list.isEmpty<caret>()) {
listOf(1)
} else {
println()
list
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(set: Set<Int>): Set<Int> {
return if (set.isEmpty<caret>()) {
setOf(1)
} else {
set
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(set: Set<Int>): Set<Int> {
return set.ifEmpty {
setOf(1)
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun test(set: Set<Int>): Set<Int> {
return if (set.isNotEmpty<caret>()) {
set
} else {
setOf(1)
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
fun test(set: Set<Int>): Set<Int> {
return set.ifEmpty {
setOf(1)
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = if (api.name.isBlank<caret>()) "John" else api.name
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = api.name.ifBlank { "John" }
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = if (api.name.isEmpty<caret>()) "John" else api.name
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = api.name.ifEmpty { "John" }
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = if (api.name.isNotBlank<caret>())
api.name
else
"John"
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = api.name.ifBlank { "John" }
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = if (api.name.isNotEmpty<caret>())
api.name
else
"John"
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
class Api(val name: String)
fun test(api: Api) {
val name = api.name.ifEmpty { "John" }
}