diff --git a/python/psi-api/src/com/jetbrains/python/PyNames.java b/python/psi-api/src/com/jetbrains/python/PyNames.java index b82576428c3c..21e5be632d59 100644 --- a/python/psi-api/src/com/jetbrains/python/PyNames.java +++ b/python/psi-api/src/com/jetbrains/python/PyNames.java @@ -472,7 +472,7 @@ public class PyNames { } public static boolean isRightOperatorName(@Nullable String name) { - return name != null && name.matches("__r[a-z]+__"); + return name != null && (name.matches("__r[a-z]+__") || CONTAINS.equals(name)); } /** 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/inspections/PyTypeCheckerInspection.java b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java index 389e4efd15f3..50230b05c795 100644 --- a/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java @@ -15,13 +15,16 @@ */ package com.jetbrains.python.inspections; +import com.google.common.collect.Sets; import com.intellij.codeInspection.LocalInspectionToolSession; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; +import com.intellij.util.Function; import com.intellij.util.containers.hash.LinkedHashMap; import com.jetbrains.python.PyNames; import com.jetbrains.python.documentation.PythonDocumentationProvider; @@ -32,6 +35,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; +import java.util.Set; /** * @author vlan @@ -108,26 +112,45 @@ public class PyTypeCheckerInspection extends PyInspection { } @Nullable - private String checkTypes(@Nullable PyType superType, @Nullable PyType subType, @Nullable PsiElement node, + private String checkTypes(@Nullable PyType expected, @Nullable PyType actual, @Nullable PsiElement node, @NotNull TypeEvalContext context, @NotNull Map substitutions) { - if (subType != null && superType != null) { - if (!PyTypeChecker.match(superType, subType, context, substitutions)) { - final String superName = PythonDocumentationProvider.getTypeName(superType, context); - String expected = String.format("'%s'", superName); - final boolean hasGenerics = PyTypeChecker.hasGenerics(superType, context); + if (actual != null && expected != null) { + if (!PyTypeChecker.match(expected, actual, context, substitutions)) { + final String expectedName = PythonDocumentationProvider.getTypeName(expected, context); + String quotedExpectedName = String.format("'%s'", expectedName); + final boolean hasGenerics = PyTypeChecker.hasGenerics(expected, context); ProblemHighlightType highlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING; if (hasGenerics) { - final PyType subst = PyTypeChecker.substitute(superType, substitutions, context); - if (subst != null) { - expected = String.format("'%s' (matched generic type '%s')", - PythonDocumentationProvider.getTypeName(subst, context), - superName); + final PyType substitute = PyTypeChecker.substitute(expected, substitutions, context); + if (substitute != null) { + quotedExpectedName = String.format("'%s' (matched generic type '%s')", + PythonDocumentationProvider.getTypeName(substitute, context), + expectedName); highlightType = ProblemHighlightType.WEAK_WARNING; } } - final String msg = String.format("Expected type %s, got '%s' instead", - expected, - PythonDocumentationProvider.getTypeName(subType, context)); + final String actualName = PythonDocumentationProvider.getTypeName(actual, context); + String msg= String.format("Expected type %s, got '%s' instead", quotedExpectedName, actualName); + if (expected instanceof PyStructuralType) { + final Set expectedAttributes = ((PyStructuralType)expected).getAttributeNames(); + final Set actualAttributes = getAttributes(actual); + if (actualAttributes != null) { + final Sets.SetView missingAttributes = Sets.difference(expectedAttributes, actualAttributes); + if (missingAttributes.size() == 1) { + msg = String.format("Type '%s' doesn't have expected attribute '%s'", actualName, missingAttributes.iterator().next()); + } + else { + msg = String.format("Type '%s' doesn't have expected attributes %s", + actualName, + StringUtil.join(missingAttributes, new Function() { + @Override + public String fun(String s) { + return String.format("'%s'", s); + } + }, ", ")); + } + } + } registerProblem(node, msg, highlightType); return msg; } @@ -136,6 +159,17 @@ public class PyTypeCheckerInspection extends PyInspection { } } + @Nullable + private static Set getAttributes(@NotNull PyType type) { + if (type instanceof PyStructuralType) { + return ((PyStructuralType)type).getAttributeNames(); + } + else if (type instanceof PyClassType) { + return PyTypeChecker.getClassAttributes(((PyClassType)type).getPyClass(), true); + } + return null; + } + @Override public void inspectionFinished(@NotNull LocalInspectionToolSession session, @NotNull ProblemsHolder problemsHolder) { if (LOG.isDebugEnabled()) { diff --git a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java index 43bc2a925c1c..781ad21bb88b 100644 --- a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java @@ -64,7 +64,6 @@ import com.jetbrains.python.psi.impl.references.PyOperatorReference; import com.jetbrains.python.psi.resolve.ImportedResolveResult; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.resolve.QualifiedNameFinder; -import com.jetbrains.python.psi.resolve.RatedResolveResult; import com.jetbrains.python.psi.types.*; import com.jetbrains.python.sdk.PythonSdkType; import com.jetbrains.python.sdk.skeletons.PySkeletonRefresher; @@ -705,6 +704,9 @@ public class PyUnresolvedReferencesInspection extends PyInspection { // this almost always means that we don't know the type, so don't show an error in this case return true; } + if (type instanceof PyStructuralType && ((PyStructuralType)type).isInferredFromUsages()) { + return true; + } if (type instanceof PyImportedModuleType) { PyImportedModule module = ((PyImportedModuleType)type).getImportedModule(); if (module.resolve() == null) { @@ -720,7 +722,7 @@ public class PyUnresolvedReferencesInspection extends PyInspection { } if (type instanceof PyClassTypeImpl) { PyClass cls = ((PyClassType)type).getPyClass(); - if (overridesGetAttr(cls, myTypeEvalContext)) { + if (PyTypeChecker.overridesGetAttr(cls, myTypeEvalContext)) { return true; } if (cls.findProperty(name, true) != null) { @@ -910,31 +912,6 @@ public class PyUnresolvedReferencesInspection extends PyInspection { return callExpression != null && node == callExpression.getCallee(); } - @Nullable - private static PsiElement resolveClassMember(@NotNull PyClass cls, @NotNull String name, @NotNull TypeEvalContext context) { - final PyType type = context.getType(cls); - if (type != null) { - final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); - final List results = type.resolveMember(name, null, AccessDirection.READ, resolveContext); - if (results != null && !results.isEmpty()) { - return results.get(0).getElement(); - } - } - return null; - } - - private static boolean overridesGetAttr(@NotNull PyClass cls, @NotNull TypeEvalContext context) { - PsiElement method = resolveClassMember(cls, PyNames.GETATTR, context); - if (method != null) { - return true; - } - method = resolveClassMember(cls, PyNames.GETATTRIBUTE, context); - if (method != null && !PyBuiltinCache.getInstance(cls).isBuiltin(method)) { - return true; - } - return false; - } - private static void addPluginQuickFixes(PsiReference reference, final List actions) { for (PyUnresolvedReferenceQuickFixProvider provider : Extensions.getExtensions(PyUnresolvedReferenceQuickFixProvider.EP_NAME)) { provider.registerQuickFixes(reference, new Consumer() { diff --git a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java index e43a51f3de36..f5355dc56bd9 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java @@ -42,9 +42,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; /** * @author yole @@ -246,8 +244,8 @@ public class PyNamedParameterImpl extends PyBaseElementImpl types = new ArrayList(); processLocalCalls(func, new Processor() { @Override @@ -276,6 +274,12 @@ public class PyNamedParameterImpl extends PyBaseElementImpl attributes = collectUsedAttributes(context); + if (!attributes.isEmpty()) { + return new PyStructuralType(attributes, true); + } + } } } return null; @@ -286,6 +290,86 @@ public class PyNamedParameterImpl extends PyBaseElementImpl collectUsedAttributes(@NotNull final TypeEvalContext context) { + final Set result = new LinkedHashSet(); + final ScopeOwner owner = ScopeUtil.getScopeOwner(this); + final String name = getName(); + if (owner != null && name != null) { + owner.accept(new PyRecursiveElementVisitor() { + @Override + public void visitPyElement(PyElement node) { + if (node instanceof ScopeOwner && node != owner) { + return; + } + if (node instanceof PyQualifiedExpression) { + final PyQualifiedExpression expr = (PyQualifiedExpression)node; + final PyExpression qualifier = expr.getQualifier(); + if (qualifier != null) { + final String attributeName = expr.getReferencedName(); + final PyExpression referencedExpr = node instanceof PyBinaryExpression && PyNames.isRightOperatorName(attributeName) ? + ((PyBinaryExpression)node).getRightExpression() : qualifier; + if (referencedExpr != null) { + final PsiReference ref = referencedExpr.getReference(); + if (ref != null && ref.isReferenceTo(PyNamedParameterImpl.this)) { + if (attributeName != null && !result.contains(attributeName)) { + result.add(attributeName); + } + } + } + } + else { + final PsiReference ref = expr.getReference(); + if (ref != null && ref.isReferenceTo(PyNamedParameterImpl.this)) { + final PyNamedParameter parameter = getParameterByCallArgument(expr, context); + if (parameter != null) { + final PyType type = context.getType(parameter); + if (type instanceof PyStructuralType) { + result.addAll(((PyStructuralType)type).getAttributeNames()); + } + } + } + } + } + super.visitPyElement(node); + } + + @Override + public void visitPyIfStatement(PyIfStatement node) { + final PyExpression ifCondition = node.getIfPart().getCondition(); + if (ifCondition != null) { + ifCondition.accept(this); + } + for (PyIfPart part : node.getElifParts()) { + final PyExpression elseIfCondition = part.getCondition(); + if (elseIfCondition != null) { + elseIfCondition.accept(this); + } + } + } + }); + } + return result; + } + + @Nullable + private static PyNamedParameter getParameterByCallArgument(@NotNull PsiElement element, @NotNull TypeEvalContext context) { + final PyCallExpression call = PsiTreeUtil.getParentOfType(element, PyCallExpression.class); + if (call != null) { + final PyArgumentList argumentList = call.getArgumentList(); + if (argumentList != null) { + final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); + final CallArgumentsMapping mapping = argumentList.analyzeCall(resolveContext); + for (Map.Entry entry : mapping.getPlainMappedParams().entrySet()) { + if (entry.getKey() == element) { + return entry.getValue(); + } + } + } + } + return null; + } + private static void processLocalCalls(@NotNull PyFunction function, @NotNull Processor processor) { final PsiFile file = function.getContainingFile(); final String name = function.getName(); 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 cc772f32554e..070ee1165c36 100644 --- a/python/src/com/jetbrains/python/psi/impl/references/PyQualifiedReference.java +++ b/python/src/com/jetbrains/python/psi/impl/references/PyQualifiedReference.java @@ -43,7 +43,10 @@ import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyImportedModule; import com.jetbrains.python.psi.impl.ResolveResultList; -import com.jetbrains.python.psi.resolve.*; +import com.jetbrains.python.psi.resolve.ImplicitResolveResult; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.QualifiedNameFinder; +import com.jetbrains.python.psi.resolve.RatedResolveResult; import com.jetbrains.python.psi.search.PyProjectScopeBuilder; import com.jetbrains.python.psi.stubs.PyClassNameIndexInsensitive; import com.jetbrains.python.psi.stubs.PyFunctionNameIndex; @@ -102,7 +105,8 @@ public class PyQualifiedReference extends PyReferenceImpl { } } - if (PyTypeChecker.isUnknown(qualifierType) && + if ((PyTypeChecker.isUnknown(qualifierType) || + (qualifierType instanceof PyStructuralType && ((PyStructuralType)qualifierType).isInferredFromUsages())) && myContext.allowImplicits() && canQualifyAnImplicitName(qualifier, qualifierType)) { addImplicitResolveResults(referencedName, ret); } @@ -260,13 +264,19 @@ 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); + final Collection variants = new ArrayList(); if (qualifierType != null) { - Collection variants = new ArrayList(); Collections.addAll(variants, getVariantFromHasAttr(qualifier)); + if (qualifierType instanceof PyStructuralType && ((PyStructuralType)qualifierType).isInferredFromUsages()) { + final PyClassType guessedType = guessClassTypeByName(); + if (guessedType != null) { + Collections.addAll(variants, getTypeCompletionVariants(myElement, guessedType)); + } + } if (qualifier instanceof PyQualifiedExpression) { Collection attrs = collectAssignedAttributes((PyQualifiedExpression)qualifier); for (PyExpression ex : attrs) { @@ -295,7 +305,16 @@ public class PyQualifiedReference extends PyReferenceImpl { return qualifierType.getCompletionVariants(element.getName(), element, ctx); } } - return getUntypedVariants(); + else { + final PyClassType guessedType = guessClassTypeByName(); + if (guessedType != null) { + Collections.addAll(variants, getTypeCompletionVariants(myElement, guessedType)); + } + if (qualifier instanceof PyReferenceExpression) { + Collections.addAll(variants, collectSeenMembers(qualifier.getText())); + } + return variants.toArray(); + } } private Object[] getVariantFromHasAttr(PyExpression qualifier) { @@ -315,7 +334,8 @@ public class PyQualifiedReference extends PyReferenceImpl { return variants.toArray(); } - private Object[] getUntypedVariants() { + @Nullable + private PyClassType guessClassTypeByName() { final PyExpression qualifierElement = myElement.getQualifier(); if (qualifierElement instanceof PyReferenceExpression) { PyReferenceExpression qualifier = (PyReferenceExpression)qualifierElement; @@ -324,13 +344,11 @@ public class PyQualifiedReference extends PyReferenceImpl { Collection classes = PyClassNameIndexInsensitive.find(className, getElement().getProject()); classes = filterByImports(classes, myElement.getContainingFile()); if (classes.size() == 1) { - final PyClassType classType = new PyClassTypeImpl(classes.iterator().next(), false); - return getTypeCompletionVariants(myElement, classType); + return new PyClassTypeImpl(classes.iterator().next(), false); } } - return collectSeenMembers(qualifier.getText()); } - return ArrayUtil.EMPTY_OBJECT_ARRAY; + return null; } private static Collection filterByImports(Collection classes, PsiFile containingFile) { @@ -358,6 +376,16 @@ public class PyQualifiedReference extends PyReferenceImpl { @Override public void visitPyReferenceExpression(PyReferenceExpression node) { super.visitPyReferenceExpression(node); + visitPyQualifiedExpression(node); + } + + @Override + public void visitPyTargetExpression(PyTargetExpression node) { + super.visitPyTargetExpression(node); + visitPyQualifiedExpression(node); + } + + private void visitPyQualifiedExpression(PyQualifiedExpression node) { if (node != myElement) { final PyExpression qualifier = node.getQualifier(); if (qualifier != null && qualifier.getText().equals(text)) { @@ -373,7 +401,7 @@ public class PyQualifiedReference extends PyReferenceImpl { for (String member : members) { results.add(AutoCompletionPolicy.NEVER_AUTOCOMPLETE.applyPolicy(LookupElementBuilder.create(member))); } - return results.toArray(new Object[results.size()]); + return ArrayUtil.toObjectArray(results); } private static Collection collectAssignedAttributes(PyQualifiedExpression qualifier) { @@ -423,7 +451,8 @@ public class PyQualifiedReference extends PyReferenceImpl { final PyExpression qualifier = myElement.getQualifier(); if (qualifier != null) { final PyType qualifierType = resolveContext.getTypeEvalContext().getType(qualifier); - if (qualifierType == null) { + if (qualifierType == null || + (qualifierType instanceof PyStructuralType && ((PyStructuralType)qualifierType).isInferredFromUsages())) { return true; } } 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/PyABCUtil.java b/python/src/com/jetbrains/python/psi/types/PyABCUtil.java index 562937855dd1..bfc90678a605 100644 --- a/python/src/com/jetbrains/python/psi/types/PyABCUtil.java +++ b/python/src/com/jetbrains/python/psi/types/PyABCUtil.java @@ -84,6 +84,10 @@ public class PyABCUtil { } public static boolean isSubtype(@NotNull PyType type, @NotNull String superClassName, @NotNull TypeEvalContext context) { + if (type instanceof PyStructuralType) { + // TODO: Convert abc types to structural types and check them properly + return true; + } if (type instanceof PyClassType) { final PyClassType classType = (PyClassType)type; final PyClass pyClass = classType.getPyClass(); 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/src/com/jetbrains/python/psi/types/PyStructuralType.java b/python/src/com/jetbrains/python/psi/types/PyStructuralType.java new file mode 100644 index 000000000000..d94de573943a --- /dev/null +++ b/python/src/com/jetbrains/python/psi/types/PyStructuralType.java @@ -0,0 +1,91 @@ +/* + * Copyright 2000-2014 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 com.jetbrains.python.psi.types; + +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.util.PlatformIcons; +import com.intellij.util.ProcessingContext; +import com.jetbrains.python.psi.AccessDirection; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.RatedResolveResult; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * @author vlan + */ +public class PyStructuralType implements PyType { + @NotNull private final Set myAttributes; + private final boolean myInferredFromUsages; + + public PyStructuralType(@NotNull Set attributes, boolean inferredFromUsages) { + myAttributes = attributes; + myInferredFromUsages = inferredFromUsages; + } + + @Nullable + @Override + public List resolveMember(@NotNull String name, + @Nullable PyExpression location, + @NotNull AccessDirection direction, + @NotNull PyResolveContext resolveContext) { + return Collections.emptyList(); + } + + @Override + public Object[] getCompletionVariants(String completionPrefix, PsiElement location, ProcessingContext context) { + final List variants = new ArrayList(); + for (String attribute : myAttributes) { + if (!attribute.equals(completionPrefix)) { + variants.add(LookupElementBuilder.create(attribute).withIcon(PlatformIcons.FIELD_ICON)); + } + } + return variants.toArray(); + } + + @Nullable + @Override + public String getName() { + return "{" + StringUtil.join(myAttributes, ", ") + "}"; + } + + @Override + public boolean isBuiltin() { + return false; + } + + @Override + public void assertValid(String message) { + } + + @Override + public String toString() { + return "PyStructuralType(" + StringUtil.join(myAttributes, ", ") + ")"; + } + + public boolean isInferredFromUsages() { + return myInferredFromUsages; + } + + public Set getAttributeNames() { + return myAttributes; + } +} diff --git a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java index d8bb9246bb41..d5c75466ddd0 100644 --- a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -21,6 +21,7 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.ResolveResult; import com.jetbrains.python.PyNames; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.resolve.RatedResolveResult; import org.jetbrains.annotations.NotNull; @@ -154,6 +155,29 @@ public class PyTypeChecker { return true; } } + if (actual instanceof PyStructuralType && ((PyStructuralType)actual).isInferredFromUsages()) { + return true; + } + if (expected instanceof PyStructuralType && actual instanceof PyStructuralType) { + final PyStructuralType expectedStructural = (PyStructuralType)expected; + final PyStructuralType actualStructural = (PyStructuralType)actual; + if (expectedStructural.isInferredFromUsages()) { + return true; + } + return expectedStructural.getAttributeNames().containsAll(actualStructural.getAttributeNames()); + } + if (expected instanceof PyStructuralType && actual instanceof PyClassType) { + final PyClass cls = ((PyClassType)actual).getPyClass(); + if (overridesGetAttr(cls, context)) { + return true; + } + final Set actualAttributes = getClassAttributes(cls, true); + return actualAttributes.containsAll(((PyStructuralType)expected).getAttributeNames()); + } + if (actual instanceof PyStructuralType && expected instanceof PyClassType) { + final Set expectedAttributes = getClassAttributes(((PyClassType)expected).getPyClass(), true); + return expectedAttributes.containsAll(((PyStructuralType)actual).getAttributeNames()); + } if (actual instanceof PyCallableType && expected instanceof PyCallableType) { final PyCallableType expectedCallable = (PyCallableType)expected; final PyCallableType actualCallable = (PyCallableType)actual; @@ -180,6 +204,26 @@ public class PyTypeChecker { return matchNumericTypes(expected, actual); } + @NotNull + public static Set getClassAttributes(@NotNull PyClass cls, boolean inherited) { + final Set attributes = new HashSet(); + for (PyFunction function : cls.getMethods(false)) { + attributes.add(function.getName()); + } + for (PyTargetExpression instanceAttribute : cls.getInstanceAttributes()) { + attributes.add(instanceAttribute.getName()); + } + for (PyTargetExpression classAttribute : cls.getClassAttributes()) { + attributes.add(classAttribute.getName()); + } + if (inherited) { + for (PyClass ancestor : cls.getAncestorClasses()) { + attributes.addAll(getClassAttributes(ancestor, false)); + } + } + return attributes; + } + private static boolean matchNumericTypes(PyType expected, PyType actual) { final String superName = expected.getName(); final String subName = actual.getName(); @@ -434,7 +478,7 @@ public class PyTypeChecker { } final Callable callable = ((PyFunctionType)type).getCallable(); final String operatorName = typedElement.getName(); - final boolean isRight = PyNames.isRightOperatorName(operatorName) || PyNames.CONTAINS.equals(operatorName); + final boolean isRight = PyNames.isRightOperatorName(operatorName); final PyExpression arg = isRight ? expr.getLeftExpression() : expr.getRightExpression(); final PyExpression receiver = isRight ? expr.getRightExpression() : expr.getLeftExpression(); final PyParameter[] parameters = callable.getParameterList().getParameters(); @@ -521,9 +565,37 @@ public class PyTypeChecker { else if (type instanceof PyCallableType) { return ((PyCallableType) type).isCallable(); } + else if (type instanceof PyStructuralType && ((PyStructuralType)type).isInferredFromUsages()) { + return true; + } return false; } + public static boolean overridesGetAttr(@NotNull PyClass cls, @NotNull TypeEvalContext context) { + PsiElement method = resolveClassMember(cls, PyNames.GETATTR, context); + if (method != null) { + return true; + } + method = resolveClassMember(cls, PyNames.GETATTRIBUTE, context); + if (method != null && !PyBuiltinCache.getInstance(cls).isBuiltin(method)) { + return true; + } + return false; + } + + @Nullable + private static PsiElement resolveClassMember(@NotNull PyClass cls, @NotNull String name, @NotNull TypeEvalContext context) { + final PyType type = context.getType(cls); + if (type != null) { + final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); + final List results = type.resolveMember(name, null, AccessDirection.READ, resolveContext); + if (results != null && !results.isEmpty()) { + return results.get(0).getElement(); + } + } + return null; + } + public static class AnalyzeCallResults { @NotNull private final Callable myCallable; @Nullable private final PyExpression myReceiver; diff --git a/python/testData/completion/seenMembers.py b/python/testData/completion/seenMembers.py index ba886b709c3e..8dc6336d489f 100644 --- a/python/testData/completion/seenMembers.py +++ b/python/testData/completion/seenMembers.py @@ -1,5 +1,3 @@ -def foo(element): - print(element.children) - -def bar(element): - element.ch \ No newline at end of file +for region in regions: + region.children = [] + region.ch diff --git a/python/testData/completion/structuralType.after.py b/python/testData/completion/structuralType.after.py new file mode 100644 index 000000000000..777f1d32c519 --- /dev/null +++ b/python/testData/completion/structuralType.after.py @@ -0,0 +1,4 @@ +def f(x): + x.foo + x.bar + x.bar diff --git a/python/testData/completion/structuralType.py b/python/testData/completion/structuralType.py new file mode 100644 index 000000000000..95de6aace20e --- /dev/null +++ b/python/testData/completion/structuralType.py @@ -0,0 +1,4 @@ +def f(x): + x.foo + x.bar + x.b diff --git a/python/testData/inspections/PyCallingNonCallableInspection/structuralType.py b/python/testData/inspections/PyCallingNonCallableInspection/structuralType.py new file mode 100644 index 000000000000..1cf114c6a6c3 --- /dev/null +++ b/python/testData/inspections/PyCallingNonCallableInspection/structuralType.py @@ -0,0 +1,3 @@ +def f(x): + x.foo + x.bar + x(0) diff --git a/python/testData/inspections/PyTypeCheckerInspection/ActualStructuralType.py b/python/testData/inspections/PyTypeCheckerInspection/ActualStructuralType.py new file mode 100644 index 000000000000..a3714f1ef1ff --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/ActualStructuralType.py @@ -0,0 +1,19 @@ +def f(x): + """ + :type x: str + """ + pass + + +def g(x): + return x.lower() + + +def test(x, y): + x.upper() + f(x) + g(x) + + y.foo() + f(y) + g(y) diff --git a/python/testData/inspections/PyTypeCheckerInspection/ExpectedStructuralType.py b/python/testData/inspections/PyTypeCheckerInspection/ExpectedStructuralType.py new file mode 100644 index 000000000000..1080906218ab --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/ExpectedStructuralType.py @@ -0,0 +1,16 @@ +def f(x): + return x.foo + x.bar() + + +def g(x): + return x.lower() + + +def test(x): + x.foo + f(x) + g(x) + + z = 'foo' + f(z) + g(z) diff --git a/python/testData/inspections/PyTypeCheckerInspection/GetAttributeAgainstStructuralType.py b/python/testData/inspections/PyTypeCheckerInspection/GetAttributeAgainstStructuralType.py new file mode 100644 index 000000000000..617df26bcbcf --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/GetAttributeAgainstStructuralType.py @@ -0,0 +1,21 @@ +def f(x): + return x.foo + + +class C(object): + def __getattribute__(self, item): + pass + + +class D(object): + def __getattr__(self, item): + pass + + +class E(object): + pass + + +f(C()) +f(D()) +f(E()) diff --git a/python/testData/inspections/PyTypeCheckerInspection/IterateOverParamWithNoAttributes.py b/python/testData/inspections/PyTypeCheckerInspection/IterateOverParamWithNoAttributes.py new file mode 100644 index 000000000000..15bf88a198d2 --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/IterateOverParamWithNoAttributes.py @@ -0,0 +1,8 @@ +def f(xs): + ys = 'string' + for x in xs: + g(ys) + + +def g(x): + return x.lower() diff --git a/python/testData/inspections/PyTypeCheckerInspection/StructuralTypesForNestedCalls.py b/python/testData/inspections/PyTypeCheckerInspection/StructuralTypesForNestedCalls.py new file mode 100644 index 000000000000..1e4ae234bbdf --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/StructuralTypesForNestedCalls.py @@ -0,0 +1,10 @@ +def f(x): + return x.foo + g(x) + + +def g(x): + return x.bar + + +def test(): + f('string') diff --git a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py index ba8ec2d9f536..5c2d00d3b53c 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py +++ b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py @@ -1,4 +1,4 @@ -def test(): +def test(c): def f1(c): if c < 0: return [] @@ -15,7 +15,7 @@ def test(): """ :type x: int """ - x1 = f1(42) + x1 = f1(c) f2(x1) # Weaker union types f3(x1) diff --git a/python/testSrc/com/jetbrains/python/PyTypeTest.java b/python/testSrc/com/jetbrains/python/PyTypeTest.java index 7453e78613a8..5e9dc354d2b7 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() { @@ -960,6 +961,42 @@ public class PyTypeTest extends PyTestCase { " expr = x\n"); } + public void testStructuralType() { + doTest("{foo, bar}", + "def f(x):\n" + + " x.foo + x.bar()\n" + + " expr = x\n"); + } + + public void testOnlyRelatedNestedAttributes() { + doTest("{foo}", + "def g(x):\n" + + " x.bar\n" + + "\n" + + "def f(x, y):\n" + + " x.foo + g(y)\n" + + " expr = x\n"); + } + + public void testNoContainsInContainsArgumentForStructuralType() { + doTest("{foo, __getitem__}", + "def f(x):\n" + + " x in []\n" + + " x.foo\n" + + " x[0]" + + " expr = x\n"); + } + + public void testStructuralTypeAndIsInstanceChecks() { + doTest("(x: {foo}) -> None", + "def f(x):\n" + + " if isinstance(x, str):\n" + + " x.lower()\n" + + " x.foo\n" + + "\n" + + "expr = f\n"); + } + private static TypeEvalContext getTypeEvalContext(@NotNull PyExpression element) { return TypeEvalContext.userInitiated(element.getProject(), element.getContainingFile()).withTracing(); } @@ -969,6 +1006,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); diff --git a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java index 68765f72eee8..487b43ef370c 100644 --- a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java +++ b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java @@ -753,4 +753,8 @@ public class PythonCompletionTest extends PyTestCase { assertNotNull(variants); assertEmpty(variants); } + + public void testStructuralType() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyCallingNonCallableInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyCallingNonCallableInspectionTest.java index b4ae36c8e668..d0c758bde64b 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyCallingNonCallableInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyCallingNonCallableInspectionTest.java @@ -99,6 +99,10 @@ public class PyCallingNonCallableInspectionTest extends PyTestCase { doTest(); } + public void testStructuralType() { + doTest(); + } + private void doTest() { setLanguageLevel(LanguageLevel.PYTHON27); try { diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index b3529d0ecbd1..3445236b0139 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -247,4 +247,24 @@ public class PyTypeCheckerInspectionTest extends PyTestCase { public void testContainsArguments() { doTest(); } + + public void testExpectedStructuralType() { + doTest(); + } + + public void testActualStructuralType() { + doTest(); + } + + public void testStructuralTypesForNestedCalls() { + doTest(); + } + + public void testIterateOverParamWithNoAttributes() { + doTest(); + } + + public void testGetAttributeAgainstStructuralType() { + doTest(); + } }