Introduce DeferredIsResultInspection #KT-25620 Fixed
This commit is contained in:
@@ -3136,6 +3136,15 @@
|
|||||||
language="kotlin"
|
language="kotlin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.coroutines.DeferredIsResultInspection"
|
||||||
|
displayName="Function returning Deferred directly"
|
||||||
|
groupPath="Kotlin"
|
||||||
|
groupName="Style issues"
|
||||||
|
enabledByDefault="true"
|
||||||
|
level="WEAK WARNING"
|
||||||
|
language="kotlin"
|
||||||
|
/>
|
||||||
|
|
||||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||||
|
|
||||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
This inspection reports functions with <b>kotlinx.coroutines.Deferred</b> result.
|
||||||
|
|
||||||
|
Functions which use <b>Deferred</b> as return type should have a name with suffix <b>Async</b>.
|
||||||
|
Otherwise, it's recommended to turn a function into a suspend function and unwrap <b>Deferred</b>.
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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.idea.caches.resolve.resolveToDescriptorIfAny
|
||||||
|
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
|
||||||
|
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getReturnTypeReference
|
||||||
|
import org.jetbrains.kotlin.psi.*
|
||||||
|
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
||||||
|
|
||||||
|
abstract class AbstractIsResultInspection(
|
||||||
|
private val typeShortName: String,
|
||||||
|
private val typeFullName: String,
|
||||||
|
private val allowedSuffix: String,
|
||||||
|
private val allowedNames: Set<String>,
|
||||||
|
private val suggestedFunctionNameToCall: String,
|
||||||
|
private val shouldMakeSuspend: Boolean = false,
|
||||||
|
private val simplify: (KtExpression) -> Unit = {}
|
||||||
|
) : AbstractKotlinInspection() {
|
||||||
|
|
||||||
|
protected fun analyzeFunction(function: KtFunction, toReport: PsiElement, holder: ProblemsHolder) {
|
||||||
|
if (function is KtConstructor<*>) return
|
||||||
|
val returnTypeText = function.getReturnTypeReference()?.text
|
||||||
|
if (returnTypeText != null && typeShortName !in returnTypeText) return
|
||||||
|
val name = (function as? KtNamedFunction)?.nameAsName?.asString()
|
||||||
|
if (name in allowedNames) return
|
||||||
|
if (function is KtNamedFunction) {
|
||||||
|
val receiverTypeReference = function.receiverTypeReference
|
||||||
|
// Filter given type extensions
|
||||||
|
if (receiverTypeReference != null && typeShortName in receiverTypeReference.text) return
|
||||||
|
}
|
||||||
|
if (function is KtFunctionLiteral || returnTypeText == null) {
|
||||||
|
// Heuristics to save performance: check if something creates given type in function text
|
||||||
|
val text = function.bodyExpression?.text
|
||||||
|
if (text != null && allowedNames.none { it in text } && typeShortName !in text && allowedSuffix !in text) 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() != typeFullName) return
|
||||||
|
|
||||||
|
if (name != null && name.endsWith(allowedSuffix)) {
|
||||||
|
analyzeFunctionWithAllowedSuffix(name, descriptor, toReport, holder)
|
||||||
|
} else {
|
||||||
|
holder.registerProblem(
|
||||||
|
toReport,
|
||||||
|
"Function returning $typeShortName with a name that does not end with $allowedSuffix",
|
||||||
|
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||||
|
*listOfNotNull(
|
||||||
|
name?.let { RenameToFix("$it$allowedSuffix") },
|
||||||
|
AddCallOrUnwrapTypeFix(
|
||||||
|
withBody = function.hasBody(),
|
||||||
|
functionName = suggestedFunctionNameToCall,
|
||||||
|
typeName = typeShortName,
|
||||||
|
shouldMakeSuspend = shouldMakeSuspend,
|
||||||
|
simplify = simplify
|
||||||
|
)
|
||||||
|
).toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun analyzeFunctionWithAllowedSuffix(name: String, descriptor: FunctionDescriptor, toReport: PsiElement, holder: ProblemsHolder) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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.LocalQuickFix
|
||||||
|
import com.intellij.codeInspection.ProblemDescriptor
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithContent
|
||||||
|
import org.jetbrains.kotlin.idea.core.replaced
|
||||||
|
import org.jetbrains.kotlin.idea.core.setType
|
||||||
|
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getReturnTypeReference
|
||||||
|
import org.jetbrains.kotlin.lexer.KtTokens
|
||||||
|
import org.jetbrains.kotlin.psi.*
|
||||||
|
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
|
||||||
|
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
|
||||||
|
import org.jetbrains.kotlin.resolve.BindingContext
|
||||||
|
import org.jetbrains.kotlin.resolve.bindingContextUtil.getTargetFunctionDescriptor
|
||||||
|
|
||||||
|
class AddCallOrUnwrapTypeFix(
|
||||||
|
val withBody: Boolean,
|
||||||
|
val functionName: String,
|
||||||
|
val typeName: String,
|
||||||
|
val shouldMakeSuspend: Boolean,
|
||||||
|
val simplify: (KtExpression) -> Unit
|
||||||
|
) : LocalQuickFix {
|
||||||
|
override fun getName(): String =
|
||||||
|
if (withBody) "Add '.$functionName()' to function result (breaks use-sites!)"
|
||||||
|
else "Unwrap '$typeName' return type (breaks use-sites!)"
|
||||||
|
|
||||||
|
override fun getFamilyName(): String = name
|
||||||
|
|
||||||
|
private fun KtExpression.addCallAndSimplify(factory: KtPsiFactory) {
|
||||||
|
val newCallExpression = factory.createExpressionByPattern("$0.$functionName()", this)
|
||||||
|
val result = replaced(newCallExpression)
|
||||||
|
simplify(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||||
|
val function = descriptor.psiElement.getNonStrictParentOfType<KtFunction>() ?: return
|
||||||
|
val returnTypeReference = function.getReturnTypeReference()
|
||||||
|
val context = function.analyzeWithContent()
|
||||||
|
val functionDescriptor = context[BindingContext.FUNCTION, function] ?: return
|
||||||
|
if (shouldMakeSuspend) {
|
||||||
|
function.addModifier(KtTokens.SUSPEND_KEYWORD)
|
||||||
|
}
|
||||||
|
if (returnTypeReference != null) {
|
||||||
|
val returnType = functionDescriptor.returnType ?: return
|
||||||
|
val returnTypeArgument = returnType.arguments.firstOrNull()?.type ?: return
|
||||||
|
function.setType(returnTypeArgument)
|
||||||
|
}
|
||||||
|
if (!withBody) return
|
||||||
|
val factory = KtPsiFactory(project)
|
||||||
|
val bodyExpression = function.bodyExpression
|
||||||
|
bodyExpression?.forEachDescendantOfType<KtReturnExpression> {
|
||||||
|
if (it.getTargetFunctionDescriptor(context) == functionDescriptor) {
|
||||||
|
it.returnedExpression?.addCallAndSimplify(factory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (function is KtFunctionLiteral) {
|
||||||
|
val lastStatement = function.bodyExpression?.statements?.lastOrNull()
|
||||||
|
if (lastStatement != null && lastStatement !is KtReturnExpression) {
|
||||||
|
lastStatement.addCallAndSimplify(factory)
|
||||||
|
}
|
||||||
|
} else if (!function.hasBlockBody()) {
|
||||||
|
bodyExpression?.addCallAndSimplify(factory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.jetbrains.kotlin.psi.KtExpression
|
||||||
|
import org.jetbrains.kotlin.psi.KtQualifiedExpression
|
||||||
|
|
||||||
|
class DeferredIsResultInspection : AbstractIsResultInspection(
|
||||||
|
typeShortName = "Deferred",
|
||||||
|
typeFullName = "kotlinx.coroutines.Deferred",
|
||||||
|
allowedSuffix = "Async",
|
||||||
|
allowedNames = setOf("async"),
|
||||||
|
suggestedFunctionNameToCall = "await",
|
||||||
|
shouldMakeSuspend = true,
|
||||||
|
simplify = fun(expression: KtExpression) {
|
||||||
|
val qualifiedExpression = expression as? KtQualifiedExpression ?: return
|
||||||
|
val redundantAsyncInspection = RedundantAsyncInspection()
|
||||||
|
val conversion = redundantAsyncInspection.generateConversion(qualifiedExpression) ?: return
|
||||||
|
redundantAsyncInspection.generateFix(conversion).apply(qualifiedExpression)
|
||||||
|
}
|
||||||
|
)
|
||||||
+23
-136
@@ -5,35 +5,25 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.idea.inspections.coroutines
|
package org.jetbrains.kotlin.idea.inspections.coroutines
|
||||||
|
|
||||||
import com.intellij.codeInspection.LocalQuickFix
|
|
||||||
import com.intellij.codeInspection.ProblemDescriptor
|
|
||||||
import com.intellij.codeInspection.ProblemHighlightType
|
import com.intellij.codeInspection.ProblemHighlightType
|
||||||
import com.intellij.codeInspection.ProblemsHolder
|
import com.intellij.codeInspection.ProblemsHolder
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import com.intellij.psi.PsiElementVisitor
|
|
||||||
import com.intellij.refactoring.rename.RenameProcessor
|
|
||||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
||||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||||
import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor
|
import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor
|
||||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
|
||||||
import org.jetbrains.kotlin.idea.core.setType
|
|
||||||
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.incremental.components.NoLookupLocation
|
||||||
import org.jetbrains.kotlin.name.Name
|
import org.jetbrains.kotlin.name.Name
|
||||||
import org.jetbrains.kotlin.psi.*
|
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
|
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
|
|
||||||
import org.jetbrains.kotlin.resolve.BindingContext
|
|
||||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.getTargetFunctionDescriptor
|
|
||||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
|
||||||
import org.jetbrains.kotlin.resolve.scopes.MemberScope
|
import org.jetbrains.kotlin.resolve.scopes.MemberScope
|
||||||
import org.jetbrains.kotlin.types.KotlinType
|
import org.jetbrains.kotlin.types.KotlinType
|
||||||
|
|
||||||
class DirectUseOfResultTypeInspection : AbstractKotlinInspection() {
|
class DirectUseOfResultTypeInspection : AbstractIsResultInspection(
|
||||||
|
typeShortName = SHORT_NAME,
|
||||||
|
typeFullName = "kotlin.Result",
|
||||||
|
allowedSuffix = CATCHING,
|
||||||
|
allowedNames = setOf("success", "failure", "runCatching"),
|
||||||
|
suggestedFunctionNameToCall = "getOrThrow"
|
||||||
|
) {
|
||||||
|
|
||||||
private fun MemberScope.hasCorrespondingNonCatchingFunction(
|
private fun MemberScope.hasCorrespondingNonCatchingFunction(
|
||||||
nameWithoutCatching: String,
|
nameWithoutCatching: String,
|
||||||
@@ -76,130 +66,27 @@ class DirectUseOfResultTypeInspection : AbstractKotlinInspection() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun analyzeFunction(function: KtFunction, toReport: PsiElement, holder: ProblemsHolder) {
|
override fun analyzeFunctionWithAllowedSuffix(
|
||||||
if (function is KtConstructor<*>) return
|
name: String,
|
||||||
val returnTypeText = function.getReturnTypeReference()?.text
|
descriptor: FunctionDescriptor,
|
||||||
if (returnTypeText != null && SHORT_NAME !in returnTypeText) return
|
toReport: PsiElement,
|
||||||
val name = (function as? KtNamedFunction)?.nameAsName?.asString()
|
holder: ProblemsHolder
|
||||||
// Filter names from stdlib
|
) {
|
||||||
if (name in ALLOWED_NAMES) return
|
|
||||||
if (function is KtNamedFunction) {
|
|
||||||
val receiverTypeReference = function.receiverTypeReference
|
|
||||||
// Filter Result extensions
|
|
||||||
if (receiverTypeReference != null && SHORT_NAME in receiverTypeReference.text) return
|
|
||||||
}
|
|
||||||
if (function is KtFunctionLiteral || returnTypeText == null) {
|
|
||||||
// Heuristics to save performance
|
|
||||||
val text = function.bodyExpression?.text
|
|
||||||
// Check there is something creating Result in function text
|
|
||||||
if (text != null && ALLOWED_NAMES.none { it in text } && SHORT_NAME !in text && CATCHING !in text) return
|
|
||||||
}
|
|
||||||
|
|
||||||
val descriptor = function.resolveToDescriptorIfAny() as? FunctionDescriptor ?: return
|
|
||||||
val returnType = descriptor.returnType ?: return
|
val returnType = descriptor.returnType ?: return
|
||||||
val returnTypeClass = returnType.constructor.declarationDescriptor as? ClassDescriptor ?: return
|
val nameWithoutCatching = name.substringBeforeLast(CATCHING)
|
||||||
if (returnTypeClass.fqNameSafe.asString() != FULL_NAME) return
|
if (descriptor.hasCorrespondingNonCatchingFunction(returnType, nameWithoutCatching) == false) {
|
||||||
|
val returnTypeArgument = returnType.arguments.firstOrNull()?.type
|
||||||
if (name != null && name.endsWith(CATCHING)) {
|
val typeName = returnTypeArgument?.constructor?.declarationDescriptor?.name?.asString() ?: "T"
|
||||||
val nameWithoutCatching = name.substringBeforeLast(CATCHING)
|
|
||||||
if (descriptor.hasCorrespondingNonCatchingFunction(returnType, nameWithoutCatching) == false) {
|
|
||||||
val returnTypeArgument = returnType.arguments.firstOrNull()?.type
|
|
||||||
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(
|
holder.registerProblem(
|
||||||
toReport,
|
toReport,
|
||||||
"Function returning $SHORT_NAME with a name that does not end with $CATCHING",
|
"Function '$name' returning '$SHORT_NAME<$typeName>' without the corresponding " +
|
||||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
"function '$nameWithoutCatching' returning '$typeName'",
|
||||||
*listOfNotNull(
|
ProblemHighlightType.GENERIC_ERROR_OR_WARNING
|
||||||
AddGetOrThrowFix(withBody = function.hasBody()),
|
|
||||||
name?.let { RenameToCatchingFix("$it$CATCHING") }
|
|
||||||
).toTypedArray()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
|
private const val SHORT_NAME = "Result"
|
||||||
return object : KtVisitorVoid() {
|
|
||||||
override fun visitNamedFunction(function: KtNamedFunction) {
|
|
||||||
analyzeFunction(function, function.nameIdentifier ?: function.funKeyword ?: function, holder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
|
private const val CATCHING = "Catching"
|
||||||
analyzeFunction(lambdaExpression.functionLiteral, lambdaExpression.functionLiteral.lBrace, holder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AddGetOrThrowFix(val withBody: Boolean) : LocalQuickFix {
|
|
||||||
override fun getName(): String =
|
|
||||||
if (withBody) "Add '.$GET_OR_THROW()' to function result (breaks use-sites!)"
|
|
||||||
else "Unwrap '$SHORT_NAME' return type (breaks use-sites!)"
|
|
||||||
|
|
||||||
override fun getFamilyName(): String = name
|
|
||||||
|
|
||||||
private fun KtExpression.addGetOrThrow(factory: KtPsiFactory) {
|
|
||||||
val getOrThrowExpression = factory.createExpressionByPattern("$0.$GET_OR_THROW()", this)
|
|
||||||
replace(getOrThrowExpression)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
|
||||||
val function = descriptor.psiElement.getNonStrictParentOfType<KtFunction>() ?: return
|
|
||||||
val returnTypeReference = function.getReturnTypeReference()
|
|
||||||
val context = function.analyze()
|
|
||||||
val functionDescriptor = context[BindingContext.FUNCTION, function] ?: return
|
|
||||||
if (returnTypeReference != null) {
|
|
||||||
val returnType = functionDescriptor.returnType ?: return
|
|
||||||
val returnTypeArgument = returnType.arguments.firstOrNull()?.type ?: return
|
|
||||||
function.setType(returnTypeArgument)
|
|
||||||
}
|
|
||||||
if (!withBody) return
|
|
||||||
val factory = KtPsiFactory(project)
|
|
||||||
val bodyExpression = function.bodyExpression
|
|
||||||
bodyExpression?.forEachDescendantOfType<KtReturnExpression> {
|
|
||||||
if (it.getTargetFunctionDescriptor(context) == functionDescriptor) {
|
|
||||||
it.returnedExpression?.addGetOrThrow(factory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (function is KtFunctionLiteral) {
|
|
||||||
val lastStatement = function.bodyExpression?.statements?.lastOrNull()
|
|
||||||
if (lastStatement != null && lastStatement !is KtReturnExpression) {
|
|
||||||
lastStatement.addGetOrThrow(factory)
|
|
||||||
}
|
|
||||||
} else if (!function.hasBlockBody()) {
|
|
||||||
bodyExpression?.addGetOrThrow(factory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RenameToCatchingFix(val newName: String) : LocalQuickFix {
|
|
||||||
override fun getName(): String = "Rename to '$newName'"
|
|
||||||
|
|
||||||
override fun getFamilyName(): String = name
|
|
||||||
|
|
||||||
override fun startInWriteAction(): Boolean = false
|
|
||||||
|
|
||||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
|
||||||
val function = descriptor.psiElement.getNonStrictParentOfType<KtFunction>() ?: return
|
|
||||||
RenameProcessor(project, function, newName, false, false).run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SHORT_NAME = "Result"
|
|
||||||
|
|
||||||
private const val FULL_NAME = "kotlin.$SHORT_NAME"
|
|
||||||
|
|
||||||
private const val CATCHING = "Catching"
|
|
||||||
|
|
||||||
private const val GET_OR_THROW = "getOrThrow"
|
|
||||||
|
|
||||||
private val ALLOWED_NAMES = setOf("success", "failure", "runCatching")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+28
-24
@@ -23,7 +23,7 @@ import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
|||||||
|
|
||||||
class RedundantAsyncInspection : AbstractCallChainChecker() {
|
class RedundantAsyncInspection : AbstractCallChainChecker() {
|
||||||
|
|
||||||
private fun generateConversion(expression: KtQualifiedExpression): Conversion? {
|
fun generateConversion(expression: KtQualifiedExpression): Conversion? {
|
||||||
var defaultContext: Boolean? = null
|
var defaultContext: Boolean? = null
|
||||||
var defaultStart: Boolean? = null
|
var defaultStart: Boolean? = null
|
||||||
|
|
||||||
@@ -63,38 +63,42 @@ class RedundantAsyncInspection : AbstractCallChainChecker() {
|
|||||||
return conversion
|
return conversion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun generateFix(conversion: Conversion): SimplifyCallChainFix {
|
||||||
|
val contextArgument = conversion.additionalArgument
|
||||||
|
return SimplifyCallChainFix(conversion, removeReceiverOfFirstCall = true, runOptimizeImports = true) { callExpression ->
|
||||||
|
if (contextArgument != null) {
|
||||||
|
val call = callExpression.resolveToCall()
|
||||||
|
if (call != null) {
|
||||||
|
for (argument in callExpression.valueArguments) {
|
||||||
|
val mapping = call.getArgumentMapping(argument) as? ArgumentMatch ?: continue
|
||||||
|
if (mapping.valueParameter.name.asString() == CONTEXT_ARGUMENT_NAME) {
|
||||||
|
val name = argument.getArgumentName()?.asName
|
||||||
|
val expressionText = contextArgument + " + " + argument.getArgumentExpression()!!.text
|
||||||
|
argument.replace(
|
||||||
|
if (name == null) {
|
||||||
|
createArgument(expressionText)
|
||||||
|
} else {
|
||||||
|
createArgument("$name = $expressionText")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) =
|
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) =
|
||||||
qualifiedExpressionVisitor(fun(expression) {
|
qualifiedExpressionVisitor(fun(expression) {
|
||||||
val conversion = generateConversion(expression) ?: return
|
val conversion = generateConversion(expression) ?: return
|
||||||
val contextArgument = conversion.additionalArgument
|
|
||||||
val descriptor = holder.manager.createProblemDescriptor(
|
val descriptor = holder.manager.createProblemDescriptor(
|
||||||
expression,
|
expression,
|
||||||
expression.firstCalleeExpression()!!.textRange.shiftRight(-expression.startOffset),
|
expression.firstCalleeExpression()!!.textRange.shiftRight(-expression.startOffset),
|
||||||
"Redundant 'async' call may be reduced to '${conversion.replacement}'",
|
"Redundant 'async' call may be reduced to '${conversion.replacement}'",
|
||||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||||
isOnTheFly,
|
isOnTheFly,
|
||||||
SimplifyCallChainFix(conversion, removeReceiverOfFirstCall = true, runOptimizeImports = true) { callExpression ->
|
generateFix(conversion)
|
||||||
if (contextArgument != null) {
|
|
||||||
val call = callExpression.resolveToCall()
|
|
||||||
if (call != null) {
|
|
||||||
for (argument in callExpression.valueArguments) {
|
|
||||||
val mapping = call.getArgumentMapping(argument) as? ArgumentMatch ?: continue
|
|
||||||
if (mapping.valueParameter.name.asString() == CONTEXT_ARGUMENT_NAME) {
|
|
||||||
val name = argument.getArgumentName()?.asName
|
|
||||||
val expressionText = contextArgument + " + " + argument.getArgumentExpression()!!.text
|
|
||||||
argument.replace(
|
|
||||||
if (name == null) {
|
|
||||||
createArgument(expressionText)
|
|
||||||
} else {
|
|
||||||
createArgument("$name = $expressionText")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
holder.registerProblem(descriptor)
|
holder.registerProblem(descriptor)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.LocalQuickFix
|
||||||
|
import com.intellij.codeInspection.ProblemDescriptor
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.refactoring.rename.RenameProcessor
|
||||||
|
import org.jetbrains.kotlin.psi.KtFunction
|
||||||
|
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
|
||||||
|
|
||||||
|
class RenameToFix(val newName: String) : LocalQuickFix {
|
||||||
|
override fun getName(): String = "Rename to '$newName'"
|
||||||
|
|
||||||
|
override fun getFamilyName(): String = name
|
||||||
|
|
||||||
|
override fun startInWriteAction(): Boolean = false
|
||||||
|
|
||||||
|
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||||
|
val function = descriptor.psiElement.getNonStrictParentOfType<KtFunction>() ?: return
|
||||||
|
RenameProcessor(project, function, newName, false, false).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
org.jetbrains.kotlin.idea.inspections.coroutines.DeferredIsResultInspection
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Unwrap 'Deferred' return type (breaks use-sites!)
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
interface My {
|
||||||
|
fun <caret>function(): Deferred<Int>
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Unwrap 'Deferred' return type (breaks use-sites!)
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
interface My {
|
||||||
|
suspend fun <caret>function(): Int
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Add '.await()' to function result (breaks use-sites!)
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
// TODO: this test contains strange formatting bug (see 0 -> return in *.after file). To be fixed.
|
||||||
|
fun <caret>myFunction(context: CoroutineContext, switch: Int): Deferred<Int> {
|
||||||
|
with (GlobalScope) {
|
||||||
|
when (switch) {
|
||||||
|
0 -> return async {
|
||||||
|
val x = 123
|
||||||
|
x * x
|
||||||
|
}
|
||||||
|
1 -> return async(context) { -1 }
|
||||||
|
else -> return async() { 9 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Add '.await()' to function result (breaks use-sites!)
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
// TODO: this test contains strange formatting bug (see 0 -> return in *.after file). To be fixed.
|
||||||
|
suspend fun myFunction(context: CoroutineContext, switch: Int): Int {
|
||||||
|
with (GlobalScope) {
|
||||||
|
when (switch) {
|
||||||
|
0 -> return withContext(Dispatchers.Default) {
|
||||||
|
val x = 123
|
||||||
|
x * x
|
||||||
|
}
|
||||||
|
1 -> return withContext(context) { -1 }
|
||||||
|
else -> return withContext(Dispatchers.Default) { 9 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
interface Deferred<T> {
|
||||||
|
suspend fun await(): T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoroutineContext
|
||||||
|
|
||||||
|
object Dispatchers {
|
||||||
|
object Default : CoroutineContext
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CoroutineStart {
|
||||||
|
DEFAULT,
|
||||||
|
LAZY,
|
||||||
|
ATOMIC,
|
||||||
|
UNDISPATCHED
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoroutineScope {
|
||||||
|
val coroutineContext: CoroutineContext get() = Dispatchers.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
object GlobalScope : CoroutineScope
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.async(
|
||||||
|
context: CoroutineContext = Dispatchers.Default,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
): Deferred<T> {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> withContext(
|
||||||
|
context: CoroutineContext,
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
): T {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R = GlobalScope.block()
|
||||||
|
|
||||||
|
operator fun CoroutineContext.plus(other: CoroutineContext): CoroutineContext {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Rename to 'myFunctionAsync'
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
fun <caret>myFunction(): Deferred<Int> {
|
||||||
|
return GlobalScope.async { 42 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Rename to 'myFunctionAsync'
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
fun myFunctionAsync(): Deferred<Int> {
|
||||||
|
return GlobalScope.async { 42 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Add '.await()' to function result (breaks use-sites!)
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
fun <caret>myFunction(): Deferred<Int> {
|
||||||
|
return GlobalScope.async { 42 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// WITH_RUNTIME
|
||||||
|
// FIX: Add '.await()' to function result (breaks use-sites!)
|
||||||
|
|
||||||
|
package kotlinx.coroutines
|
||||||
|
|
||||||
|
suspend fun myFunction(): Int {
|
||||||
|
return withContext(Dispatchers.Default) { 42 }
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ fun <T> CoroutineScope.async(
|
|||||||
suspend fun <T> withContext(
|
suspend fun <T> withContext(
|
||||||
context: CoroutineContext,
|
context: CoroutineContext,
|
||||||
block: suspend CoroutineScope.() -> T
|
block: suspend CoroutineScope.() -> T
|
||||||
) {
|
): T {
|
||||||
TODO()
|
TODO()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
@@ -1990,6 +1990,39 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
|||||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/coroutines"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
|
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/coroutines"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("idea/testData/inspectionsLocal/coroutines/deferredIsResult")
|
||||||
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
public static class DeferredIsResult extends AbstractLocalInspectionTest {
|
||||||
|
private void runTest(String testDataFilePath) throws Exception {
|
||||||
|
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("abstract.kt")
|
||||||
|
public void testAbstract() throws Exception {
|
||||||
|
runTest("idea/testData/inspectionsLocal/coroutines/deferredIsResult/abstract.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testAllFilesPresentInDeferredIsResult() throws Exception {
|
||||||
|
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/coroutines/deferredIsResult"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("complex.kt")
|
||||||
|
public void testComplex() throws Exception {
|
||||||
|
runTest("idea/testData/inspectionsLocal/coroutines/deferredIsResult/complex.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("rename.kt")
|
||||||
|
public void testRename() throws Exception {
|
||||||
|
runTest("idea/testData/inspectionsLocal/coroutines/deferredIsResult/rename.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("simple.kt")
|
||||||
|
public void testSimple() throws Exception {
|
||||||
|
runTest("idea/testData/inspectionsLocal/coroutines/deferredIsResult/simple.kt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("idea/testData/inspectionsLocal/coroutines/directUseOfResultType")
|
@TestMetadata("idea/testData/inspectionsLocal/coroutines/directUseOfResultType")
|
||||||
@TestDataPath("$PROJECT_ROOT")
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
@RunWith(JUnit3RunnerWithInners.class)
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
|||||||
Reference in New Issue
Block a user