If mro() is overridden in metaclass, use normal MRO + unresolved ancestor (PY-11401)
authorAndrey Vlasovskikh <andrey.vlasovskikh@jetbrains.com>
Tue, 23 Dec 2014 15:05:48 +0000 (18:05 +0300)
committerAndrey Vlasovskikh <andrey.vlasovskikh@jetbrains.com>
Tue, 23 Dec 2014 15:05:48 +0000 (18:05 +0300)
We cannot evaluate the result of an overridden mro() method, so we
don't know the actual MRO chain. Let's assume that MRO is almost the
same as without this override and add an element of uncertainty by
appending a fake unresolved ancestor type to the MRO chain. Doing so
results in, for example, the unresolved references inspection ignoring
unresolved references for such a class.

python/psi-api/src/com/jetbrains/python/PyNames.java
python/src/com/jetbrains/python/psi/impl/PyClassImpl.java
python/testData/inspections/PyUnresolvedReferencesInspection/overriddenMRO.py [new file with mode: 0644]
python/testData/inspections/PyUnresolvedReferencesInspection/overriddenMROInAncestors.py [new file with mode: 0644]
python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java

index 21e5be632d59f7610ccfc6d8f212d75b8132fabb..20a37449454e32a3a87759c2a2ff61bf925a7ab7 100644 (file)
@@ -503,4 +503,6 @@ public class PyNames {
   public static final ImmutableSet<String> METHOD_SPECIAL_ATTRIBUTES = ImmutableSet.of("__func__", "__self__");
 
   public static final ImmutableSet<String> LEGACY_METHOD_SPECIAL_ATTRIBUTES = ImmutableSet.of("im_func", "im_self", "im_class");
+
+  public static final String MRO = "mro";
 }
index 5532fa37da30244a44d6dc4fb4df0eb3fbaf9dab..c03fe346b6e365d8b56884e9150520cb450aa1a2 100644 (file)
@@ -1330,13 +1330,47 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
   private List<PyClassLikeType> getMROAncestorTypes(@NotNull TypeEvalContext context) throws MROException {
     final PyType thisType = context.getType(this);
     if (thisType instanceof PyClassLikeType) {
-      return mroLinearize((PyClassLikeType)thisType, new HashSet<PyClassLikeType>(), false, context);
+      final PyClassLikeType thisClassLikeType = (PyClassLikeType)thisType;
+      final List<PyClassLikeType> ancestorTypes = mroLinearize(thisClassLikeType, new HashSet<PyClassLikeType>(), false, context);
+      if (isOverriddenMRO(ancestorTypes, context)) {
+        ancestorTypes.add(null);
+      }
+      return ancestorTypes;
     }
     else {
       return Collections.emptyList();
     }
   }
 
+  private boolean isOverriddenMRO(@NotNull List<PyClassLikeType> ancestorTypes, @NotNull TypeEvalContext context) {
+    final List<PyClass> classes = new ArrayList<PyClass>();
+    classes.add(this);
+    for (PyClassLikeType ancestorType : ancestorTypes) {
+      if (ancestorType instanceof PyClassType) {
+        final PyClassType classType = (PyClassType)ancestorType;
+        classes.add(classType.getPyClass());
+      }
+    }
+
+    final PyClass typeClass = PyBuiltinCache.getInstance(this).getClass("type");
+
+    for (PyClass cls : classes) {
+      final PyType metaClassType = cls.getMetaClassType(context);
+      if (metaClassType instanceof PyClassType) {
+        final PyClass metaClass = ((PyClassType)metaClassType).getPyClass();
+        final PyFunction mroMethod = metaClass.findMethodByName(PyNames.MRO, true);
+        if (mroMethod != null) {
+          final PyClass mroClass = mroMethod.getContainingClass();
+          if (mroClass != null && mroClass != typeClass) {
+            return true;
+          }
+        }
+      }
+    }
+
+    return false;
+  }
+
   @NotNull
   private List<PyClassLikeType> getOldStyleAncestorTypes(@NotNull TypeEvalContext context) {
     final List<PyClassLikeType> results = new ArrayList<PyClassLikeType>();
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/overriddenMRO.py b/python/testData/inspections/PyUnresolvedReferencesInspection/overriddenMRO.py
new file mode 100644 (file)
index 0000000..e1f25b3
--- /dev/null
@@ -0,0 +1,22 @@
+class A(object):
+    def foo(self):
+        return 0
+
+
+class B(object):
+    def bar(self):
+        return 0
+
+
+class MyMeta(type):
+    def mro(cls):
+        return A, B
+
+
+class C(B):
+    __metaclass__ = MyMeta
+
+
+c = C()
+print(c.foo().lower())  # pass
+print(c.bar().<warning descr="Unresolved attribute reference 'lower' for class 'int'">lower</warning>())
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/overriddenMROInAncestors.py b/python/testData/inspections/PyUnresolvedReferencesInspection/overriddenMROInAncestors.py
new file mode 100644 (file)
index 0000000..15cd8ae
--- /dev/null
@@ -0,0 +1,28 @@
+class A(object):
+    def foo(self):
+        return 0
+
+
+class MyMeta(type):
+    def mro(cls):
+        return A, B
+
+
+class MyMeta2(MyMeta):
+    pass
+
+
+class B(object):
+    __metaclass__ = MyMeta2
+
+    def bar(self):
+        return 0
+
+
+class C(B):
+    pass
+
+
+c = C()
+print(c.foo().lower())  # pass
+print(c.bar().<warning descr="Unresolved attribute reference 'lower' for class 'int'">lower</warning>())
index d67766e314687fc5deab859779999af22a3959a2..327cecabf147f550ebc4752b4d2dc3f18ff041ab 100644 (file)
@@ -450,6 +450,16 @@ public class PyUnresolvedReferencesInspectionTest extends PyInspectionTestCase {
     doTest();
   }
 
+  // PY-11401
+  public void testOverriddenMRO() {
+    doTest();
+  }
+
+  // PY-11401
+  public void testOverriddenMROInAncestors() {
+    doTest();
+  }
+
   @NotNull
   @Override
   protected Class<? extends PyInspection> getInspectionClass() {