Intention to simplify for using destructing declaration

This commit is contained in:
Natalia Ukhorskaya
2016-01-13 14:47:02 +03:00
parent 5667a92472
commit ea3ba6f534
34 changed files with 631 additions and 0 deletions
@@ -216,6 +216,10 @@ class KtPsiFactory(private val project: Project) {
return (createFunction("fun foo() {$text}").bodyExpression as KtBlockExpression).statements.first() as KtDestructuringDeclaration
}
fun createDestructuringDeclarationInFor(text: String): KtDestructuringDeclaration {
return ((createFunction("fun foo() {for ($text in foo) {} }").bodyExpression as KtBlockExpression).statements.first() as KtForExpression).destructuringParameter!!
}
fun createDestructuringParameter(text: String): KtDestructuringDeclaration {
val dummyFun = createFunction("fun foo() { for ($text in foo) {} }")
return ((dummyFun.bodyExpression as KtBlockExpression).statements.first() as KtForExpression).destructuringParameter!!
@@ -0,0 +1,5 @@
<html>
<body>
This inspection reports any for expressions that can be simplified
</body>
</html>
@@ -0,0 +1,3 @@
val map = hashMapOf(1 to 2)
for (<spot>(key, value)</spot> in map) {
}
@@ -0,0 +1,5 @@
val map = hashMapOf(1 to 2)
for (<spot>entry</spot> in map.entries) {
val key = entry.key
val value = entry.value
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention simplifies for expression by replacing iteration over map entries and collections of data classes with destructing declaration
</body>
</html>
+12
View File
@@ -1105,6 +1105,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.SimplifyForIntention</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.AnonymousFunctionToLambdaIntention</className>
<category>Kotlin</category>
@@ -1315,6 +1320,13 @@
cleanupTool="true"
level="WARNING"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.SimplifyForInspection"
displayName="Simplify 'for' using destructing declaration"
groupName="Kotlin"
enabledByDefault="true"
cleanupTool="false"
level="WEAK WARNING"/>
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
@@ -0,0 +1,183 @@
/*
* Copyright 2010-2016 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.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiReference
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.util.Query
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.chooseApplicableComponentFunctions
import org.jetbrains.kotlin.platform.JvmBuiltIns
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
class SimplifyForInspection : IntentionBasedInspection<KtForExpression>(SimplifyForIntention())
class SimplifyForIntention : SelfTargetingRangeIntention<KtForExpression>(
KtForExpression::class.java,
"Simplify 'for' using destructing declaration",
"Simplify 'for'"
) {
override fun applyTo(element: KtForExpression, editor: Editor?) {
applyTo(element)
}
fun applyTo(element: KtForExpression) {
val (propertiesToRemove, removeSelectorInLoopRange) = collectPropertiesToRemove(element) ?: return
val loopRange = element.loopRange ?: return
val loopParameter = element.loopParameter ?: return
loopParameter.replace(KtPsiFactory(element).createDestructuringDeclarationInFor("(${propertiesToRemove.joinToString { it.name!! }})"))
propertiesToRemove.forEach { p -> p.delete() }
if (removeSelectorInLoopRange && loopRange is KtDotQualifiedExpression) {
loopRange.replace(loopRange.receiverExpression)
}
}
override fun applicabilityRange(element: KtForExpression): TextRange? {
if (element.destructuringParameter != null) return null
if (collectPropertiesToRemove(element) != null) {
return element.loopParameter!!.textRange
}
return null
}
// Note: list should contains properties in order to create destructing declaration
private fun collectPropertiesToRemove(element: KtForExpression): Pair<List<KtProperty>, Boolean>? {
val loopParameter = element.loopParameter ?: return null
val context = element.analyzeFullyAndGetResult().bindingContext
val loopParameterDescriptor = context.get(BindingContext.VALUE_PARAMETER, loopParameter) ?: return null
val classDescriptor = loopParameterDescriptor.type.constructor.declarationDescriptor as? ClassDescriptor ?: return null
var otherUsages = false
var removeSelectorInLoopRange = false
val propertiesToRemove: Array<KtProperty?>
if (DescriptorUtils.isSubclass(classDescriptor, JvmBuiltIns.Instance.mapEntry)) {
val loopRangeDescriptorName = element.loopRange.getResolvedCall(context)?.resultingDescriptor?.name
if (loopRangeDescriptorName != null &&
(loopRangeDescriptorName.asString().equals("entries") || loopRangeDescriptorName.asString().equals("entrySet"))) {
removeSelectorInLoopRange = true
}
propertiesToRemove = arrayOfNulls<KtProperty>(2)
ReferencesSearch.search(loopParameter).iterateOverMapEntryPropertiesUsages(
context,
{ index, property -> propertiesToRemove[index] = property },
{ otherUsages = true }
)
if (!otherUsages && propertiesToRemove.all { it != null }) {
return propertiesToRemove.mapNotNull { it } to removeSelectorInLoopRange
}
}
else if (classDescriptor.isData) {
val valueParameters = classDescriptor.unsubstitutedPrimaryConstructor?.valueParameters ?: return null
propertiesToRemove = arrayOfNulls<KtProperty>(valueParameters.size)
ReferencesSearch.search(loopParameter).iterateOverDataClassPropertiesUsagesWithIndex(
context,
classDescriptor,
{ index, property -> propertiesToRemove[index] = property },
{ otherUsages = true }
)
if (otherUsages) return null
val notNullProperties = propertiesToRemove.filterNotNull()
val droppedLastUnused = propertiesToRemove.dropLastWhile { it == null }
if (droppedLastUnused.size == notNullProperties.size) {
return notNullProperties to removeSelectorInLoopRange
}
}
return null
}
private fun Query<PsiReference>.iterateOverMapEntryPropertiesUsages(
context: BindingContext,
process: (Int, KtProperty) -> Unit,
cancel: () -> Unit
) {
forEach {
val applicableUsage = getDataIfUsageIsApplicable(it, context)
if (applicableUsage != null) {
val (property, descriptor) = applicableUsage
if (descriptor.name.asString().equals("key") || descriptor.name.asString().equals("getKey")) {
process(0, property)
return@forEach true
}
else if (descriptor.name.asString().equals("value") || descriptor.name.asString().equals("getValue")) {
process(1, property)
return@forEach true
}
}
cancel()
return@forEach false
}
}
private fun Query<PsiReference>.iterateOverDataClassPropertiesUsagesWithIndex(
context: BindingContext,
dataClass: ClassDescriptor,
process: (Int, KtProperty) -> Unit,
cancel: () -> Unit
) {
val valueParameters = dataClass.unsubstitutedPrimaryConstructor?.valueParameters ?: return
forEach {
val applicableUsage = getDataIfUsageIsApplicable(it, context)
if (applicableUsage != null) {
val (property, descriptor) = applicableUsage
for (valueParameter in valueParameters) {
if (context.get(BindingContext.VALUE_PARAMETER_AS_PROPERTY, valueParameter) == descriptor) {
process(valueParameter.index, property)
return@forEach true
}
}
}
cancel()
return@forEach false
}
}
private fun getDataIfUsageIsApplicable(usage: PsiReference, context: BindingContext): UsageData? {
val parentCall = usage.element.parent as? KtExpression ?: return null
val property = parentCall.parent as? KtProperty ?: return null
val resolvedCall = parentCall.getResolvedCall(context) ?: return null
val descriptor = resolvedCall.resultingDescriptor
return UsageData(property, descriptor)
}
private data class UsageData(val property: KtProperty, val descriptor: CallableDescriptor)
}
+1
View File
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.SimplifyForIntention
@@ -0,0 +1,10 @@
// IS_APPLICABLE: false
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (<caret>(key, value) in map) {
println(key)
println(value)
}
}
@@ -0,0 +1,13 @@
// IS_APPLICABLE: false
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (entry <caret>in map) {
val key = entry.key
val value = entry.value
println(key)
println(value)
}
}
+12
View File
@@ -0,0 +1,12 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val list = listOf(MyClass(1, 2, 3), MyClass(2, 3, 4))
for (<caret>klass in list) {
val a = klass.a
val b = klass.b
val c = klass.c
}
}
data class MyClass(val a: Int, val b: Int, val c: Int)
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val list = listOf(MyClass(1, 2, 3), MyClass(2, 3, 4))
for ((a, b, c) in list) {
}
}
data class MyClass(val a: Int, val b: Int, val c: Int)
@@ -0,0 +1,11 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val list = listOf(MyClass(1, 2, 3), MyClass(2, 3, 4))
for (<caret>klass in list) {
val a = klass.a
val b = klass.b
}
}
data class MyClass(val a: Int, val b: Int, val c: Int)
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val list = listOf(MyClass(1, 2, 3), MyClass(2, 3, 4))
for ((a, b) in list) {
}
}
data class MyClass(val a: Int, val b: Int, val c: Int)
@@ -0,0 +1,12 @@
// IS_APPLICABLE: false
// WITH_RUNTIME
fun main(args: Array<String>) {
val list = listOf(MyClass(1, 2, 3, 4))
for (<caret>klass in list) {
val a = klass.a
val c = klass.c
}
}
data class MyClass(val a: Int, val b: Int, val c: Int, val d: Int)
@@ -0,0 +1,12 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val list = listOf(MyClass(1, 2, 3), MyClass(2, 3, 4))
for (<caret>klass in list) {
val a1 = klass.a
val c1 = klass.c
val b1 = klass.b
}
}
data class MyClass(val a: Int, val b: Int, val c: Int)
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val list = listOf(MyClass(1, 2, 3), MyClass(2, 3, 4))
for ((a1, b1, c1) in list) {
}
}
data class MyClass(val a: Int, val b: Int, val c: Int)
@@ -0,0 +1,13 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
val entries = map.entries
for (<caret>entry in entries) {
val key = entry.key
val value = entry.value
println(key)
println(value)
}
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
val entries = map.entries
for ((key, value) in entries) {
println(key)
println(value)
}
}
+10
View File
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (<caret>entry in map.entrySet()) {
val key = entry.getKey()
val value = entry.getValue()
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for ((key, value) in map) {
}
}
@@ -0,0 +1,12 @@
// IS_APPLICABLE: false
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (<caret>entry in map) {
val key = entry.key
val key2 = entry.key
println(key)
}
}
@@ -0,0 +1,13 @@
// IS_APPLICABLE: false
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (<caret>entry in map) {
val myKey = entry.key
val myValue = entry.value
println(entry)
}
}
@@ -0,0 +1,12 @@
// IS_APPLICABLE: false
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (<caret>entry in map) {
val myKey = entry.key
println(entry)
}
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (<caret>entry in map) {
val myKey = entry.key
val myValue = entry.value
}
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for ((myKey, myValue) in map) {
}
}
+12
View File
@@ -0,0 +1,12 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for (<caret>entry in map) {
val key = entry.key
val value = entry.value
println(key)
println(value)
}
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
val map = hashMapOf(1 to 1)
for ((key, value) in map) {
println(key)
println(value)
}
}
@@ -0,0 +1,12 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
for (<caret>entry in 1.createListOfDataClasses()) {
val int1 = entry.a
val int2 = entry.b
}
}
fun Int.createListOfDataClasses() = listOf(MyClass(this, this))
data class MyClass(val a: Int, val b: Int)
@@ -0,0 +1,10 @@
// WITH_RUNTIME
fun main(args: Array<String>) {
for ((int1, int2) in 1.createListOfDataClasses()) {
}
}
fun Int.createListOfDataClasses() = listOf(MyClass(this, this))
data class MyClass(val a: Int, val b: Int)
@@ -0,0 +1,74 @@
<problems>
<problem>
<file>SomeQualifiedExpressionInRange.kt</file>
<line>4</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="SomeQualifiedExpressionInRange.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>Simple.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="Simple.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>PropertiesNames.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="PropertiesNames.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>Getters.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="Getters.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>DataClassParametersOrder.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="DataClassParametersOrder.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>DataClass.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="DataClass.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>EntriesCallIsMissing.kt</file>
<line>6</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="EntriesCallIsMissing.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>DataClassFirstNPropertiesUsed.kt</file>
<line>5</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="DataClassFirstNPropertiesUsed.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
<problem>
<file>CaretOffset.kt</file>
<line>6</line>
<module>light_idea_test_case</module>
<entry_point TYPE="file" FQNAME="CaretOffset.kt" />
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Simplify 'for' using destructing declaration</problem_class>
<description>Simplify 'for' using destructing declaration</description>
</problem>
</problems>
@@ -0,0 +1 @@
// INSPECTION_CLASS: org.jetbrains.kotlin.idea.intentions.SimplifyForInspection
@@ -67,6 +67,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest {
doTest(fileName);
}
@TestMetadata("iterationOverMap/inspectionData/inspections.test")
public void testIterationOverMap_inspectionData_Inspections_test() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/inspectionData/inspections.test");
doTest(fileName);
}
@TestMetadata("objectLiteralToLambda/inspectionData/inspections.test")
public void testObjectLiteralToLambda_inspectionData_Inspections_test() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/objectLiteralToLambda/inspectionData/inspections.test");
@@ -6199,6 +6199,100 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/iterationOverMap")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class IterationOverMap extends AbstractIntentionTest {
public void testAllFilesPresentInIterationOverMap() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/iterationOverMap"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("AlreadyDestructing.kt")
public void testAlreadyDestructing() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/AlreadyDestructing.kt");
doTest(fileName);
}
@TestMetadata("CaretOffset.kt")
public void testCaretOffset() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/CaretOffset.kt");
doTest(fileName);
}
@TestMetadata("DataClass.kt")
public void testDataClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/DataClass.kt");
doTest(fileName);
}
@TestMetadata("DataClassFirstNPropertiesUsed.kt")
public void testDataClassFirstNPropertiesUsed() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/DataClassFirstNPropertiesUsed.kt");
doTest(fileName);
}
@TestMetadata("DataClassNotAllPropertiesUsed.kt")
public void testDataClassNotAllPropertiesUsed() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/DataClassNotAllPropertiesUsed.kt");
doTest(fileName);
}
@TestMetadata("DataClassParametersOrder.kt")
public void testDataClassParametersOrder() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/DataClassParametersOrder.kt");
doTest(fileName);
}
@TestMetadata("EntriesCallIsMissing.kt")
public void testEntriesCallIsMissing() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/EntriesCallIsMissing.kt");
doTest(fileName);
}
@TestMetadata("Getters.kt")
public void testGetters() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/Getters.kt");
doTest(fileName);
}
@TestMetadata("OnlyKeyUsed.kt")
public void testOnlyKeyUsed() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/OnlyKeyUsed.kt");
doTest(fileName);
}
@TestMetadata("OtherUsages.kt")
public void testOtherUsages() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/OtherUsages.kt");
doTest(fileName);
}
@TestMetadata("OtherUsages2.kt")
public void testOtherUsages2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/OtherUsages2.kt");
doTest(fileName);
}
@TestMetadata("PropertiesNames.kt")
public void testPropertiesNames() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/PropertiesNames.kt");
doTest(fileName);
}
@TestMetadata("Simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/Simple.kt");
doTest(fileName);
}
@TestMetadata("SomeQualifiedExpressionInRange.kt")
public void testSomeQualifiedExpressionInRange() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterationOverMap/SomeQualifiedExpressionInRange.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/moveAssignmentToInitializer")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)