From c90056f8c4911c732ec419530ba9f71016fab2ae Mon Sep 17 00:00:00 2001 From: Mikhail Glukhikh Date: Fri, 19 Jan 2018 11:32:12 +0300 Subject: [PATCH] Introduce inspection for nullable extension receiver of inline function So #KT-22303 Fixed --- .../PlatformExtensionReceiverOfInline.html | 9 ++ idea/src/META-INF/plugin.xml | 11 +- ...formExtensionReceiverOfInlineInspection.kt | 100 ++++++++++++++++++ .../before/ToBoolean/ToBoolean.iml | 11 ++ .../before/ToBoolean/src/JavaClass.java | 9 ++ .../before/ToBoolean/src/Test.kt | 15 +++ .../expected.xml | 9 ++ .../platformExtensionReceiverOfInline.test | 5 + .../MultiFileInspectionTestGenerated.java | 6 ++ 9 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 idea/resources/inspectionDescriptions/PlatformExtensionReceiverOfInline.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/PlatformExtensionReceiverOfInlineInspection.kt create mode 100644 idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/ToBoolean.iml create mode 100644 idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/JavaClass.java create mode 100644 idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/Test.kt create mode 100644 idea/testData/multiFileInspections/platformExtensionReceiverOfInline/expected.xml create mode 100644 idea/testData/multiFileInspections/platformExtensionReceiverOfInline/platformExtensionReceiverOfInline.test diff --git a/idea/resources/inspectionDescriptions/PlatformExtensionReceiverOfInline.html b/idea/resources/inspectionDescriptions/PlatformExtensionReceiverOfInline.html new file mode 100644 index 00000000000..fceeb563d1c --- /dev/null +++ b/idea/resources/inspectionDescriptions/PlatformExtensionReceiverOfInline.html @@ -0,0 +1,9 @@ + + +Reports potentially unsafe calls of inline functions with flexible nullable (platform type with unknown nullability) extension receivers. +In Kotlin 1.0 or 1.1, such calls do not include nullability check in bytecode, +but in Kotlin 1.2, nullability check is included and can provoke NPE if actual receiver is null. +It's recommended to add explicit '!!' if exception is what you want, +or consider changing the function' receiver type to nullable if it should work without exceptions. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 68e642296d0..4e86a9ea8c9 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -2527,7 +2527,7 @@ language="kotlin" /> - + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/PlatformExtensionReceiverOfInlineInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/PlatformExtensionReceiverOfInlineInspection.kt new file mode 100644 index 00000000000..73341b61371 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/PlatformExtensionReceiverOfInlineInspection.kt @@ -0,0 +1,100 @@ +/* + * 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.IntentionWrapper +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.ui.LabeledComponent +import com.intellij.ui.EditorTextField +import org.intellij.lang.regexp.RegExpFileType +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.project.languageVersionSettings +import org.jetbrains.kotlin.idea.quickfix.AddExclExclCallFix +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtVisitorVoid +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.isNullabilityFlexible +import java.awt.BorderLayout +import java.util.regex.PatternSyntaxException +import javax.swing.JPanel + +class PlatformExtensionReceiverOfInlineInspection : AbstractKotlinInspection() { + + private var nameRegex: Regex? = defaultNamePattern.toRegex() + var namePattern: String = defaultNamePattern + set(value) { + field = value + nameRegex = try { + value.toRegex() + } catch (e: PatternSyntaxException) { + null + } + } + + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession) = + object : KtVisitorVoid() { + override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) { + super.visitDotQualifiedExpression(expression) + + if (!expression.languageVersionSettings.supportsFeature(LanguageFeature.NullabilityAssertionOnExtensionReceiver)) { + return + } + val nameRegex = nameRegex + val callExpression = expression.selectorExpression as? KtCallExpression ?: return + val calleeText = callExpression.calleeExpression?.text ?: return + if (nameRegex != null && !nameRegex.matches(calleeText)) { + return + } + + val context = expression.analyze(BodyResolveMode.PARTIAL) + val resolvedCall = expression.getResolvedCall(context) ?: return + val extensionReceiverType = resolvedCall.extensionReceiver?.type ?: return + if (!extensionReceiverType.isNullabilityFlexible()) return + val descriptor = resolvedCall.resultingDescriptor as? FunctionDescriptor ?: return + if (!descriptor.isInline) return + + val receiverExpression = expression.receiverExpression + holder.registerProblem( + receiverExpression, + "Call of inline function with nullable extension receiver can provoke NPE in Kotlin 1.2+", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + IntentionWrapper(AddExclExclCallFix(receiverExpression), receiverExpression.containingKtFile) + ) + } + } + + override fun createOptionsPanel() = OptionsPanel(this) + + class OptionsPanel internal constructor(owner: PlatformExtensionReceiverOfInlineInspection) : JPanel() { + init { + layout = BorderLayout() + + val regexField = EditorTextField(owner.namePattern, null, RegExpFileType.INSTANCE).apply { + setOneLineMode(true) + } + regexField.document.addDocumentListener(object : DocumentListener { + override fun documentChanged(e: DocumentEvent?) { + owner.namePattern = regexField.text + } + }) + val labeledComponent = LabeledComponent.create(regexField, "Pattern:", BorderLayout.WEST) + add(labeledComponent, BorderLayout.NORTH) + } + } + + companion object { + const val defaultNamePattern = "(toBoolean)|(content.*)" + } +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/ToBoolean.iml b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/ToBoolean.iml new file mode 100644 index 00000000000..c90834f2d60 --- /dev/null +++ b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/ToBoolean.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/JavaClass.java b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/JavaClass.java new file mode 100644 index 00000000000..77fb3a8fc3f --- /dev/null +++ b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/JavaClass.java @@ -0,0 +1,9 @@ +public class JavaClass { + public static String getNull() { + return null; + } + + public static My getMy() { + return null; + } +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/Test.kt b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/Test.kt new file mode 100644 index 00000000000..b409c1e7aaf --- /dev/null +++ b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/before/ToBoolean/src/Test.kt @@ -0,0 +1,15 @@ +fun test() { + JavaClass.getNull().toBoolean() // ISE in runtime + JavaClass.getNull().toInt() // OK + "123".toBoolean() // OK + null.toBoolean() // Compilation error, no inspection message + + JavaClass.getNull().contentNotInline() // OK + JavaClass.getMy().contentNonExtensionInlineFun() // OK +} + +fun String.contentNotInline() {} + +class My { + inline fun contentNonExtensionInlineFun() {} +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/expected.xml b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/expected.xml new file mode 100644 index 00000000000..0f42edd53d7 --- /dev/null +++ b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/expected.xml @@ -0,0 +1,9 @@ + + + Test.kt + 2 + + Unsafe call of inline function with nullable extension receiver + Call of inline function with nullable extension receiver can provoke NPE in Kotlin 1.2+ + + diff --git a/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/platformExtensionReceiverOfInline.test b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/platformExtensionReceiverOfInline.test new file mode 100644 index 00000000000..a689a9f467d --- /dev/null +++ b/idea/testData/multiFileInspections/platformExtensionReceiverOfInline/platformExtensionReceiverOfInline.test @@ -0,0 +1,5 @@ +{ + "inspectionClass": "org.jetbrains.kotlin.idea.inspections.PlatformExtensionReceiverOfInlineInspection", + "withRuntime": "true", + "isMultiModule": "false" +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/MultiFileInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/MultiFileInspectionTestGenerated.java index 507076654bb..f17810bb673 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/MultiFileInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/MultiFileInspectionTestGenerated.java @@ -54,4 +54,10 @@ public class MultiFileInspectionTestGenerated extends AbstractMultiFileInspectio String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileInspections/mismatchedProjectAndDirectory/mismatchedProjectAndDirectory.test"); doTest(fileName); } + + @TestMetadata("platformExtensionReceiverOfInline/platformExtensionReceiverOfInline.test") + public void testPlatformExtensionReceiverOfInline_PlatformExtensionReceiverOfInline() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileInspections/platformExtensionReceiverOfInline/platformExtensionReceiverOfInline.test"); + doTest(fileName); + } }