Repository: python/mypy

Issue:
Check first argument of NamedTuple/namedtuple
Mypy doesn't complain about this code, even though the name arguments to `NamedTuple` and `namedtuple` are inconsistent with the assignment targets:

```py
from typing import NamedTuple
from collections import namedtuple

A = NamedTuple('X', [('a', int)])  # First argument should be 'A'?
B = namedtuple('X', ['a'])  # First argument should be 'B'?
```

I'd expect mypy to complain about the `'X'` arguments in both cases.


Discussion:
By the way I have noticed this kind of mistake several times during last week, so it may be important to fix.

Resolving patch:
diff --git a/mypy/plugin.py b/mypy/plugin.py
--- a/mypy/plugin.py
+++ b/mypy/plugin.py
@@ -359,7 +359,7 @@ def final_iteration(self) -> bool:
 # A context for querying for configuration data about a module for
 # cache invalidation purposes.
 ReportConfigContext = NamedTuple(
-    'DynamicClassDefContext', [
+    'ReportConfigContext', [
         ('id', str),        # Module name
         ('path', str),      # Module file path
         ('is_check', bool)  # Is this invocation for checking whether the config matches
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -2177,13 +2177,17 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
             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:
+        internal_name, info = self.named_tuple_analyzer.check_namedtuple(s.rvalue, name,
+                                                                         self.is_func_scope())
+        if internal_name is None:
             return False
         if isinstance(lvalue, MemberExpr):
             self.fail("NamedTuple type as an attribute is not supported", lvalue)
             return False
+        if internal_name != name:
+            self.fail("First argument to namedtuple() should be '{}', not '{}'".format(
+                name, internal_name), s.rvalue)
+            return True
         # Yes, it's a valid namedtuple, but defer if it is not ready.
         if not info:
             self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
@@ -4819,9 +4823,9 @@ def expr_to_analyzed_type(self,
                               allow_placeholder: bool = False) -> Optional[Type]:
         if isinstance(expr, CallExpr):
             expr.accept(self)
-            is_named_tuple, info = self.named_tuple_analyzer.check_namedtuple(expr, None,
-                                                                              self.is_func_scope())
-            if not is_named_tuple:
+            internal_name, info = self.named_tuple_analyzer.check_namedtuple(expr, None,
+                                                                             self.is_func_scope())
+            if internal_name is None:
                 # Some form of namedtuple is the only valid type that looks like a call
                 # expression. This isn't a valid type.
                 raise TypeTranslationError()
diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py
--- a/mypy/semanal_namedtuple.py
+++ b/mypy/semanal_namedtuple.py
@@ -138,39 +138,37 @@ def check_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool
     def check_namedtuple(self,
                          node: Expression,
                          var_name: Optional[str],
-                         is_func_scope: bool) -> Tuple[bool, Optional[TypeInfo]]:
+                         is_func_scope: bool) -> Tuple[Optional[str], 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.
 
         Return a tuple of two items:
-          * Can it be a valid named tuple?
+          * Internal name of the named tuple (e.g. the name passed as an argument to namedtuple)
+            or None if it is not 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 False, None
+            return None, None
         call = node
         callee = call.callee
         if not isinstance(callee, RefExpr):
-            return False, None
+            return None, None
         fullname = callee.fullname
         if fullname == 'collections.namedtuple':
             is_typed = False
         elif fullname == 'typing.NamedTuple':
             is_typed = True
         else:
-            return False, None
+            return None, None
         result = self.parse_namedtuple_args(call, fullname)
         if result:
-            items, types, defaults, ok = result
+            items, types, defaults, typename, 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:
                 name = var_name
@@ -178,7 +176,10 @@ def check_namedtuple(self,
                 name = 'namedtuple@' + str(call.line)
             info = self.build_namedtuple_typeinfo(name, [], [], {}, node.line)
             self.store_namedtuple_info(info, name, call, is_typed)
-            return True, info
+            return name, info
+        if not ok:
+            # This is a valid named tuple but some types are not ready.
+            return typename, None
 
         # We use the variable name as the class name if it exists. If
         # it doesn't, we use the name passed as an argument. We prefer
@@ -188,7 +189,7 @@ def check_namedtuple(self,
         if var_name:
             name = var_name
         else:
-            name = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value
+            name = typename
 
         if var_name is None or is_func_scope:
             # There are two special cases where need to give it a unique name derived
@@ -228,7 +229,7 @@ def check_namedtuple(self,
         if name != var_name or is_func_scope:
             # NOTE: we skip local namespaces since they are not serialized.
             self.api.add_symbol_skip_local(name, info)
-        return True, info
+        return typename, info
 
     def store_namedtuple_info(self, info: TypeInfo, name: str,
                               call: CallExpr, is_typed: bool) -> None:
@@ -237,26 +238,30 @@ 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
-                              ) -> Optional[Tuple[List[str], List[Type], List[Expression], bool]]:
+                              ) -> Optional[Tuple[List[str], List[Type], List[Expression],
+                                            str, bool]]:
         """Parse a namedtuple() call into data needed to construct a type.
 
-        Returns a 4-tuple:
+        Returns a 5-tuple:
         - List of argument names
         - List of argument types
-        - Number of arguments that have a default value
-        - Whether the definition typechecked.
+        - List of default values
+        - First argument of namedtuple
+        - Whether all types are ready.
 
-        Return None if at least one of the types is not ready.
+        Return None if the definition didn't typecheck.
         """
         # TODO: Share code with check_argument_count in checkexpr.py?
         args = call.args
         if len(args) < 2:
-            return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call)
+            self.fail("Too few arguments for namedtuple()", call)
+            return None
         defaults = []  # type: List[Expression]
         if len(args) > 2:
             # Typed namedtuple doesn't support additional arguments.
             if fullname == 'typing.NamedTuple':
-                return self.fail_namedtuple_arg("Too many arguments for NamedTuple()", call)
+                self.fail("Too many arguments for NamedTuple()", call)
+                return None
             for i, arg_name in enumerate(call.arg_names[2:], 2):
                 if arg_name == 'defaults':
                     arg = args[i]
@@ -272,38 +277,42 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
                         )
                     break
         if call.arg_kinds[:2] != [ARG_POS, ARG_POS]:
-            return self.fail_namedtuple_arg("Unexpected arguments to namedtuple()", call)
+            self.fail("Unexpected arguments to namedtuple()", call)
+            return None
         if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)):
-            return self.fail_namedtuple_arg(
+            self.fail(
                 "namedtuple() expects a string literal as the first argument", call)
+            return None
+        typename = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value
         types = []  # type: List[Type]
-        ok = True
         if not isinstance(args[1], (ListExpr, TupleExpr)):
             if (fullname == 'collections.namedtuple'
                     and isinstance(args[1], (StrExpr, BytesExpr, UnicodeExpr))):
                 str_expr = args[1]
                 items = str_expr.value.replace(',', ' ').split()
             else:
-                return self.fail_namedtuple_arg(
+                self.fail(
                     "List or tuple literal expected as the second argument to namedtuple()", call)
+                return None
         else:
             listexpr = args[1]
             if fullname == 'collections.namedtuple':
                 # The fields argument contains just names, with implicit Any types.
                 if any(not isinstance(item, (StrExpr, BytesExpr, UnicodeExpr))
                        for item in listexpr.items):
-                    return self.fail_namedtuple_arg("String literal expected as namedtuple() item",
-                                                    call)
+                    self.fail("String literal expected as namedtuple() item", call)
+                    return None
                 items = [cast(Union[StrExpr, BytesExpr, UnicodeExpr], item).value
                          for item in listexpr.items]
             else:
                 # The fields argument contains (name, type) tuples.
                 result = self.parse_namedtuple_fields_with_types(listexpr.items, call)
-                if result:
-                    items, types, _, ok = result
-                else:
+                if result is None:
                     # One of the types is not ready, defer.
                     return None
+                items, types, _, ok = result
+                if not ok:
+                    return [], [], [], typename, False
         if not types:
             types = [AnyType(TypeOfAny.unannotated) for _ in items]
         underscore = [item for item in items if item.startswith('_')]
@@ -313,50 +322,46 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
         if len(defaults) > len(items):
             self.fail("Too many defaults given in call to namedtuple()", call)
             defaults = defaults[:len(items)]
-        return items, types, defaults, ok
+        return items, types, defaults, typename, True
 
     def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: Context
                                            ) -> Optional[Tuple[List[str], List[Type],
-                                                               List[Expression],
-                                                         bool]]:
+                                                               List[Expression], bool]]:
         """Parse typed named tuple fields.
 
-        Return (names, types, defaults, error occurred), or None if at least one of
-        the types is not ready.
+        Return (names, types, defaults, whether types are all ready), or None if error occurred.
         """
         items = []  # type: List[str]
         types = []  # type: List[Type]
         for item in nodes:
             if isinstance(item, TupleExpr):
                 if len(item.items) != 2:
-                    return self.fail_namedtuple_arg("Invalid NamedTuple field definition",
-                                                    item)
+                    self.fail("Invalid NamedTuple field definition", item)
+                    return None
                 name, type_node = item.items
                 if isinstance(name, (StrExpr, BytesExpr, UnicodeExpr)):
                     items.append(name.value)
                 else:
-                    return self.fail_namedtuple_arg("Invalid NamedTuple() field name", item)
+                    self.fail("Invalid NamedTuple() field name", item)
+                    return None
                 try:
                     type = expr_to_unanalyzed_type(type_node)
                 except TypeTranslationError:
-                    return self.fail_namedtuple_arg('Invalid field type', type_node)
+                    self.fail('Invalid field type', type_node)
+                    return None
                 analyzed = self.api.anal_type(type)
                 # Workaround #4987 and avoid introducing a bogus UnboundType
                 if isinstance(analyzed, UnboundType):
                     analyzed = AnyType(TypeOfAny.from_error)
                 # These should be all known, otherwise we would defer in visit_assignment_stmt().
                 if analyzed is None:
-                    return None
+                    return [], [], [], False
                 types.append(analyzed)
             else:
-                return self.fail_namedtuple_arg("Tuple expected as NamedTuple() field", item)
+                self.fail("Tuple expected as NamedTuple() field", item)
+                return None
         return items, types, [], True
 
-    def fail_namedtuple_arg(self, message: str, context: Context
-                            ) -> Tuple[List[str], List[Type], List[Expression], bool]:
-        self.fail(message, context)
-        return [], [], [], False
-
     def build_namedtuple_typeinfo(self,
                                   name: str,
                                   items: List[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
@@ -5056,7 +5056,9 @@ from typing import NamedTuple
 NT = NamedTuple('BadName', [('x', int)])
 [builtins fixtures/tuple.pyi]
 [out]
+tmp/b.py:2: error: First argument to namedtuple() should be 'NT', not 'BadName'
 [out2]
+tmp/b.py:2: error: First argument to namedtuple() should be 'NT', not 'BadName'
 tmp/a.py:3: note: Revealed type is 'Tuple[builtins.int, fallback=b.NT]'
 
 [case testNewAnalyzerIncrementalBrokenNamedTupleNested]
@@ -5076,7 +5078,9 @@ def test() -> None:
     NT = namedtuple('BadName', ['x', 'y'])
 [builtins fixtures/list.pyi]
 [out]
+tmp/b.py:4: error: First argument to namedtuple() should be 'NT', not 'BadName'
 [out2]
+tmp/b.py:4: error: First argument to namedtuple() should be 'NT', not 'BadName'
 
 [case testNewAnalyzerIncrementalMethodNamedTuple]
 
diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test
--- a/test-data/unit/check-namedtuple.test
+++ b/test-data/unit/check-namedtuple.test
@@ -962,3 +962,14 @@ def foo():
 Type1 = NamedTuple('Type1', [('foo', foo)])  # E: Function "b.foo" is not valid as a type  # N: Perhaps you need "Callable[...]" or a callback protocol?
 
 [builtins fixtures/tuple.pyi]
+
+[case testNamedTupleTypeNameMatchesVariableName]
+from typing import NamedTuple
+from collections import namedtuple
+
+A = NamedTuple('X', [('a', int)])  # E: First argument to namedtuple() should be 'A', not 'X'
+B = namedtuple('X', ['a'])         # E: First argument to namedtuple() should be 'B', not 'X'
+
+C = NamedTuple('X', [('a', 'Y')])  # E: First argument to namedtuple() should be 'C', not 'X'
+class Y: ...
+[builtins fixtures/tuple.pyi]
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
@@ -9622,26 +9622,3 @@ class C:
 [out]
 ==
 main:5: error: Unsupported left operand type for + ("str")
-
-[case testReexportNamedTupleChange]
-from m import M
-
-def f(x: M) -> None: ...
-
-f(M(0))
-
-[file m.py]
-from n import M
-
-[file n.py]
-from typing import NamedTuple
-M = NamedTuple('_N', [('x', int)])
-
-[file n.py.2]
-# change the line numbers
-from typing import NamedTuple
-M = NamedTuple('_N', [('x', int)])
-
-[builtins fixtures/tuple.pyi]
-[out]
-==