PY-8936 Infer return type for __get__ descriptor attributes

This fix is useful even if we cannot infer the type. Returning Any
instead of the descriptor instance suppresses the false positive error.
This commit is contained in:
Andrey Vlasovskikh
2017-08-21 19:28:08 +03:00
parent 40471ab23a
commit cd34e00998
4 changed files with 69 additions and 0 deletions
@@ -85,6 +85,7 @@ public class PyNames {
public static final String NEW = "__new__";
public static final String GETATTR = "__getattr__";
public static final String GETATTRIBUTE = "__getattribute__";
public static final String GET = "__get__";
public static final String __CLASS__ = "__class__";
public static final String DUNDER_METACLASS = "__metaclass__";
public static final String METACLASS = "metaclass";
@@ -48,6 +48,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.jetbrains.python.psi.PyUtil.as;
/**
* Implements reference expression PSI.
*
@@ -252,6 +254,10 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere
if (qualified && typeFromTargets instanceof PyNoneType) {
return null;
}
final Ref<PyType> descriptorType = getDescriptorType(typeFromTargets, context);
if (descriptorType != null) {
return descriptorType.get();
}
return typeFromTargets;
}
finally {
@@ -259,6 +265,24 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere
}
}
@Nullable
private Ref<PyType> getDescriptorType(@Nullable PyType typeFromTargets, @NotNull TypeEvalContext context) {
if (!isQualified()) return null;
final PyClassLikeType targetType = as(typeFromTargets, PyClassLikeType.class);
if (targetType == null) return null;
final PyResolveContext resolveContext = PyResolveContext.noProperties().withTypeEvalContext(context);
final List<? extends RatedResolveResult> members = targetType.resolveMember(PyNames.GET, this, AccessDirection.READ,
resolveContext);
if (members == null || members.isEmpty()) return null;
final List<PyType> types = StreamEx.of(members)
.map((result) -> result.getElement())
.select(PyCallable.class)
.map((callable) -> context.getReturnType(callable))
.toList();
final PyType type = PyUnionType.union(types);
return Ref.create(type);
}
@Nullable
private Ref<PyType> getQualifiedReferenceType(@NotNull TypeEvalContext context) {
if (!context.maySwitchToAST(this)) {
@@ -0,0 +1,39 @@
from typing import Any
class StringDescriptor:
def __get__(self, instance, owner):
return 'foo'
class AnyDescriptor:
def __get__(self, instance, owner) -> Any:
return 'bar'
class ListDescriptor:
def __get__(self, instance: Any, owner: Any) -> list:
return 'baz'
class C:
foo = StringDescriptor()
bar = AnyDescriptor()
baz = ListDescriptor()
# Instance level
c = C()
c.foo.upper()
c.foo.<warning descr="Unresolved attribute reference 'non_existent' for class 'str'">non_existent</warning>()
c.bar.upper()
c.bar.non_existent()
c.baz.append()
c.baz.<warning descr="Unresolved attribute reference 'non_existent' for class 'list'">non_existent</warning>()
# Class level
C.foo.upper()
C.foo.<warning descr="Unresolved attribute reference 'non_existent' for class 'str'">non_existent</warning>()
C.bar.upper()
C.bar.non_existent()
@@ -220,4 +220,9 @@ public class Py3UnresolvedReferencesInspectionTest extends PyTestCase {
public void testCallTypeGetAttributeAndSetAttrInInheritor() {
doTest();
}
// PY-8936
public void testDescriptorAttribute() {
doTest();
}
}