Repository: python/mypy

Issue:
New semantic analyzer: max iteration count reached when base class is bad
This code causes max iteration count error when using the new semantic analyzer:

```py
class C:
    class E: pass

    def f(self) -> None:
        class D(self.E): # Should generate error here
            pass
```


Discussion:

Resolving patch:
diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py
--- a/mypy/newsemanal/semanal.py
+++ b/mypy/newsemanal/semanal.py
@@ -405,9 +405,11 @@ def add_implicit_module_attrs(self, file_node: MypyFile) -> None:
                 var = Var(name, an_type)
                 var._fullname = self.qualified_name(name)
                 var.is_ready = True
-                self.add_symbol(name, var, None)
+                self.add_symbol(name, var, dummy_context())
             else:
-                self.add_symbol(name, PlaceholderNode(self.qualified_name(name), file_node), None)
+                self.add_symbol(name,
+                                PlaceholderNode(self.qualified_name(name), file_node),
+                                dummy_context())
 
     def add_builtin_aliases(self, tree: MypyFile) -> None:
         """Add builtin type aliases to typing module.
@@ -3944,7 +3946,7 @@ def lookup_current_scope(self, name: str) -> Optional[SymbolTableNode]:
     def add_symbol(self,
                    name: str,
                    node: SymbolNode,
-                   context: Optional[Context],
+                   context: Context,
                    module_public: bool = True,
                    module_hidden: bool = False,
                    can_defer: bool = True) -> bool:
@@ -4000,10 +4002,21 @@ def add_symbol_table_node(self,
                               can_defer: bool = True) -> bool:
         """Add symbol table node to the currently active symbol table.
 
-        Return True if we actually added the symbol, or False if we refused to do so
-        (because something is not ready).
+        Return True if we actually added the symbol, or False if we refused
+        to do so (because something is not ready or it was a no-op).
 
-        If can_defer is True, defer current target if adding a placeholder.
+        Generate an error if there is an invalid redefinition.
+
+        If context is None, unconditionally add node, since we can't report
+        an error. Note that this is used by plugins to forcibly replace nodes!
+
+        TODO: Prevent plugins from replacing nodes, as it could cause problems?
+
+        Args:
+            name: short name of symbol
+            symbol: Node to add
+            can_defer: if True, defer current target if adding a placeholder
+            context: error context (see above about None value)
         """
         names = self.current_symbol_table()
         existing = names.get(name)
@@ -4011,25 +4024,18 @@ def add_symbol_table_node(self,
             self.defer()
         if (existing is not None
                 and context is not None
-                and (not isinstance(existing.node, PlaceholderNode)
-                     or (isinstance(symbol.node, PlaceholderNode) and
-                         # Allow replacing becomes_typeinfo=False with becomes_typeinfo=True.
-                         # This can happen for type aliases and NewTypes.
-                         not symbol.node.becomes_typeinfo))):
+                and not is_valid_replacement(existing, symbol)):
             # There is an existing node, so this may be a redefinition.
             # If the new node points to the same node as the old one,
             # or if both old and new nodes are placeholders, we don't
             # need to do anything.
-            old_node = existing.node
-            new_node = symbol.node
-            if (old_node != new_node
-                    and not (isinstance(old_node, PlaceholderNode)
-                             and isinstance(new_node, PlaceholderNode))
-                    and not is_same_var_from_getattr(old_node, new_node)):
-                if isinstance(new_node, (FuncDef, Decorator, OverloadedFuncDef, TypeInfo)):
+            old = existing.node
+            new = symbol.node
+            if not is_same_symbol(old, new):
+                if isinstance(new, (FuncDef, Decorator, OverloadedFuncDef, TypeInfo)):
                     self.add_redefinition(names, name, symbol)
-                if not (isinstance(new_node, (FuncDef, Decorator))
-                        and self.set_original_def(old_node, new_node)):
+                if not (isinstance(new, (FuncDef, Decorator))
+                        and self.set_original_def(old, new)):
                     self.name_already_defined(name, context, existing)
         elif name not in self.missing_names and '*' not in self.missing_names:
             names[name] = symbol
@@ -4149,6 +4155,9 @@ def defer(self) -> None:
 
         This must be called if something in the current target is
         incomplete or has a placeholder node.
+
+        This must not be called during the final analysis iteration!
+        Instead, an error should be generated.
         """
         self.deferred = True
 
@@ -4182,7 +4191,9 @@ def mark_incomplete(self, name: str, node: Node,
             self.incomplete = True
         elif name not in self.current_symbol_table() and not self.is_global_or_nonlocal(name):
             fullname = self.qualified_name(name)
-            self.add_symbol(name, PlaceholderNode(fullname, node, becomes_typeinfo), context=None)
+            self.add_symbol(name,
+                            PlaceholderNode(fullname, node, becomes_typeinfo),
+                            context=dummy_context())
         self.missing_names.add(name)
 
     def is_incomplete_namespace(self, fullname: str) -> bool:
@@ -4203,11 +4214,13 @@ def process_placeholder(self, name: str, kind: str, ctx: Context) -> None:
         (used for better error message).
         """
         if self.final_iteration:
-            self.fail('Cannot resolve {} "{}" (possible cyclic definition)'.format(kind, name),
-                      ctx)
+            self.cannot_resolve_name(name, kind, ctx)
         else:
             self.defer()
 
+    def cannot_resolve_name(self, name: str, kind: str, ctx: Context) -> None:
+        self.fail('Cannot resolve {} "{}" (possible cyclic definition)'.format(kind, name), ctx)
+
     def rebind_symbol_table_node(self, n: SymbolTableNode) -> Optional[SymbolTableNode]:
         """If node refers to old version of module, return reference to new version.
 
@@ -4686,3 +4699,29 @@ def is_same_var_from_getattr(n1: Optional[SymbolNode], n2: Optional[SymbolNode])
             and isinstance(n2, Var)
             and n2.from_module_getattr
             and n1.fullname() == n2.fullname())
+
+
+def dummy_context() -> Context:
+    return TempNode(AnyType(TypeOfAny.special_form))
+
+
+def is_valid_replacement(old: SymbolTableNode, new: SymbolTableNode) -> bool:
+    """Can symbol table node replace an existing one?
+
+    These are the only valid cases:
+
+    1. Placeholder gets replaced with a non-placeholder
+    2. Placeholder that isn't known to become type replaced with a
+       placeholder that can become a type
+    """
+    return (isinstance(old.node, PlaceholderNode)
+            and (not isinstance(new.node, PlaceholderNode)
+                 or (not old.node.becomes_typeinfo
+                     and new.node.becomes_typeinfo)))
+
+
+def is_same_symbol(a: Optional[SymbolNode], b: Optional[SymbolNode]) -> bool:
+    return (a == b
+            or (isinstance(a, PlaceholderNode)
+                and isinstance(b, PlaceholderNode))
+            or is_same_var_from_getattr(a, b))
diff --git a/mypy/newsemanal/semanal_shared.py b/mypy/newsemanal/semanal_shared.py
--- a/mypy/newsemanal/semanal_shared.py
+++ b/mypy/newsemanal/semanal_shared.py
@@ -131,7 +131,7 @@ def current_symbol_table(self) -> SymbolTable:
         raise NotImplementedError
 
     @abstractmethod
-    def add_symbol(self, name: str, node: SymbolNode, context: Optional[Context],
+    def add_symbol(self, name: str, node: SymbolNode, context: Context,
                    module_public: bool = True, module_hidden: bool = False,
                    can_defer: bool = True) -> bool:
         """Add symbol to the current symbol table."""
diff --git a/mypy/newsemanal/typeanal.py b/mypy/newsemanal/typeanal.py
--- a/mypy/newsemanal/typeanal.py
+++ b/mypy/newsemanal/typeanal.py
@@ -164,7 +164,15 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
             if isinstance(node, PlaceholderNode):
                 if node.becomes_typeinfo:
                     # Reference to placeholder type.
-                    if self.allow_placeholder:
+                    if self.api.final_iteration:
+                        # TODO: Move error message generation to messages.py. We'd first
+                        #       need access to MessageBuilder here. Also move the similar
+                        #       message generation logic in semanal.py.
+                        self.api.fail(
+                            'Cannot resolve name "{}" (possible cyclic definition)'.format(t.name),
+                            t)
+                        return AnyType(TypeOfAny.from_error)
+                    elif self.allow_placeholder:
                         self.api.defer()
                     else:
                         self.api.record_incomplete_ref()


Test patch:
diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test
--- a/test-data/unit/check-newsemanal.test
+++ b/test-data/unit/check-newsemanal.test
@@ -2240,9 +2240,8 @@ def force1(x: Literal[1]) -> None: pass
 force1(reveal_type(var1))            # E: Revealed type is 'Literal[1]'
 
 [case testNewAnalyzerReportLoopInMRO]
-class A(A): ...
+class A(A): ... # E: Cannot resolve name "A" (possible cyclic definition)
 [out]
-main: error: Internal error: maximum semantic analysis iteration count reached
 
 [case testNewSemanticAnalyzerUnimportedSpecialCase]
 # flags: --ignore-missing-imports
@@ -2280,7 +2279,7 @@ import n
 [file n.pyi]
 class C: pass
 
-[case testNextSemanticAnalyzerModuleGetAttrInPython36]
+[case testNewAnalyzerModuleGetAttrInPython36]
 # flags: --python-version 3.6
 import m
 import n
@@ -2293,7 +2292,7 @@ import n
 def __getattr__(x): pass
 
 
-[case testNextSemanticAnalyzerModuleGetAttrInPython37]
+[case testNewAnalyzerModuleGetAttrInPython37]
 # flags: --python-version 3.7
 import m
 import n
@@ -2307,6 +2306,13 @@ def __getattr__(x): pass
 
 [case testNewAnalyzerReportLoopInMRO2]
 def f() -> None:
-    class A(A): ...
-[out]
-main: error: Internal error: maximum semantic analysis iteration count reached
+    class A(A): ... # E: Cannot resolve name "A" (possible cyclic definition)
+
+[case testNewAnalyzerUnsupportedBaseClassInsideFunction]
+class C:
+    class E: pass
+
+    def f(self) -> None:
+        # TODO: Error message could be better
+        class D(self.E): # E: Name 'self.E' is not defined
+            pass