From 41226d7f4b1811dba5f9c1481930818f446d2993 Mon Sep 17 00:00:00 2001 From: Andrey Vlasovskikh Date: Mon, 15 Dec 2014 17:08:54 +0300 Subject: [PATCH] Use separate type eval context for code completion We used to have context.allowLocalUsages() for permitting a code analysis to guess types from file-local function calls. This analysis mode came into conflict with the new structural types, since they both provided types for function parameters. Another problem is that the user used to see the types inferred from local calls in the documentation pop-up and it was not clear where did they come from. The solution is to introduce a new type eval context for code completion only named TypeEvalContext.codeCompletion() and allow file-local call based type inference only in this context. --- .../python/psi/types/TypeEvalConstraints.java | 9 ++++-- .../python/psi/types/TypeEvalContext.java | 32 ++++++++++++------- .../PyDictKeyNamesCompletionContributor.java | 2 +- .../python/psi/impl/PyNamedParameterImpl.java | 4 +-- .../KeywordArgumentCompletionUtil.java | 2 +- .../impl/references/PyImportReference.java | 3 +- .../impl/references/PyQualifiedReference.java | 2 +- .../resolve/CompletionVariantsProcessor.java | 2 +- .../python/psi/types/PyClassTypeImpl.java | 2 +- .../python/psi/types/PyFunctionType.java | 2 +- .../com/jetbrains/python/PyTypeTest.java | 21 ++++++++---- 11 files changed, 52 insertions(+), 29 deletions(-) diff --git a/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalConstraints.java b/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalConstraints.java index ebeaf366a6ad..f3a94b336de2 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalConstraints.java +++ b/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalConstraints.java @@ -30,14 +30,17 @@ import org.jetbrains.annotations.Nullable; class TypeEvalConstraints { final boolean myAllowDataFlow; final boolean myAllowStubToAST; + final boolean myAllowCallContext; @Nullable final PsiFile myOrigin; /** * @see com.jetbrains.python.psi.types.TypeEvalContext */ - TypeEvalConstraints(final boolean allowDataFlow, final boolean allowStubToAST, @Nullable final PsiFile origin) { + TypeEvalConstraints(final boolean allowDataFlow, final boolean allowStubToAST, final boolean allowCallContext, + @Nullable final PsiFile origin) { myAllowDataFlow = allowDataFlow; myAllowStubToAST = allowStubToAST; + myAllowCallContext = allowCallContext; myOrigin = origin; } @@ -50,6 +53,7 @@ class TypeEvalConstraints { if (myAllowDataFlow != that.myAllowDataFlow) return false; if (myAllowStubToAST != that.myAllowStubToAST) return false; + if (myAllowCallContext != that.myAllowCallContext) return false; if (myOrigin != null ? !myOrigin.equals(that.myOrigin) : that.myOrigin != null) return false; return true; @@ -60,11 +64,12 @@ class TypeEvalConstraints { int result = (myAllowDataFlow ? 1 : 0); result = 31 * result + (myAllowStubToAST ? 1 : 0); result = 31 * result + (myOrigin != null ? myOrigin.hashCode() : 0); + result = 31 * result + (myAllowCallContext ? 1 : 0); return result; } @Override public String toString() { - return String.format("TypeEvalConstraints(%b, %b, %s)", myAllowDataFlow, myAllowStubToAST, myOrigin); + return String.format("TypeEvalConstraints(%b, %b, %b, %s)", myAllowDataFlow, myAllowStubToAST, myAllowCallContext, myOrigin); } } diff --git a/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalContext.java b/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalContext.java index 002a566eeb39..54af2a23e3c4 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalContext.java +++ b/python/psi-api/src/com/jetbrains/python/psi/types/TypeEvalContext.java @@ -65,8 +65,8 @@ public class TypeEvalContext { } }; - private TypeEvalContext(boolean allowDataFlow, boolean allowStubToAST, @Nullable PsiFile origin) { - myConstraints = new TypeEvalConstraints(allowDataFlow, allowStubToAST, origin); + private TypeEvalContext(boolean allowDataFlow, boolean allowStubToAST, boolean allowCallContext, @Nullable PsiFile origin) { + myConstraints = new TypeEvalConstraints(allowDataFlow, allowStubToAST, allowCallContext, origin); } @Override @@ -83,17 +83,29 @@ public class TypeEvalContext { return myConstraints.myAllowDataFlow || element.getContainingFile() == myConstraints.myOrigin; } - public boolean allowLocalUsages(@NotNull PsiElement element) { - return myConstraints.myAllowStubToAST && myConstraints.myAllowDataFlow && element.getContainingFile() == myConstraints.myOrigin; + public boolean allowCallContext(@NotNull PsiElement element) { + return myConstraints.myAllowCallContext && element.getContainingFile() == myConstraints.myOrigin; + } + + /** + * Create a context for code completion. + *

+ * It is as detailed as {@link TypeEvalContext#userInitiated(Project, PsiFile)}, but allows inferring types based on the context in which + * the analyzed code was called or may be called. Since this is basically guesswork, the results should be used only for code completion. + */ + public static TypeEvalContext codeCompletion(@NotNull final Project project, @Nullable final PsiFile origin) { + return CACHE.getContext(project, new TypeEvalContext(true, true, true, origin)); } /** * Create the most detailed type evaluation context for user-initiated actions. *

- * Should be used for code completion, go to definition, find usages, refactorings, documentation. + * Should be used go to definition, find usages, refactorings, documentation. + *

+ * For code completion see {@link TypeEvalContext#codeCompletion(Project, PsiFile)}. */ public static TypeEvalContext userInitiated(@NotNull final Project project, @Nullable final PsiFile origin) { - return CACHE.getContext(project, new TypeEvalContext(true, true, origin)); + return CACHE.getContext(project, new TypeEvalContext(true, true, false, origin)); } /** @@ -103,20 +115,18 @@ public class TypeEvalContext { * Inspections should not create a new type evaluation context. They should re-use the context of the inspection session. */ public static TypeEvalContext codeAnalysis(@NotNull final Project project, @Nullable final PsiFile origin) { - return CACHE.getContext(project, new TypeEvalContext(false, false, origin)); + return CACHE.getContext(project, new TypeEvalContext(false, false, false, origin)); } /** * Create the most shallow type evaluation context for code insight purposes when other more detailed contexts are not available. * It's use should be minimized. - *

- *

* * @param project pass project here to enable cache. Pass null if you do not have any project. * Always do your best to pass project here: it increases performance! */ public static TypeEvalContext codeInsightFallback(@Nullable final Project project) { - final TypeEvalContext anchor = new TypeEvalContext(false, false, null); + final TypeEvalContext anchor = new TypeEvalContext(false, false, false, null); if (project != null) { return CACHE.getContext(project, anchor); } @@ -129,7 +139,7 @@ public class TypeEvalContext { * Should be used only when normal code insight context is not enough for getting good results. */ public static TypeEvalContext deepCodeInsight(@NotNull final Project project) { - return CACHE.getContext(project, new TypeEvalContext(false, true, null)); + return CACHE.getContext(project, new TypeEvalContext(false, true, false, null)); } public TypeEvalContext withTracing() { diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyDictKeyNamesCompletionContributor.java b/python/src/com/jetbrains/python/codeInsight/completion/PyDictKeyNamesCompletionContributor.java index e5b2dcc2dc65..efdb192ae6f4 100644 --- a/python/src/com/jetbrains/python/codeInsight/completion/PyDictKeyNamesCompletionContributor.java +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyDictKeyNamesCompletionContributor.java @@ -129,7 +129,7 @@ public class PyDictKeyNamesCompletionContributor extends CompletionContributor { if (callee == null) return; final String name = callee.getText(); if ("dict".equals(name)) { - final TypeEvalContext context = TypeEvalContext.userInitiated(callee.getProject(), callee.getContainingFile()); + final TypeEvalContext context = TypeEvalContext.codeCompletion(callee.getProject(), callee.getContainingFile()); final PyType type = context.getType(dictConstructor); if (type != null && type.isBuiltin()) { final PyArgumentList list = dictConstructor.getArgumentList(); diff --git a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java index 222a98df8a5f..921c43e2b866 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java @@ -244,8 +244,8 @@ public class PyNamedParameterImpl extends PyBaseElementImpl types = new ArrayList(); processLocalCalls(func, new Processor() { @Override diff --git a/python/src/com/jetbrains/python/psi/impl/references/KeywordArgumentCompletionUtil.java b/python/src/com/jetbrains/python/psi/impl/references/KeywordArgumentCompletionUtil.java index e6edd70a516e..8736e54766ac 100644 --- a/python/src/com/jetbrains/python/psi/impl/references/KeywordArgumentCompletionUtil.java +++ b/python/src/com/jetbrains/python/psi/impl/references/KeywordArgumentCompletionUtil.java @@ -66,7 +66,7 @@ public class KeywordArgumentCompletionUtil { visited.add(def); boolean needSelf = def.getContainingClass() != null && def.getModifier() != PyFunction.Modifier.STATICMETHOD; final KwArgParameterCollector collector = new KwArgParameterCollector(needSelf, ret); - final TypeEvalContext context = TypeEvalContext.userInitiated(def.getProject(), def.getContainingFile()); + final TypeEvalContext context = TypeEvalContext.codeCompletion(def.getProject(), def.getContainingFile()); final List parameters = PyUtil.getParameters(def, context); for (PyParameter parameter : parameters) { parameter.accept(collector); diff --git a/python/src/com/jetbrains/python/psi/impl/references/PyImportReference.java b/python/src/com/jetbrains/python/psi/impl/references/PyImportReference.java index be7921bd6b22..13100e9c0600 100644 --- a/python/src/com/jetbrains/python/psi/impl/references/PyImportReference.java +++ b/python/src/com/jetbrains/python/psi/impl/references/PyImportReference.java @@ -97,7 +97,8 @@ public class PyImportReference extends PyReferenceImpl { } PyExpression qualifier = myElement.getQualifier(); - final TypeEvalContext context = TypeEvalContext.userInitiated(myElement.getProject(), CompletionUtil.getOriginalOrSelf(myElement).getContainingFile()); + final TypeEvalContext context = TypeEvalContext.codeCompletion(myElement.getProject(), + CompletionUtil.getOriginalOrSelf(myElement).getContainingFile()); if (qualifier != null) { // qualifier's type must be module, it should know how to complete PyType type = context.getType(qualifier); diff --git a/python/src/com/jetbrains/python/psi/impl/references/PyQualifiedReference.java b/python/src/com/jetbrains/python/psi/impl/references/PyQualifiedReference.java index 65b15fad3458..070ee1165c36 100644 --- a/python/src/com/jetbrains/python/psi/impl/references/PyQualifiedReference.java +++ b/python/src/com/jetbrains/python/psi/impl/references/PyQualifiedReference.java @@ -264,7 +264,7 @@ public class PyQualifiedReference extends PyReferenceImpl { } final PyQualifiedExpression element = CompletionUtil.getOriginalOrSelf(myElement); - PyType qualifierType = TypeEvalContext.userInitiated(element.getProject(), element.getContainingFile()).getType(qualifier); + PyType qualifierType = TypeEvalContext.codeCompletion(element.getProject(), element.getContainingFile()).getType(qualifier); ProcessingContext ctx = new ProcessingContext(); final Set namesAlready = new HashSet(); ctx.put(PyType.CTX_NAMES, namesAlready); diff --git a/python/src/com/jetbrains/python/psi/resolve/CompletionVariantsProcessor.java b/python/src/com/jetbrains/python/psi/resolve/CompletionVariantsProcessor.java index ddc8f939970b..7f8799fde597 100644 --- a/python/src/com/jetbrains/python/psi/resolve/CompletionVariantsProcessor.java +++ b/python/src/com/jetbrains/python/psi/resolve/CompletionVariantsProcessor.java @@ -68,7 +68,7 @@ public class CompletionVariantsProcessor extends VariantsProcessor { !isSingleArgDecoratorCall(myContext, (PyFunction)object)) { final Project project = ((PyFunction)object).getProject(); item = item.withInsertHandler(PyFunctionInsertHandler.INSTANCE); - final TypeEvalContext context = TypeEvalContext.userInitiated(project, myContext != null ? myContext.getContainingFile() : null); + final TypeEvalContext context = TypeEvalContext.codeCompletion(project, myContext != null ? myContext.getContainingFile() : null); final List parameters = PyUtil.getParameters((PyFunction)object, context); final String params = StringUtil.join(parameters, new Function() { @Override diff --git a/python/src/com/jetbrains/python/psi/types/PyClassTypeImpl.java b/python/src/com/jetbrains/python/psi/types/PyClassTypeImpl.java index 25de66311ee1..a7b0d548ea23 100644 --- a/python/src/com/jetbrains/python/psi/types/PyClassTypeImpl.java +++ b/python/src/com/jetbrains/python/psi/types/PyClassTypeImpl.java @@ -418,7 +418,7 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType { CompletionUtil.getOriginalOrSelf(location) .getContainingFile() : null; - final TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(myClass.getProject(), origin); + final TypeEvalContext typeEvalContext = TypeEvalContext.codeCompletion(myClass.getProject(), origin); addInheritedMembers(prefix, location, namesAlready, context, ret, typeEvalContext); // from providers diff --git a/python/src/com/jetbrains/python/psi/types/PyFunctionType.java b/python/src/com/jetbrains/python/psi/types/PyFunctionType.java index c393dfb219b1..fde4e18fabe4 100644 --- a/python/src/com/jetbrains/python/psi/types/PyFunctionType.java +++ b/python/src/com/jetbrains/python/psi/types/PyFunctionType.java @@ -88,7 +88,7 @@ public class PyFunctionType implements PyCallableType { @Override public Object[] getCompletionVariants(String completionPrefix, PsiElement location, ProcessingContext context) { - final TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(location.getProject(), location.getContainingFile()); + final TypeEvalContext typeEvalContext = TypeEvalContext.codeCompletion(location.getProject(), location.getContainingFile()); final PyClassType delegate; if (location instanceof PyReferenceExpression) { delegate = selectFakeType(((PyReferenceExpression)location).getQualifier(), typeEvalContext); diff --git a/python/testSrc/com/jetbrains/python/PyTypeTest.java b/python/testSrc/com/jetbrains/python/PyTypeTest.java index 8a3b9ae9c934..6d7737bc31d4 100644 --- a/python/testSrc/com/jetbrains/python/PyTypeTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypeTest.java @@ -589,13 +589,14 @@ public class PyTypeTest extends PyTestCase { } public void testParameterFromUsages() { - doTest("int | str | unknown", - "def foo(bar):\n" + - " expr = bar\n" + - "def use_foo(x):\n" + - " foo(x)\n" + - " foo(3)\n" + - " foo('bar')\n"); + final String text = "def foo(bar):\n" + + " expr = bar\n" + + "def use_foo(x):\n" + + " foo(x)\n" + + " foo(3)\n" + + " foo('bar')\n"; + final PyExpression expr = parseExpr(text); + doTest("int | str | unknown", expr, TypeEvalContext.codeCompletion(expr.getProject(), expr.getContainingFile())); } public void testUpperBoundGeneric() { @@ -995,6 +996,12 @@ public class PyTypeTest extends PyTestCase { return myFixture.findElementByText("expr", PyExpression.class); } + private static void doTest(final String expectedType, final PyExpression expr, final TypeEvalContext context) { + PyType actual = context.getType(expr); + final String actualType = PythonDocumentationProvider.getTypeName(actual, context); + assertEquals(expectedType, actualType); + } + private void doTest(final String expectedType, final String text) { PyExpression expr = parseExpr(text); TypeEvalContext context = getTypeEvalContext(expr);