From 220645987436bafee5ecb062520853135c722d81 Mon Sep 17 00:00:00 2001 From: Valentin Kipyatkov Date: Fri, 26 Jun 2015 14:12:43 +0200 Subject: [PATCH] Initial implementation of KT-7688 Intention action to iterate over iterable value #KT-7688 Fixed --- .../idea/completion/smart/SmartCompletion.kt | 4 +- .../kotlin/idea/core/IterableTypesDetector.kt | 27 ++++--- .../after.kt.template | 3 + .../before.kt.template | 1 + .../description.html | 5 ++ idea/src/META-INF/plugin.xml | 5 ++ .../idea/intentions/ChooseValueExpression.kt | 10 +++ .../intentions/IterateExpressionIntention.kt | 79 +++++++++++++++++++ .../intentions/DoubleBangToIfThenIntention.kt | 10 +-- .../macro/BaseJetVariableMacro.java | 2 +- .../macro/JetIterableVariableMacro.java | 6 +- .../intentions/iterateExpression/.intention | 1 + .../iterateExpression/functionCall.kt | 7 ++ .../iterateExpression/functionCall.kt.after | 9 +++ .../intentions/iterateExpression/simple.kt | 3 + .../iterateExpression/simple.kt.after | 5 ++ .../intentions/IntentionTestGenerated.java | 21 +++++ 17 files changed, 173 insertions(+), 25 deletions(-) create mode 100644 idea/resources/intentionDescriptions/IterateExpressionIntention/after.kt.template create mode 100644 idea/resources/intentionDescriptions/IterateExpressionIntention/before.kt.template create mode 100644 idea/resources/intentionDescriptions/IterateExpressionIntention/description.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt create mode 100644 idea/testData/intentions/iterateExpression/.intention create mode 100644 idea/testData/intentions/iterateExpression/functionCall.kt create mode 100644 idea/testData/intentions/iterateExpression/functionCall.kt.after create mode 100644 idea/testData/intentions/iterateExpression/simple.kt create mode 100644 idea/testData/intentions/iterateExpression/simple.kt.after diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt index 444fdcc5f65..50bf4e5f953 100644 --- a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt @@ -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? { diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/IterableTypesDetector.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/IterableTypesDetector.kt index 58b922de711..15c1d162bb7 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/IterableTypesDetector.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/IterableTypesDetector.kt @@ -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() + private val cache = HashMap() private val iteratorName = Name.identifier("iterator") private val typesWithExtensionIterator: Collection = 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 { diff --git a/idea/resources/intentionDescriptions/IterateExpressionIntention/after.kt.template b/idea/resources/intentionDescriptions/IterateExpressionIntention/after.kt.template new file mode 100644 index 00000000000..6815f2899b6 --- /dev/null +++ b/idea/resources/intentionDescriptions/IterateExpressionIntention/after.kt.template @@ -0,0 +1,3 @@ +for (i in listOf(1, 2, 3)) { +} + diff --git a/idea/resources/intentionDescriptions/IterateExpressionIntention/before.kt.template b/idea/resources/intentionDescriptions/IterateExpressionIntention/before.kt.template new file mode 100644 index 00000000000..79d0b9a2d2f --- /dev/null +++ b/idea/resources/intentionDescriptions/IterateExpressionIntention/before.kt.template @@ -0,0 +1 @@ +listOf(1, 2, 3) diff --git a/idea/resources/intentionDescriptions/IterateExpressionIntention/description.html b/idea/resources/intentionDescriptions/IterateExpressionIntention/description.html new file mode 100644 index 00000000000..2c06025dfc4 --- /dev/null +++ b/idea/resources/intentionDescriptions/IterateExpressionIntention/description.html @@ -0,0 +1,5 @@ + + + Uses for-loop to iterate over sequence of elements. + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 24fa29733d3..cb76edf5d4e 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -933,6 +933,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.IterateExpressionIntention + Kotlin + + ( lookupItems: List, protected val defaultItem: T, @@ -62,3 +63,12 @@ public abstract class ChooseValueExpression( override fun getAdvertisingText() = advertisementText } + +public class ChooseStringExpression( + suggestions: List, + default: String = suggestions[0], + advertisementText: String? = null +) : ChooseValueExpression(suggestions, default, advertisementText) { + override fun getLookupString(element: String) = element + override fun getResult(element: String) = element +} diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt new file mode 100644 index 00000000000..0715771131b --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt @@ -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(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) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/branchedTransformations/intentions/DoubleBangToIfThenIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/branchedTransformations/intentions/DoubleBangToIfThenIntention.kt index f557bef6ecd..e4838abf944 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/branchedTransformations/intentions/DoubleBangToIfThenIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/branchedTransformations/intentions/DoubleBangToIfThenIntention.kt @@ -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(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); diff --git a/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/BaseJetVariableMacro.java b/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/BaseJetVariableMacro.java index 69122e0310c..96ec7ac5bf4 100644 --- a/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/BaseJetVariableMacro.java +++ b/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/BaseJetVariableMacro.java @@ -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); diff --git a/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/JetIterableVariableMacro.java b/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/JetIterableVariableMacro.java index fd4fdd469b1..6db48ec3190 100644 --- a/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/JetIterableVariableMacro.java +++ b/idea/src/org/jetbrains/kotlin/idea/liveTemplates/macro/JetIterableVariableMacro.java @@ -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.emptyList())); + return iterableTypesDetector.isIterable(variableDescriptor.getType(), null); } } diff --git a/idea/testData/intentions/iterateExpression/.intention b/idea/testData/intentions/iterateExpression/.intention new file mode 100644 index 00000000000..1ed217b7e32 --- /dev/null +++ b/idea/testData/intentions/iterateExpression/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.IterateExpressionIntention diff --git a/idea/testData/intentions/iterateExpression/functionCall.kt b/idea/testData/intentions/iterateExpression/functionCall.kt new file mode 100644 index 00000000000..3f155808f7a --- /dev/null +++ b/idea/testData/intentions/iterateExpression/functionCall.kt @@ -0,0 +1,7 @@ +// WITH_RUNTIME + +fun foo() { + f() +} + +fun f(): List = emptyList() \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/functionCall.kt.after b/idea/testData/intentions/iterateExpression/functionCall.kt.after new file mode 100644 index 00000000000..6a56ef5d5d1 --- /dev/null +++ b/idea/testData/intentions/iterateExpression/functionCall.kt.after @@ -0,0 +1,9 @@ +// WITH_RUNTIME + +fun foo() { + for (i in f()) { + + } +} + +fun f(): List = emptyList() \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/simple.kt b/idea/testData/intentions/iterateExpression/simple.kt new file mode 100644 index 00000000000..d2cc88740d1 --- /dev/null +++ b/idea/testData/intentions/iterateExpression/simple.kt @@ -0,0 +1,3 @@ +fun foo(list: List) { + list +} \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/simple.kt.after b/idea/testData/intentions/iterateExpression/simple.kt.after new file mode 100644 index 00000000000..421790fb968 --- /dev/null +++ b/idea/testData/intentions/iterateExpression/simple.kt.after @@ -0,0 +1,5 @@ +fun foo(list: List) { + for (s in list) { + + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index d9e5a4cbf84..5fcb286fa39 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -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)