Introduce "Useless call on not-null type" inspection #KT-18386 Fixed

Supported functions: orEmpty(), isNullOrEmpty(), isNullOrBlank()
This commit is contained in:
Mikhail Glukhikh
2017-06-16 17:14:59 +03:00
parent 8c21ff66a4
commit 0f4ae3b727
21 changed files with 322 additions and 0 deletions
@@ -0,0 +1,8 @@
<html>
<body>
This inspection reports calls on not-null receiver that make sense only for nullable receiver, e.g.
<pre><code>
listOf(1).orEmpty()
</code></pre>
</body>
</html>
+8
View File
@@ -2242,6 +2242,14 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.collections.UselessCallOnNotNullInspection"
displayName="Useless call on not-null type"
groupName="Kotlin"
enabledByDefault="true"
level="WARNING"
language="kotlin"
/>
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.inspections.collections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.psi.KtQualifiedExpression
class RemoveUselessCallFix : LocalQuickFix {
override fun getName() = "Remove useless call"
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
(descriptor.psiElement as? KtQualifiedExpression)?.let {
it.replaced(it.receiverExpression)
}
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.inspections.collections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.createExpressionByPattern
class RenameUselessCallFix(val newName: String) : LocalQuickFix {
override fun getName() = "Rename useless call to '$newName'"
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
(descriptor.psiElement as? KtQualifiedExpression)?.let {
val factory = KtPsiFactory(it)
val sign = if (it is KtSafeQualifiedExpression) "?." else "."
it.replaced(factory.createExpressionByPattern("$0$sign$newName()", it.receiverExpression))
}
}
}
@@ -0,0 +1,105 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.inspections.collections
import com.intellij.codeInspection.IntentionWrapper
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElementVisitor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.quickfix.ReplaceWithDotCallFix
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.KtVisitorVoid
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.TypeUtils
class UselessCallOnNotNullInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : KtVisitorVoid() {
override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
super.visitQualifiedExpression(expression)
val selector = expression.selectorExpression as? KtCallExpression ?: return
val calleeExpression = selector.calleeExpression ?: return
if (calleeExpression.text !in names) return
val context = expression.analyze(BodyResolveMode.PARTIAL)
val resolvedCall = expression.getResolvedCall(context) ?: return
val conversion = fqNames[resolvedCall.resultingDescriptor.fqNameOrNull()?.asString()] ?: return
val newName = conversion.replacementName
val safeExpression = expression as? KtSafeQualifiedExpression
val notNullType = expression.receiverExpression.getType(context)?.let { TypeUtils.isNullableType(it) } == false
if (newName != null && (notNullType || safeExpression != null)) {
val descriptor = holder.manager.createProblemDescriptor(
expression,
TextRange(expression.operationTokenNode.startOffset - expression.startOffset,
calleeExpression.endOffset - expression.startOffset),
"Call on not-null type may be reduced",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
isOnTheFly,
RenameUselessCallFix(newName)
)
holder.registerProblem(descriptor)
}
else if (notNullType) {
val descriptor = holder.manager.createProblemDescriptor(
expression,
TextRange(expression.operationTokenNode.startOffset - expression.startOffset,
calleeExpression.endOffset - expression.startOffset),
"Useless call on not-null type",
ProblemHighlightType.LIKE_UNUSED_SYMBOL,
isOnTheFly,
RemoveUselessCallFix()
)
holder.registerProblem(descriptor)
}
else if (safeExpression != null) {
holder.registerProblem(
safeExpression.operationTokenNode.psi,
"This call is useless with ?.",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
IntentionWrapper(ReplaceWithDotCallFix(safeExpression), safeExpression.containingKtFile)
)
}
}
}
}
companion object {
private data class Conversion(val replacementName: String? = null)
private val deleteConversion = Conversion()
private val fqNames = mapOf("kotlin.collections.orEmpty" to deleteConversion,
"kotlin.text.orEmpty" to deleteConversion,
"kotlin.text.isNullOrEmpty" to Conversion("isEmpty"),
"kotlin.text.isNullOrBlank" to Conversion("isBlank"))
private val names = fqNames.keys.mapTo(mutableSetOf()) { fqName -> fqName.takeLastWhile { it != '.' } }
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.inspections.collections.UselessCallOnNotNullInspection
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val list1: List<Int> = listOf(1)
val list = list1<caret>.orEmpty()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val list1: List<Int> = listOf(1)
val list = list1
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val list1: List<Int> = listOf(1)
val list = list1.<caret>orEmpty().map { "$it" }
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val list1: List<Int> = listOf(1)
val list = list1.map { "$it" }
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val s: String? = ""
val blank = s<caret>?.isNullOrBlank()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val s: String? = ""
val blank = s?.isBlank()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val s = ""
val empty = s<caret>.isNullOrEmpty()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val s = ""
val empty = s.isEmpty()
@@ -0,0 +1,5 @@
// PROBLEM: none
// WITH_RUNTIME
val s: String? = null
val empty = s<caret>.isNullOrEmpty()
@@ -0,0 +1,5 @@
// PROBLEM: none
// WITH_RUNTIME
val list: List<String>? = null
val empty = list<caret>.orEmpty()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val list1: List<Int>? = listOf(1)
val list = list1<caret>?.orEmpty()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val list1: List<Int>? = listOf(1)
val list = list1.orEmpty()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val s = ""
val s1 = s<caret>.orEmpty()
@@ -0,0 +1,4 @@
// WITH_RUNTIME
val s = ""
val s1 = s
@@ -51,6 +51,72 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
}
}
@TestMetadata("idea/testData/inspectionsLocal/collections")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Collections extends AbstractLocalInspectionTest {
public void testAllFilesPresentInCollections() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/collections"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class UselessCallOnNotNull extends AbstractLocalInspectionTest {
public void testAllFilesPresentInUselessCallOnNotNull() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("NotNullType.kt")
public void testNotNullType() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/NotNullType.kt");
doTest(fileName);
}
@TestMetadata("NotNullTypeChain.kt")
public void testNotNullTypeChain() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/NotNullTypeChain.kt");
doTest(fileName);
}
@TestMetadata("NullOrBlankSafe.kt")
public void testNullOrBlankSafe() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/NullOrBlankSafe.kt");
doTest(fileName);
}
@TestMetadata("NullOrEmpty.kt")
public void testNullOrEmpty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/NullOrEmpty.kt");
doTest(fileName);
}
@TestMetadata("NullOrEmptyFake.kt")
public void testNullOrEmptyFake() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/NullOrEmptyFake.kt");
doTest(fileName);
}
@TestMetadata("OrEmptyFake.kt")
public void testOrEmptyFake() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/OrEmptyFake.kt");
doTest(fileName);
}
@TestMetadata("SafeCall.kt")
public void testSafeCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/SafeCall.kt");
doTest(fileName);
}
@TestMetadata("String.kt")
public void testString() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/collections/uselessCallOnNotNull/String.kt");
doTest(fileName);
}
}
}
@TestMetadata("idea/testData/inspectionsLocal/copyWithoutNamedArguments")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)