Repository: python/mypy

Issue:
False negative in incremental mode
We don't yet fully understood this, but @msullivan and I observed the following:

- the code was of the form `assert x.y.z == <constant>`
- the expected error was of the form `Item "None" of "Optional[Y]" has no attribute "z"`
- the type of `x.y` was inferred from a method `f() -> Optional[Y]`
- previously, `f` (defined in a different file) did not have an annotation
- the file with the error was not modified

Apparently the cache for the file with the error was considered up-to-date, and not dependent on the file where `f` was defined. It's possible that interface hash calculation is at fault.


Discussion:
I've produced a small reduced test case, but haven't started debugging the failure yet:
```[case testIncrementalStrictOptionalInheritance]
# flags: --strict-optional
import a
[file a.py]
import b
def foo() -> None:
    1 + b.Bar().get()
[file b.py]
from c import Baz
class Bar(Baz): pass

[file c.py]
class Baz:
    def get(self):
        return 1
[file c.py.2]
from typing import Optional
class Baz:
    def get(self) -> Optional[int]:
        return 1
[out]
[out2]
tmp/a.py:3: error: Unsupported operand types for + ("int" and "Optional[int]")
```
Instead of the error, we produce no message.
If `a` uses `Baz` directly, if `Bar` is defined in `a`, or if the original version of `get` is annotated---either with `int` or with `Any`---then we pass the test.

It appears that b's interface hash is not updated when `Bar`'s base class changes.
The interface hash is just a hash of the cache file written. This is clearly wrong and will be tricky to fix.
Here is a very related test case that also fails:
```
[case testIncrementalLotsOfInheritance]
import a
[file a.py]
from b import B
from d import D
def take(d: D) -> None: pass
def foo() -> None:
    take(B())
[file b.py]
from c import C
class B(C): pass

[file c.py]
from d import D
class C(D): pass

[file c.py.2]
from d import D
class C: pass

[file d.py]
class D: pass
[out]
[out2]
tmp/a.py:5: error: Argument 1 to "take" has incompatible type "B"; expected "D"
```

The problem in both of these cases is that we fail to detect the indirect dependencies that `a` has on `c` (which we *do* correctly do if `get` is annotated in the first test). We shouldn't need `b`'s interface hash to change if the indirect dependencies are properly calculated.

In the first test case, we fail to detect the indirect dependencies because the `CallableType`s assigned to unannotated methods lack a `definition` field, so `TypeIndirectionVisitor` can't find that it is defined in `C`.

To get the second test case right, `a` needs to indirectly depend on every class in `Baz`'s mro, so that it can detect mro changes. Doing this also fixes the unannotated method failure.

Good sleuthing! 
(I think this is important, so I set priority to high.)

Resolving patch:
diff --git a/mypy/indirection.py b/mypy/indirection.py
--- a/mypy/indirection.py
+++ b/mypy/indirection.py
@@ -69,7 +69,11 @@ def visit_type_var(self, t: types.TypeVarType) -> Set[str]:
     def visit_instance(self, t: types.Instance) -> Set[str]:
         out = self._visit(*t.args)
         if t.type is not None:
-            out.update(split_module_names(t.type.module_name))
+            # Uses of a class depend on everything in the MRO,
+            # as changes to classes in the MRO can add types to methods,
+            # change property types, change the MRO itself, etc.
+            for s in t.type.mro:
+                out.update(split_module_names(s.module_name))
         return out
 
     def visit_callable_type(self, t: types.CallableType) -> Set[str]:


Test patch:
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -3783,3 +3783,76 @@ import a
 [out2]
 main:2: error: Cannot find module named 'a'
 main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
+
+[case testIncrementalInheritanceAddAnnotation]
+# flags: --strict-optional
+import a
+[file a.py]
+import b
+def foo() -> None:
+    1 + b.Bar().get()
+[file b.py]
+from c import Baz
+class Bar(Baz): pass
+
+[file c.py]
+class Baz:
+    def get(self):
+        return 1
+[file c.py.2]
+from typing import Optional
+class Baz:
+    def get(self) -> Optional[int]:
+        return 1
+[out]
+[out2]
+tmp/a.py:3: error: Unsupported operand types for + ("int" and "Optional[int]")
+
+
+[case testIncrementalLotsOfInheritance]
+import a
+[file a.py]
+from b import B
+from d import D
+def take(d: D) -> None: pass
+def foo() -> None:
+    take(B())
+[file b.py]
+from c import C
+class B(C): pass
+
+[file c.py]
+from d import D
+class C(D): pass
+
+[file c.py.2]
+from d import D
+class C: pass
+
+[file d.py]
+class D: pass
+[out]
+[out2]
+tmp/a.py:5: error: Argument 1 to "take" has incompatible type "B"; expected "D"
+
+[case testIncrementalInheritanceProperty]
+import a
+[file a.py]
+import b
+def foo() -> None:
+    1 + b.Bar().x
+[file b.py]
+from c import Baz
+class Bar(Baz): pass
+
+[file c.py]
+class Baz:
+    def __init__(self):
+        self.x = 12  # type: int
+[file c.py.2]
+class Baz:
+    def __init__(self):
+        self.x = 'lol'  # type: str
+[out]
+[out2]
+tmp/a.py:3: error: Unsupported operand types for + ("int" and "str")