Repository: python/mypy

Issue:
Quick mode crash when run twice in strict-optional mode
In our codebase we have a case where running quick mode twice crashes.

@euresti reported this.

**UPDATE:** it's something like
```
$ rm -rf .mypy_cache
$ mypy -i --strict-optional <files>
<lots of errors>
$ mypy -quick --strict-optional <files>
<lots of errors>
$ mypy -quick --strict-optional <files>
<lots of errors>
<crash>
$
```
Quick mode crash when class is deleted
On first run, type check `a.py`:
```py
import b
def f() -> b.C: pass
```

`b.py`:

```py
import a
class C: pass
```

Updated contents of `b.py` on the second run (note `C` was deleted, but there still is a reference to it in `a`):

```py
import a
a.f().x
```

Now mypy crashes:

```
$ mypy --quick --show-traceback a.py
/Users/jukka/src/mypy/b.py:3: error: INTERNAL ERROR -- please report a bug at https://github.com/python/mypy/issues version: 0.510-dev-c856a53fe06c0ad9193d86ab35fb86693940f273-dirty
Traceback (most recent call last):
  File "/Users/jukka/src/mypy/scripts/mypy", line 6, in <module>
    main(__file__)
  File "/Users/jukka/src/mypy/mypy/main.py", line 46, in main
    res = type_check_only(sources, bin_dir, options)
  File "/Users/jukka/src/mypy/mypy/main.py", line 93, in type_check_only
    options=options)
  File "/Users/jukka/src/mypy/mypy/build.py", line 188, in build
    graph = dispatch(sources, manager)
  File "/Users/jukka/src/mypy/mypy/build.py", line 1570, in dispatch
    process_graph(graph, manager)
  File "/Users/jukka/src/mypy/mypy/build.py", line 1813, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/Users/jukka/src/mypy/mypy/build.py", line 1912, in process_stale_scc
    graph[id].type_check_first_pass()
  File "/Users/jukka/src/mypy/mypy/build.py", line 1485, in type_check_first_pass
    self.type_checker.check_first_pass()
  File "/Users/jukka/src/mypy/mypy/checker.py", line 177, in check_first_pass
    self.accept(d)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 262, in accept
    stmt.accept(self)
  File "/Users/jukka/src/mypy/mypy/nodes.py", line 823, in accept
    return visitor.visit_expression_stmt(self)
  File "/Users/jukka/src/mypy/mypy/checker.py", line 1817, in visit_expression_stmt
    self.expr_checker.accept(s.expr, allow_none_return=True)
  File "/Users/jukka/src/mypy/mypy/checkexpr.py", line 2056, in accept
    typ = node.accept(self)
  File "/Users/jukka/src/mypy/mypy/nodes.py", line 1254, in accept
    return visitor.visit_member_expr(self)
  File "/Users/jukka/src/mypy/mypy/checkexpr.py", line 1010, in visit_member_expr
    result = self.analyze_ordinary_member_access(e, False)
  File "/Users/jukka/src/mypy/mypy/checkexpr.py", line 1025, in analyze_ordinary_member_access
    original_type=original_type, chk=self.chk)
  File "/Users/jukka/src/mypy/mypy/checkmember.py", line 74, in analyze_member_access
    method = info.get_method(name)
AttributeError: 'NoneType' object has no attribute 'get_method'
/Users/jukka/src/mypy/b.py:3: note: use --pdb to drop into pdb
```


Discussion:
There are lots of errors too. Traceback:
```
...
  File "/Users/david/src/client/.mypy/venv/lib/python3.6/site-packages/mypy/subtypes.py", line 33, in check_type_parameter
    return is_equivalent(lefta, righta, check_type_parameter)
  File "/Users/david/src/client/.mypy/venv/lib/python3.6/site-packages/mypy/subtypes.py", line 93, in is_equivalent
    is_subtype(a, b, type_parameter_checker, ignore_pos_arg_names=ignore_pos_arg_names)
  File "/Users/david/src/client/.mypy/venv/lib/python3.6/site-packages/mypy/subtypes.py", line 77, in is_subtype
    ignore_pos_arg_names=ignore_pos_arg_names))
  File "/Users/david/src/client/.mypy/venv/lib/python3.6/site-packages/mypy/types.py", line 381, in accept
    return visitor.visit_instance(self)  
  File "/Users/david/src/client/.mypy/venv/lib/python3.6/site-packages/mypy/subtypes.py", line 146, in visit_instance
    rname = right.type.fullname()
AttributeError: 'NoneType' object has no attribute 'fullname'
```
Hm, I can repro this with an earlier mypy commit (a27963bf14d3c6d7535a6356912d8661f4918371, from last Thu) but not with master. The repro is kind of expensive so I won't bisect further (though I suspect it's Ivan's 8ecccda69ec8860647931d3f9299020376e95e66 "Use dummy TypeInfo ..."), but I'm just going to close this as fixed.


Resolving patch:
diff --git a/mypy/fixup.py b/mypy/fixup.py
--- a/mypy/fixup.py
+++ b/mypy/fixup.py
@@ -5,7 +5,7 @@
 from mypy.nodes import (
     MypyFile, SymbolNode, SymbolTable, SymbolTableNode,
     TypeInfo, FuncDef, OverloadedFuncDef, Decorator, Var,
-    TypeVarExpr, ClassDef,
+    TypeVarExpr, ClassDef, Block,
     LDEF, MDEF, GDEF
 )
 from mypy.types import (
@@ -90,6 +90,11 @@ def visit_symbol_table(self, symtab: SymbolTable) -> None:
                         value.type_override = stnode.type_override
                     elif not self.quick_and_dirty:
                         assert stnode is not None, "Could not find cross-ref %s" % (cross_ref,)
+                    else:
+                        # We have a missing crossref in quick mode, need to put something
+                        value.node = stale_info()
+                        if value.type_override is not None:
+                            value.type_override.accept(self.type_fixer)
             else:
                 if isinstance(value.node, TypeInfo):
                     # TypeInfo has no accept().  TODO: Add it?
@@ -162,6 +167,10 @@ def visit_instance(self, inst: Instance) -> None:
             for base in inst.type.bases:
                 if base.type is NOT_READY:
                     base.accept(self)
+        else:
+            # Looks like a missing TypeInfo in quick mode, put something there
+            assert self.quick_and_dirty, "Should never get here in normal mode"
+            inst.type = stale_info()
         for a in inst.args:
             a.accept(self)
 
@@ -267,12 +276,23 @@ def lookup_qualified_stnode(modules: Dict[str, MypyFile], name: str,
             return None
         key = rest.pop()
         if key not in names:
+            if not quick_and_dirty:
+                assert key in names, "Cannot find %s for %s" % (key, name)
             return None
-        elif not quick_and_dirty:
-            assert key in names, "Cannot find %s for %s" % (key, name)
         stnode = names[key]
         if not rest:
             return stnode
         node = stnode.node
         assert isinstance(node, TypeInfo)
         names = node.names
+
+
+def stale_info() -> TypeInfo:
+    suggestion = "<stale cache: consider running mypy without --quick>"
+    dummy_def = ClassDef(suggestion, Block([]))
+    dummy_def.fullname = suggestion
+
+    info = TypeInfo(SymbolTable(), dummy_def, "<stale>")
+    info.mro = [info]
+    info.bases = []
+    return info
diff --git a/mypy/nodes.py b/mypy/nodes.py
--- a/mypy/nodes.py
+++ b/mypy/nodes.py
@@ -2228,7 +2228,10 @@ class FakeInfo(TypeInfo):
     #    pass cleanly.
     # 2. If NOT_READY value is accidentally used somewhere, it will be obvious where the value
     #    is from, whereas a 'None' value could come from anywhere.
-    def __getattr__(self, attr: str) -> None:
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        pass
+
+    def __getattribute__(self, attr: str) -> None:
         raise AssertionError('De-serialization failure: TypeInfo not fixed')
 
 
diff --git a/mypy/types.py b/mypy/types.py
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -401,7 +401,7 @@ class Instance(Type):
 
     def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type],
                  line: int = -1, column: int = -1, erased: bool = False) -> None:
-        assert(typ is None or typ.fullname() not in ["builtins.Any", "typing.Any"])
+        assert(typ is NOT_READY or typ.fullname() not in ["builtins.Any", "typing.Any"])
         self.type = typ
         self.args = args
         self.erased = erased
@@ -1796,8 +1796,10 @@ def union_items(typ: Type) -> List[Type]:
         return [typ]
 
 
+names = globals().copy()
+names.pop('NOT_READY', None)
 deserialize_map = {
     key: obj.deserialize  # type: ignore
-    for key, obj in globals().items()
+    for key, obj in names.items()
     if isinstance(obj, type) and issubclass(obj, Type) and obj is not Type
 }


Test patch: