Skip to content

Commit

Permalink
Fix daemon crash on malformed NamedTuple (#14119)
Browse files Browse the repository at this point in the history
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 <12621235+hauntsaninja@users.noreply.github.com>
  • Loading branch information
ilevkivskyi and hauntsaninja committed Nov 18, 2022
1 parent b650d96 commit 1d6a5b1
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 7 deletions.
4 changes: 4 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,7 @@ class ClassDef(Statement):
"analyzed",
"has_incompatible_baseclass",
"deco_line",
"removed_statements",
)

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

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

def accept(self, visitor: StatementVisitor[T]) -> T:
return visitor.visit_class_def(self)
Expand Down
19 changes: 14 additions & 5 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
NameExpr,
PassStmt,
RefExpr,
Statement,
StrExpr,
SymbolTable,
SymbolTableNode,
Expand Down Expand Up @@ -111,7 +112,7 @@ def analyze_namedtuple_classdef(
if result is None:
# This is a valid named tuple, but some types are incomplete.
return True, None
items, types, default_items = result
items, types, default_items, statements = result
if is_func_scope and "@" not in defn.name:
defn.name += "@" + str(defn.line)
existing_info = None
Expand All @@ -123,31 +124,35 @@ def analyze_namedtuple_classdef(
defn.analyzed = NamedTupleExpr(info, is_typed=True)
defn.analyzed.line = defn.line
defn.analyzed.column = defn.column
defn.defs.body = statements
# 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, is_stub_file: bool
) -> tuple[list[str], list[Type], dict[str, Expression]] | None:
) -> tuple[list[str], list[Type], dict[str, Expression], list[Statement]] | None:
"""Parse and validate fields in named tuple class definition.
Return a three tuple:
Return a four tuple:
* field names
* field types
* field default values
* valid statements
or None, if any of the types are not ready.
"""
if self.options.python_version < (3, 6) and not is_stub_file:
self.fail("NamedTuple class syntax is only supported in Python 3.6", defn)
return [], [], {}
return [], [], {}, []
if len(defn.base_type_exprs) > 1:
self.fail("NamedTuple should be a single base", defn)
items: list[str] = []
types: list[Type] = []
default_items: dict[str, Expression] = {}
statements: list[Statement] = []
for stmt in defn.defs.body:
statements.append(stmt)
if not isinstance(stmt, AssignmentStmt):
# Still allow pass or ... (for empty namedtuples).
if isinstance(stmt, PassStmt) or (
Expand All @@ -160,9 +165,13 @@ def check_namedtuple_classdef(
# And docstrings.
if isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
continue
statements.pop()
defn.removed_statements.append(stmt)
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
# An assignment, but an invalid one.
statements.pop()
defn.removed_statements.append(stmt)
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
else:
# Append name and type in this case...
Expand Down Expand Up @@ -199,7 +208,7 @@ def check_namedtuple_classdef(
)
else:
default_items[name] = stmt.rvalue
return items, types, default_items
return items, types, default_items, statements

def check_namedtuple(
self, node: Expression, var_name: str | None, is_func_scope: bool
Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,11 @@ def analyze_typeddict_classdef_fields(
):
statements.append(stmt)
else:
defn.removed_statements.append(stmt)
self.fail(TPDICT_CLASS_ERROR, stmt)
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
# An assignment, but an invalid one.
defn.removed_statements.append(stmt)
self.fail(TPDICT_CLASS_ERROR, stmt)
else:
name = stmt.lvalues[0].name
Expand Down
2 changes: 2 additions & 0 deletions mypy/server/aststrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ def visit_class_def(self, node: ClassDef) -> None:
]
with self.enter_class(node.info):
super().visit_class_def(node)
node.defs.body.extend(node.removed_statements)
node.removed_statements = []
TypeState.reset_subtype_caches_for(node.info)
# Kill the TypeInfo, since there is none before semantic analysis.
node.info = CLASSDEF_NO_INFO
Expand Down
2 changes: 0 additions & 2 deletions test-data/unit/check-class-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,6 @@ class X(typing.NamedTuple):
[out]
main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
main:7: error: Type cannot be declared in assignment to non-self attribute
main:7: error: "int" has no attribute "x"
main:9: error: Non-default NamedTuple fields cannot follow default fields

[builtins fixtures/list.pyi]
Expand Down
72 changes: 72 additions & 0 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -10205,3 +10205,75 @@ C
[builtins fixtures/dict.pyi]
[out]
==

[case testNamedTupleNestedCrash]
import m
[file m.py]
from typing import NamedTuple

class NT(NamedTuple):
class C: ...
x: int
y: int

[file m.py.2]
from typing import NamedTuple

class NT(NamedTuple):
class C: ...
x: int
y: int
# change
[builtins fixtures/tuple.pyi]
[out]
m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
==
m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"

[case testNamedTupleNestedClassRecheck]
import n
[file n.py]
import m
x: m.NT
[file m.py]
from typing import NamedTuple
from f import A

class NT(NamedTuple):
class C: ...
x: int
y: A

[file f.py]
A = int
[file f.py.2]
A = str
[builtins fixtures/tuple.pyi]
[out]
m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
==
m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"

[case testTypedDictNestedClassRecheck]
import n
[file n.py]
import m
x: m.TD
[file m.py]
from typing_extensions import TypedDict
from f import A

class TD(TypedDict):
class C: ...
x: int
y: A

[file f.py]
A = int
[file f.py.2]
A = str
[builtins fixtures/dict.pyi]
[out]
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"
==
m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type"

0 comments on commit 1d6a5b1

Please sign in to comment.