diff --git a/idea/resources/inspectionDescriptions/ReplaceNegatedIsEmptyWithIsNotEmpty.html b/idea/resources/inspectionDescriptions/ReplaceNegatedIsEmptyWithIsNotEmpty.html index 6bcceb66364..cd61646701f 100644 --- a/idea/resources/inspectionDescriptions/ReplaceNegatedIsEmptyWithIsNotEmpty.html +++ b/idea/resources/inspectionDescriptions/ReplaceNegatedIsEmptyWithIsNotEmpty.html @@ -1,5 +1,5 @@ -This inspection reports !collection.isEmpty() call can be replaced with collection.isNotEmpty() +This inspection reports !collection.isEmpty()/isNotEmpty() call can be replaced with collection.isNotEmpty()/isEmpty(). \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceNegatedIsEmptyWithIsNotEmptyInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceNegatedIsEmptyWithIsNotEmptyInspection.kt index 86836b56644..b011a6042e2 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceNegatedIsEmptyWithIsNotEmptyInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceNegatedIsEmptyWithIsNotEmptyInspection.kt @@ -14,69 +14,76 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import org.jetbrains.kotlin.idea.core.replaced import org.jetbrains.kotlin.idea.inspections.collections.isCalling +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.getLastParentOfTypeInRow +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType class ReplaceNegatedIsEmptyWithIsNotEmptyInspection : AbstractKotlinInspection() { override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return qualifiedExpressionVisitor(fun(expression) { + val callExpression = expression.callExpression ?: return + val calleeExpression = callExpression.calleeExpression ?: return + val calleeText = calleeExpression.text + val isEmptyCall = calleeText == "isEmpty" + val isNotEmptyCall = calleeText == "isNotEmpty" + if (!isEmptyCall && !isNotEmptyCall) return - return simpleNameExpressionVisitor { simpleNameExpression -> - if (simpleNameExpression.isEmptyNegation()) { - holder.registerProblem( - simpleNameExpression, - "Replace negated 'isEmpty' with 'isNotEmpty'", - ProblemHighlightType.GENERIC_ERROR_OR_WARNING, - ReplaceNegatedIsEmptyWithIsNotEmptyQuickFix() - ) - } - } + val prefixExpression = expression.getWrappingPrefixExpressionIfAny() ?: return + if (prefixExpression.operationToken != KtTokens.EXCL) return + + if (isEmptyCall && isEmptyFunctions.none { callExpression.isCalling(FqName(it)) } + || isNotEmptyCall && isNotEmptyFunctions.none { callExpression.isCalling(FqName(it)) }) return + + val (from, to) = if (isEmptyCall) "isEmpty" to "isNotEmpty" else "isNotEmpty" to "isEmpty" + holder.registerProblem( + calleeExpression, + "Replace negated '$from' with '$to'", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + ReplaceNegatedIsEmptyWithIsNotEmptyQuickFix(from, to) + ) + }) } } -class ReplaceNegatedIsEmptyWithIsNotEmptyQuickFix : LocalQuickFix { - override fun getName() = "Replace negated 'isEmpty' with 'isNotEmpty'" +class ReplaceNegatedIsEmptyWithIsNotEmptyQuickFix(private val from: String, private val to: String) : LocalQuickFix { + override fun getName() = "Replace negated '$from' with '$to'" override fun getFamilyName() = name override fun applyFix(project: Project, descriptor: ProblemDescriptor) { - val element = descriptor.psiElement as? KtSimpleNameExpression ?: return - val callExpression = (element.parent as? KtCallExpression) ?: return - val qualifiedExpression = (callExpression.parent as? KtDotQualifiedExpression) ?: return + val qualifiedExpression = descriptor.psiElement.getStrictParentOfType() ?: return val prefixExpression = qualifiedExpression.getWrappingPrefixExpressionIfAny() ?: return - prefixExpression.replaced( - KtPsiFactory(element).createExpressionByPattern( - "$0.isNotEmpty()", + KtPsiFactory(qualifiedExpression).createExpressionByPattern( + "$0.$to()", qualifiedExpression.receiverExpression ) ) } } -private fun KtSimpleNameExpression.isEmptyNegation(): Boolean { - val callExpression = (parent as? KtCallExpression) ?: return false - val qualifiedExpression = (callExpression.parent as? KtDotQualifiedExpression) ?: return false - val prefixExpression = qualifiedExpression.getWrappingPrefixExpressionIfAny() ?: return false - if (prefixExpression.operationToken != KtTokens.EXCL) return false - return transformations.any { callExpression.isCalling(FqName(it)) } -} - private fun PsiElement.getWrappingPrefixExpressionIfAny() = (getLastParentOfTypeInRow() ?: this).parent as? KtPrefixExpression -private val transformations = listOf( - "java.util.ArrayList.isEmpty", - "java.util.HashMap.isEmpty", - "java.util.HashSet.isEmpty", - "java.util.LinkedHashMap.isEmpty", - "java.util.LinkedHashSet.isEmpty", - "kotlin.collections.isEmpty", - "kotlin.collections.List.isEmpty", - "kotlin.collections.Set.isEmpty", - "kotlin.collections.Map.isEmpty", - "kotlin.collections.MutableList.isEmpty", - "kotlin.collections.MutableSet.isEmpty", - "kotlin.collections.MutableMap.isEmpty" -) \ No newline at end of file +private val packages = listOf( + "java.util.ArrayList", + "java.util.HashMap", + "java.util.HashSet", + "java.util.LinkedHashMap", + "java.util.LinkedHashSet", + "kotlin.collections", + "kotlin.collections.List", + "kotlin.collections.Set", + "kotlin.collections.Map", + "kotlin.collections.MutableList", + "kotlin.collections.MutableSet", + "kotlin.collections.MutableMap", + "kotlin.text" +) + +private val isEmptyFunctions = packages.map { "$it.isEmpty" } + +private val isNotEmptyFunctions = packages.map { "$it.isNotEmpty" } \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/notNegate.kt b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/notNegate.kt new file mode 100644 index 00000000000..6ed626d6eed --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/notNegate.kt @@ -0,0 +1,5 @@ +// PROBLEM: none +// WITH_RUNTIME +fun test(s: String) { + val b = s.isEmpty() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsEmpty.kt b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsEmpty.kt new file mode 100644 index 00000000000..cfb358a07bb --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsEmpty.kt @@ -0,0 +1,6 @@ +// PROBLEM: Replace negated 'isEmpty' with 'isNotEmpty' +// FIX: Replace negated 'isEmpty' with 'isNotEmpty' +// WITH_RUNTIME +fun test(s: String) { + val b = !s.isEmpty() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsEmpty.kt.after b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsEmpty.kt.after new file mode 100644 index 00000000000..a5e88f434c1 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsEmpty.kt.after @@ -0,0 +1,6 @@ +// PROBLEM: Replace negated 'isEmpty' with 'isNotEmpty' +// FIX: Replace negated 'isEmpty' with 'isNotEmpty' +// WITH_RUNTIME +fun test(s: String) { + val b = s.isNotEmpty() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsNotEmpty.kt b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsNotEmpty.kt new file mode 100644 index 00000000000..cbb364bfb5b --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsNotEmpty.kt @@ -0,0 +1,6 @@ +// PROBLEM: Replace negated 'isNotEmpty' with 'isEmpty' +// FIX: Replace negated 'isNotEmpty' with 'isEmpty' +// WITH_RUNTIME +fun test(s: String) { + val b = !s.isNotEmpty() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsNotEmpty.kt.after b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsNotEmpty.kt.after new file mode 100644 index 00000000000..50b44e206a3 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsNotEmpty.kt.after @@ -0,0 +1,6 @@ +// PROBLEM: Replace negated 'isNotEmpty' with 'isEmpty' +// FIX: Replace negated 'isNotEmpty' with 'isEmpty' +// WITH_RUNTIME +fun test(s: String) { + val b = s.isEmpty() +} \ 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 f767641f153..b93783810b7 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -6614,10 +6614,25 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); } + @TestMetadata("notNegate.kt") + public void testNotNegate() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/notNegate.kt"); + } + @TestMetadata("simple.kt") public void testSimple() throws Exception { runTest("idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/simple.kt"); } + + @TestMetadata("stringIsEmpty.kt") + public void testStringIsEmpty() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsEmpty.kt"); + } + + @TestMetadata("stringIsNotEmpty.kt") + public void testStringIsNotEmpty() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNegatedIsEmptyWithIsNotEmpty/stringIsNotEmpty.kt"); + } } @TestMetadata("idea/testData/inspectionsLocal/replacePutWithAssignment")