diff --git a/idea/resources/inspectionDescriptions/NullChecksToSafeCall.html b/idea/resources/inspectionDescriptions/NullChecksToSafeCall.html new file mode 100644 index 00000000000..7e617508bb5 --- /dev/null +++ b/idea/resources/inspectionDescriptions/NullChecksToSafeCall.html @@ -0,0 +1,5 @@ + + +This inspection reports when chained null-checks can be replaced with safe-calls + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 860cdbf4dac..95fa69f7bf3 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -2378,6 +2378,14 @@ language="kotlin" /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/NullChecksToSafeCallInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/NullChecksToSafeCallInspection.kt new file mode 100644 index 00000000000..5aa894e519b --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/NullChecksToSafeCallInspection.kt @@ -0,0 +1,113 @@ +/* + * 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 + +import com.intellij.codeInsight.FileModificationService +import com.intellij.codeInspection.* +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.core.replaced +import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isStable +import org.jetbrains.kotlin.lexer.KtToken +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.types.TypeUtils + +class NullChecksToSafeCallInspection : AbstractKotlinInspection(), CleanupLocalInspectionTool { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession) = + object : KtVisitorVoid() { + override fun visitBinaryExpression(expression: KtBinaryExpression) { + if (isNullChecksToSafeCallFixAvailable(expression)) { + holder.registerProblem(expression, + "Null-checks replaceable with safe-calls", + ProblemHighlightType.WEAK_WARNING, + NullChecksToSafeCallCheckFix()) + } + } + } + + private class NullChecksToSafeCallCheckFix : LocalQuickFix { + override fun getName() = "Replace chained null-checks with safe-calls" + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + applyFix(descriptor.psiElement as? KtBinaryExpression ?: return) + } + + private fun applyFix(expression: KtBinaryExpression) { + if (!FileModificationService.getInstance().preparePsiElementForWrite(expression)) return + val (lte, rte, isAnd) = collectNullCheckExpressions(expression) ?: return + val parent = expression.parent + expression.replaced(KtPsiFactory(lte).buildExpression { + appendExpression(lte) + appendFixedText("?.") + appendExpression(rte.selectorExpression) + appendFixedText(if (isAnd) "!= null" else "== null") + }) + if (isNullChecksToSafeCallFixAvailable(parent as? KtBinaryExpression ?: return)) { + applyFix(parent) + } + } + } + + companion object { + private fun isNullChecksToSafeCallFixAvailable(expression: KtBinaryExpression): Boolean { + fun String.afterIgnoreCalls() = replace("?.", ".") + + val (lte, rte) = collectNullCheckExpressions(expression) ?: return false + val context = expression.analyze() + if (!lte.isChainStable(context)) return false + + val resolvedCall = rte.getResolvedCall(context) ?: return false + val extensionReceiver = resolvedCall.extensionReceiver + if (extensionReceiver != null && TypeUtils.isNullableType(extensionReceiver.type)) return false + + return rte.receiverExpression.text.afterIgnoreCalls() == lte.text.afterIgnoreCalls() + } + + private fun collectNullCheckExpressions(expression: KtBinaryExpression): Triple? { + val isAnd = when (expression.operationToken) { + KtTokens.ANDAND -> true + KtTokens.OROR -> false + else -> return null + } + val lhs = expression.left as? KtBinaryExpression ?: return null + val rhs = expression.right as? KtBinaryExpression ?: return null + val expectedOperation = if (isAnd) KtTokens.EXCLEQ else KtTokens.EQEQ + val lte = lhs.getNullTestableExpression(expectedOperation) ?: return null + val rte = rhs.getNullTestableExpression(expectedOperation) as? KtQualifiedExpression ?: return null + return Triple(lte, rte, isAnd) + } + + private fun KtBinaryExpression.getNullTestableExpression(expectedOperation: KtToken): KtExpression? { + if (operationToken != expectedOperation) return null + val lhs = left ?: return null + val rhs = right ?: return null + if (KtPsiUtil.isNullConstant(lhs)) return rhs + if (KtPsiUtil.isNullConstant(rhs)) return lhs + return null + } + + private fun KtExpression.isChainStable(context: BindingContext): Boolean = when (this) { + is KtReferenceExpression -> isStable(context) + is KtQualifiedExpression -> selectorExpression?.isStable(context) == true && receiverExpression.isChainStable(context) + else -> false + } + } +} diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/.inspection b/idea/testData/inspectionsLocal/nullChecksToSafeCall/.inspection new file mode 100644 index 00000000000..2c99e113e8b --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.NullChecksToSafeCallInspection \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/andCase.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/andCase.kt new file mode 100644 index 00000000000..9b7d391f985 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/andCase.kt @@ -0,0 +1,10 @@ +fun main(args: Array) { + val a: Testtt? = Testtt() + if (a != null && a.a != null && a.a.a != null && a.a.a.a != null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/andCase.kt.after b/idea/testData/inspectionsLocal/nullChecksToSafeCall/andCase.kt.after new file mode 100644 index 00000000000..e88cd1e0ae5 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/andCase.kt.after @@ -0,0 +1,10 @@ +fun main(args: Array) { + val a: Testtt? = Testtt() + if (a?.a?.a?.a != null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/function.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/function.kt new file mode 100644 index 00000000000..55ea1103ddc --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/function.kt @@ -0,0 +1,7 @@ +class My { + fun foo(): String? = null +} + +fun test(my: My?) { + if (my != null && my.foo() != null) {} +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/function.kt.after b/idea/testData/inspectionsLocal/nullChecksToSafeCall/function.kt.after new file mode 100644 index 00000000000..d4f21a33c84 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/function.kt.after @@ -0,0 +1,7 @@ +class My { + fun foo(): String? = null +} + +fun test(my: My?) { + if (my?.foo() != null) {} +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/kotlinType.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/kotlinType.kt new file mode 100644 index 00000000000..2d10eb0da10 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/kotlinType.kt @@ -0,0 +1,10 @@ +// PROBLEM: none + +class KotlinType +class KotlinTypeInfo(val type: KotlinType?) { + fun foo(other: KotlinTypeInfo) { + if (type != null && other.type != null) { + + } + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/longRightExpression.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/longRightExpression.kt new file mode 100644 index 00000000000..3a009c619af --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/longRightExpression.kt @@ -0,0 +1,13 @@ +// PROBLEM: none + +fun main(args: Array) { + val a: Testtt? = Testtt() + // Controversial case, better to do nothing here + if (a != null && a.a?.a != null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/orCase.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/orCase.kt new file mode 100644 index 00000000000..af0d1c62543 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/orCase.kt @@ -0,0 +1,10 @@ +fun main(args: Array) { + val a: Testtt? = Testtt() + if (a == null || a.a == null || a.a.a == null || a.a.a.a == null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/orCase.kt.after b/idea/testData/inspectionsLocal/nullChecksToSafeCall/orCase.kt.after new file mode 100644 index 00000000000..1cdb322df75 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/orCase.kt.after @@ -0,0 +1,10 @@ +fun main(args: Array) { + val a: Testtt? = Testtt() + if (a?.a?.a?.a == null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/safeCall.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/safeCall.kt new file mode 100644 index 00000000000..b6bc3ee9224 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/safeCall.kt @@ -0,0 +1,11 @@ + +fun main(args: Array) { + val a: Testtt? = Testtt() + if (a?.a != null && a.a.a != null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/safeCall.kt.after b/idea/testData/inspectionsLocal/nullChecksToSafeCall/safeCall.kt.after new file mode 100644 index 00000000000..1de017e0c10 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/safeCall.kt.after @@ -0,0 +1,11 @@ + +fun main(args: Array) { + val a: Testtt? = Testtt() + if (a?.a?.a != null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableChain.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableChain.kt new file mode 100644 index 00000000000..2f7e2b00783 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableChain.kt @@ -0,0 +1,12 @@ +// PROBLEM: none +var a: Testtt? = Testtt() + +fun main(args: Array) { + if (a != null && a?.a != null) { + + } +} + +class Testtt { + val a: Testtt? = null +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableExtensionInChain.kt b/idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableExtensionInChain.kt new file mode 100644 index 00000000000..fe641f30201 --- /dev/null +++ b/idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableExtensionInChain.kt @@ -0,0 +1,12 @@ +// PROBLEM: none +val a: Testtt? = Testtt() + +fun main(args: Array) { + if (a != null && a.a != null) { + + } +} + +class Testtt + +val Testtt?.a: Testtt? get() = null \ 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 9d6ce6195bb..e2c39f4e9fa 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -957,6 +957,63 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { } } + @TestMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class NullChecksToSafeCall extends AbstractLocalInspectionTest { + public void testAllFilesPresentInNullChecksToSafeCall() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/nullChecksToSafeCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("andCase.kt") + public void testAndCase() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/andCase.kt"); + doTest(fileName); + } + + @TestMetadata("function.kt") + public void testFunction() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/function.kt"); + doTest(fileName); + } + + @TestMetadata("kotlinType.kt") + public void testKotlinType() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/kotlinType.kt"); + doTest(fileName); + } + + @TestMetadata("longRightExpression.kt") + public void testLongRightExpression() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/longRightExpression.kt"); + doTest(fileName); + } + + @TestMetadata("orCase.kt") + public void testOrCase() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/orCase.kt"); + doTest(fileName); + } + + @TestMetadata("safeCall.kt") + public void testSafeCall() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/safeCall.kt"); + doTest(fileName); + } + + @TestMetadata("unstableChain.kt") + public void testUnstableChain() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableChain.kt"); + doTest(fileName); + } + + @TestMetadata("unstableExtensionInChain.kt") + public void testUnstableExtensionInChain() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspectionsLocal/nullChecksToSafeCall/unstableExtensionInChain.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/inspectionsLocal/nullableBooleanElvis") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)