Repository: python/mypy

Issue:
Crash in fine grained mode likely related to enums
Here is a traceback:
```
Traceback (most recent call last):
  File "[somewhere]/mypy/dmypy_server.py", line 135, in serve
    resp = self.run_command(command, data)
  File "[somewhere]/mypy/dmypy_server.py", line 169, in run_command
    return method(self, **data)
  File "[somewhere]/mypy/dmypy_server.py", line 200, in cmd_check
    return self.check(self.last_sources)
  File "[somewhere]/mypy/dmypy_server.py", line 214, in check
    return self.check_fine_grained(sources)
  File "[somewhere]/mypy/dmypy_server.py", line 246, in check_fine_grained
    return self.initialize_fine_grained(sources)
  File "[somewhere]/mypy/dmypy_server.py", line 291, in initialize_fine_grained
    messages = self.fine_grained_manager.update(changed)
  File "[somewhere]/mypy/server/update.py", line 222, in update
    result = self.update_single(next_id, next_path)
  File "[somewhere]/mypy/server/update.py", line 264, in update_single
    result = update_single_isolated(module, path, manager, previous_modules)
  File "[somewhere]/mypy/server/update.py", line 437, in update_single_isolated
    replace_modules_with_new_variants(manager, graph, old_modules, new_modules)
  File "[somewhere]/mypy/server/update.py", line 702, in replace_modules_with_new_variants
    new_module, new_module.names)
  File "[somewhere]/mypy/server/astmerge.py", line 85, in merge_asts
    node = replace_nodes_in_ast(new, replacement_map)
  File "[somewhere]/mypy/server/astmerge.py", line 130, in replace_nodes_in_ast
    node.accept(visitor)
  File "[somewhere]/mypy/nodes.py", line 230, in accept
    return visitor.visit_mypy_file(self)
  File "[somewhere]/mypy/server/astmerge.py", line 151, in visit_mypy_file
    super().visit_mypy_file(node)
  File "[somewhere]/mypy/traverser.py", line 30, in visit_mypy_file
    d.accept(self)
  File "[somewhere]/mypy/nodes.py", line 931, in accept
    return visitor.visit_if_stmt(self)
  File "[somewhere]/mypy/traverser.py", line 114, in visit_if_stmt
    o.else_body.accept(self)
  File "[somewhere]/mypy/nodes.py", line 766, in accept
    return visitor.visit_block(self)
  File "[somewhere]/mypy/server/astmerge.py", line 154, in visit_block
    super().visit_block(node)
  File "[somewhere]/mypy/traverser.py", line 34, in visit_block
    s.accept(self)
  File "[somewhere]/mypy/nodes.py", line 704, in accept
    return visitor.visit_class_def(self)
  File "[somewhere]/mypy/server/astmerge.py", line 176, in visit_class_def
    self.process_type_info(node.info)
  File "[somewhere]/mypy/server/astmerge.py", line 264, in process_type_info
    replace_nodes_in_symbol_table(info.names, self.replacements)
AttributeError: 'NoneType' object has no attribute 'names'
```
Unfortunately, there is no simple repro. The only hint is that this likely related to `Enum` and/or `IntEnum` (or a class being changed from a normal class to enum).


Discussion:
Here's a test case that generates a similar-looking traceback:

```
[case testSkippedClass]
import a
[file a.py]
import sys
if sys.platform == 'xyz':
    class A: pass
[file a.py.2]
if sys.platform == 'xyz':
    class A: pass
[out]
==
```

Resolving patch:
diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py
--- a/mypy/server/astmerge.py
+++ b/mypy/server/astmerge.py
@@ -254,7 +254,9 @@ def fixup(self, node: SN) -> SN:
     def fixup_type(self, typ: Type) -> None:
         typ.accept(TypeReplaceVisitor(self.replacements))
 
-    def process_type_info(self, info: TypeInfo) -> None:
+    def process_type_info(self, info: Optional[TypeInfo]) -> None:
+        if info is None:
+            return
         # TODO: Additional things:
         # - declared_metaclass
         # - metaclass_type


Test patch:
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -1861,3 +1861,59 @@ def f() -> None:
 [out]
 a.py:1: error: Need type annotation for 'y'
 ==
+
+[case testSkippedClass1]
+import a
+[file a.py]
+class A: pass
+[file a.py.2]
+import sys
+if sys.platform == 'xyz':
+    class A: pass
+[builtins fixtures/ops.pyi]
+[out]
+==
+
+[case testSkippedClass2]
+import a
+[file a.py]
+import sys
+if sys.platform == 'xyz':
+    class A: pass
+[file a.py.2]
+import sys
+if sys.platform == 'xyz':
+    class A: pass
+[builtins fixtures/ops.pyi]
+[out]
+==
+
+[case testSkippedClass3]
+import a
+[file a.py]
+import sys
+if sys.platform == 'xyz':
+    class A: pass
+[file a.py.2]
+class A: pass
+[builtins fixtures/ops.pyi]
+[out]
+==
+
+[case testSkippedClass4]
+import a
+[file a.py]
+import sys
+if sys.platform == 'xyz':
+    class A: pass
+else:
+    class A: pass
+[file a.py.2]
+import sys
+if sys.platform == 'xyz':
+    class A: pass
+else:
+    class A: pass
+[builtins fixtures/ops.pyi]
+[out]
+==