Add inspection: Java mutator method used on immutable Kotlin Collections

In particular, fill, reverse, shuffle, sort calls are reported
So #KT-22011 Fixed
This commit is contained in:
Toshiaki Kameyama
2018-02-19 09:56:28 +03:00
committed by Mikhail Glukhikh
parent bec28c8388
commit 7d6cb7805c
8 changed files with 113 additions and 27 deletions
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports immutable Kotlin collection may be changed with Java Collections method.
</body>
</html>
+9
View File
@@ -2623,6 +2623,15 @@
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.JavaCollectionsStaticMethodOnImmutableListInspection"
displayName="Immutable Kotlin collection may be changed with Java Collections method"
groupPath="Kotlin"
groupName="Probable bugs"
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin"
/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.RecursiveEqualsCallInspection"
displayName="Recursive equals call"
groupPath="Kotlin"
@@ -29,17 +29,7 @@ import org.jetbrains.kotlin.types.KotlinType
class JavaCollectionsStaticMethodInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return dotQualifiedExpressionVisitor(fun(expression) {
val callExpression = expression.callExpression ?: return
val args = callExpression.valueArguments
val firstArg = args.firstOrNull() ?: return
val context = expression.analyze(BodyResolveMode.PARTIAL)
if (firstArg.getArgumentExpression()?.getType(context)?.isMutableListOrSubtype() != true) return
val descriptor = expression.getResolvedCall(context)?.resultingDescriptor as? JavaMethodDescriptor ?: return
val fqName = descriptor.importableFqName?.asString() ?: return
if (!canReplaceWithStdLib(expression, fqName, args)) return
val methodName = fqName.split(".").last()
val (methodName, firstArg) = getTargetMethodOnMutableList(expression) ?: return
holder.registerProblem(
expression,
"Java Collections static method call should be replaced with Kotlin stdlib",
@@ -49,23 +39,49 @@ class JavaCollectionsStaticMethodInspection : AbstractKotlinInspection() {
})
}
private fun canReplaceWithStdLib(expression: KtDotQualifiedExpression, fqName: String, args: List<KtValueArgument>): Boolean {
if (!fqName.startsWith("java.util.Collections.")) return false
val size = args.size
return when (fqName) {
"java.util.Collections.fill" -> checkApiVersion(ApiVersion.KOTLIN_1_2, expression) && size == 2
"java.util.Collections.reverse" -> size == 1
"java.util.Collections.shuffle" -> checkApiVersion(ApiVersion.KOTLIN_1_2, expression) && (size == 1 || size == 2)
"java.util.Collections.sort" -> {
size == 1 || (size == 2 && args.getOrNull(1)?.getArgumentExpression() is KtLambdaExpression)
}
else -> false
}
}
companion object {
fun getTargetMethodOnImmutableList(expression: KtDotQualifiedExpression) =
getTargetMethod(expression) { isListOrSubtype() && !isMutableListOrSubtype() }
private fun checkApiVersion(requiredVersion: ApiVersion, expression: KtDotQualifiedExpression): Boolean {
val module = ModuleUtilCore.findModuleForPsiElement(expression) ?: return true
return module.languageVersionSettings.apiVersion >= requiredVersion
private fun getTargetMethodOnMutableList(expression: KtDotQualifiedExpression) =
getTargetMethod(expression) { isMutableListOrSubtype() }
private fun getTargetMethod(
expression: KtDotQualifiedExpression,
isValidFirstArgument: KotlinType.() -> Boolean
): Pair<String, KtValueArgument>? {
val callExpression = expression.callExpression ?: return null
val args = callExpression.valueArguments
val firstArg = args.firstOrNull() ?: return null
val context = expression.analyze(BodyResolveMode.PARTIAL)
if (firstArg.getArgumentExpression()?.getType(context)?.isValidFirstArgument() != true) return null
val descriptor = expression.getResolvedCall(context)?.resultingDescriptor as? JavaMethodDescriptor ?: return null
val fqName = descriptor.importableFqName?.asString() ?: return null
if (!canReplaceWithStdLib(expression, fqName, args)) return null
val methodName = fqName.split(".").last()
return methodName to firstArg
}
private fun canReplaceWithStdLib(expression: KtDotQualifiedExpression, fqName: String, args: List<KtValueArgument>): Boolean {
if (!fqName.startsWith("java.util.Collections.")) return false
val size = args.size
return when (fqName) {
"java.util.Collections.fill" -> checkApiVersion(ApiVersion.KOTLIN_1_2, expression) && size == 2
"java.util.Collections.reverse" -> size == 1
"java.util.Collections.shuffle" -> checkApiVersion(ApiVersion.KOTLIN_1_2, expression) && (size == 1 || size == 2)
"java.util.Collections.sort" -> {
size == 1 || (size == 2 && args.getOrNull(1)?.getArgumentExpression() is KtLambdaExpression)
}
else -> false
}
}
private fun checkApiVersion(requiredVersion: ApiVersion, expression: KtDotQualifiedExpression): Boolean {
val module = ModuleUtilCore.findModuleForPsiElement(expression) ?: return true
return module.languageVersionSettings.apiVersion >= requiredVersion
}
}
}
@@ -77,6 +93,13 @@ private fun KotlinType.isMutableListOrSubtype(): Boolean {
return isMutableList() || constructor.supertypes.reversed().any { it.isMutableList() }
}
private fun KotlinType.isList() =
constructor.declarationDescriptor?.fqNameSafe == KotlinBuiltIns.FQ_NAMES.list
private fun KotlinType.isListOrSubtype(): Boolean {
return isList() || constructor.supertypes.reversed().any { it.isList() }
}
private class ReplaceWithStdLibFix(private val methodName: String, private val receiver: String) : LocalQuickFix {
override fun getName() = "Replace with $receiver.$methodName"
@@ -0,0 +1,22 @@
/*
* Copyright 2000-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
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import org.jetbrains.kotlin.psi.dotQualifiedExpressionVisitor
class JavaCollectionsStaticMethodOnImmutableListInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return dotQualifiedExpressionVisitor(fun(expression) {
val (methodName, firstArg) = JavaCollectionsStaticMethodInspection.getTargetMethodOnImmutableList(expression) ?: return
holder.registerProblem(
expression,
"The '${firstArg.text}' may be changed with Java Collections method '$methodName'"
)
})
}
}
@@ -0,0 +1,10 @@
<problems>
<problem>
<file>test.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="test.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Immutable Kotlin collection may be changed with Java Collections method</problem_class>
<description>The 'immutableList' may be changed with Java Collections method 'reverse'</description>
</problem>
</problems>
@@ -0,0 +1,2 @@
// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.JavaCollectionsStaticMethodOnImmutableListInspection
// RUNTIME_WITH_FULL_JDK
@@ -0,0 +1,9 @@
import java.util.Collections
fun test() {
val immutableList = listOf(1, 2)
Collections.reverse(immutableList)
val mutableList = mutableListOf(1)
Collections.reverse(mutableList)
}
@@ -198,6 +198,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest {
doTest(fileName);
}
@TestMetadata("javaCollectionsStaticMethodOnImmutableList/inspectionData/inspections.test")
public void testJavaCollectionsStaticMethodOnImmutableList_inspectionData_Inspections_test() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/javaCollectionsStaticMethodOnImmutableList/inspectionData/inspections.test");
doTest(fileName);
}
@TestMetadata("kt18195/inspectionData/inspections.test")
public void testKt18195_inspectionData_Inspections_test() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/kt18195/inspectionData/inspections.test");