Repository: python/mypy

Issue:
New semantic analyzer: assignment-based named tuple



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
@@ -870,6 +870,9 @@ def analyze_class(self, defn: ClassDef) -> None:
                 self.prepare_class_def(defn, info)
             return
 
+        if self.analyze_namedtuple_classdef(defn):
+            return
+
         # Create TypeInfo for class now that base classes and the MRO can be calculated.
         self.prepare_class_def(defn)
 
@@ -880,10 +883,6 @@ def analyze_class(self, defn: ClassDef) -> None:
 
         with self.scope.class_scope(defn.info):
             self.configure_base_classes(defn, base_types)
-
-            if self.analyze_namedtuple_classdef(defn):
-                return
-
             defn.info.is_protocol = is_protocol
             self.analyze_metaclass(defn)
             defn.info.runtime_protocol = False
@@ -904,40 +903,23 @@ def analyze_class_body_common(self, defn: ClassDef) -> None:
         self.leave_class()
 
     def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool:
-        """Analyze class-based named tuple if the NamedTuple base class is present.
-
-        TODO: Move this to NamedTupleAnalyzer?
-
-        Return True only if the class is a NamedTuple class.
-        """
-        named_tuple_info = self.named_tuple_analyzer.analyze_namedtuple_classdef(defn)
-        if named_tuple_info is None:
-            return False
-        # Temporarily clear the names dict so we don't get errors about duplicate names
-        # that were already set in build_namedtuple_typeinfo.
-        nt_names = named_tuple_info.names
-        named_tuple_info.names = SymbolTable()
-
-        self.analyze_class_body_common(defn)
-
-        # Make sure we didn't use illegal names, then reset the names in the typeinfo.
-        for prohibited in NAMEDTUPLE_PROHIBITED_NAMES:
-            if prohibited in named_tuple_info.names:
-                if nt_names.get(prohibited) is named_tuple_info.names[prohibited]:
-                    continue
-                ctx = named_tuple_info.names[prohibited].node
-                assert ctx is not None
-                self.fail('Cannot overwrite NamedTuple attribute "{}"'.format(prohibited),
-                          ctx)
-
-        # Restore the names in the original symbol table. This ensures that the symbol
-        # table contains the field objects created by build_namedtuple_typeinfo. Exclude
-        # __doc__, which can legally be overwritten by the class.
-        named_tuple_info.names.update({
-            key: value for key, value in nt_names.items()
-            if key not in named_tuple_info.names or key != '__doc__'
-        })
-        return True
+        """Check if this class can define a named tuple."""
+        if defn.info and defn.info.tuple_type:
+            # Don't reprocess everything. We just need to process methods defined
+            # in the named tuple class body.
+            is_named_tuple, info = True, defn.info  # type: bool, Optional[TypeInfo]
+        else:
+            is_named_tuple, info = self.named_tuple_analyzer.analyze_namedtuple_classdef(defn)
+        if is_named_tuple:
+            if info is None:
+                self.mark_incomplete(defn.name, defn)
+            else:
+                self.prepare_class_def(defn, info)
+                with self.scope.class_scope(defn.info):
+                    with self.named_tuple_analyzer.save_namedtuple_body(info):
+                        self.analyze_class_body_common(defn)
+            return True
+        return False
 
     def apply_class_plugin_hooks(self, defn: ClassDef) -> None:
         """Apply a plugin hook that may infer a more precise definition for a class."""
@@ -1229,10 +1211,15 @@ def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) ->
             # in globals under mangled unique names
             #
             # TODO: Putting local classes into globals breaks assumptions in fine-grained
-            #       incremental mode and we should avoid it.
+            #       incremental mode and we should avoid it. In general, this logic is too
+            #       ad-hoc and needs to be removed/refactored.
             if '@' not in defn.info._fullname:
                 local_name = defn.info._fullname + '@' + str(defn.line)
-                defn.info._fullname = self.cur_mod_id + '.' + local_name
+                if defn.info.is_named_tuple:
+                    # Module is already correctly set in _fullname for named tuples.
+                    defn.info._fullname += '@' + str(defn.line)
+                else:
+                    defn.info._fullname = self.cur_mod_id + '.' + local_name
             else:
                 # Preserve name from previous fine-grained incremental run.
                 local_name = defn.info._fullname
@@ -1298,7 +1285,9 @@ def configure_base_classes(self,
 
         for base, base_expr in bases:
             if isinstance(base, TupleType):
-                if info.tuple_type:
+                # There may be an existing valid tuple type from previous semanal iterations.
+                # Use equality to check if it is the case.
+                if info.tuple_type and info.tuple_type != base:
                     self.fail("Class has two incompatible bases derived from tuple", defn)
                     defn.has_incompatible_baseclass = True
                 info.tuple_type = base
@@ -1426,11 +1415,15 @@ def expr_to_analyzed_type(self,
                               allow_placeholder: bool = False) -> Optional[Type]:
         if isinstance(expr, CallExpr):
             expr.accept(self)
-            info = self.named_tuple_analyzer.check_namedtuple(expr, None, self.is_func_scope())
-            if info is None:
+            is_named_tuple, info = self.named_tuple_analyzer.check_namedtuple(expr, None,
+                                                                              self.is_func_scope())
+            if not is_named_tuple:
                 # Some form of namedtuple is the only valid type that looks like a call
                 # expression. This isn't a valid type.
                 raise TypeTranslationError()
+            elif not info:
+                self.defer()
+                return None
             assert info.tuple_type, "NamedTuple without tuple type"
             fallback = Instance(info, [])
             return TupleType(info.tuple_type.items, fallback=fallback)
@@ -1888,6 +1881,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
             for expr in names_modified_by_assignment(s):
                 self.mark_incomplete(expr.name, expr)
             return
+        if self.analyze_namedtuple_assign(s):
+            return
         self.analyze_lvalues(s)
         self.check_final_implicit_def(s)
         self.check_classvar(s)
@@ -1896,7 +1891,6 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
         self.check_and_set_up_type_alias(s)
         self.newtype_analyzer.process_newtype_declaration(s)
         self.process_typevar_declaration(s)
-        self.named_tuple_analyzer.process_namedtuple_definition(s, self.is_func_scope())
         self.typed_dict_analyzer.process_typeddict_definition(s, self.is_func_scope())
         self.enum_call_analyzer.process_enum_call(s, self.is_func_scope())
         self.store_final_status(s)
@@ -1904,6 +1898,23 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
             self.process_module_assignment(s.lvalues, s.rvalue, s)
         self.process__all__(s)
 
+    def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
+        """Check if s defines a namedtuple."""
+        if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.analyzed, NamedTupleExpr):
+            return True  # This is a valid and analyzed named tuple definition, nothing to do here.
+        if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
+            return False
+        lvalue = s.lvalues[0]
+        name = lvalue.name
+        is_named_tuple, info = self.named_tuple_analyzer.check_namedtuple(s.rvalue, name,
+                                                                          self.is_func_scope())
+        if not is_named_tuple:
+            return False
+        # Yes, it's a valid namedtuple, but defer if it is not ready.
+        if not info:
+            self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
+        return True
+
     def analyze_lvalues(self, s: AssignmentStmt) -> None:
         for lval in s.lvalues:
             self.analyze_lvalue(lval,
@@ -3674,7 +3685,8 @@ def record_incomplete_ref(self) -> None:
         self.defer()
         self.num_incomplete_refs += 1
 
-    def mark_incomplete(self, name: str, node: Node) -> None:
+    def mark_incomplete(self, name: str, node: Node,
+                        becomes_typeinfo: bool = False) -> None:
         """Mark a definition as incomplete (and defer current analysis target).
 
         Also potentially mark the current namespace as incomplete.
@@ -3682,13 +3694,15 @@ def mark_incomplete(self, name: str, node: Node) -> None:
         Args:
             name: The name that we weren't able to define (or '*' if the name is unknown)
             node: The node that refers to the name (definition or lvalue)
+            becomes_typeinfo: Pass this to PlaceholderNode (used by special forms like
+                named tuples that will create TypeInfos).
         """
         self.defer()
         if name == '*':
             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, False), context=None)
+            self.add_symbol(name, PlaceholderNode(fullname, node, becomes_typeinfo), context=None)
         self.missing_names.add(name)
 
     def is_incomplete_namespace(self, fullname: str) -> bool:
diff --git a/mypy/newsemanal/semanal_main.py b/mypy/newsemanal/semanal_main.py
--- a/mypy/newsemanal/semanal_main.py
+++ b/mypy/newsemanal/semanal_main.py
@@ -123,7 +123,7 @@ def process_top_level_function(analyzer: 'NewSemanticAnalyzer',
     analyzer.incomplete_namespaces.add(module)
     while deferred and more_iterations:
         iteration += 1
-        if not incomplete or iteration == MAX_ITERATIONS:
+        if not (deferred or incomplete) or iteration == MAX_ITERATIONS:
             # OK, this is one last pass, now missing names will be reported.
             more_iterations = False
             analyzer.incomplete_namespaces.discard(module)
diff --git a/mypy/newsemanal/semanal_namedtuple.py b/mypy/newsemanal/semanal_namedtuple.py
--- a/mypy/newsemanal/semanal_namedtuple.py
+++ b/mypy/newsemanal/semanal_namedtuple.py
@@ -2,11 +2,11 @@
 
 This is conceptually part of mypy.semanal (semantic analyzer pass 2).
 """
-
-from typing import Tuple, List, Dict, Mapping, Optional, Union, cast
+from contextlib import contextmanager
+from typing import Tuple, List, Dict, Mapping, Optional, Union, cast, Iterator
 
 from mypy.types import (
-    Type, TupleType, NoneTyp, AnyType, TypeOfAny, TypeVarType, TypeVarDef, CallableType, TypeType
+    Type, TupleType, AnyType, TypeOfAny, TypeVarDef, CallableType, TypeType, TypeVarType
 )
 from mypy.newsemanal.semanal_shared import (
     SemanticAnalyzerInterface, set_callable_name, PRIORITY_FALLBACKS
@@ -15,7 +15,7 @@
     Var, EllipsisExpr, Argument, StrExpr, BytesExpr, UnicodeExpr, ExpressionStmt, NameExpr,
     AssignmentStmt, PassStmt, Decorator, FuncBase, ClassDef, Expression, RefExpr, TypeInfo,
     NamedTupleExpr, CallExpr, Context, TupleExpr, ListExpr, SymbolTableNode, FuncDef, Block,
-    TempNode, ARG_POS, ARG_NAMED_OPT, ARG_OPT, MDEF, GDEF
+    TempNode, SymbolTable, TypeVarExpr, ARG_POS, ARG_NAMED_OPT, ARG_OPT, MDEF, GDEF
 )
 from mypy.options import Options
 from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
@@ -35,35 +35,51 @@
 NAMEDTUP_CLASS_ERROR = ('Invalid statement in NamedTuple definition; '
                         'expected "field_name: field_type [= default]"')  # type: Final
 
+SELF_TVAR_NAME = '_NT'  # type: Final
+
 
 class NamedTupleAnalyzer:
     def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
         self.options = options
         self.api = api
 
-    def analyze_namedtuple_classdef(self, defn: ClassDef) -> Optional[TypeInfo]:
-        # special case for NamedTuple
+    def analyze_namedtuple_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[TypeInfo]]:
+        """Analyze if given class definition can be a named tuple definition.
+
+        Return a tuple where first item indicates whether this can possibly be a named tuple,
+        and the second item is the corresponding TypeInfo (may be None if not ready and should be
+        deferred).
+        """
         for base_expr in defn.base_type_exprs:
             if isinstance(base_expr, RefExpr):
                 self.api.accept(base_expr)
                 if base_expr.fullname == 'typing.NamedTuple':
-                    node = self.api.lookup(defn.name, defn)
-                    if node is not None:
-                        node.kind = GDEF  # TODO in process_namedtuple_definition also applies here
-                        items, types, default_items = self.check_namedtuple_classdef(defn)
-                        info = self.build_namedtuple_typeinfo(
-                            defn.name, items, types, default_items)
-                        node.node = info
-                        defn.info.replaced = info
-                        defn.info = info
-                        defn.analyzed = NamedTupleExpr(info, is_typed=True)
-                        defn.analyzed.line = defn.line
-                        defn.analyzed.column = defn.column
-                        return info
-        return None
+                    result = self.check_namedtuple_classdef(defn)
+                    if result is None:
+                        # This is a valid named tuple, but some types are incomplete.
+                        return True, None
+                    items, types, default_items = result
+                    info = self.build_namedtuple_typeinfo(
+                        defn.name, items, types, default_items)
+                    defn.info = info
+                    defn.analyzed = NamedTupleExpr(info, is_typed=True)
+                    defn.analyzed.line = defn.line
+                    defn.analyzed.column = defn.column
+                    # All done: this is a valid named tuple with all types known.
+                    return True, info
+        # This can't be a valid named tuple.
+        return False, None
 
     def check_namedtuple_classdef(
-            self, defn: ClassDef) -> Tuple[List[str], List[Type], Dict[str, Expression]]:
+            self, defn: ClassDef) -> Optional[Tuple[List[str], List[Type], Dict[str, Expression]]]:
+        """Parse and validate fields in named tuple class definition.
+
+        Return a three tuple:
+          * field names
+          * field types
+          * field default values
+        or None, if any of the types are not ready.
+        """
         if self.options.python_version < (3, 6):
             self.fail('NamedTuple class syntax is only supported in Python 3.6', defn)
             return [], [], {}
@@ -98,7 +114,9 @@ def check_namedtuple_classdef(
                     types.append(AnyType(TypeOfAny.unannotated))
                 else:
                     analyzed = self.api.anal_type(stmt.type)
-                    assert analyzed is not None  # TODO: Handle None values
+                    if analyzed is None:
+                        # Something is incomplete. We need to defer this named tuple.
+                        return None
                     types.append(analyzed)
                 # ...despite possible minor failures that allow further analyzis.
                 if name.startswith('_'):
@@ -115,52 +133,41 @@ def check_namedtuple_classdef(
                     default_items[name] = stmt.rvalue
         return items, types, default_items
 
-    def process_namedtuple_definition(self, s: AssignmentStmt, is_func_scope: bool) -> None:
-        """Check if s defines a namedtuple; if yes, store the definition in symbol table.
-
-        Assume that s has already been successfully analyzed.
-        """
-        if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
-            return
-        lvalue = s.lvalues[0]
-        name = lvalue.name
-        named_tuple = self.check_namedtuple(s.rvalue, name, is_func_scope)
-        if named_tuple is None:
-            return
-        # Yes, it's a valid namedtuple definition. Add it to the symbol table.
-        node = self.api.lookup(name, s)
-        assert node is not None
-        node.kind = GDEF   # TODO locally defined namedtuple
-        node.node = named_tuple
-
     def check_namedtuple(self,
                          node: Expression,
                          var_name: Optional[str],
-                         is_func_scope: bool) -> Optional[TypeInfo]:
+                         is_func_scope: bool) -> Tuple[bool, Optional[TypeInfo]]:
         """Check if a call defines a namedtuple.
 
         The optional var_name argument is the name of the variable to
         which this is assigned, if any.
 
-        If it does, return the corresponding TypeInfo. Return None otherwise.
+        Return a tuple of two items:
+          * Can it be a valid named tuple?
+          * Corresponding TypeInfo, or None if not ready.
 
         If the definition is invalid but looks like a namedtuple,
         report errors but return (some) TypeInfo.
         """
         if not isinstance(node, CallExpr):
-            return None
+            return False, None
         call = node
         callee = call.callee
         if not isinstance(callee, RefExpr):
-            return None
+            return False, None
         fullname = callee.fullname
         if fullname == 'collections.namedtuple':
             is_typed = False
         elif fullname == 'typing.NamedTuple':
             is_typed = True
         else:
-            return None
-        items, types, defaults, ok = self.parse_namedtuple_args(call, fullname)
+            return False, None
+        result = self.parse_namedtuple_args(call, fullname)
+        if result:
+            items, types, defaults, ok = result
+        else:
+            # This is a valid named tuple but some types are not ready.
+            return True, None
         if not ok:
             # Error. Construct dummy return value.
             if var_name:
@@ -169,7 +176,7 @@ def check_namedtuple(self,
                 name = 'namedtuple@' + str(call.line)
             info = self.build_namedtuple_typeinfo(name, [], [], {})
             self.store_namedtuple_info(info, name, call, is_typed)
-            return info
+            return True, info
         name = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value
         if name != var_name or is_func_scope:
             # Give it a unique name derived from the line number.
@@ -184,8 +191,8 @@ def check_namedtuple(self,
         info = self.build_namedtuple_typeinfo(name, items, types, default_items)
         # Store it as a global just in case it would remain anonymous.
         # (Or in the nearest class if there is one.)
-        self.store_namedtuple_info(info, name, call, is_typed)
-        return info
+        self.store_namedtuple_info(info, var_name or name, call, is_typed)
+        return True, info
 
     def store_namedtuple_info(self, info: TypeInfo, name: str,
                               call: CallExpr, is_typed: bool) -> None:
@@ -195,7 +202,7 @@ def store_namedtuple_info(self, info: TypeInfo, name: str,
         call.analyzed.set_line(call.line, call.column)
 
     def parse_namedtuple_args(self, call: CallExpr, fullname: str
-                              ) -> Tuple[List[str], List[Type], List[Expression], bool]:
+                              ) -> Optional[Tuple[List[str], List[Type], List[Expression], bool]]:
         """Parse a namedtuple() call into data needed to construct a type.
 
         Returns a 4-tuple:
@@ -204,6 +211,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
         - Number of arguments that have a default value
         - Whether the definition typechecked.
 
+        Return None if at least one of the types is not ready.
         """
         # TODO: Share code with check_argument_count in checkexpr.py?
         args = call.args
@@ -255,7 +263,12 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
                          for item in listexpr.items]
             else:
                 # The fields argument contains (name, type) tuples.
-                items, types, _, ok = self.parse_namedtuple_fields_with_types(listexpr.items, call)
+                result = self.parse_namedtuple_fields_with_types(listexpr.items, call)
+                if result:
+                    items, types, _, ok = result
+                else:
+                    # One of the types is not ready, defer.
+                    return None
         if not types:
             types = [AnyType(TypeOfAny.unannotated) for _ in items]
         underscore = [item for item in items if item.startswith('_')]
@@ -268,8 +281,14 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
         return items, types, defaults, ok
 
     def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: Context
-                                           ) -> Tuple[List[str], List[Type], List[Expression],
-                                                      bool]:
+                                           ) -> Optional[Tuple[List[str], List[Type],
+                                                               List[Expression],
+                                                         bool]]:
+        """Parse typed named tuple fields.
+
+        Return (names, types, defaults, error ocurred), or None if at least one of
+        the types is not ready.
+        """
         items = []  # type: List[str]
         types = []  # type: List[Type]
         for item in nodes:
@@ -287,7 +306,9 @@ def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: C
                 except TypeTranslationError:
                     return self.fail_namedtuple_arg('Invalid field type', type_node)
                 analyzed = self.api.anal_type(type)
-                assert analyzed is not None  # TODO: Handle None values
+                # These should be all known, otherwise we would defer in visit_assignment_stmt().
+                if analyzed is None:
+                    return None
                 types.append(analyzed)
             else:
                 return self.fail_namedtuple_arg("Tuple expected as NamedTuple() field", item)
@@ -348,13 +369,13 @@ def add_field(var: Var, is_initialized_in_class: bool = False,
         add_field(Var('__annotations__', ordereddictype), is_initialized_in_class=True)
         add_field(Var('__doc__', strtype), is_initialized_in_class=True)
 
-        tvd = TypeVarDef('NT', 'NT', -1, [], info.tuple_type)
+        tvd = TypeVarDef(SELF_TVAR_NAME, info.fullname() + '.' + SELF_TVAR_NAME,
+                         -1, [], info.tuple_type)
         selftype = TypeVarType(tvd)
 
         def add_method(funcname: str,
                        ret: Type,
                        args: List[Argument],
-                       name: Optional[str] = None,
                        is_classmethod: bool = False,
                        is_new: bool = False,
                        ) -> None:
@@ -381,6 +402,7 @@ def add_method(funcname: str,
                 v.is_classmethod = True
                 v.info = info
                 v._fullname = func._fullname
+                func.is_decorated = True
                 dec = Decorator(func, [NameExpr('classmethod')], v)
                 info.names[funcname] = SymbolTableNode(MDEF, dec)
             else:
@@ -394,7 +416,7 @@ def make_init_arg(var: Var) -> Argument:
             kind = ARG_POS if default is None else ARG_OPT
             return Argument(var, var.type, default, kind)
 
-        add_method('__new__', ret=selftype, name=info.name(),
+        add_method('__new__', ret=selftype,
                    args=[make_init_arg(var) for var in vars],
                    is_new=True)
         add_method('_asdict', args=[], ret=ordereddictype)
@@ -403,8 +425,44 @@ def make_init_arg(var: Var) -> Argument:
                    args=[Argument(Var('iterable', iterable_type), iterable_type, None, ARG_POS),
                          Argument(Var('new'), special_form_any, EllipsisExpr(), ARG_NAMED_OPT),
                          Argument(Var('len'), special_form_any, EllipsisExpr(), ARG_NAMED_OPT)])
+
+        self_tvar_expr = TypeVarExpr(SELF_TVAR_NAME, info.fullname() + '.' + SELF_TVAR_NAME,
+                                     [], info.tuple_type)
+        info.names[SELF_TVAR_NAME] = SymbolTableNode(MDEF, self_tvar_expr)
         return info
 
+    @contextmanager
+    def save_namedtuple_body(self, named_tuple_info: TypeInfo) -> Iterator[None]:
+        """Preserve the generated body of class-based named tuple and then restore it.
+
+        Temporarily clear the names dict so we don't get errors about duplicate names
+        that were already set in build_namedtuple_typeinfo (we already added the tuple
+        field names while generating the TypeInfo, and actual duplicates are
+        already reported).
+        """
+        nt_names = named_tuple_info.names
+        named_tuple_info.names = SymbolTable()
+
+        yield
+
+        # Make sure we didn't use illegal names, then reset the names in the typeinfo.
+        for prohibited in NAMEDTUPLE_PROHIBITED_NAMES:
+            if prohibited in named_tuple_info.names:
+                if nt_names.get(prohibited) is named_tuple_info.names[prohibited]:
+                    continue
+                ctx = named_tuple_info.names[prohibited].node
+                assert ctx is not None
+                self.fail('Cannot overwrite NamedTuple attribute "{}"'.format(prohibited),
+                          ctx)
+
+        # Restore the names in the original symbol table. This ensures that the symbol
+        # table contains the field objects created by build_namedtuple_typeinfo. Exclude
+        # __doc__, which can legally be overwritten by the class.
+        named_tuple_info.names.update({
+            key: value for key, value in nt_names.items()
+            if key not in named_tuple_info.names or key != '__doc__'
+        })
+
     # Helpers
 
     def fail(self, msg: str, ctx: Context) -> None:


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
@@ -685,6 +685,163 @@ import a
 reveal_type(a.C()) # E: Revealed type is 'a.C'
 def f(): pass
 
+[case testNewAnalyzerNamedTupleCall]
+from typing import NamedTuple
+
+o: Out
+i: In
+
+Out = NamedTuple('Out', [('x', In), ('y', Other)])
+
+reveal_type(o)  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.Other, fallback=__main__.In], __main__.Other, fallback=__main__.Out]'
+reveal_type(o.x)  # E: Revealed type is 'Tuple[builtins.str, __main__.Other, fallback=__main__.In]'
+reveal_type(o.y)  # E: Revealed type is '__main__.Other'
+reveal_type(o.x.t)  # E: Revealed type is '__main__.Other'
+reveal_type(i.t)  # E: Revealed type is '__main__.Other'
+
+In = NamedTuple('In', [('s', str), ('t', Other)])
+class Other: pass
+
+[case testNewAnalyzerNamedTupleClass]
+from typing import NamedTuple
+
+o: Out
+i: In
+
+class Out(NamedTuple):
+    x: In
+    y: Other
+
+reveal_type(o)  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.Other, fallback=__main__.In], __main__.Other, fallback=__main__.Out]'
+reveal_type(o.x)  # E: Revealed type is 'Tuple[builtins.str, __main__.Other, fallback=__main__.In]'
+reveal_type(o.y)  # E: Revealed type is '__main__.Other'
+reveal_type(o.x.t)  # E: Revealed type is '__main__.Other'
+reveal_type(i.t)  # E: Revealed type is '__main__.Other'
+
+class In(NamedTuple):
+    s: str
+    t: Other
+class Other: pass
+
+[case testNewAnalyzerNamedTupleCallNested]
+from typing import NamedTuple
+
+o: C.Out
+i: C.In
+
+reveal_type(o)  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.C.Other, fallback=__main__.C.In], __main__.C.Other, fallback=__main__.C.Out]'
+reveal_type(o.x)  # E: Revealed type is 'Tuple[builtins.str, __main__.C.Other, fallback=__main__.C.In]'
+reveal_type(o.y)  # E: Revealed type is '__main__.C.Other'
+reveal_type(o.x.t)  # E: Revealed type is '__main__.C.Other'
+reveal_type(i.t)  # E: Revealed type is '__main__.C.Other'
+
+class C:
+    Out = NamedTuple('Out', [('x', In), ('y', Other)])
+    In = NamedTuple('In', [('s', str), ('t', Other)])
+    class Other: pass
+
+
+[case testNewAnalyzerNamedTupleClassNested]
+from typing import NamedTuple
+
+o: C.Out
+i: C.In
+
+reveal_type(o)  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.C.Other, fallback=__main__.C.In], __main__.C.Other, fallback=__main__.C.Out]'
+reveal_type(o.x)  # E: Revealed type is 'Tuple[builtins.str, __main__.C.Other, fallback=__main__.C.In]'
+reveal_type(o.y)  # E: Revealed type is '__main__.C.Other'
+reveal_type(o.x.t)  # E: Revealed type is '__main__.C.Other'
+reveal_type(i.t)  # E: Revealed type is '__main__.C.Other'
+
+class C:
+    class Out(NamedTuple):
+        x: C.In
+        y: C.Other
+    class In(NamedTuple):
+        s: str
+        t: C.Other
+    class Other: pass
+
+[case testNewAnalyzerNamedTupleCallNestedMethod]
+from typing import NamedTuple
+
+c = C()
+reveal_type(c.o)  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.Other@12, fallback=__main__.C.In@11], __main__.Other@12, fallback=__main__.C.Out@10]'
+reveal_type(c.o.x)  # E: Revealed type is 'Tuple[builtins.str, __main__.Other@12, fallback=__main__.C.In@11]'
+
+class C:
+    def get_tuple(self) -> None:
+        self.o: Out
+        Out = NamedTuple('Out', [('x', In), ('y', Other)])
+        In = NamedTuple('In', [('s', str), ('t', Other)])
+        class Other: pass
+
+[case testNewAnalyzerNamedTupleClassNestedMethod]
+from typing import NamedTuple
+
+c = C()
+reveal_type(c.o)  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.Other@18, fallback=__main__.C.In@15], __main__.Other@18, fallback=__main__.C.Out@11]'
+reveal_type(c.o.x)  # E: Revealed type is 'Tuple[builtins.str, __main__.Other@18, fallback=__main__.C.In@15]'
+reveal_type(c.o.method())  # E: Revealed type is 'Tuple[builtins.str, __main__.Other@18, fallback=__main__.C.In@15]'
+
+class C:
+    def get_tuple(self) -> None:
+        self.o: Out
+        class Out(NamedTuple):
+            x: In
+            y: Other
+            def method(self) -> In: ...
+        class In(NamedTuple):
+            s: str
+            t: Other
+        class Other: pass
+
+[case testNewAnalyzerNamedTupleClassForwardMethod]
+from typing import NamedTuple
+
+n: NT
+reveal_type(n.get_other())  # E: Revealed type is 'Tuple[builtins.str, fallback=__main__.Other]'
+reveal_type(n.get_other().s)  # E: Revealed type is 'builtins.str'
+
+class NT(NamedTuple):
+    x: int
+    y: int
+    def get_other(self) -> Other: pass
+
+class Other(NamedTuple):
+    s: str
+
+[case testNewAnalyzerNamedTupleSpecialMethods]
+from typing import NamedTuple
+
+o: SubO
+
+reveal_type(SubO._make)  # E: Revealed type is 'def (iterable: typing.Iterable[Any], *, new: Any =, len: Any =) -> Tuple[Tuple[builtins.str, __main__.Other, fallback=__main__.In], __main__.Other, fallback=__main__.SubO]'
+reveal_type(o._replace(y=Other()))  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.Other, fallback=__main__.In], __main__.Other, fallback=__main__.SubO]'
+
+class SubO(Out): pass
+
+Out = NamedTuple('Out', [('x', In), ('y', Other)])
+In = NamedTuple('In', [('s', str), ('t', Other)])
+class Other: pass
+
+[case testNewAnalyzerNamedTupleBaseClass]
+from typing import NamedTuple
+
+o: Out
+reveal_type(o)  # E: Revealed type is 'Tuple[Tuple[builtins.str, __main__.Other, fallback=__main__.In], __main__.Other, fallback=__main__.Out]'
+reveal_type(o.x)  # E: Revealed type is 'Tuple[builtins.str, __main__.Other, fallback=__main__.In]'
+reveal_type(o.x.t)  # E: Revealed type is '__main__.Other'
+reveal_type(Out._make)  # E: Revealed type is 'def (iterable: typing.Iterable[Any], *, new: Any =, len: Any =) -> Tuple[Tuple[builtins.str, __main__.Other, fallback=__main__.In], __main__.Other, fallback=__main__.Out]'
+
+class Out(NamedTuple('Out', [('x', In), ('y', Other)])):
+    pass
+
+class In(NamedTuple):
+    s: str
+    t: Other
+class Other: pass
+
 [case testNewAnalyzerIncompleteRefShadowsBuiltin1]
 import a