Introduce "simplifiable call chain on collection" inspection

Related to KT-12165
So #KT-18274 Fixed
So #KT-17198 Fixed
This commit is contained in:
Mikhail Glukhikh
2017-06-21 19:54:35 +03:00
committed by Mikhail Glukhikh
parent bdb9f00c75
commit 36be1fdaef
35 changed files with 428 additions and 10 deletions
+8
View File
@@ -2266,6 +2266,14 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.collections.SimplifiableCallChainInspection"
displayName="Call chain on collection type can be simplified"
groupName="Kotlin"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
/>
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.inspections.collections
import org.jetbrains.kotlin.builtins.getFunctionalClassKind
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.types.KotlinType
fun KotlinType.isFunctionOfAnyKind() = constructor.declarationDescriptor?.getFunctionalClassKind() != null
fun ResolvedCall<*>.hasLastFunctionalParameterWithResult(context: BindingContext, predicate: (KotlinType) -> Boolean): Boolean {
val lastParameter = resultingDescriptor.valueParameters.lastOrNull() ?: return false
val lastArgument = valueArguments[lastParameter]?.arguments?.singleOrNull() ?: return false
val functionalType = lastArgument.getArgumentExpression()?.getType(context) ?: return false
// Both Function & KFunction must pass here
if (!functionalType.isFunctionOfAnyKind()) return false
val resultType = functionalType.arguments.lastOrNull()?.type ?: return false
return predicate(resultType)
}
@@ -0,0 +1,117 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.inspections.collections
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
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.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.DefaultValueArgument
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
class SimplifiableCallChainInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) =
object : KtVisitorVoid() {
override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
super.visitQualifiedExpression(expression)
val firstQualifiedExpression = expression.receiverExpression as? KtQualifiedExpression ?: return
val firstCallExpression = firstQualifiedExpression.selectorExpression as? KtCallExpression ?: return
val secondCallExpression = expression.selectorExpression as? KtCallExpression ?: return
val actualConversions = conversionGroups[
firstCallExpression.calleeExpression?.text to secondCallExpression.calleeExpression?.text
] ?: return
val context = expression.analyze(BodyResolveMode.PARTIAL)
val firstResolvedCall = firstQualifiedExpression.getResolvedCall(context) ?: return
val conversion = actualConversions.firstOrNull {
firstResolvedCall.resultingDescriptor.fqNameOrNull()?.asString() == it.firstFqName
} ?: 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) { KotlinBuiltIns.isString(it) }) return
}
val descriptor = holder.manager.createProblemDescriptor(
expression,
TextRange(firstCallExpression.startOffset, firstCallExpression.endOffset).shiftRight(-expression.startOffset),
"Call chain on collection type may be simplified",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
isOnTheFly,
SimplifyCallChainFix(conversion.replacement)
)
holder.registerProblem(descriptor)
}
}
companion object {
private val conversions = listOf(
Conversion("kotlin.collections.filter", "kotlin.collections.first", "first"),
Conversion("kotlin.collections.filter", "kotlin.collections.firstOrNull", "firstOrNull"),
Conversion("kotlin.collections.filter", "kotlin.collections.last", "last"),
Conversion("kotlin.collections.filter", "kotlin.collections.lastOrNull", "lastOrNull"),
Conversion("kotlin.collections.filter", "kotlin.collections.single", "single"),
Conversion("kotlin.collections.filter", "kotlin.collections.singleOrNull", "singleOrNull"),
Conversion("kotlin.collections.filter", "kotlin.collections.isNotEmpty", "any"),
Conversion("kotlin.collections.filter", "kotlin.collections.List.isEmpty", "none"),
Conversion("kotlin.text.filter", "kotlin.text.first", "first"),
Conversion("kotlin.text.filter", "kotlin.text.firstOrNull", "firstOrNull"),
Conversion("kotlin.text.filter", "kotlin.text.last", "last"),
Conversion("kotlin.text.filter", "kotlin.text.lastOrNull", "lastOrNull"),
Conversion("kotlin.text.filter", "kotlin.text.single", "single"),
Conversion("kotlin.text.filter", "kotlin.text.singleOrNull", "singleOrNull"),
Conversion("kotlin.text.filter", "kotlin.text.isNotEmpty", "any"),
Conversion("kotlin.text.filter", "kotlin.text.isEmpty", "none"),
Conversion("kotlin.collections.map", "kotlin.collections.joinTo", "joinTo"),
Conversion("kotlin.collections.map", "kotlin.collections.joinToString", "joinToString"),
Conversion("kotlin.collections.map", "kotlin.collections.filterNotNull", "mapNotNull")
)
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()
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.inspections.collections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
class SimplifyCallChainFix(val newName: String) : LocalQuickFix {
override fun getName() = "Merge call chain to '$newName'"
override fun getFamilyName() = name
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
(descriptor.psiElement as? KtQualifiedExpression)?.let { secondQualifiedExpression ->
val factory = KtPsiFactory(secondQualifiedExpression)
val firstQualifiedExpression = secondQualifiedExpression.receiverExpression as? KtQualifiedExpression ?: return
val operationSign = if (firstQualifiedExpression is KtSafeQualifiedExpression) "?." else "."
val firstCallExpression = firstQualifiedExpression.selectorExpression as? KtCallExpression ?: 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}"}
val lambdaArgument = firstCallExpression.lambdaArguments.singleOrNull()
val argumentsText = arguments.ifNotEmpty { joinToString(prefix = "(", postfix = ")") } ?: ""
val newQualifiedExpression = if (lambdaArgument != null) factory.createExpressionByPattern(
"$0$1$2 $3 $4",
firstQualifiedExpression.receiverExpression,
operationSign,
newName,
argumentsText,
lambdaArgument.getLambdaExpression().text
)
else factory.createExpressionByPattern(
"$0$1$2 $3",
firstQualifiedExpression.receiverExpression,
operationSign,
newName,
argumentsText
)
secondQualifiedExpression.replaced(newQualifiedExpression)
}
}
}
@@ -18,9 +18,6 @@ package org.jetbrains.kotlin.idea.inspections.collections
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.builtins.getFunctionalClassKind
import org.jetbrains.kotlin.builtins.isFunctionType
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.psiUtil.endOffset
@@ -61,13 +58,7 @@ class UselessCallOnCollectionInspection : AbstractUselessCallInspection() {
if (TypeUtils.isNullableType(receiverTypeArgument)) return
if (calleeExpression.text != "filterNotNull") {
// Also check last argument functional type to have not-null result
val lastParameter = resolvedCall.resultingDescriptor.valueParameters.lastOrNull() ?: return
val lastArgument = resolvedCall.valueArguments[lastParameter]?.arguments?.singleOrNull() ?: return
val functionalType = lastArgument.getArgumentExpression()?.getType(context) ?: return
// Both Function & KFunction must pass here
if (functionalType.constructor.declarationDescriptor?.getFunctionalClassKind() == null) return
val resultType = functionalType.arguments.lastOrNull()?.type ?: return
if (TypeUtils.isNullableType(resultType)) return
if (!resolvedCall.hasLastFunctionalParameterWithResult(context) { !TypeUtils.isNullableType(it) }) return
}
}