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"
|
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"
|
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.RecursiveEqualsCallInspection"
|
||||||
displayName="Recursive equals call"
|
displayName="Recursive equals call"
|
||||||
groupPath="Kotlin"
|
groupPath="Kotlin"
|
||||||
|
|||||||
+50
-27
@@ -29,17 +29,7 @@ import org.jetbrains.kotlin.types.KotlinType
|
|||||||
class JavaCollectionsStaticMethodInspection : AbstractKotlinInspection() {
|
class JavaCollectionsStaticMethodInspection : AbstractKotlinInspection() {
|
||||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
|
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
|
||||||
return dotQualifiedExpressionVisitor(fun(expression) {
|
return dotQualifiedExpressionVisitor(fun(expression) {
|
||||||
val callExpression = expression.callExpression ?: return
|
val (methodName, firstArg) = getTargetMethodOnMutableList(expression) ?: 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()
|
|
||||||
holder.registerProblem(
|
holder.registerProblem(
|
||||||
expression,
|
expression,
|
||||||
"Java Collections static method call should be replaced with Kotlin stdlib",
|
"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 {
|
companion object {
|
||||||
if (!fqName.startsWith("java.util.Collections.")) return false
|
fun getTargetMethodOnImmutableList(expression: KtDotQualifiedExpression) =
|
||||||
val size = args.size
|
getTargetMethod(expression) { isListOrSubtype() && !isMutableListOrSubtype() }
|
||||||
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 {
|
private fun getTargetMethodOnMutableList(expression: KtDotQualifiedExpression) =
|
||||||
val module = ModuleUtilCore.findModuleForPsiElement(expression) ?: return true
|
getTargetMethod(expression) { isMutableListOrSubtype() }
|
||||||
return module.languageVersionSettings.apiVersion >= requiredVersion
|
|
||||||
|
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() }
|
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 {
|
private class ReplaceWithStdLibFix(private val methodName: String, private val receiver: String) : LocalQuickFix {
|
||||||
override fun getName() = "Replace with $receiver.$methodName"
|
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);
|
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")
|
@TestMetadata("kt18195/inspectionData/inspections.test")
|
||||||
public void testKt18195_inspectionData_Inspections_test() throws Exception {
|
public void testKt18195_inspectionData_Inspections_test() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/kt18195/inspectionData/inspections.test");
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/kt18195/inspectionData/inspections.test");
|
||||||
|
|||||||
Reference in New Issue
Block a user