mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Merge branch 'python-structural-types'
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p/>
|
||||
* 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.
|
||||
* <p/>
|
||||
* Should be used for code completion, go to definition, find usages, refactorings, documentation.
|
||||
* Should be used go to definition, find usages, refactorings, documentation.
|
||||
* <p/>
|
||||
* 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.
|
||||
* <p/>
|
||||
* <p/>
|
||||
*
|
||||
* @param project pass project here to enable cache. Pass null if you do not have any project.
|
||||
* <strong>Always</strong> 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() {
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
@@ -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<PyGenericType, PyType> 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<String> expectedAttributes = ((PyStructuralType)expected).getAttributeNames();
|
||||
final Set<String> actualAttributes = getAttributes(actual);
|
||||
if (actualAttributes != null) {
|
||||
final Sets.SetView<String> 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<String, String>() {
|
||||
@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<String> 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()) {
|
||||
|
||||
+4
-27
@@ -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<? extends RatedResolveResult> 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<LocalQuickFix> actions) {
|
||||
for (PyUnresolvedReferenceQuickFixProvider provider : Extensions.getExtensions(PyUnresolvedReferenceQuickFixProvider.EP_NAME)) {
|
||||
provider.registerQuickFixes(reference, new Consumer<LocalQuickFix>() {
|
||||
|
||||
@@ -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<PyNamedParameterStub
|
||||
}
|
||||
}
|
||||
}
|
||||
// Guess the type from file-local usages
|
||||
if (context.allowLocalUsages(this)) {
|
||||
// Guess the type from file-local calls
|
||||
if (context.allowCallContext(this)) {
|
||||
final List<PyType> types = new ArrayList<PyType>();
|
||||
processLocalCalls(func, new Processor<PyCallExpression>() {
|
||||
@Override
|
||||
@@ -276,6 +274,12 @@ public class PyNamedParameterImpl extends PyBaseElementImpl<PyNamedParameterStub
|
||||
return PyUnionType.createWeakType(PyUnionType.union(types));
|
||||
}
|
||||
}
|
||||
if (context.maySwitchToAST(this)) {
|
||||
final Set<String> attributes = collectUsedAttributes(context);
|
||||
if (!attributes.isEmpty()) {
|
||||
return new PyStructuralType(attributes, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -286,6 +290,86 @@ public class PyNamedParameterImpl extends PyBaseElementImpl<PyNamedParameterStub
|
||||
return new PyElementPresentation(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Set<String> collectUsedAttributes(@NotNull final TypeEvalContext context) {
|
||||
final Set<String> result = new LinkedHashSet<String>();
|
||||
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<PyExpression, PyNamedParameter> entry : mapping.getPlainMappedParams().entrySet()) {
|
||||
if (entry.getKey() == element) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void processLocalCalls(@NotNull PyFunction function, @NotNull Processor<PyCallExpression> processor) {
|
||||
final PsiFile file = function.getContainingFile();
|
||||
final String name = function.getName();
|
||||
|
||||
+1
-1
@@ -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<PyParameter> parameters = PyUtil.getParameters(def, context);
|
||||
for (PyParameter parameter : parameters) {
|
||||
parameter.accept(collector);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String> namesAlready = new HashSet<String>();
|
||||
ctx.put(PyType.CTX_NAMES, namesAlready);
|
||||
final Collection<Object> variants = new ArrayList<Object>();
|
||||
if (qualifierType != null) {
|
||||
Collection<Object> variants = new ArrayList<Object>();
|
||||
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<PyExpression> 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<PyClass> 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<PyClass> filterByImports(Collection<PyClass> 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<PyExpression> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PyParameter> parameters = PyUtil.getParameters((PyFunction)object, context);
|
||||
final String params = StringUtil.join(parameters, new Function<PyParameter, String>() {
|
||||
@Override
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String> myAttributes;
|
||||
private final boolean myInferredFromUsages;
|
||||
|
||||
public PyStructuralType(@NotNull Set<String> attributes, boolean inferredFromUsages) {
|
||||
myAttributes = attributes;
|
||||
myInferredFromUsages = inferredFromUsages;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<? extends RatedResolveResult> 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<Object> variants = new ArrayList<Object>();
|
||||
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<String> getAttributeNames() {
|
||||
return myAttributes;
|
||||
}
|
||||
}
|
||||
@@ -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<String> actualAttributes = getClassAttributes(cls, true);
|
||||
return actualAttributes.containsAll(((PyStructuralType)expected).getAttributeNames());
|
||||
}
|
||||
if (actual instanceof PyStructuralType && expected instanceof PyClassType) {
|
||||
final Set<String> 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<String> getClassAttributes(@NotNull PyClass cls, boolean inherited) {
|
||||
final Set<String> attributes = new HashSet<String>();
|
||||
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<? extends RatedResolveResult> 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;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
def foo(element):
|
||||
print(element.children)
|
||||
|
||||
def bar(element):
|
||||
element.ch<caret>
|
||||
for region in regions:
|
||||
region.children = []
|
||||
region.ch<caret>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
def f(x):
|
||||
x.foo
|
||||
x.bar
|
||||
x.bar
|
||||
@@ -0,0 +1,4 @@
|
||||
def f(x):
|
||||
x.foo
|
||||
x.bar
|
||||
x.b<caret>
|
||||
@@ -0,0 +1,3 @@
|
||||
def f(x):
|
||||
x.foo + x.bar
|
||||
x(0)
|
||||
@@ -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)
|
||||
@@ -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(<warning descr="Type 'str' doesn't have expected attributes 'foo', 'bar'">z</warning>)
|
||||
g(z)
|
||||
+21
@@ -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(<warning descr="Type 'E' doesn't have expected attribute 'foo'">E()</warning>)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
def f(xs):
|
||||
ys = 'string'
|
||||
for x in xs:
|
||||
g(ys)
|
||||
|
||||
|
||||
def g(x):
|
||||
return x.lower()
|
||||
@@ -0,0 +1,10 @@
|
||||
def f(x):
|
||||
return x.foo + g(x)
|
||||
|
||||
|
||||
def g(x):
|
||||
return x.bar
|
||||
|
||||
|
||||
def test():
|
||||
f(<warning descr="Type 'str' doesn't have expected attributes 'foo', 'bar'">'string'</warning>)
|
||||
@@ -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(<warning descr="Expected type 'int', got 'list | str | None' instead">x1</warning>)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -753,4 +753,8 @@ public class PythonCompletionTest extends PyTestCase {
|
||||
assertNotNull(variants);
|
||||
assertEmpty(variants);
|
||||
}
|
||||
|
||||
public void testStructuralType() {
|
||||
doTest();
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -99,6 +99,10 @@ public class PyCallingNonCallableInspectionTest extends PyTestCase {
|
||||
doTest();
|
||||
}
|
||||
|
||||
public void testStructuralType() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
private void doTest() {
|
||||
setLanguageLevel(LanguageLevel.PYTHON27);
|
||||
try {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user