From 7995ae05c51daa5aeac4d5d5bfba7ba9298d224a Mon Sep 17 00:00:00 2001 From: Mikhail Glukhikh Date: Fri, 19 Jan 2018 16:14:30 +0300 Subject: [PATCH] Introduce inspection for redundant not-null extension receiver of inline Related to KT-22303 --- .../org/jetbrains/kotlin/types/KotlinType.kt | 2 + ...ndantNotNullExtensionReceiverOfInline.html | 7 ++ idea/src/META-INF/plugin.xml | 9 ++ ...NullExtensionReceiverOfInlineInspection.kt | 113 ++++++++++++++++++ .../inspectionData/expected.xml | 42 +++++++ .../inspectionData/inspections.test | 1 + .../test.kt | 51 ++++++++ .../codeInsight/InspectionTestGenerated.java | 6 + 8 files changed, 231 insertions(+) create mode 100644 idea/resources/inspectionDescriptions/RedundantNotNullExtensionReceiverOfInline.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/RedundantNotNullExtensionReceiverOfInlineInspection.kt create mode 100644 idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/expected.xml create mode 100644 idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/inspections.test create mode 100644 idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/test.kt diff --git a/core/descriptors/src/org/jetbrains/kotlin/types/KotlinType.kt b/core/descriptors/src/org/jetbrains/kotlin/types/KotlinType.kt index 3dcab44929b..25232e74673 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/types/KotlinType.kt +++ b/core/descriptors/src/org/jetbrains/kotlin/types/KotlinType.kt @@ -66,6 +66,8 @@ sealed class KotlinType : Annotated { } } +fun KotlinType.isNullable(): Boolean = TypeUtils.isNullableType(this) + abstract class WrappedType : KotlinType() { open fun isComputed(): Boolean = true protected abstract val delegate: KotlinType diff --git a/idea/resources/inspectionDescriptions/RedundantNotNullExtensionReceiverOfInline.html b/idea/resources/inspectionDescriptions/RedundantNotNullExtensionReceiverOfInline.html new file mode 100644 index 00000000000..75df227e05b --- /dev/null +++ b/idea/resources/inspectionDescriptions/RedundantNotNullExtensionReceiverOfInline.html @@ -0,0 +1,7 @@ + + +Reports inline functions with not-null extension receivers which does not use the fact that extension receiver is not null. +This function are dangerous to call in Kotlin 1.2 on actual nullable flexible receiver type. +Consider making receiver type nullable. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 4e86a9ea8c9..14f2ea8a811 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -2682,6 +2682,15 @@ language="kotlin" /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantNotNullExtensionReceiverOfInlineInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantNotNullExtensionReceiverOfInlineInspection.kt new file mode 100644 index 00000000000..920ca0eed6f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantNotNullExtensionReceiverOfInlineInspection.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2000-2018 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.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully +import org.jetbrains.kotlin.idea.project.languageVersionSettings +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.callUtil.isSafeCall +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver +import org.jetbrains.kotlin.resolve.scopes.receivers.ReceiverValue +import org.jetbrains.kotlin.types.isNullable + +class RedundantNotNullExtensionReceiverOfInlineInspection : AbstractKotlinInspection() { + + private fun ReceiverValue?.isThisExpressionReceiver(): Boolean = + this is ExpressionReceiver && this.expression is KtThisExpression + + private fun ResolvedCall<*>?.usesNotNullThisReceiverIn(expression: KtExpression, thisReceiverValue: ReceiverValue): Boolean { + if (this == null || expression.parent is KtThisExpression || call.isSafeCall()) { + return false + } + val descriptor = resultingDescriptor + val extensionReceiverType = descriptor?.extensionReceiverParameter?.type + return if (extensionReceiverType != null) { + !extensionReceiverType.isNullable() && (extensionReceiver == thisReceiverValue || extensionReceiver.isThisExpressionReceiver()) + } else { + dispatchReceiver == thisReceiverValue || dispatchReceiver.isThisExpressionReceiver() + } + } + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession) = + namedFunctionVisitor(fun(function) { + + val receiverTypeReference = function.receiverTypeReference ?: return + if (!function.hasModifier(KtTokens.INLINE_KEYWORD) || !function.hasBody()) return + if (!function.languageVersionSettings.supportsFeature(LanguageFeature.NullabilityAssertionOnExtensionReceiver)) { + return + } + + val context = function.analyzeFully() + val functionDescriptor = context[BindingContext.FUNCTION, function] ?: return + val receiverParameter = functionDescriptor.extensionReceiverParameter ?: return + val receiverValue = receiverParameter.value + val receiverType = receiverParameter.type + if (receiverType.isNullable()) return + if ((receiverType.constructor.declarationDescriptor as? ClassDescriptor)?.isCompanionObject == true) return + + if (function.anyDescendantOfType { + when (it) { + is KtNameReferenceExpression -> { + val resolvedCall = it.getResolvedCall(context) + resolvedCall.usesNotNullThisReceiverIn(it, receiverValue) + } + is KtThisExpression -> { + val expectedType = context[BindingContext.EXPECTED_EXPRESSION_TYPE, it] + expectedType != null && !expectedType.isNullable() + } + is KtBinaryExpressionWithTypeRHS -> { + val type = context[BindingContext.TYPE, it.right] + it.left is KtThisExpression && type != null && !type.isNullable() + && it.operationReference.getReferencedNameElementType() == KtTokens.AS_KEYWORD + } + is KtForExpression -> { + it.loopRange is KtThisExpression + } + is KtBinaryExpression -> { + if (it.operationToken == KtTokens.EQEQ || + it.operationToken == KtTokens.EQEQEQ || + it.operationToken == KtTokens.EXCLEQ || + it.operationToken == KtTokens.EXCLEQEQEQ + ) { + false + } else { + val resolvedCall = it.operationReference.getResolvedCall(context) + resolvedCall.usesNotNullThisReceiverIn(it, receiverValue) + } + } + is KtOperationReferenceExpression -> { + if (it.parent is KtBinaryExpression) { + false + } else { + val resolvedCall = it.getResolvedCall(context) + resolvedCall.usesNotNullThisReceiverIn(it, receiverValue) + } + } + is KtArrayAccessExpression -> { + val resolvedCall = it.getResolvedCall(context) + resolvedCall.usesNotNullThisReceiverIn(it, receiverValue) + } + else -> false + } + }) return + + holder.registerProblem( + receiverTypeReference, + "This type probably can be changed to nullable", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + ) + }) +} \ No newline at end of file diff --git a/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/expected.xml b/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/expected.xml new file mode 100644 index 00000000000..d024464271c --- /dev/null +++ b/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/expected.xml @@ -0,0 +1,42 @@ + + + test.kt + 2 + light_idea_test_case + + Not-null extension receiver of inline function can be made nullable + This type probably can be changed to nullable + + + test.kt + 4 + light_idea_test_case + + Not-null extension receiver of inline function can be made nullable + This type probably can be changed to nullable + + + test.kt + 6 + light_idea_test_case + + Not-null extension receiver of inline function can be made nullable + This type probably can be changed to nullable + + + test.kt + 7 + light_idea_test_case + + Not-null extension receiver of inline function can be made nullable + This type probably can be changed to nullable + + + test.kt + 8 + light_idea_test_case + + Not-null extension receiver of inline function can be made nullable + This type probably can be changed to nullable + + \ No newline at end of file diff --git a/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/inspections.test b/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/inspections.test new file mode 100644 index 00000000000..14acd36549d --- /dev/null +++ b/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.RedundantNotNullExtensionReceiverOfInlineInspection diff --git a/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/test.kt b/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/test.kt new file mode 100644 index 00000000000..77792c30bdd --- /dev/null +++ b/idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/test.kt @@ -0,0 +1,51 @@ +// Typical true positive +inline fun String.foo() = foo(this) +fun String.notInlineFoo() = foo(this) +inline fun String.eq(other: Any?) = this == other +// Other positives +inline fun String.fooo1() = fooo() +inline fun String.fooo2() = this.fooo() +inline fun String.fooo3() = this?.foo() + +// Just functions +fun foo(s: String?) {} +fun String?.fooo() {} +fun bar(s: String) {} +fun String.baz() = this + +inline fun String.bar() = bar(this) // No problem +inline fun String.bazz() = baz() // No problem +inline fun String.bazzx() = baz().baz() // No problem +inline fun String.bazzz() = this.baz() // No problem + +interface My { + inline fun String.bar() // No problem (no body) +} + +inline fun String.myLength() = length // No problem (this.length) + +// No problem (this.size) +inline fun List.count() { + return size +} + +// No problem (in this) +inline fun List.foo() { + for (element in this) {} +} + +// No problem (cast) +inline fun String.cast(): Any = this as Any + +class Some { + companion object {} +} + +fun Some.Companion.foo() {} // No problem (on companion) +inline fun Int.baz(x: Int) = this + x // No problem +inline fun Int.bar(x: Int) = x - this // No problem + +inline fun Int.unary() = -this // No problem +inline fun String.indexed(arg: Int) = this[arg] // No problem + + diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java index 506d46b2122..cc461ca36ad 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java @@ -288,6 +288,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest { doTest(fileName); } + @TestMetadata("redundantNotNullExtensionReceiverOfInline/inspectionData/inspections.test") + public void testRedundantNotNullExtensionReceiverOfInline_inspectionData_Inspections_test() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/redundantNotNullExtensionReceiverOfInline/inspectionData/inspections.test"); + doTest(fileName); + } + @TestMetadata("redundantSamConstructor/inspectionData/inspections.test") public void testRedundantSamConstructor_inspectionData_Inspections_test() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/redundantSamConstructor/inspectionData/inspections.test");