From 87d750aa1c7c33552130186b5dbd60f31aac5102 Mon Sep 17 00:00:00 2001 From: Mikhail Glukhikh Date: Tue, 21 Aug 2018 20:56:45 +0300 Subject: [PATCH] Add inspection to detect functions with SuccessOrFailure return type Partial implementation of KT-25621 (no quick-fixes yet) --- .../ResultIsSuccessOrFailure.html | 8 ++ idea/src/META-INF/plugin.xml | 9 ++ idea/src/META-INF/plugin.xml.173 | 9 ++ idea/src/META-INF/plugin.xml.181 | 9 ++ idea/src/META-INF/plugin.xml.183 | 9 ++ idea/src/META-INF/plugin.xml.as31 | 9 ++ idea/src/META-INF/plugin.xml.as32 | 9 ++ idea/src/META-INF/plugin.xml.as33 | 9 ++ .../ResultIsSuccessOrFailureInspection.kt | 84 +++++++++++++++++ .../inspectionData/expected.xml | 92 +++++++++++++++++++ .../inspectionData/inspections.test | 1 + .../resultIsSuccessOrFailure/test.kt | 43 +++++++++ .../codeInsight/InspectionTestGenerated.java | 5 + 13 files changed, 296 insertions(+) create mode 100644 idea/resources/inspectionDescriptions/ResultIsSuccessOrFailure.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/ResultIsSuccessOrFailureInspection.kt create mode 100644 idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/expected.xml create mode 100644 idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/inspections.test create mode 100644 idea/testData/inspections/coroutines/resultIsSuccessOrFailure/test.kt diff --git a/idea/resources/inspectionDescriptions/ResultIsSuccessOrFailure.html b/idea/resources/inspectionDescriptions/ResultIsSuccessOrFailure.html new file mode 100644 index 00000000000..a5707e1053f --- /dev/null +++ b/idea/resources/inspectionDescriptions/ResultIsSuccessOrFailure.html @@ -0,0 +1,8 @@ + + +This inspection reports functions with SuccessOrFailure result. + +SuccessOrFailure should never be used as return type. +Throw exception, or use nullable type, or use domain-specific result class to indicate failure. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index a1e1dc8b841..07d3721ecba 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -3021,6 +3021,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.173 b/idea/src/META-INF/plugin.xml.173 index ff73b7e1f8f..ef3653a0822 100644 --- a/idea/src/META-INF/plugin.xml.173 +++ b/idea/src/META-INF/plugin.xml.173 @@ -3019,6 +3019,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.181 b/idea/src/META-INF/plugin.xml.181 index 7f6d7e91e66..c9ec7c48914 100644 --- a/idea/src/META-INF/plugin.xml.181 +++ b/idea/src/META-INF/plugin.xml.181 @@ -3020,6 +3020,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.183 b/idea/src/META-INF/plugin.xml.183 index 3ed6b1fdc91..69b9706687d 100644 --- a/idea/src/META-INF/plugin.xml.183 +++ b/idea/src/META-INF/plugin.xml.183 @@ -3021,6 +3021,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.as31 b/idea/src/META-INF/plugin.xml.as31 index 9e569610a1c..28bd6f9d9c5 100644 --- a/idea/src/META-INF/plugin.xml.as31 +++ b/idea/src/META-INF/plugin.xml.as31 @@ -3019,6 +3019,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.as32 b/idea/src/META-INF/plugin.xml.as32 index c6d5d2c516f..a11e151bdb4 100644 --- a/idea/src/META-INF/plugin.xml.as32 +++ b/idea/src/META-INF/plugin.xml.as32 @@ -3019,6 +3019,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.as33 b/idea/src/META-INF/plugin.xml.as33 index d9e77a8cfbf..508155b8aec 100644 --- a/idea/src/META-INF/plugin.xml.as33 +++ b/idea/src/META-INF/plugin.xml.as33 @@ -3021,6 +3021,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. language="kotlin" /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/ResultIsSuccessOrFailureInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/ResultIsSuccessOrFailureInspection.kt new file mode 100644 index 00000000000..808572689df --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/ResultIsSuccessOrFailureInspection.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2010-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.coroutines + +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection +import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getReturnTypeReference +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe + +class ResultIsSuccessOrFailureInspection : AbstractKotlinInspection() { + private fun analyzeFunction(function: KtFunction, toReport: PsiElement, holder: ProblemsHolder) { + if (function is KtConstructor<*>) return + val returnTypeText = function.getReturnTypeReference()?.text + if (returnTypeText != null && SHORT_NAME !in returnTypeText) return + + val descriptor = function.resolveToDescriptorIfAny() as? FunctionDescriptor ?: return + val returnType = descriptor.returnType ?: return + val returnTypeClass = returnType.constructor.declarationDescriptor as? ClassDescriptor ?: return + if (returnTypeClass.fqNameSafe.asString() != FULL_NAME) return + + val name = (function as? KtNamedFunction)?.nameAsName?.asString() + if (name != null && name.endsWith(CATCHING)) { + val nameWithoutCatching = name.substringBeforeLast(CATCHING) + val containingDescriptor = descriptor.containingDeclaration + val scope = when (containingDescriptor) { + is ClassDescriptor -> containingDescriptor.unsubstitutedMemberScope + is PackageFragmentDescriptor -> containingDescriptor.getMemberScope() + else -> return + } + val returnTypeArgument = returnType.arguments.firstOrNull()?.type + val nonCatchingFunctions = scope.getContributedFunctions(Name.identifier(nameWithoutCatching), NoLookupLocation.FROM_IDE) + if (nonCatchingFunctions.none { nonCatchingFun -> + nonCatchingFun.returnType == returnTypeArgument + }) { + val typeName = returnTypeArgument?.constructor?.declarationDescriptor?.name?.asString() ?: "T" + holder.registerProblem( + toReport, + "Function '$name' returning '$SHORT_NAME<$typeName>' without the corresponding " + + "function '$nameWithoutCatching' returning '$typeName'", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + ) + } + } else { + holder.registerProblem( + toReport, + "Function returning $SHORT_NAME with a name that does not end with $CATCHING", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + ) + } + } + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : KtVisitorVoid() { + override fun visitNamedFunction(function: KtNamedFunction) { + analyzeFunction(function, function.nameIdentifier ?: function.funKeyword ?: function, holder) + } + + override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { + analyzeFunction(lambdaExpression.functionLiteral, lambdaExpression.functionLiteral.lBrace, holder) + } + } + } + + companion object { + private const val SHORT_NAME = "SuccessOrFailure" + + private const val FULL_NAME = "kotlin.$SHORT_NAME" + + private const val CATCHING = "Catching" + } +} \ No newline at end of file diff --git a/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/expected.xml b/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/expected.xml new file mode 100644 index 00000000000..a2823c81296 --- /dev/null +++ b/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/expected.xml @@ -0,0 +1,92 @@ + + + test.kt + 7 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function returning SuccessOrFailure with a name that does not end with Catching + + + test.kt + 9 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function returning SuccessOrFailure with a name that does not end with Catching + + + test.kt + 15 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function 'incorrectCatching' returning 'SuccessOrFailure<Double>' without the corresponding function 'incorrect' returning 'Double' + + + test.kt + 17 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function 'strangeCatching' returning 'SuccessOrFailure<Boolean>' without the corresponding function 'strange' returning 'Boolean' + + + test.kt + 23 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function returning SuccessOrFailure with a name that does not end with Catching + + + test.kt + 25 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function returning SuccessOrFailure with a name that does not end with Catching + + + test.kt + 31 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function 'classIncorrectCatching' returning 'SuccessOrFailure<Double>' without the corresponding function 'classIncorrect' returning 'Double' + + + test.kt + 36 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function returning SuccessOrFailure with a name that does not end with Catching + + + test.kt + 38 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function returning SuccessOrFailure with a name that does not end with Catching + + + test.kt + 40 + light_idea_test_case + <default> + + Function returning SuccessOrFailure + Function returning SuccessOrFailure with a name that does not end with Catching + + \ No newline at end of file diff --git a/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/inspections.test b/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/inspections.test new file mode 100644 index 00000000000..df31c1167b7 --- /dev/null +++ b/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.coroutines.ResultIsSuccessOrFailureInspection diff --git a/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/test.kt b/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/test.kt new file mode 100644 index 00000000000..d74649a3fde --- /dev/null +++ b/idea/testData/inspections/coroutines/resultIsSuccessOrFailure/test.kt @@ -0,0 +1,43 @@ +package kotlin +// NO (constructor) +class SuccessOrFailure(val value: T?) { + fun getOrThrow(): T = value ?: throw AssertionError("") +} +// YES +fun getSuccess() = SuccessOrFailure("123") +// YES +fun getSuccessExplicit(): SuccessOrFailure = SuccessOrFailure(456) +// NO (noCatching available) +fun correctCatching() = SuccessOrFailure(true) +// NO (not SuccessOrFailure) +fun correct() = true +// YES +fun incorrectCatching() = SuccessOrFailure(3.14) +// YES +fun strangeCatching() = SuccessOrFailure(false) +// YES +fun strange() = 1 + +class Container { + // YES + fun classGetSuccess() = SuccessOrFailure("123") + // YES + fun classGetSuccessExplicit(): SuccessOrFailure = SuccessOrFailure(456) + // NO (noCatching available) + fun classCorrectCatching() = SuccessOrFailure(true) + // NO (not SuccessOrFailure) + fun classCorrect() = true + // YES + fun classIncorrectCatching() = SuccessOrFailure(3.14) +} + +fun test() { + // YES + fun localGetSuccess() = SuccessOrFailure("123") + // YES + val anonymous = fun() = SuccessOrFailure(45) + // YES + val lambda = { SuccessOrFailure(true) } + // NO yet + fun localCatching() = SuccessOrFailure(2.72) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java index 669d0ea338f..bf1c3be5f7b 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java @@ -144,6 +144,11 @@ public class InspectionTestGenerated extends AbstractInspectionTest { runTest("idea/testData/inspections/coroutines/asyncResultUnused/inspectionData/inspections.test"); } + @TestMetadata("coroutines/resultIsSuccessOrFailure/inspectionData/inspections.test") + public void testCoroutines_resultIsSuccessOrFailure_inspectionData_Inspections_test() throws Exception { + runTest("idea/testData/inspections/coroutines/resultIsSuccessOrFailure/inspectionData/inspections.test"); + } + @TestMetadata("dataClassPrivateConstructor/inspectionData/inspections.test") public void testDataClassPrivateConstructor_inspectionData_Inspections_test() throws Exception { runTest("idea/testData/inspections/dataClassPrivateConstructor/inspectionData/inspections.test");