diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/collections/AbstractCallChainChecker.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/AbstractCallChainChecker.kt new file mode 100644 index 00000000000..ed8aafbf95f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/AbstractCallChainChecker.kt @@ -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>, + 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() == 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.group(): Map> = + 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 + + + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifiableCallChainInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifiableCallChainInspection.kt index 90f75a62b69..faf85044436 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifiableCallChainInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifiableCallChainInspection.kt @@ -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() == 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 - } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt index 3268f51a156..882c6608454 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt @@ -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)