Skip to content

Commit 1d6a5b1

Browse files
Fix daemon crash on malformed NamedTuple (#14119)
Fixes #14098 Having invalid statements in a NamedTuple is almost like a syntax error, we can remove them after giving an error (without further analysis). This PR does almost exactly the same as #13963 did for TypedDicts. Co-authored-by: Shantanu <[email protected]>
1 parent b650d96 commit 1d6a5b1

File tree

6 files changed

+94
-7
lines changed

6 files changed

+94
-7
lines changed

mypy/nodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ class ClassDef(Statement):
10681068
"analyzed",
10691069
"has_incompatible_baseclass",
10701070
"deco_line",
1071+
"removed_statements",
10711072
)
10721073

10731074
__match_args__ = ("name", "defs")
@@ -1086,6 +1087,8 @@ class ClassDef(Statement):
10861087
keywords: dict[str, Expression]
10871088
analyzed: Expression | None
10881089
has_incompatible_baseclass: bool
1090+
# Used by special forms like NamedTuple and TypedDict to store invalid statements
1091+
removed_statements: list[Statement]
10891092

10901093
def __init__(
10911094
self,
@@ -1111,6 +1114,7 @@ def __init__(
11111114
self.has_incompatible_baseclass = False
11121115
# Used for error reporting (to keep backwad compatibility with pre-3.8)
11131116
self.deco_line: int | None = None
1117+
self.removed_statements = []
11141118

11151119
def accept(self, visitor: StatementVisitor[T]) -> T:
11161120
return visitor.visit_class_def(self)

mypy/semanal_namedtuple.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
NameExpr,
3333
PassStmt,
3434
RefExpr,
35+
Statement,
3536
StrExpr,
3637
SymbolTable,
3738
SymbolTableNode,
@@ -111,7 +112,7 @@ def analyze_namedtuple_classdef(
111112
if result is None:
112113
# This is a valid named tuple, but some types are incomplete.
113114
return True, None
114-
items, types, default_items = result
115+
items, types, default_items, statements = result
115116
if is_func_scope and "@" not in defn.name:
116117
defn.name += "@" + str(defn.line)
117118
existing_info = None
@@ -123,31 +124,35 @@ def analyze_namedtuple_classdef(
123124
defn.analyzed = NamedTupleExpr(info, is_typed=True)
124125
defn.analyzed.line = defn.line
125126
defn.analyzed.column = defn.column
127+
defn.defs.body = statements
126128
# All done: this is a valid named tuple with all types known.
127129
return True, info
128130
# This can't be a valid named tuple.
129131
return False, None
130132

131133
def check_namedtuple_classdef(
132134
self, defn: ClassDef, is_stub_file: bool
133-
) -> tuple[list[str], list[Type], dict[str, Expression]] | None:
135+
) -> tuple[list[str], list[Type], dict[str, Expression], list[Statement]] | None:
134136
"""Parse and validate fields in named tuple class definition.
135137
136-
Return a three tuple:
138+
Return a four tuple:
137139
* field names
138140
* field types
139141
* field default values
142+
* valid statements
140143
or None, if any of the types are not ready.
141144
"""
142145
if self.options.python_version < (3, 6) and not is_stub_file:
143146
self.fail("NamedTuple class syntax is only supported in Python 3.6", defn)
144-
return [], [], {}
147+
return [], [], {}, []
145148
if len(defn.base_type_exprs) > 1:
146149
self.fail("NamedTuple should be a single base", defn)
147150
items: list[str] = []
148151
types: list[Type] = []
149152
default_items: dict[str, Expression] = {}
153+
statements: list[Statement] = []
150154
for stmt in defn.defs.body:
155+
statements.append(stmt)
151156
if not isinstance(stmt, AssignmentStmt):
152157
# Still allow pass or ... (for empty namedtuples).
153158
if isinstance(stmt, PassStmt) or (
@@ -160,9 +165,13 @@ def check_namedtuple_classdef(
160165
# And docstrings.
161166
if isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
162167
continue
168+
statements.pop()
169+
defn.removed_statements.append(stmt)
163170
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
164171
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
165172
# An assignment, but an invalid one.
173+
statements.pop()
174+
defn.removed_statements.append(stmt)
166175
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
167176
else:
168177
# Append name and type in this case...
@@ -199,7 +208,7 @@ def check_namedtuple_classdef(
199208
)
200209
else:
201210
default_items[name] = stmt.rvalue
202-
return items, types, default_items
211+
return items, types, default_items, statements
203212

204213
def check_namedtuple(
205214
self, node: Expression, var_name: str | None, is_func_scope: bool

mypy/semanal_typeddict.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,11 @@ def analyze_typeddict_classdef_fields(
283283
):
284284
statements.append(stmt)
285285
else:
286+
defn.removed_statements.append(stmt)
286287
self.fail(TPDICT_CLASS_ERROR, stmt)
287288
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
288289
# An assignment, but an invalid one.
290+
defn.removed_statements.append(stmt)
289291
self.fail(TPDICT_CLASS_ERROR, stmt)
290292
else:
291293
name = stmt.lvalues[0].name

mypy/server/aststrip.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ def visit_class_def(self, node: ClassDef) -> None:
140140
]
141141
with self.enter_class(node.info):
142142
super().visit_class_def(node)
143+
node.defs.body.extend(node.removed_statements)
144+
node.removed_statements = []
143145
TypeState.reset_subtype_caches_for(node.info)
144146
# Kill the TypeInfo, since there is none before semantic analysis.
145147
node.info = CLASSDEF_NO_INFO

test-data/unit/check-class-namedtuple.test

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,6 @@ class X(typing.NamedTuple):
393393
[out]
394394
main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
395395
main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
396-
main:7: error: Type cannot be declared in assignment to non-self attribute
397-
main:7: error: "int" has no attribute "x"
398396
main:9: error: Non-default NamedTuple fields cannot follow default fields
399397

400398
[builtins fixtures/list.pyi]

test-data/unit/fine-grained.test

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10205,3 +10205,75 @@ C
1020510205
[builtins fixtures/dict.pyi]
1020610206
[out]
1020710207
==
10208+
10209+
[case testNamedTupleNestedCrash]
10210+
import m
10211+
[file m.py]
10212+
from typing import NamedTuple
10213+
10214+
class NT(NamedTuple):
10215+
class C: ...
10216+
x: int
10217+
y: int
10218+
10219+
[file m.py.2]
10220+
from typing import NamedTuple
10221+
10222+
class NT(NamedTuple):
10223+
class C: ...
10224+
x: int
10225+
y: int
10226+
# change
10227+
[builtins fixtures/tuple.pyi]
10228+
[out]
10229+
m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
10230+
==
10231+
m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
10232+
10233+
[case testNamedTupleNestedClassRecheck]
10234+
import n
10235+
[file n.py]
10236+
import m
10237+
x: m.NT
10238+
[file m.py]
10239+
from typing import NamedTuple
10240+
from f import A
10241+
10242+
class NT(NamedTuple):
10243+
class C: ...
10244+
x: int
10245+
y: A
10246+
10247+
[file f.py]
10248+
A = int
10249+
[file f.py.2]
10250+
A = str
10251+
[builtins fixtures/tuple.pyi]
10252+
[out]
10253+
m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
10254+
==
10255+
m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
10256+
10257+
[case testTypedDictNestedClassRecheck]
10258+
import n
10259+
[file n.py]
10260+
import m
10261+
x: m.TD
10262+
[file m.py]
10263+
from typing_extensions import TypedDict
10264+
from f import A
10265+
10266+
class TD(TypedDict):
10267+
class C: ...
10268+
x: int
10269+
y: A
10270+
10271+
[file f.py]
10272+
A = int
10273+
[file f.py.2]
10274+
A = str
10275+
[builtins fixtures/dict.pyi]
10276+
[out]
10277+
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"
10278+
==
10279+
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"

0 commit comments

Comments
 (0)