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 (
@NotNull ReceiverValue receiverArgument,
@NotNull CallableDescriptor callableDescriptor,
boolean isInfixCall,
@NotNull BindingContext bindingContext,
@NotNull DataFlowInfo dataFlowInfo
) {
if (isInfixCall
&& (!(callableDescriptor instanceof SimpleFunctionDescriptor) || callableDescriptor.getValueParameters().size() != 1)) {
return false;
}
List<JetType> types = AutoCastUtils.getAutoCastVariants(receiverArgument, bindingContext, dataFlowInfo);
for (JetType type : types) {
@@ -133,6 +133,7 @@ public class KotlinIndicesHelper(private val project: Project) {
scope: GlobalSearchScope): Collection<CallableDescriptor> {
val context = resolveSession.resolveToElement(expression)
val receiverExpression = expression.getReceiverExpression() ?: return listOf()
val isInfixCall = expression.getParent() is JetBinaryExpression
val expressionType = context.get<JetExpression, JetType>(BindingContext.EXPRESSION_TYPE, receiverExpression)
val jetScope = context.get(BindingContext.RESOLUTION_SCOPE, receiverExpression)
@@ -147,7 +148,7 @@ public class KotlinIndicesHelper(private val project: Project) {
return allFqNames
.filter { nameFilter(it.shortName().asString()) }
.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,
receiverExpression: JetExpression,
receiverType: JetType,
isInfixCall: Boolean,
scope: JetScope,
module: ModuleDescriptor,
bindingContext: BindingContext): List<CallableDescriptor> {
@@ -167,7 +169,7 @@ public class KotlinIndicesHelper(private val project: Project) {
return declarationDescriptors
.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> {
@@ -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.JetScopeUtils
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 java.util.*
@@ -38,15 +37,21 @@ public object TipsManager{
val parent = expression.getParent()
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) {
val isInfixCall = parent is JetBinaryExpression
fun filterIfInfix(descriptor: DeclarationDescriptor)
= if (isInfixCall) descriptor is SimpleFunctionDescriptor && descriptor.getValueParameters().size == 1 else true
// Process as call expression
val descriptors = HashSet<DeclarationDescriptor>()
val qualifier = context[BindingContext.QUALIFIER, receiverExpression]
if (qualifier != 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]
@@ -55,11 +60,11 @@ public object TipsManager{
val dataFlowInfo = context.getDataFlowInfo(expression)
for (variant in AutoCastUtils.getAutoCastVariants(receiverValue, context, dataFlowInfo)) {
descriptors.addAll(variant.getMemberScope().getAllDescriptors())
variant.getMemberScope().getAllDescriptors().filterTo(descriptors, ::filterIfInfix)
}
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())
}
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)
set.excludeNotCallableExtensions(scope, context, dataFlowInfo)
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()
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 org.jetbrains.jet.plugin.completion.DeclarationLookupObject
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 override fun handleInsert(context: InsertionContext, item: LookupElement) {
@@ -97,19 +99,33 @@ public class JetFunctionInsertHandler(val caretPosition : CaretPosition, val lam
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.getDocument())
if (context.getCompletionChar() == '(') {
context.setAddCompletionChar(false)
}
val startOffset = context.getStartOffset()
val element = context.getFile().findElementAt(startOffset)
if (element != null && shouldAddBrackets(element)) {
addBrackets(context, element)
if (element != null) {
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) {
if (context.getCompletionChar() == '(') { //TODO: more correct behavior related to braces type
context.setAddCompletionChar(false)
}
val offset = context.getTailOffset()
val document = context.getDocument()
val completionChar = context.getCompletionChar()
@@ -1,7 +1,8 @@
fun Int.func(s: String): Int{}
fun test() {
val floor = "Floor"
val a = 1/**/f<caret>
}
// EXIST: floor
// EXIST: false
// EXIST: func
@@ -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.jet.InTextDirectivesUtils;
import org.jetbrains.jet.plugin.project.TargetPlatform;
import org.jetbrains.jet.plugin.stubs.AstAccessControl;
import org.junit.Assert;
import java.util.*;
@@ -126,7 +127,8 @@ public class ExpectedCompletionUtils {
NUMBER_JS_LINE_PREFIX,
NUMBER_JAVA_LINE_PREFIX,
INVOCATION_COUNT_PREFIX,
WITH_ORDER_PREFIX);
WITH_ORDER_PREFIX,
AstAccessControl.INSTANCE$.getALLOW_AST_ACCESS_DIRECTIVE());
@NotNull
public static CompletionProposal[] itemsShouldExist(String fileText, @Nullable TargetPlatform platform) {
@@ -378,6 +378,12 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes
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")
public void testJavaPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/JavaPackage.kt");
@@ -378,6 +378,12 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT
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")
public void testJavaPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/JavaPackage.kt");
@@ -17,9 +17,13 @@
package org.jetbrains.jet.completion;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.jet.JetTestUtils;
import org.jetbrains.jet.test.TestMetadata;
import junit.framework.Test;
import junit.framework.TestSuite;
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.util.regex.Pattern;
@@ -17,13 +17,9 @@
package org.jetbrains.jet.completion;
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.test.InnerTestClasses;
import org.jetbrains.jet.test.TestMetadata;
import org.jetbrains.jet.JUnit3RunnerWithInners;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
@@ -134,6 +130,12 @@ public class MultiFileJvmBasicCompletionTestGenerated extends AbstractMultiFileJ
doTest(fileName);
}
@TestMetadata("NotImportedInfixExtension")
public void testNotImportedInfixExtension() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/multifile/NotImportedInfixExtension/");
doTest(fileName);
}
@TestMetadata("NotImportedJavaClass")
public void testNotImportedJavaClass() throws Exception {
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 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();
}
public void testInfixExtensionCallImport() throws Exception {
doTest();
}
public void doTest() throws Exception {
String fileName = getTestName(false);
@@ -23,15 +23,12 @@ import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.jet.plugin.JetFileType
import com.intellij.openapi.project.Project
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 org.jetbrains.jet.InTextDirectivesUtils
import junit.framework.TestCase
import kotlin.test.fail
object AstAccessControl {
private val ALLOW_AST_ACCESS_DIRECTIVE = "ALLOW_AST_ACCESS"
public object AstAccessControl {
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)
// This kind of inconvenience is justified by the fact that the check can be invalidated by slight misconfiguration of the test