Extract AbstractCallChainChecker from SimplifiableCallChainInspection
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.collections
|
||||
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtQualifiedExpression
|
||||
import org.jetbrains.kotlin.psi.KtReturnExpression
|
||||
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.model.DefaultValueArgument
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
|
||||
|
||||
abstract class AbstractCallChainChecker : AbstractKotlinInspection() {
|
||||
|
||||
protected fun findQualifiedConversion(
|
||||
expression: KtQualifiedExpression,
|
||||
conversionGroups: Map<ConversionId, List<Conversion>>,
|
||||
additionalCallCheck: (Conversion, ResolvedCall<*>, ResolvedCall<*>, BindingContext) -> Boolean
|
||||
): Conversion? {
|
||||
val firstExpression = expression.receiverExpression
|
||||
val firstCallExpression = getCallExpression(firstExpression) ?: return null
|
||||
val firstCalleeExpression = firstCallExpression.calleeExpression ?: return null
|
||||
|
||||
val secondCallExpression = expression.selectorExpression as? KtCallExpression ?: return null
|
||||
|
||||
val secondCalleeExpression = secondCallExpression.calleeExpression ?: return null
|
||||
val actualConversions = conversionGroups[ConversionId(firstCalleeExpression, secondCalleeExpression)] ?: return null
|
||||
|
||||
val context = expression.analyze()
|
||||
val firstResolvedCall = firstExpression.getResolvedCall(context) ?: return null
|
||||
val conversion = actualConversions.firstOrNull {
|
||||
firstResolvedCall.resultingDescriptor.fqNameOrNull()?.asString() == it.firstFqName
|
||||
} ?: return null
|
||||
val secondResolvedCall = expression.getResolvedCall(context) ?: return null
|
||||
|
||||
if (!additionalCallCheck(conversion, firstResolvedCall, secondResolvedCall, context)) return null
|
||||
|
||||
// Do not apply for lambdas with return inside
|
||||
val lambdaArgument = firstCallExpression.lambdaArguments.firstOrNull()
|
||||
if (lambdaArgument?.anyDescendantOfType<KtReturnExpression>() == true) return null
|
||||
|
||||
val secondResultingDescriptor = secondResolvedCall.resultingDescriptor
|
||||
if (secondResultingDescriptor.fqNameOrNull()?.asString() != conversion.secondFqName) return null
|
||||
if (secondResolvedCall.valueArguments.any { (parameter, resolvedArgument) ->
|
||||
parameter.type.isFunctionOfAnyKind() &&
|
||||
resolvedArgument !is DefaultValueArgument
|
||||
}
|
||||
) return null
|
||||
|
||||
return conversion
|
||||
}
|
||||
|
||||
protected fun List<Conversion>.group(): Map<ConversionId, List<Conversion>> =
|
||||
groupBy { conversion -> conversion.id }
|
||||
|
||||
data class ConversionId(val firstFqName: String, val secondFqName: String) {
|
||||
constructor(first: KtExpression, second: KtExpression) : this(first.text, second.text)
|
||||
}
|
||||
|
||||
data class Conversion(val firstFqName: String, val secondFqName: String, val replacement: String) {
|
||||
private fun String.convertToShort() = takeLastWhile { it != '.' }
|
||||
|
||||
val id: ConversionId get() = ConversionId(firstName, secondName)
|
||||
|
||||
val firstName = firstFqName.convertToShort()
|
||||
|
||||
val secondName = secondFqName.convertToShort()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun KtQualifiedExpression.firstCalleeExpression(): KtExpression? {
|
||||
val firstExpression = receiverExpression
|
||||
val firstCallExpression = getCallExpression(firstExpression) ?: return null
|
||||
return firstCallExpression.calleeExpression
|
||||
}
|
||||
|
||||
fun getCallExpression(firstExpression: KtExpression) =
|
||||
(firstExpression as? KtQualifiedExpression)?.selectorExpression as? KtCallExpression
|
||||
?: firstExpression as? KtCallExpression
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+24
-70
@@ -7,73 +7,40 @@ package org.jetbrains.kotlin.idea.inspections.collections
|
||||
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
|
||||
import org.jetbrains.kotlin.js.resolve.JsPlatform
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
import org.jetbrains.kotlin.psi.qualifiedExpressionVisitor
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.calls.model.DefaultValueArgument
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
|
||||
import org.jetbrains.kotlin.types.typeUtil.builtIns
|
||||
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
|
||||
|
||||
class SimplifiableCallChainInspection : AbstractKotlinInspection() {
|
||||
class SimplifiableCallChainInspection : AbstractCallChainChecker() {
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) =
|
||||
qualifiedExpressionVisitor(fun(expression) {
|
||||
val firstExpression = expression.receiverExpression
|
||||
val firstCallExpression = getCallExpression(firstExpression) ?: return
|
||||
|
||||
val secondCallExpression = expression.selectorExpression as? KtCallExpression ?: return
|
||||
|
||||
val firstCalleeExpression = firstCallExpression.calleeExpression ?: return
|
||||
val secondCalleeExpression = secondCallExpression.calleeExpression ?: return
|
||||
val actualConversions = conversionGroups[
|
||||
firstCalleeExpression.text to secondCalleeExpression.text
|
||||
] ?: return
|
||||
|
||||
val context = expression.analyze()
|
||||
val firstResolvedCall = firstExpression.getResolvedCall(context) ?: return
|
||||
val conversion = actualConversions.firstOrNull {
|
||||
firstResolvedCall.resultingDescriptor.fqNameOrNull()?.asString() == it.firstFqName
|
||||
val conversion = findQualifiedConversion(expression, conversionGroups) check@{ conversion, firstResolvedCall, _, context ->
|
||||
// Do not apply on maps due to lack of relevant stdlib functions
|
||||
val builtIns = context[BindingContext.EXPRESSION_TYPE_INFO, expression]?.type?.builtIns ?: return@check false
|
||||
val firstReceiverType = firstResolvedCall.extensionReceiver?.type
|
||||
val firstReceiverRawType = firstReceiverType?.constructor?.declarationDescriptor?.defaultType
|
||||
if (firstReceiverRawType != null) {
|
||||
if (firstReceiverRawType.isSubtypeOf(builtIns.map.defaultType) ||
|
||||
firstReceiverRawType.isSubtypeOf(builtIns.mutableMap.defaultType)
|
||||
) return@check false
|
||||
}
|
||||
if (conversion.replacement.startsWith("joinTo")) {
|
||||
// Function parameter in map must have String result type
|
||||
if (!firstResolvedCall.hasLastFunctionalParameterWithResult(context) {
|
||||
it.isSubtypeOf(JsPlatform.builtIns.charSequence.defaultType)
|
||||
}
|
||||
) return@check false
|
||||
}
|
||||
true
|
||||
} ?: return
|
||||
|
||||
val builtIns = context[BindingContext.EXPRESSION_TYPE_INFO, expression]?.type?.builtIns ?: return
|
||||
|
||||
// Do not apply on maps due to lack of relevant stdlib functions
|
||||
val firstReceiverType = firstResolvedCall.extensionReceiver?.type
|
||||
val firstReceiverRawType = firstReceiverType?.constructor?.declarationDescriptor?.defaultType
|
||||
if (firstReceiverRawType != null) {
|
||||
if (firstReceiverRawType.isSubtypeOf(builtIns.map.defaultType) ||
|
||||
firstReceiverRawType.isSubtypeOf(builtIns.mutableMap.defaultType)
|
||||
) return
|
||||
}
|
||||
|
||||
// Do not apply for lambdas with return inside
|
||||
val lambdaArgument = firstCallExpression.lambdaArguments.firstOrNull()
|
||||
if (lambdaArgument?.anyDescendantOfType<KtReturnExpression>() == true) return
|
||||
|
||||
val secondResolvedCall = expression.getResolvedCall(context) ?: return
|
||||
val secondResultingDescriptor = secondResolvedCall.resultingDescriptor
|
||||
if (secondResultingDescriptor.fqNameOrNull()?.asString() != conversion.secondFqName) return
|
||||
if (secondResolvedCall.valueArguments.any { (parameter, resolvedArgument) ->
|
||||
parameter.type.isFunctionOfAnyKind() &&
|
||||
resolvedArgument !is DefaultValueArgument
|
||||
}
|
||||
) return
|
||||
|
||||
if (conversion.replacement.startsWith("joinTo")) {
|
||||
// Function parameter in map must have String result type
|
||||
if (!firstResolvedCall.hasLastFunctionalParameterWithResult(context) {
|
||||
it.isSubtypeOf(builtIns.charSequence.defaultType)
|
||||
}
|
||||
) return
|
||||
}
|
||||
|
||||
val descriptor = holder.manager.createProblemDescriptor(
|
||||
expression,
|
||||
firstCalleeExpression.textRange.shiftRight(-expression.startOffset),
|
||||
expression.firstCalleeExpression()!!.textRange.shiftRight(-expression.startOffset),
|
||||
"Call chain on collection type may be simplified",
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
isOnTheFly,
|
||||
@@ -82,6 +49,8 @@ class SimplifiableCallChainInspection : AbstractKotlinInspection() {
|
||||
holder.registerProblem(descriptor)
|
||||
})
|
||||
|
||||
private val conversionGroups = conversions.group()
|
||||
|
||||
companion object {
|
||||
|
||||
private val conversions = listOf(
|
||||
@@ -109,20 +78,5 @@ class SimplifiableCallChainInspection : AbstractKotlinInspection() {
|
||||
|
||||
Conversion("kotlin.collections.listOf", "kotlin.collections.filterNotNull", "listOfNotNull")
|
||||
)
|
||||
|
||||
private val conversionGroups = conversions.groupBy { it.firstName to it.secondName }
|
||||
|
||||
data class Conversion(val firstFqName: String, val secondFqName: String, val replacement: String) {
|
||||
private fun String.convertToShort() = takeLastWhile { it != '.' }
|
||||
|
||||
val firstName = firstFqName.convertToShort()
|
||||
|
||||
val secondName = secondFqName.convertToShort()
|
||||
}
|
||||
|
||||
fun getCallExpression(firstExpression: KtExpression) =
|
||||
(firstExpression as? KtQualifiedExpression)?.selectorExpression as? KtCallExpression
|
||||
?: firstExpression as? KtCallExpression
|
||||
|
||||
}
|
||||
}
|
||||
@@ -41,12 +41,12 @@ class SimplifyCallChainFix(val newName: String) : LocalQuickFix {
|
||||
|
||||
val receiverExpression = (firstExpression as? KtQualifiedExpression)?.receiverExpression
|
||||
|
||||
val firstCallExpression = SimplifiableCallChainInspection.getCallExpression(firstExpression) ?: return
|
||||
val firstCallExpression = AbstractCallChainChecker.getCallExpression(firstExpression) ?: return
|
||||
val secondCallExpression = secondQualifiedExpression.selectorExpression as? KtCallExpression ?: return
|
||||
|
||||
val lastArgumentPrefix = if (newName.startsWith("joinTo")) "transform = " else ""
|
||||
val arguments = secondCallExpression.valueArgumentList?.arguments.orEmpty().map { it.text } +
|
||||
firstCallExpression.valueArgumentList?.arguments.orEmpty().map { "$lastArgumentPrefix${it.text}" }
|
||||
firstCallExpression.valueArgumentList?.arguments.orEmpty().map { "$lastArgumentPrefix${it.text}" }
|
||||
val lambdaExpression = firstCallExpression.lambdaArguments.singleOrNull()?.getLambdaExpression()
|
||||
|
||||
val argumentsText = arguments.ifNotEmpty { joinToString(prefix = "(", postfix = ")") } ?: ""
|
||||
@@ -59,11 +59,11 @@ class SimplifyCallChainFix(val newName: String) : LocalQuickFix {
|
||||
lambdaExpression.text
|
||||
)
|
||||
else factory.createExpressionByPattern(
|
||||
"$0$1$2 $3",
|
||||
receiverExpression ?: "",
|
||||
operationSign,
|
||||
newName,
|
||||
argumentsText
|
||||
"$0$1$2 $3",
|
||||
receiverExpression ?: "",
|
||||
operationSign,
|
||||
newName,
|
||||
argumentsText
|
||||
)
|
||||
|
||||
secondQualifiedExpression.replaced(newQualifiedExpression)
|
||||
|
||||
Reference in New Issue
Block a user