diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index f8b0e4f6439..ed85296b866 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -3355,6 +3355,15 @@ language="kotlin" /> + + diff --git a/idea/resources/inspectionDescriptions/ReplaceNotNullAssertionWithElvisReturn.html b/idea/resources/inspectionDescriptions/ReplaceNotNullAssertionWithElvisReturn.html new file mode 100644 index 00000000000..0577164d742 --- /dev/null +++ b/idea/resources/inspectionDescriptions/ReplaceNotNullAssertionWithElvisReturn.html @@ -0,0 +1,13 @@ + + +This inspection reports not-null assertion (!!) calls that can be replaced with elvis and return (?: return). For example: +

+
+val number: Int? = 42
+fun foo() {
+    val a = number!! // Replace '!!' with '?: return'
+    println(1 + a)
+}
+
+ + \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceNotNullAssertionWithElvisReturnInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceNotNullAssertionWithElvisReturnInspection.kt new file mode 100644 index 00000000000..f312a980b89 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/ReplaceNotNullAssertionWithElvisReturnInspection.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.codeInsight.intention.LowPriorityAction +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.core.replaced +import org.jetbrains.kotlin.idea.intentions.getParentLambdaLabelName +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypes +import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.isNullable +import org.jetbrains.kotlin.types.typeUtil.isUnit + +class ReplaceNotNullAssertionWithElvisReturnInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = postfixExpressionVisitor(fun(postfix) { + if (postfix.baseExpression == null) return + val operationReference = postfix.operationReference + if (operationReference.getReferencedNameElementType() != KtTokens.EXCLEXCL) return + + if ((postfix.getTopmostParentOfType() ?: postfix).parent is KtReturnExpression) return + + val parent = postfix.getParentOfTypes(true, KtLambdaExpression::class.java, KtNamedFunction::class.java) + if (parent !is KtNamedFunction && parent !is KtLambdaExpression) return + val context = postfix.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS) + val (isNullable, returnLabelName) = when (parent) { + is KtNamedFunction -> { + val returnType = parent.descriptor(context)?.returnType ?: return + val isNullable = returnType.isNullable() + if (!returnType.isUnit() && !isNullable) return + isNullable to null + } + is KtLambdaExpression -> { + val functionLiteral = parent.functionLiteral + val returnType = functionLiteral.descriptor(context)?.returnType ?: return + if (!returnType.isUnit()) return + val lambdaLabelName = functionLiteral.bodyBlockExpression?.getParentLambdaLabelName() ?: return + false to lambdaLabelName + } + else -> return + } + + if (context.diagnostics.forElement(operationReference).any { it.factory == Errors.UNNECESSARY_NOT_NULL_ASSERTION }) return + + holder.registerProblem( + postfix.operationReference, + "Replace '!!' with '?: return'", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + ReplaceWithElvisReturnFix(isNullable, returnLabelName) + ) + }) + + private fun KtFunction.descriptor(context: BindingContext): FunctionDescriptor? { + return context[BindingContext.DECLARATION_TO_DESCRIPTOR, this] as? FunctionDescriptor + } + + private class ReplaceWithElvisReturnFix( + private val returnNull: Boolean, + private val returnLabelName: String? + ) : LocalQuickFix, LowPriorityAction { + override fun getName() = "Replace with '?: return${if (returnNull) " null" else ""}'" + override fun getFamilyName() = name + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val postfix = descriptor.psiElement.parent as? KtPostfixExpression ?: return + val base = postfix.baseExpression ?: return + val psiFactory = KtPsiFactory(postfix) + postfix.replaced( + psiFactory.createExpressionByPattern( + "$0 ?: return$1$2", + base, + returnLabelName?.let { "@$it" } ?: "", + if (returnNull) " null" else "" + ) + ) + } + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/.inspection b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/.inspection new file mode 100644 index 00000000000..30474f8901a --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.ReplaceNotNullAssertionWithElvisReturnInspection \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic.kt new file mode 100644 index 00000000000..3c596344e6d --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic.kt @@ -0,0 +1,3 @@ +fun test(i: Int?) { + val x = i!! +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic.kt.after b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic.kt.after new file mode 100644 index 00000000000..80dba28f1ac --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic.kt.after @@ -0,0 +1,3 @@ +fun test(i: Int?) { + val x = i ?: return +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic2.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic2.kt new file mode 100644 index 00000000000..8b720b34350 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic2.kt @@ -0,0 +1,5 @@ +fun foo(): Int? = null + +fun test() { + foo()!! +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic2.kt.after b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic2.kt.after new file mode 100644 index 00000000000..5e8deb99f4b --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic2.kt.after @@ -0,0 +1,5 @@ +fun foo(): Int? = null + +fun test() { + foo() ?: return +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic3.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic3.kt new file mode 100644 index 00000000000..15de7bf7b53 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic3.kt @@ -0,0 +1,6 @@ +class Foo(val i: Int?) + +fun test(foo: Foo): Int? { + val x = foo.i!! + return x +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic3.kt.after b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic3.kt.after new file mode 100644 index 00000000000..f2b8b9c6c10 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic3.kt.after @@ -0,0 +1,6 @@ +class Foo(val i: Int?) + +fun test(foo: Foo): Int? { + val x = foo.i ?: return null + return x +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic4.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic4.kt new file mode 100644 index 00000000000..b83e67d6ed2 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic4.kt @@ -0,0 +1,8 @@ +class Foo { + fun baz(): Int? = null +} + +fun test(foo: Foo): Int? { + foo.baz()!! + return 0 +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic4.kt.after b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic4.kt.after new file mode 100644 index 00000000000..a28f4e3b1eb --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic4.kt.after @@ -0,0 +1,8 @@ +class Foo { + fun baz(): Int? = null +} + +fun test(foo: Foo): Int? { + foo.baz() ?: return null + return 0 +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic5.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic5.kt new file mode 100644 index 00000000000..65a5ad0d810 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic5.kt @@ -0,0 +1,3 @@ +fun test(i: Int?) { + val x = i!! + 1 +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic5.kt.after b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic5.kt.after new file mode 100644 index 00000000000..395950d389d --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic5.kt.after @@ -0,0 +1,3 @@ +fun test(i: Int?) { + val x = (i ?: return) + 1 +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda.kt new file mode 100644 index 00000000000..f827a310ad8 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +fun test(list: List, number: Int?) { + list.forEach { + number!! + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda.kt.after b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda.kt.after new file mode 100644 index 00000000000..c6a258ae7d8 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME +fun test(list: List, number: Int?) { + list.forEach { + number ?: return@forEach + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda2.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda2.kt new file mode 100644 index 00000000000..3f976940848 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda2.kt @@ -0,0 +1,7 @@ +// PROBLEM: none +// WITH_RUNTIME +fun test(list: List, number: Int?) { + val x: List = list.map { + number!! + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/notNullFunction.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/notNullFunction.kt new file mode 100644 index 00000000000..782a37f4332 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/notNullFunction.kt @@ -0,0 +1,5 @@ +// PROBLEM: none +fun test(i: Int?): Int { + val x = i!! + return x +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn.kt new file mode 100644 index 00000000000..76b422c8b47 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn.kt @@ -0,0 +1,4 @@ +// PROBLEM: none +fun test(i: Int?): Int? { + return i!! +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn2.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn2.kt new file mode 100644 index 00000000000..7a8cd1b9607 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn2.kt @@ -0,0 +1,4 @@ +// PROBLEM: none +fun test(i: Int?): Int? { + return (i!!) +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn3.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn3.kt new file mode 100644 index 00000000000..a4db7cb0bf5 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn3.kt @@ -0,0 +1,5 @@ +fun test(a: Int?): Int? { + return check(a!!) +} + +fun check(i: Int): Int = i \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn3.kt.after b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn3.kt.after new file mode 100644 index 00000000000..3829dddde31 --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn3.kt.after @@ -0,0 +1,5 @@ +fun test(a: Int?): Int? { + return check(a ?: return null) +} + +fun check(i: Int): Int = i \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/unnecessaryNotNullAssertion.kt b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/unnecessaryNotNullAssertion.kt new file mode 100644 index 00000000000..19b786f8ecb --- /dev/null +++ b/idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/unnecessaryNotNullAssertion.kt @@ -0,0 +1,4 @@ +// PROBLEM: none +fun test(i: Int) { + val x = i!! +} \ 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 03b371efe49..0cf7b0a8c2e 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -7117,6 +7117,79 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { } } + @TestMetadata("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ReplaceNotNullAssertionWithElvisReturn extends AbstractLocalInspectionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInReplaceNotNullAssertionWithElvisReturn() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); + } + + @TestMetadata("basic.kt") + public void testBasic() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic.kt"); + } + + @TestMetadata("basic2.kt") + public void testBasic2() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic2.kt"); + } + + @TestMetadata("basic3.kt") + public void testBasic3() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic3.kt"); + } + + @TestMetadata("basic4.kt") + public void testBasic4() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic4.kt"); + } + + @TestMetadata("basic5.kt") + public void testBasic5() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/basic5.kt"); + } + + @TestMetadata("inLambda.kt") + public void testInLambda() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda.kt"); + } + + @TestMetadata("inLambda2.kt") + public void testInLambda2() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/inLambda2.kt"); + } + + @TestMetadata("notNullFunction.kt") + public void testNotNullFunction() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/notNullFunction.kt"); + } + + @TestMetadata("onReturn.kt") + public void testOnReturn() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn.kt"); + } + + @TestMetadata("onReturn2.kt") + public void testOnReturn2() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn2.kt"); + } + + @TestMetadata("onReturn3.kt") + public void testOnReturn3() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/onReturn3.kt"); + } + + @TestMetadata("unnecessaryNotNullAssertion.kt") + public void testUnnecessaryNotNullAssertion() throws Exception { + runTest("idea/testData/inspectionsLocal/replaceNotNullAssertionWithElvisReturn/unnecessaryNotNullAssertion.kt"); + } + } + @TestMetadata("idea/testData/inspectionsLocal/replacePutWithAssignment") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)