Dealing with infix calls in completion

#KT-4846 Fixed
This commit is contained in:
Valentin Kipyatkov
2014-09-18 17:17:35 +04:00
committed by valentin
parent 9961a5ff16
commit 2bb553612c
23 changed files with 177 additions and 31 deletions
@@ -194,9 +194,15 @@ public class ExpressionTypingUtils {
public static boolean checkIsExtensionCallable ( public static boolean checkIsExtensionCallable (
@NotNull ReceiverValue receiverArgument, @NotNull ReceiverValue receiverArgument,
@NotNull CallableDescriptor callableDescriptor, @NotNull CallableDescriptor callableDescriptor,
boolean isInfixCall,
@NotNull BindingContext bindingContext, @NotNull BindingContext bindingContext,
@NotNull DataFlowInfo dataFlowInfo @NotNull DataFlowInfo dataFlowInfo
) { ) {
if (isInfixCall
&& (!(callableDescriptor instanceof SimpleFunctionDescriptor) || callableDescriptor.getValueParameters().size() != 1)) {
return false;
}
List<JetType> types = AutoCastUtils.getAutoCastVariants(receiverArgument, bindingContext, dataFlowInfo); List<JetType> types = AutoCastUtils.getAutoCastVariants(receiverArgument, bindingContext, dataFlowInfo);
for (JetType type : types) { for (JetType type : types) {
@@ -133,6 +133,7 @@ public class KotlinIndicesHelper(private val project: Project) {
scope: GlobalSearchScope): Collection<CallableDescriptor> { scope: GlobalSearchScope): Collection<CallableDescriptor> {
val context = resolveSession.resolveToElement(expression) val context = resolveSession.resolveToElement(expression)
val receiverExpression = expression.getReceiverExpression() ?: return listOf() val receiverExpression = expression.getReceiverExpression() ?: return listOf()
val isInfixCall = expression.getParent() is JetBinaryExpression
val expressionType = context.get<JetExpression, JetType>(BindingContext.EXPRESSION_TYPE, receiverExpression) val expressionType = context.get<JetExpression, JetType>(BindingContext.EXPRESSION_TYPE, receiverExpression)
val jetScope = context.get(BindingContext.RESOLUTION_SCOPE, receiverExpression) val jetScope = context.get(BindingContext.RESOLUTION_SCOPE, receiverExpression)
@@ -147,7 +148,7 @@ public class KotlinIndicesHelper(private val project: Project) {
return allFqNames return allFqNames
.filter { nameFilter(it.shortName().asString()) } .filter { nameFilter(it.shortName().asString()) }
.toSet() .toSet()
.flatMap { findSuitableExtensions(it, receiverExpression, expressionType, jetScope, resolveSession.getModuleDescriptor(), context) } .flatMap { findSuitableExtensions(it, receiverExpression, expressionType, isInfixCall, jetScope, resolveSession.getModuleDescriptor(), context) }
} }
/** /**
@@ -156,6 +157,7 @@ public class KotlinIndicesHelper(private val project: Project) {
private fun findSuitableExtensions(callableFQN: FqName, private fun findSuitableExtensions(callableFQN: FqName,
receiverExpression: JetExpression, receiverExpression: JetExpression,
receiverType: JetType, receiverType: JetType,
isInfixCall: Boolean,
scope: JetScope, scope: JetScope,
module: ModuleDescriptor, module: ModuleDescriptor,
bindingContext: BindingContext): List<CallableDescriptor> { bindingContext: BindingContext): List<CallableDescriptor> {
@@ -167,7 +169,7 @@ public class KotlinIndicesHelper(private val project: Project) {
return declarationDescriptors return declarationDescriptors
.filterIsInstance(javaClass<CallableDescriptor>()) .filterIsInstance(javaClass<CallableDescriptor>())
.filter { it.getReceiverParameter() != null && ExpressionTypingUtils.checkIsExtensionCallable(receiverValue, it, bindingContext, dataFlowInfo) } .filter { it.getReceiverParameter() != null && ExpressionTypingUtils.checkIsExtensionCallable(receiverValue, it, isInfixCall, bindingContext, dataFlowInfo) }
} }
public fun getClassDescriptors(nameFilter: (String) -> Boolean, analyzer: KotlinCodeAnalyzer, scope: GlobalSearchScope): Collection<ClassDescriptor> { public fun getClassDescriptors(nameFilter: (String) -> Boolean, analyzer: KotlinCodeAnalyzer, scope: GlobalSearchScope): Collection<ClassDescriptor> {
@@ -25,7 +25,6 @@ import org.jetbrains.jet.lang.resolve.calls.autocasts.DataFlowInfo
import org.jetbrains.jet.lang.resolve.scopes.JetScope import org.jetbrains.jet.lang.resolve.scopes.JetScope
import org.jetbrains.jet.lang.resolve.scopes.JetScopeUtils import org.jetbrains.jet.lang.resolve.scopes.JetScopeUtils
import org.jetbrains.jet.lang.resolve.scopes.receivers.ExpressionReceiver import org.jetbrains.jet.lang.resolve.scopes.receivers.ExpressionReceiver
import org.jetbrains.jet.lang.resolve.scopes.receivers.ReceiverValue
import org.jetbrains.jet.lang.types.expressions.ExpressionTypingUtils import org.jetbrains.jet.lang.types.expressions.ExpressionTypingUtils
import java.util.* import java.util.*
@@ -38,15 +37,21 @@ public object TipsManager{
val parent = expression.getParent() val parent = expression.getParent()
val resolutionScope = context[BindingContext.RESOLUTION_SCOPE, expression] ?: return listOf() val resolutionScope = context[BindingContext.RESOLUTION_SCOPE, expression] ?: return listOf()
val inPositionForCompletionWithReceiver = parent is JetCallExpression || parent is JetQualifiedExpression val inPositionForCompletionWithReceiver = parent is JetCallExpression
|| parent is JetQualifiedExpression
|| parent is JetBinaryExpression
if (receiverExpression != null && inPositionForCompletionWithReceiver) { if (receiverExpression != null && inPositionForCompletionWithReceiver) {
val isInfixCall = parent is JetBinaryExpression
fun filterIfInfix(descriptor: DeclarationDescriptor)
= if (isInfixCall) descriptor is SimpleFunctionDescriptor && descriptor.getValueParameters().size == 1 else true
// Process as call expression // Process as call expression
val descriptors = HashSet<DeclarationDescriptor>() val descriptors = HashSet<DeclarationDescriptor>()
val qualifier = context[BindingContext.QUALIFIER, receiverExpression] val qualifier = context[BindingContext.QUALIFIER, receiverExpression]
if (qualifier != null) { if (qualifier != null) {
// It's impossible to add extension function for package or class (if it's class object, expression type is not null) // It's impossible to add extension function for package or class (if it's class object, expression type is not null)
descriptors.addAll(qualifier.scope.getAllDescriptors()) qualifier.scope.getAllDescriptors().filterTo(descriptors, ::filterIfInfix)
} }
val expressionType = context[BindingContext.EXPRESSION_TYPE, receiverExpression] val expressionType = context[BindingContext.EXPRESSION_TYPE, receiverExpression]
@@ -55,11 +60,11 @@ public object TipsManager{
val dataFlowInfo = context.getDataFlowInfo(expression) val dataFlowInfo = context.getDataFlowInfo(expression)
for (variant in AutoCastUtils.getAutoCastVariants(receiverValue, context, dataFlowInfo)) { for (variant in AutoCastUtils.getAutoCastVariants(receiverValue, context, dataFlowInfo)) {
descriptors.addAll(variant.getMemberScope().getAllDescriptors()) variant.getMemberScope().getAllDescriptors().filterTo(descriptors, ::filterIfInfix)
} }
JetScopeUtils.getAllExtensions(resolutionScope).filterTo(descriptors) { JetScopeUtils.getAllExtensions(resolutionScope).filterTo(descriptors) {
ExpressionTypingUtils.checkIsExtensionCallable(receiverValue, it, context, dataFlowInfo) ExpressionTypingUtils.checkIsExtensionCallable(receiverValue, it, isInfixCall, context, dataFlowInfo)
} }
} }
@@ -90,16 +95,21 @@ public object TipsManager{
return excludeNonPackageDescriptors(resolutionScope.getAllDescriptors()) return excludeNonPackageDescriptors(resolutionScope.getAllDescriptors())
} }
public fun excludeNotCallableExtensions(descriptors: Collection<DeclarationDescriptor>, scope: JetScope, context: BindingContext, dataFlowInfo: DataFlowInfo): Collection<DeclarationDescriptor> { public fun excludeNotCallableExtensions(descriptors: Collection<DeclarationDescriptor>,
scope: JetScope,
context: BindingContext,
dataFlowInfo: DataFlowInfo): Collection<DeclarationDescriptor> {
val set = HashSet(descriptors) val set = HashSet(descriptors)
set.excludeNotCallableExtensions(scope, context, dataFlowInfo) set.excludeNotCallableExtensions(scope, context, dataFlowInfo)
return set return set
} }
private fun MutableSet<DeclarationDescriptor>.excludeNotCallableExtensions(scope: JetScope, context: BindingContext, dataFlowInfo: DataFlowInfo) { private fun MutableSet<DeclarationDescriptor>.excludeNotCallableExtensions(scope: JetScope,
context: BindingContext,
dataFlowInfo: DataFlowInfo) {
val implicitReceivers = scope.getImplicitReceiversHierarchy() val implicitReceivers = scope.getImplicitReceiversHierarchy()
removeAll(JetScopeUtils.getAllExtensions(scope).filter { callable -> removeAll(JetScopeUtils.getAllExtensions(scope).filter { callable ->
implicitReceivers.none { ExpressionTypingUtils.checkIsExtensionCallable(it.getValue(), callable, context, dataFlowInfo) } implicitReceivers.none { ExpressionTypingUtils.checkIsExtensionCallable(it.getValue(), callable, false, context, dataFlowInfo) }
}) })
} }
@@ -36,6 +36,8 @@ import org.jetbrains.jet.lang.types.JetType
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import org.jetbrains.jet.plugin.completion.DeclarationLookupObject import org.jetbrains.jet.plugin.completion.DeclarationLookupObject
import org.jetbrains.jet.lang.descriptors.CallableDescriptor import org.jetbrains.jet.lang.descriptors.CallableDescriptor
import org.jetbrains.jet.lang.psi.JetBinaryExpression
import org.jetbrains.jet.lang.psi.JetSimpleNameExpression
public abstract class JetCallableInsertHandler : BaseDeclarationInsertHandler() { public abstract class JetCallableInsertHandler : BaseDeclarationInsertHandler() {
public override fun handleInsert(context: InsertionContext, item: LookupElement) { public override fun handleInsert(context: InsertionContext, item: LookupElement) {
@@ -97,19 +99,33 @@ public class JetFunctionInsertHandler(val caretPosition : CaretPosition, val lam
psiDocumentManager.commitAllDocuments() psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.getDocument()) psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.getDocument())
if (context.getCompletionChar() == '(') {
context.setAddCompletionChar(false)
}
val startOffset = context.getStartOffset() val startOffset = context.getStartOffset()
val element = context.getFile().findElementAt(startOffset) val element = context.getFile().findElementAt(startOffset)
if (element != null && shouldAddBrackets(element)) { if (element != null) {
addBrackets(context, element) if (PsiTreeUtil.getParentOfType(element, javaClass<JetImportDirective>()) != null) return
val parent = element.getParent()
val grandParent = parent?.getParent()
if (parent is JetSimpleNameExpression && grandParent is JetBinaryExpression && parent == grandParent.getOperationReference()) { // infix call
if (context.getCompletionChar() == ' ') {
context.setAddCompletionChar(false)
}
val tailOffset = context.getTailOffset()
context.getDocument().insertString(tailOffset, " ")
context.getEditor().getCaretModel().moveToOffset(tailOffset + 1)
}
else {
addBrackets(context, element)
}
} }
} }
private fun addBrackets(context : InsertionContext, offsetElement : PsiElement) { private fun addBrackets(context : InsertionContext, offsetElement : PsiElement) {
if (context.getCompletionChar() == '(') { //TODO: more correct behavior related to braces type
context.setAddCompletionChar(false)
}
val offset = context.getTailOffset() val offset = context.getTailOffset()
val document = context.getDocument() val document = context.getDocument()
val completionChar = context.getCompletionChar() val completionChar = context.getCompletionChar()
@@ -1,7 +1,8 @@
fun Int.func(s: String): Int{}
fun test() { fun test() {
val floor = "Floor" val floor = "Floor"
val a = 1/**/f<caret> val a = 1/**/f<caret>
} }
// EXIST: floor // EXIST: func
// EXIST: false
@@ -0,0 +1,33 @@
class C {
fun foo(){}
fun bar(p: Int) {}
fun zoo(p1: Int, p2: Int){}
val prop: Int = 1
}
fun C.xxx() {}
fun C.yyy(p: Int) {}
fun C.zzz(p1: Int, p2: Int) {}
val C.extensionProp: Int get() = 1
fun <A, B> A.and(that: B): Pair<A, B> = Pair(this, that)
fun String.ttt(p: Int) {}
fun f() {
C() <caret>
}
// ABSENT: "foo"
// EXIST: "bar"
// ABSENT: "zoo"
// ABSENT: "prop"
// ABSENT: "xxx"
// EXIST: "yyy"
// ABSENT: "zzz"
// ABSENT: "extensionProp"
// EXIST: "and"
// ABSENT: "ttt"
@@ -0,0 +1,10 @@
package other
import pack.C
fun C.xxx() {}
fun C.yyy(p: Int) {}
fun C.zzz(p1: Int, p2: Int) {}
val C.extensionProp: Int get() = 1
// ALLOW_AST_ACCESS
@@ -0,0 +1,13 @@
package pack
class C
fun f() {
C() <caret>
}
// INVOCATION_COUNT: 2
// ABSENT: "xxx"
// EXIST: "yyy"
// ABSENT: "zzz"
// ABSENT: "extensionProp"
@@ -0,0 +1,5 @@
fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
fun foo() {
val pair = 1 to<caret>
}
@@ -0,0 +1,5 @@
fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
fun foo() {
val pair = 1 to <caret>
}
@@ -0,0 +1,5 @@
fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
fun foo() {
val pair = 1 to<caret>
}
@@ -0,0 +1,5 @@
fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
fun foo() {
val pair = 1 to <caret>
}
@@ -0,0 +1,3 @@
fun foo() {
val pair = 1 makeP<caret>
}
@@ -0,0 +1,3 @@
package other
fun <A, B> A.makePair(that: B): Pair<A, B> = Pair(this, that)
@@ -0,0 +1,5 @@
import other.makePair
fun foo() {
val pair = 1 makePair <caret>
}
@@ -30,6 +30,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.InTextDirectivesUtils; import org.jetbrains.jet.InTextDirectivesUtils;
import org.jetbrains.jet.plugin.project.TargetPlatform; import org.jetbrains.jet.plugin.project.TargetPlatform;
import org.jetbrains.jet.plugin.stubs.AstAccessControl;
import org.junit.Assert; import org.junit.Assert;
import java.util.*; import java.util.*;
@@ -126,7 +127,8 @@ public class ExpectedCompletionUtils {
NUMBER_JS_LINE_PREFIX, NUMBER_JS_LINE_PREFIX,
NUMBER_JAVA_LINE_PREFIX, NUMBER_JAVA_LINE_PREFIX,
INVOCATION_COUNT_PREFIX, INVOCATION_COUNT_PREFIX,
WITH_ORDER_PREFIX); WITH_ORDER_PREFIX,
AstAccessControl.INSTANCE$.getALLOW_AST_ACCESS_DIRECTIVE());
@NotNull @NotNull
public static CompletionProposal[] itemsShouldExist(String fileText, @Nullable TargetPlatform platform) { public static CompletionProposal[] itemsShouldExist(String fileText, @Nullable TargetPlatform platform) {
@@ -378,6 +378,12 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes
doTest(fileName); doTest(fileName);
} }
@TestMetadata("InfixCall.kt")
public void testInfixCall() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/InfixCall.kt");
doTest(fileName);
}
@TestMetadata("JavaPackage.kt") @TestMetadata("JavaPackage.kt")
public void testJavaPackage() throws Exception { public void testJavaPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/JavaPackage.kt"); String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/JavaPackage.kt");
@@ -378,6 +378,12 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT
doTest(fileName); doTest(fileName);
} }
@TestMetadata("InfixCall.kt")
public void testInfixCall() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/InfixCall.kt");
doTest(fileName);
}
@TestMetadata("JavaPackage.kt") @TestMetadata("JavaPackage.kt")
public void testJavaPackage() throws Exception { public void testJavaPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/JavaPackage.kt"); String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/JavaPackage.kt");
@@ -17,9 +17,13 @@
package org.jetbrains.jet.completion; package org.jetbrains.jet.completion;
import com.intellij.testFramework.TestDataPath; import com.intellij.testFramework.TestDataPath;
import org.jetbrains.jet.JetTestUtils; import junit.framework.Test;
import org.jetbrains.jet.test.TestMetadata; import junit.framework.TestSuite;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.jetbrains.jet.JetTestUtils;
import org.jetbrains.jet.test.InnerTestClasses;
import org.jetbrains.jet.test.TestMetadata;
import org.jetbrains.jet.JUnit3RunnerWithInners;
import java.io.File; import java.io.File;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -17,13 +17,9 @@
package org.jetbrains.jet.completion; package org.jetbrains.jet.completion;
import com.intellij.testFramework.TestDataPath; import com.intellij.testFramework.TestDataPath;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.junit.runner.RunWith;
import org.jetbrains.jet.JetTestUtils; import org.jetbrains.jet.JetTestUtils;
import org.jetbrains.jet.test.InnerTestClasses;
import org.jetbrains.jet.test.TestMetadata; import org.jetbrains.jet.test.TestMetadata;
import org.jetbrains.jet.JUnit3RunnerWithInners; import org.junit.runner.RunWith;
import java.io.File; import java.io.File;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -134,6 +130,12 @@ public class MultiFileJvmBasicCompletionTestGenerated extends AbstractMultiFileJ
doTest(fileName); doTest(fileName);
} }
@TestMetadata("NotImportedInfixExtension")
public void testNotImportedInfixExtension() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/multifile/NotImportedInfixExtension/");
doTest(fileName);
}
@TestMetadata("NotImportedJavaClass") @TestMetadata("NotImportedJavaClass")
public void testNotImportedJavaClass() throws Exception { public void testNotImportedJavaClass() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/multifile/NotImportedJavaClass/"); String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/multifile/NotImportedJavaClass/");
@@ -150,4 +150,7 @@ public class BasicCompletionHandlerTest : CompletionHandlerTestBase(){
fun testKeywordClassName() = doTest(1, "class", null, '\n') fun testKeywordClassName() = doTest(1, "class", null, '\n')
fun testKeywordFunctionName() = doTest(1, "fun", "fun()", null, '\n') fun testKeywordFunctionName() = doTest(1, "fun", "fun()", null, '\n')
fun testInfixCall() = doTest(1, "to", null, null, '\n')
fun testInfixCallOnSpace() = doTest(1, "to", null, null, ' ')
} }
@@ -58,6 +58,10 @@ public class CompletionMultifileHandlerTest extends KotlinCompletionTestCase {
doTest(); doTest();
} }
public void testInfixExtensionCallImport() throws Exception {
doTest();
}
public void doTest() throws Exception { public void doTest() throws Exception {
String fileName = getTestName(false); String fileName = getTestName(false);
@@ -23,15 +23,12 @@ import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.jet.plugin.JetFileType import org.jetbrains.jet.plugin.JetFileType
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VfsUtilCore
import org.jetbrains.jet.InTextDirectivesUtils import org.jetbrains.jet.InTextDirectivesUtils
import junit.framework.TestCase
import kotlin.test.fail import kotlin.test.fail
object AstAccessControl { public object AstAccessControl {
private val ALLOW_AST_ACCESS_DIRECTIVE = "ALLOW_AST_ACCESS" public val ALLOW_AST_ACCESS_DIRECTIVE: String = "ALLOW_AST_ACCESS"
// Please provide at least one test that fails ast switch check (shouldFail should be true for at least one test) // Please provide at least one test that fails ast switch check (shouldFail should be true for at least one test)
// This kind of inconvenience is justified by the fact that the check can be invalidated by slight misconfiguration of the test // This kind of inconvenience is justified by the fact that the check can be invalidated by slight misconfiguration of the test