Skip to content

Commit e4a5400

Browse files
Aaron WieczorekAaron Wieczorek
authored andcommitted
Fix crash in dataclass plugin when attribute is redefined
1 parent 0cc21d9 commit e4a5400

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

mypy/plugins/dataclasses.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,19 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
575575
# Second, collect attributes belonging to the current class.
576576
current_attr_names: set[str] = set()
577577
kw_only = self._get_bool_arg("kw_only", self._spec.kw_only_default)
578+
all_assignments = self._get_assignment_statements_from_block(cls.defs)
579+
redefined_attrs: dict[str, list[AssignmentStmt]] = {}
580+
last_def_with_type: dict[str, AssignmentStmt] = {}
581+
for stmt in all_assignments:
582+
if not isinstance(stmt.lvalues[0], NameExpr):
583+
continue
584+
name = stmt.lvalues[0].name
585+
if stmt.type is not None:
586+
last_def_with_type[name] = stmt
587+
if name in redefined_attrs:
588+
redefined_attrs[name].append(stmt)
589+
else:
590+
redefined_attrs[name] = [stmt]
578591
for stmt in self._get_assignment_statements_from_block(cls.defs):
579592
# Any assignment that doesn't use the new type declaration
580593
# syntax can be ignored out of hand.
@@ -608,7 +621,39 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
608621
# This might be a property / field name clash.
609622
# We will issue an error later.
610623
continue
624+
if not isinstance(node, Var):
625+
if name in redefined_attrs and len(redefined_attrs[name]) > 1:
626+
continue
627+
self._api.fail(
628+
f"Dataclass attribute '{name}' cannot be a function. "
629+
f"Use a variable with type annotation instead.",
630+
stmt,
631+
)
632+
continue
633+
634+
assert isinstance(node, Var), node
611635

636+
if not isinstance(node, Var):
637+
if name in redefined_attrs and len(redefined_attrs[name]) > 1:
638+
if name in last_def_with_type:
639+
continue
640+
last_def = redefined_attrs.get(name, [stmt])[-1]
641+
if last_def.type is not None:
642+
var = Var(name)
643+
var.is_property = False
644+
var.info = cls.info
645+
var.line = last_def.line
646+
var.column = last_def.column
647+
var.type = self._api.anal_type(last_def.type)
648+
cls.info.names[name] = SymbolTableNode(MDEF, var)
649+
node = var
650+
else:
651+
self._api.fail(
652+
f"Dataclass attribute '{name}' cannot be a function. "
653+
f"Use a variable with type annotation instead.",
654+
stmt,
655+
)
656+
continue
612657
assert isinstance(node, Var), node
613658

614659
# x: ClassVar[int] is ignored by dataclasses.

mypy/semanal_main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ def apply_class_plugin_hooks(graph: Graph, scc: list[str], errors: Errors) -> No
486486
"""
487487
num_passes = 0
488488
incomplete = True
489+
already_processed: dict[TypeInfo, set[int]] = {}
489490
# If we encounter a base class that has not been processed, we'll run another
490491
# pass. This should eventually reach a fixed point.
491492
while incomplete:
@@ -498,6 +499,13 @@ def apply_class_plugin_hooks(graph: Graph, scc: list[str], errors: Errors) -> No
498499
assert tree
499500
for _, node, _ in tree.local_definitions():
500501
if isinstance(node.node, TypeInfo):
502+
if node.node in already_processed:
503+
pass_count = len(already_processed[node.node])
504+
if pass_count >= 3 and num_passes > 3:
505+
continue
506+
else:
507+
already_processed[node.node] = set()
508+
already_processed[node.node].add(num_passes)
501509
if not apply_hooks_to_class(
502510
state.manager.semantic_analyzer,
503511
module,

test-data/unit/check-dataclasses.test

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,3 +2737,13 @@ class ClassB(ClassA):
27372737
def value(self) -> int:
27382738
return 0
27392739
[builtins fixtures/dict.pyi]
2740+
2741+
[case testDataclassNameCollisionNoCrash]
2742+
from dataclasses import dataclass
2743+
def fn(a: int) -> int:
2744+
return a
2745+
@dataclass
2746+
class Test:
2747+
foo = fn
2748+
foo: int = 42 # E: Name "foo" already defined on line 6
2749+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)