Initial implementation of KT-7688 Intention action to iterate over iterable value

#KT-7688 Fixed
This commit is contained in:
Valentin Kipyatkov
2015-06-26 14:12:43 +02:00
parent 4fc67c27b0
commit 2206459874
17 changed files with 173 additions and 25 deletions
@@ -392,9 +392,9 @@ class SmartCompletion(
}
val scope = bindingContext.get(BindingContext.RESOLUTION_SCOPE, expressionWithType)
val iterableDetector = IterableTypesDetector(project, moduleDescriptor, scope, loopVarType)
val iterableDetector = IterableTypesDetector(project, moduleDescriptor, scope)
return buildResultByTypeFilter(expressionWithType, receiver, Tail.RPARENTH) { iterableDetector.isIterable(it) }
return buildResultByTypeFilter(expressionWithType, receiver, Tail.RPARENTH) { iterableDetector.isIterable(it, loopVarType) }
}
private fun buildForInOperatorArgument(expressionWithType: JetExpression, receiver: JetExpression?): Result? {
@@ -34,12 +34,11 @@ import java.util.HashMap
public class IterableTypesDetector(
private val project: Project,
private val moduleDescriptor: ModuleDescriptor,
private val scope: JetScope,
private val loopVarType: JetType? = null
private val scope: JetScope
) {
private val container = createContainerForMacros(project, moduleDescriptor)
private val cache = HashMap<FuzzyType, Boolean>()
private val cache = HashMap<FuzzyType, FuzzyType?>()
private val iteratorName = Name.identifier("iterator")
private val typesWithExtensionIterator: Collection<JetType> = scope.getFunctions(iteratorName)
@@ -47,13 +46,24 @@ public class IterableTypesDetector(
.filterNotNull()
.map { it.getType() }
public fun isIterable(type: FuzzyType): Boolean {
return cache.getOrPut(type, { isIterableNoCache(type) })
public fun isIterable(type: FuzzyType, loopVarType: JetType? = null): Boolean {
val elementType = elementType(type) ?: return false
return loopVarType == null || elementType.checkIsSubtypeOf(loopVarType) != null
}
private fun isIterableNoCache(type: FuzzyType): Boolean {
public fun isIterable(type: JetType, loopVarType: JetType? = null): Boolean
= isIterable(FuzzyType(type, emptyList()), loopVarType)
public fun elementType(type: FuzzyType): FuzzyType? {
return cache.getOrPut(type, { elementTypeNoCache(type) })
}
public fun elementType(type: JetType): FuzzyType?
= elementType(FuzzyType(type, emptyList()))
private fun elementTypeNoCache(type: FuzzyType): FuzzyType? {
// optimization
if (!canBeIterable(type)) return false
if (!canBeIterable(type)) return null
val expression = JetPsiFactory(project).createExpression("fake")
val expressionReceiver = ExpressionReceiver(expression, type.type)
@@ -61,8 +71,7 @@ public class IterableTypesDetector(
val context = ExpressionTypingContext.newContext(expressionTypingComponents.getAdditionalCheckerProvider(),
BindingTraceContext(), scope, DataFlowInfo.EMPTY, TypeUtils.NO_EXPECTED_TYPE)
val elementType = expressionTypingComponents.getForLoopConventionsChecker().checkIterableConvention(expressionReceiver, context)
if (elementType == null) return false
return loopVarType == null || FuzzyType(elementType, type.freeParameters).checkIsSubtypeOf(loopVarType) != null
return elementType?.let { FuzzyType(it, type.freeParameters) }
}
private fun canBeIterable(type: FuzzyType): Boolean {
@@ -0,0 +1,3 @@
for (i in <spot>listOf(1, 2, 3)</spot>) {
}
@@ -0,0 +1 @@
<spot>listOf(1, 2, 3)</spot>
@@ -0,0 +1,5 @@
<html>
<body>
Uses for-loop to iterate over sequence of elements.
</body>
</html>
+5
View File
@@ -933,6 +933,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.IterateExpressionIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.DeprecatedCallableAddReplaceWithInspection"
displayName="Add 'replaceWith' argument to 'deprecated' annotation"
groupName="Kotlin"
@@ -30,6 +30,7 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
//TODO: move it somewhere else and reuse
public abstract class ChooseValueExpression<T : Any>(
lookupItems: List<T>,
protected val defaultItem: T,
@@ -62,3 +63,12 @@ public abstract class ChooseValueExpression<T : Any>(
override fun getAdvertisingText() = advertisementText
}
public class ChooseStringExpression(
suggestions: List<String>,
default: String = suggestions[0],
advertisementText: String? = null
) : ChooseValueExpression<String>(suggestions, default, advertisementText) {
override fun getLookupString(element: String) = element
override fun getResult(element: String) = element
}
@@ -0,0 +1,79 @@
/*
* Copyright 2010-2015 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.codeInsight.intention.HighPriorityAction
import com.intellij.codeInsight.template.TemplateBuilderImpl
import com.intellij.codeInsight.template.impl.ConstantNode
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiDocumentManager
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.core.IterableTypesDetector
import org.jetbrains.kotlin.idea.core.refactoring.EmptyValidator
import org.jetbrains.kotlin.idea.core.refactoring.JetNameSuggester
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.JetType
public class IterateExpressionIntention : JetSelfTargetingIntention<JetExpression>(javaClass(), "Iterate over collection"), HighPriorityAction {
override fun isApplicableTo(element: JetExpression, caretOffset: Int): Boolean {
if (element.getParent() !is JetBlockExpression) return false
val range = element.getTextRange()
if (caretOffset != range.getStartOffset() && caretOffset != range.getEndOffset()) return false
val data = data(element) ?: return false
setText("Iterate over '${IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(data.collectionType)}'")
return true
}
private data class Data(val collectionType: JetType, val elementType: JetType)
private fun data(expression: JetExpression): Data? {
val bindingContext = expression.analyze(BodyResolveMode.PARTIAL)
val type = bindingContext.getType(expression) ?: return null
val moduleDescriptor = expression.getResolutionFacade().findModuleDescriptor(expression)
val scope = bindingContext[BindingContext.RESOLUTION_SCOPE, expression]
val elementType = IterableTypesDetector(expression.getProject(), moduleDescriptor, scope).elementType(type)?.type ?: return null
return Data(type, elementType)
}
override fun applyTo(element: JetExpression, editor: Editor) {
//TODO: multi-declaration (when?)
val elementType = data(element)!!.elementType
//TODO: base on expression too
//TODO: name validation
val names = JetNameSuggester.suggestNames(elementType, EmptyValidator, "e")
var forExpression = JetPsiFactory(element).createExpressionByPattern("for($0 in $1) {\nx\n}", names[0], element) as JetForExpression
forExpression = element.replaced(forExpression)
PsiDocumentManager.getInstance(forExpression.getProject()).doPostponedOperationsAndUnblockDocument(editor.getDocument())
val bodyPlaceholder = (forExpression.getBody() as JetBlockExpression).getStatements().single()
val templateBuilder = TemplateBuilderImpl(forExpression)
templateBuilder.replaceElement(forExpression.getLoopParameter()!!, ChooseStringExpression(names.asList()))
templateBuilder.replaceElement(bodyPlaceholder, ConstantNode(""), false)
templateBuilder.setEndVariableAfter(bodyPlaceholder)
templateBuilder.run(editor, true)
}
}
@@ -26,8 +26,8 @@ import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import org.apache.commons.lang.StringEscapeUtils.escapeJava
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.intentions.ChooseStringExpression
import org.jetbrains.kotlin.idea.intentions.JetSelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.intentions.ChooseValueExpression
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.convertToIfNotNullExpression
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.convertToIfNullExpression
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.introduceValueForCondition
@@ -68,13 +68,7 @@ public class DoubleBangToIfThenIntention : JetSelfTargetingRangeIntention<JetPos
val nullPtrExceptionText = "NullPointerException(\"$message\")"
val kotlinNullPtrExceptionText = "KotlinNullPointerException()"
val exceptionLookupExpression =
object: ChooseValueExpression<String>(listOf(nullPtrExceptionText, kotlinNullPtrExceptionText),
nullPtrExceptionText) {
override fun getLookupString(element: String) = element
override fun getResult(element: String) = element
}
val exceptionLookupExpression = ChooseStringExpression(listOf(nullPtrExceptionText, kotlinNullPtrExceptionText))
val project = element.getProject()
val builder = TemplateBuilderImpl(thrownExpression)
builder.replaceElement(thrownExpression, exceptionLookupExpression);
@@ -71,7 +71,7 @@ public abstract class BaseJetVariableMacro extends Macro {
return null;
}
IterableTypesDetector iterableTypesDetector = new IterableTypesDetector(project, analysisResult.getModuleDescriptor(), scope, null);
IterableTypesDetector iterableTypesDetector = new IterableTypesDetector(project, analysisResult.getModuleDescriptor(), scope);
DataFlowInfo dataFlowInfo = getDataFlowInfo(bindingContext, contextExpression);
@@ -18,14 +18,10 @@ package org.jetbrains.kotlin.idea.liveTemplates.macro;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor;
import org.jetbrains.kotlin.descriptors.VariableDescriptor;
import org.jetbrains.kotlin.idea.JetBundle;
import org.jetbrains.kotlin.idea.util.FuzzyType;
import org.jetbrains.kotlin.idea.core.IterableTypesDetector;
import java.util.Collections;
public class JetIterableVariableMacro extends BaseJetVariableMacro {
@Override
@@ -45,6 +41,6 @@ public class JetIterableVariableMacro extends BaseJetVariableMacro {
@NotNull IterableTypesDetector iterableTypesDetector
) {
//TODO: smart-casts
return iterableTypesDetector.isIterable(new FuzzyType(variableDescriptor.getType(), Collections.<TypeParameterDescriptor>emptyList()));
return iterableTypesDetector.isIterable(variableDescriptor.getType(), null);
}
}
+1
View File
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.IterateExpressionIntention
@@ -0,0 +1,7 @@
// WITH_RUNTIME
fun foo() {
f()<caret>
}
fun f(): List<Int> = emptyList()
@@ -0,0 +1,9 @@
// WITH_RUNTIME
fun foo() {
for (i in f()) {
<caret>
}
}
fun f(): List<Int> = emptyList()
+3
View File
@@ -0,0 +1,3 @@
fun foo(list: List<String>) {
list<caret>
}
@@ -0,0 +1,5 @@
fun foo(list: List<String>) {
for (s in list) {
<caret>
}
}
@@ -5221,6 +5221,27 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/iterateExpression")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class IterateExpression extends AbstractIntentionTest {
public void testAllFilesPresentInIterateExpression() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/iterateExpression"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("functionCall.kt")
public void testFunctionCall() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/iterateExpression/functionCall.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/iterateExpression/simple.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/moveLambdaInsideParentheses")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)