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:
committed by
Mikhail Glukhikh
parent
bec28c8388
commit
7d6cb7805c
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
This inspection reports immutable Kotlin collection may be changed with Java Collections method.
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
|
||||
+50
-27
@@ -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"
|
||||
|
||||
+22
@@ -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'"
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+10
@@ -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>
|
||||
idea/testData/inspections/javaCollectionsStaticMethodOnImmutableList/inspectionData/inspections.test
Vendored
+2
@@ -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)
|
||||
}
|
||||
+6
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user