From 6d06436c34ece5d72d3ea1c56e911938b0748496 Mon Sep 17 00:00:00 2001 From: Valentin Kipyatkov Date: Fri, 16 Jan 2015 23:01:12 +0300 Subject: [PATCH] KT-6376 Smart completion should work after "in" and "!in" #KT-6376 Fixed --- .../jetbrains/kotlin/idea/util/FuzzyType.kt | 2 +- .../idea/completion/smart/SmartCompletion.kt | 36 ++++++++++ .../smart/TypesWithContainsDetector.kt | 60 ++++++++++++++++ .../smart/inOperator/ExtensionContains.kt | 13 ++++ .../smart/inOperator/FilterByArgumentType.kt | 19 +++++ .../smart/inOperator/GenericMethod.kt | 5 ++ .../smart/inOperator/GenericMethod2.kt | 13 ++++ .../smart/inOperator/GenericMethod3.kt | 13 ++++ .../smart/inOperator/NonBooleanContains.kt | 9 +++ .../completion/smart/inOperator/NotIn.kt | 6 ++ .../completion/smart/inOperator/Nullable.kt | 7 ++ .../completion/smart/inOperator/Simple.kt | 6 ++ .../completion/smart/inOperator/SmartCasts.kt | 6 ++ .../JvmSmartCompletionTestGenerated.java | 71 ++++++++++++++++++- 14 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/completion/smart/TypesWithContainsDetector.kt create mode 100644 idea/testData/completion/smart/inOperator/ExtensionContains.kt create mode 100644 idea/testData/completion/smart/inOperator/FilterByArgumentType.kt create mode 100644 idea/testData/completion/smart/inOperator/GenericMethod.kt create mode 100644 idea/testData/completion/smart/inOperator/GenericMethod2.kt create mode 100644 idea/testData/completion/smart/inOperator/GenericMethod3.kt create mode 100644 idea/testData/completion/smart/inOperator/NonBooleanContains.kt create mode 100644 idea/testData/completion/smart/inOperator/NotIn.kt create mode 100644 idea/testData/completion/smart/inOperator/Nullable.kt create mode 100644 idea/testData/completion/smart/inOperator/Simple.kt create mode 100644 idea/testData/completion/smart/inOperator/SmartCasts.kt diff --git a/idea/ide-common/src/org/jetbrains/kotlin/idea/util/FuzzyType.kt b/idea/ide-common/src/org/jetbrains/kotlin/idea/util/FuzzyType.kt index e1609141d30..76cd85c7e2a 100644 --- a/idea/ide-common/src/org/jetbrains/kotlin/idea/util/FuzzyType.kt +++ b/idea/ide-common/src/org/jetbrains/kotlin/idea/util/FuzzyType.kt @@ -43,7 +43,7 @@ fun FuzzyType.makeNotNullable() = FuzzyType(type.makeNotNullable(), freeParamete fun FuzzyType.makeNullable() = FuzzyType(type.makeNullable(), freeParameters) fun FuzzyType.nullability() = type.nullability() -class FuzzyType( +data class FuzzyType( val type: JetType, val freeParameters: Collection ) { diff --git a/idea/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt b/idea/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt index e93bc305ed4..81635605839 100644 --- a/idea/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt +++ b/idea/src/org/jetbrains/kotlin/idea/completion/smart/SmartCompletion.kt @@ -117,6 +117,9 @@ class SmartCompletion( val loopRangePositionResult = buildForLoopRangePosition(expressionWithType, receiver) if (loopRangePositionResult != null) return loopRangePositionResult + val inOperatorArgumentResult = buildForInOperatorArgument(expressionWithType, receiver) + if (inOperatorArgumentResult != null) return inOperatorArgumentResult + val allExpectedInfos = calcExpectedInfos(expressionWithType) ?: return null val filteredExpectedInfos = allExpectedInfos.filter { !it.type.isError() } if (filteredExpectedInfos.isEmpty()) return null @@ -409,6 +412,39 @@ class SmartCompletion( return Result(::filterDeclaration, listOf(), null) } + //TODO: reuse code from above + private fun buildForInOperatorArgument(expressionWithType: JetExpression, receiver: JetExpression?): Result? { + val binaryExpression = expressionWithType.getParent() as? JetBinaryExpression ?: return null + val operationToken = binaryExpression.getOperationToken() + if (operationToken != JetTokens.IN_KEYWORD && operationToken != JetTokens.NOT_IN || expressionWithType != binaryExpression.getRight()) return null + + val leftOperandType = bindingContext.get(BindingContext.EXPRESSION_TYPE, binaryExpression.getLeft()) ?: return null + + val smartCastTypes: (VariableDescriptor) -> Collection = SmartCastCalculator(bindingContext).calculate(expressionWithType, receiver) + + val scope = bindingContext.get(BindingContext.RESOLUTION_SCOPE, expressionWithType) + + val detector = TypesWithContainsDetector(scope, leftOperandType) + + fun filterDeclaration(descriptor: DeclarationDescriptor): Collection { + val types = descriptor.fuzzyTypes(smartCastTypes) + + fun createLookupElement() = lookupElementFactory.createLookupElement(descriptor, resolutionFacade, bindingContext, true) + + if (types.any { detector.hasContains(it) }) { + return listOf(createLookupElement()) + } + + if (types.any { it.nullability() == TypeNullability.NULLABLE && detector.hasContains(it.makeNotNullable()) }) { + return lookupElementsForNullable(::createLookupElement) + } + + return listOf() + } + + return Result(::filterDeclaration, listOf(), null) + } + private fun lookupElementForType(jetType: JetType): LookupElement? { if (jetType.isError()) return null val classifier = jetType.getConstructor().getDeclarationDescriptor() ?: return null diff --git a/idea/src/org/jetbrains/kotlin/idea/completion/smart/TypesWithContainsDetector.kt b/idea/src/org/jetbrains/kotlin/idea/completion/smart/TypesWithContainsDetector.kt new file mode 100644 index 00000000000..fac12cd7bac --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/completion/smart/TypesWithContainsDetector.kt @@ -0,0 +1,60 @@ +/* + * 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.completion.smart + +import org.jetbrains.kotlin.types.JetType +import org.jetbrains.kotlin.resolve.scopes.JetScope +import java.util.HashMap +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.types.TypeUtils +import org.jetbrains.kotlin.idea.util.nullability +import org.jetbrains.kotlin.idea.util.TypeNullability +import org.jetbrains.kotlin.idea.util.FuzzyType +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor + +//TODO: heuristics for collection's? +class TypesWithContainsDetector( + private val scope: JetScope, + private val argumentType: JetType +) { + + private val cache = HashMap() + private val containsName = Name.identifier("contains") + private val booleanType = KotlinBuiltIns.getInstance().getBooleanType() + + private val typesWithExtensionContains: Collection = scope.getFunctions(containsName) + .filter { it.getExtensionReceiverParameter() != null && isGoodContainsFunction(it, listOf()) } + .map { it.getExtensionReceiverParameter()!!.getType() } + + public fun hasContains(type: FuzzyType): Boolean { + return cache.getOrPut(type, { hasContainsNoCache(type) }) + } + + private fun hasContainsNoCache(type: FuzzyType): Boolean { + return type.nullability() != TypeNullability.NULLABLE && type.type.getMemberScope().getFunctions(containsName).any { isGoodContainsFunction(it, type.freeParameters) } + || typesWithExtensionContains.any { type.checkIsSubtypeOf(it) != null } + } + + private fun isGoodContainsFunction(function: FunctionDescriptor, freeTypeParams: Collection): Boolean { + if (!TypeUtils.equalTypes(function.getReturnType(), booleanType)) return false + val parameter = function.getValueParameters().singleOrNull() ?: return false + val parameterType = FuzzyType(parameter.getType(), function.getTypeParameters() + freeTypeParams) + return parameterType.checkIsSuperTypeOf(argumentType) != null + } +} diff --git a/idea/testData/completion/smart/inOperator/ExtensionContains.kt b/idea/testData/completion/smart/inOperator/ExtensionContains.kt new file mode 100644 index 00000000000..ad388021f37 --- /dev/null +++ b/idea/testData/completion/smart/inOperator/ExtensionContains.kt @@ -0,0 +1,13 @@ +trait X +trait Y : X +trait Z + +fun X.contains(s: String): Boolean + +fun foo(s: String, x: X, y: Y, z: Z) { + if (s in ) +} + +// EXIST: x +// EXIST: y +// ABSENT: z diff --git a/idea/testData/completion/smart/inOperator/FilterByArgumentType.kt b/idea/testData/completion/smart/inOperator/FilterByArgumentType.kt new file mode 100644 index 00000000000..7a4cfd3f15e --- /dev/null +++ b/idea/testData/completion/smart/inOperator/FilterByArgumentType.kt @@ -0,0 +1,19 @@ +trait X { + fun contains(s: String): Boolean +} + +trait Y { + fun contains(i: Int): Boolean +} + +trait Z { + fun contains(o: Any): Boolean +} + +fun foo(s: String, x: X, y: Y, z: Z) { + if (s in ) +} + +// EXIST: x +// ABSENT: y +// EXIST: z diff --git a/idea/testData/completion/smart/inOperator/GenericMethod.kt b/idea/testData/completion/smart/inOperator/GenericMethod.kt new file mode 100644 index 00000000000..ff89ba598d4 --- /dev/null +++ b/idea/testData/completion/smart/inOperator/GenericMethod.kt @@ -0,0 +1,5 @@ +fun foo(s: String) { + if (s in ) +} + +// EXIST: { lookupString:"listOf", itemText: "listOf", tailText: "(vararg values: T) (kotlin)", typeText:"List" } diff --git a/idea/testData/completion/smart/inOperator/GenericMethod2.kt b/idea/testData/completion/smart/inOperator/GenericMethod2.kt new file mode 100644 index 00000000000..1ed6bc8da81 --- /dev/null +++ b/idea/testData/completion/smart/inOperator/GenericMethod2.kt @@ -0,0 +1,13 @@ +trait X { + fun contains(t: T): Boolean +} + +trait A { + fun createX(t: T): X + + fun foo(s: String) { + if (s in ) + } +} + +// EXIST: { lookupString:"createX", itemText: "createX", tailText: "(t: T)", typeText:"X" } diff --git a/idea/testData/completion/smart/inOperator/GenericMethod3.kt b/idea/testData/completion/smart/inOperator/GenericMethod3.kt new file mode 100644 index 00000000000..afa74802019 --- /dev/null +++ b/idea/testData/completion/smart/inOperator/GenericMethod3.kt @@ -0,0 +1,13 @@ +trait X + +fun X.contains(t: T): Boolean + +trait A { + fun createX(t: T): X + + fun foo(s: String) { + if (s in ) + } +} + +// EXIST: { lookupString:"createX", itemText: "createX", tailText: "(t: T)", typeText:"X" } diff --git a/idea/testData/completion/smart/inOperator/NonBooleanContains.kt b/idea/testData/completion/smart/inOperator/NonBooleanContains.kt new file mode 100644 index 00000000000..7b68a46a24a --- /dev/null +++ b/idea/testData/completion/smart/inOperator/NonBooleanContains.kt @@ -0,0 +1,9 @@ +trait X { + fun contains(s: String): Boolean? +} + +fun foo(s: String, x: X) { + if (s in ) +} + +// ABSENT: x diff --git a/idea/testData/completion/smart/inOperator/NotIn.kt b/idea/testData/completion/smart/inOperator/NotIn.kt new file mode 100644 index 00000000000..c8e95364cc0 --- /dev/null +++ b/idea/testData/completion/smart/inOperator/NotIn.kt @@ -0,0 +1,6 @@ +fun foo(s: String, list: List, o: Any) { + if (s !in ) +} + +// EXIST: list +// ABSENT: o diff --git a/idea/testData/completion/smart/inOperator/Nullable.kt b/idea/testData/completion/smart/inOperator/Nullable.kt new file mode 100644 index 00000000000..d776fa3f727 --- /dev/null +++ b/idea/testData/completion/smart/inOperator/Nullable.kt @@ -0,0 +1,7 @@ +fun foo(s: String, list: List?) { + if (s in ) +} + +// ABSENT: { lookupString:"list", itemText: "list" } +// EXIST: { lookupString:"list", itemText: "!! list", typeText:"List?" } +// EXIST: { lookupString:"list", itemText: "?: list", typeText:"List?" } diff --git a/idea/testData/completion/smart/inOperator/Simple.kt b/idea/testData/completion/smart/inOperator/Simple.kt new file mode 100644 index 00000000000..1ed1bfde4f0 --- /dev/null +++ b/idea/testData/completion/smart/inOperator/Simple.kt @@ -0,0 +1,6 @@ +fun foo(s: String, list: List, o: Any) { + if (s in ) +} + +// EXIST: list +// ABSENT: o diff --git a/idea/testData/completion/smart/inOperator/SmartCasts.kt b/idea/testData/completion/smart/inOperator/SmartCasts.kt new file mode 100644 index 00000000000..086428cf00d --- /dev/null +++ b/idea/testData/completion/smart/inOperator/SmartCasts.kt @@ -0,0 +1,6 @@ +fun foo(s: String, o1: Any, o2: Any) { + if (o1 is Collection && s in +} + +// EXIST: o1 +// ABSENT: o2 diff --git a/idea/tests/org/jetbrains/kotlin/completion/JvmSmartCompletionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/completion/JvmSmartCompletionTestGenerated.java index 0c41952cbdf..d93b4a022c1 100644 --- a/idea/tests/org/jetbrains/kotlin/completion/JvmSmartCompletionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/completion/JvmSmartCompletionTestGenerated.java @@ -30,7 +30,7 @@ import java.util.regex.Pattern; @SuppressWarnings("all") @TestMetadata("idea/testData/completion/smart") @TestDataPath("$PROJECT_ROOT") -@InnerTestClasses({JvmSmartCompletionTestGenerated.AfterAs.class, JvmSmartCompletionTestGenerated.AnonymousObject.class, JvmSmartCompletionTestGenerated.Constructor.class, JvmSmartCompletionTestGenerated.ForLoopRange.class, JvmSmartCompletionTestGenerated.FunctionLiterals.class, JvmSmartCompletionTestGenerated.FunctionReference.class, JvmSmartCompletionTestGenerated.Generics.class, JvmSmartCompletionTestGenerated.IfValue.class, JvmSmartCompletionTestGenerated.InElvisOperator.class, JvmSmartCompletionTestGenerated.Inheritors.class, JvmSmartCompletionTestGenerated.MultipleArgsItem.class, JvmSmartCompletionTestGenerated.SmartCasts.class, JvmSmartCompletionTestGenerated.This.class, JvmSmartCompletionTestGenerated.WhenEntry.class}) +@InnerTestClasses({JvmSmartCompletionTestGenerated.AfterAs.class, JvmSmartCompletionTestGenerated.AnonymousObject.class, JvmSmartCompletionTestGenerated.Constructor.class, JvmSmartCompletionTestGenerated.ForLoopRange.class, JvmSmartCompletionTestGenerated.FunctionLiterals.class, JvmSmartCompletionTestGenerated.FunctionReference.class, JvmSmartCompletionTestGenerated.Generics.class, JvmSmartCompletionTestGenerated.IfValue.class, JvmSmartCompletionTestGenerated.InElvisOperator.class, JvmSmartCompletionTestGenerated.InOperator.class, JvmSmartCompletionTestGenerated.Inheritors.class, JvmSmartCompletionTestGenerated.MultipleArgsItem.class, JvmSmartCompletionTestGenerated.SmartCasts.class, JvmSmartCompletionTestGenerated.This.class, JvmSmartCompletionTestGenerated.WhenEntry.class}) @RunWith(JUnit3RunnerWithInners.class) public class JvmSmartCompletionTestGenerated extends AbstractJvmSmartCompletionTest { @TestMetadata("AfterExclSign.kt") @@ -826,6 +826,75 @@ public class JvmSmartCompletionTestGenerated extends AbstractJvmSmartCompletionT } } + @TestMetadata("idea/testData/completion/smart/inOperator") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class InOperator extends AbstractJvmSmartCompletionTest { + public void testAllFilesPresentInInOperator() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/completion/smart/inOperator"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("ExtensionContains.kt") + public void testExtensionContains() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/ExtensionContains.kt"); + doTest(fileName); + } + + @TestMetadata("FilterByArgumentType.kt") + public void testFilterByArgumentType() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/FilterByArgumentType.kt"); + doTest(fileName); + } + + @TestMetadata("GenericMethod.kt") + public void testGenericMethod() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/GenericMethod.kt"); + doTest(fileName); + } + + @TestMetadata("GenericMethod2.kt") + public void testGenericMethod2() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/GenericMethod2.kt"); + doTest(fileName); + } + + @TestMetadata("GenericMethod3.kt") + public void testGenericMethod3() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/GenericMethod3.kt"); + doTest(fileName); + } + + @TestMetadata("NonBooleanContains.kt") + public void testNonBooleanContains() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/NonBooleanContains.kt"); + doTest(fileName); + } + + @TestMetadata("NotIn.kt") + public void testNotIn() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/NotIn.kt"); + doTest(fileName); + } + + @TestMetadata("Nullable.kt") + public void testNullable() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/Nullable.kt"); + doTest(fileName); + } + + @TestMetadata("Simple.kt") + public void testSimple() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/Simple.kt"); + doTest(fileName); + } + + @TestMetadata("SmartCasts.kt") + public void testSmartCasts() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/smart/inOperator/SmartCasts.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/completion/smart/inheritors") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)