Support module with __getattr__ while resolving and type checking (PY-28017)

This commit is contained in:
Semyon Proshev
2018-01-22 21:43:35 +03:00
parent cff0305fb8
commit fb0c5cc58b
16 changed files with 140 additions and 61 deletions
@@ -51,6 +51,7 @@ import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyImportStatementNavigator;
import com.jetbrains.python.psi.impl.PyImportedModule;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.impl.references.PyFromImportNameReference;
import com.jetbrains.python.psi.impl.references.PyImportReference;
import com.jetbrains.python.psi.impl.references.PyOperatorReference;
import com.jetbrains.python.psi.resolve.ImportedResolveResult;
@@ -557,43 +558,43 @@ public class PyUnresolvedReferencesInspection extends PyInspection {
if (PyNames.COMPARISON_OPERATORS.contains(expr.getReferencedName())) {
return;
}
final PyExpression qualifier = expr.getQualifier();
if (qualifier != null) {
PyType type = myTypeEvalContext.getType(qualifier);
if (type != null) {
if (ignoreUnresolvedMemberForType(type, reference, refName)) {
return;
}
addCreateMemberFromUsageFixes(type, reference, refText, actions);
if (type instanceof PyClassType) {
final PyClassType classType = (PyClassType)type;
if (reference instanceof PyOperatorReference) {
String className = type.getName();
if (classType.isDefinition()) {
final PyClassLikeType metaClassType = classType.getMetaClassType(myTypeEvalContext, true);
if (metaClassType != null) {
className = metaClassType.getName();
}
}
final PyExpression qualifier = getReferenceQualifier(reference);
if (qualifier != null) {
final PyType type = myTypeEvalContext.getType(qualifier);
if (type != null) {
if (ignoreUnresolvedMemberForType(type, reference, refName)) {
return;
}
addCreateMemberFromUsageFixes(type, reference, refText, actions);
if (type instanceof PyClassType) {
final PyClassType classType = (PyClassType)type;
if (reference instanceof PyOperatorReference) {
String className = type.getName();
if (classType.isDefinition()) {
final PyClassLikeType metaClassType = classType.getMetaClassType(myTypeEvalContext, true);
if (metaClassType != null) {
className = metaClassType.getName();
}
description = PyBundle.message("INSP.unresolved.operator.ref",
className, refName,
((PyOperatorReference)reference).getReadableOperatorName());
}
else {
final List<String> slots = classType.getPyClass().getOwnSlots();
if (slots != null && slots.contains(refName)) {
return;
}
description = PyBundle.message("INSP.unresolved.ref.$0.for.class.$1", refText, type.getName());
}
markedQualified = true;
description = PyBundle.message("INSP.unresolved.operator.ref",
className, refName,
((PyOperatorReference)reference).getReadableOperatorName());
}
else {
description = PyBundle.message("INSP.cannot.find.$0.in.$1", refText, type.getName());
markedQualified = true;
final List<String> slots = classType.getPyClass().getOwnSlots();
if (slots != null && slots.contains(refName)) {
return;
}
description = PyBundle.message("INSP.unresolved.ref.$0.for.class.$1", refText, type.getName());
}
markedQualified = true;
}
else {
description = PyBundle.message("INSP.cannot.find.$0.in.$1", refText, type.getName());
markedQualified = true;
}
}
}
@@ -657,6 +658,30 @@ public class PyUnresolvedReferencesInspection extends PyInspection {
registerProblem(node, description, hl_type, null, rangeInElement, actions.toArray(LocalQuickFix.EMPTY_ARRAY));
}
@Nullable
private static PyExpression getReferenceQualifier(@NotNull PsiReference reference) {
final PsiElement element = reference.getElement();
if (element instanceof PyQualifiedExpression) {
final PyExpression qualifier = ((PyQualifiedExpression)element).getQualifier();
if (qualifier != null) {
return qualifier;
}
}
if (reference instanceof PyFromImportNameReference) {
final PyFromImportStatement statement = PsiTreeUtil.getParentOfType(element, PyFromImportStatement.class);
if (statement != null) {
final PyReferenceExpression source = statement.getImportSource();
if (source != null) {
return source;
}
}
}
return null;
}
private static void addInstallPackageAction(List<LocalQuickFix> actions, String packageName, Module module, Sdk sdk) {
final List<PyRequirement> requirements = Collections.singletonList(new PyRequirement(packageName));
final String name = "Install package " + packageName;
@@ -808,6 +833,12 @@ public class PyUnresolvedReferencesInspection extends PyInspection {
if (type instanceof PyUnionType) {
return ContainerUtil.exists(((PyUnionType)type).getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name));
}
if (type instanceof PyModuleType) {
final PyFile module = ((PyModuleType)type).getModule();
if (module.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON37)) {
return PyTypeChecker.definesGetAttr(module, myTypeEvalContext);
}
}
for (PyInspectionExtension extension : Extensions.getExtensions(PyInspectionExtension.EP_NAME)) {
if (extension.ignoreUnresolvedMember(type, name, myTypeEvalContext)) {
return true;
@@ -247,6 +247,12 @@ public class PyTypeChecker {
final Set<String> actualAttributes = actualClassType.getMemberNames(true, context);
return actualAttributes.containsAll(((PyStructuralType)expected).getAttributeNames());
}
if (expected instanceof PyStructuralType && actual instanceof PyModuleType) {
final PyFile module = ((PyModuleType)actual).getModule();
if (module.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON37) && definesGetAttr(module, context)) {
return true;
}
}
if (expected instanceof PyStructuralType) {
final Set<String> expectedAttributes = ((PyStructuralType)expected).getAttributeNames();
final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context);
@@ -702,29 +708,36 @@ public class PyTypeChecker {
return false;
}
public static boolean overridesGetAttr(@NotNull PyClass cls, @NotNull TypeEvalContext context) {
PsiElement method = resolveClassMember(cls, PyNames.GETATTR, context);
if (method != null) {
return true;
public static boolean definesGetAttr(@NotNull PyFile file, @NotNull TypeEvalContext context) {
if (file instanceof PyTypedElement) {
final PyType type = context.getType((PyTypedElement)file);
if (type != null) {
return resolveTypeMember(type, PyNames.GETATTR, context) != null;
}
}
method = resolveClassMember(cls, PyNames.GETATTRIBUTE, context);
if (method != null && !PyBuiltinCache.getInstance(cls).isBuiltin(method)) {
return true;
return false;
}
public static boolean overridesGetAttr(@NotNull PyClass cls, @NotNull TypeEvalContext context) {
final PyType type = context.getType(cls);
if (type != null) {
if (resolveTypeMember(type, PyNames.GETATTR, context) != null) {
return true;
}
final PsiElement method = resolveTypeMember(type, 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;
private static PsiElement resolveTypeMember(@NotNull PyType type, @NotNull String name, @NotNull TypeEvalContext context) {
final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context);
final List<? extends RatedResolveResult> results = type.resolveMember(name, null, AccessDirection.READ, resolveContext);
return !ContainerUtil.isEmpty(results) ? results.get(0).getElement() : null;
}
@Nullable
@@ -0,0 +1,6 @@
import mymod
def foo(mod):
return mod.myfunc
print(foo(mymod)())
@@ -0,0 +1,7 @@
def myhiddenfunc():
return "ok"
def __getattr__(name):
if name == "myfunc":
return myhiddenfunc
raise AttributeError
@@ -1,3 +1,3 @@
from importSource import <error descr="Unresolved reference 'len'">len</error>
from importSource import <error descr="Cannot find reference 'len' in '__init__.py'">len</error>
len()
@@ -0,0 +1,5 @@
from mymod import myfunc
import mymod
print(myfunc())
print(mymod.myfunc())
@@ -0,0 +1,7 @@
def myhiddenfunc():
return "ok"
def __getattr__(name):
if name == "myfunc":
return myhiddenfunc
raise AttributeError
@@ -1,4 +1,4 @@
from library import foo,<error descr="Unresolved reference 'bar'">bar</error>
from library import foo,<error descr="Cannot find reference 'bar' in 'library.py'">bar</error>
print(foo)
@@ -1 +1 @@
<warning descr="Unused import statement">from my_module import <error descr="Unresolved reference 'eggs'">eggs</error></warning>
<warning descr="Unused import statement">from my_module import <error descr="Cannot find reference 'eggs' in 'my_module.py'">eggs</error></warning>
@@ -1,2 +1,2 @@
<warning descr="Unused import statement">from my_module import <error descr="Unresolved reference 'eggs'">eggs</error></warning>
<warning descr="Unused import statement">from my_module import <error descr="Unresolved reference 'eggs'">eggs</error></warning>
<warning descr="Unused import statement">from my_module import <error descr="Cannot find reference 'eggs' in 'my_module.py'">eggs</error></warning>
<warning descr="Unused import statement">from my_module import <error descr="Cannot find reference 'eggs' in 'my_module.py'">eggs</error></warning>
@@ -1,4 +1,4 @@
<warning descr="Unused import statement">from my_package.my_module import <error descr="Unresolved reference 'eggs'">eggs</error></warning>
from my_package.my_module import <error descr="Unresolved reference 'eggs'">eggs</error>
<warning descr="Unused import statement">from my_package.my_module import <error descr="Cannot find reference 'eggs' in 'my_module.py'">eggs</error></warning>
from my_package.my_module import <error descr="Cannot find reference 'eggs' in 'my_module.py'">eggs</error>
eggs()
@@ -1,6 +1,6 @@
from lib1 import m1
from lib1.m1 import x
from lib1.m1 import <error descr="Unresolved reference 'something'">something</error>
from lib1.m1 import <error descr="Cannot find reference 'something' in 'm1.py'">something</error>
print(m1, x, something)
@@ -1,7 +1,7 @@
from m1 import <error descr="Unresolved reference 'foo'">foo</error>
from m1 import <error descr="Unresolved reference 'bar'">bar</error>
from m1 import <error descr="Cannot find reference 'foo' in 'm1.pyi'">foo</error>
from m1 import <error descr="Cannot find reference 'bar' in 'm1.pyi'">bar</error>
from m1 import bar_imported
from m1 import <error descr="Unresolved reference 'm2'">m2</error>
from m1 import <error descr="Cannot find reference 'm2' in 'm1.pyi'">m2</error>
from m1 import m2_imported
print(foo, bar, bar_imported, m2, m2_imported)
@@ -1,5 +1,5 @@
from .m1 import foo
from .m1 import <error descr="Unresolved reference 'bar'">bar</error>
from .m1 import <error descr="Cannot find reference 'bar' in 'm1.pyi'">bar</error>
foo2 = foo
bar2 = bar
@@ -465,4 +465,9 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase {
public void testDefinitionAgainstStructural() {
runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest);
}
// PY-28017
public void testModuleWithGetAttr() {
runWithLanguageLevel(LanguageLevel.PYTHON37, this::doMultiFileTest);
}
}
@@ -660,6 +660,11 @@ public class PyUnresolvedReferencesInspectionTest extends PyInspectionTestCase {
doMultiFileTest();
}
// PY-28017
public void testModuleWithGetAttr() {
runWithLanguageLevel(LanguageLevel.PYTHON37, this::doMultiFileTest);
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {