From e4a540053d63518e2cb0b68d701d76c230d9173d Mon Sep 17 00:00:00 2001 From: Aaron Wieczorek Date: Sun, 4 Jan 2026 11:42:54 +0000 Subject: [PATCH 1/3] Fix crash in dataclass plugin when attribute is redefined --- mypy/plugins/dataclasses.py | 45 +++++++++++++++++++++++++++ mypy/semanal_main.py | 8 +++++ test-data/unit/check-dataclasses.test | 10 ++++++ 3 files changed, 63 insertions(+) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index e916ded01dd2..80f13279f437 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -575,6 +575,19 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: # Second, collect attributes belonging to the current class. current_attr_names: set[str] = set() kw_only = self._get_bool_arg("kw_only", self._spec.kw_only_default) + all_assignments = self._get_assignment_statements_from_block(cls.defs) + redefined_attrs: dict[str, list[AssignmentStmt]] = {} + last_def_with_type: dict[str, AssignmentStmt] = {} + for stmt in all_assignments: + if not isinstance(stmt.lvalues[0], NameExpr): + continue + name = stmt.lvalues[0].name + if stmt.type is not None: + last_def_with_type[name] = stmt + if name in redefined_attrs: + redefined_attrs[name].append(stmt) + else: + redefined_attrs[name] = [stmt] for stmt in self._get_assignment_statements_from_block(cls.defs): # Any assignment that doesn't use the new type declaration # syntax can be ignored out of hand. @@ -608,7 +621,39 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: # This might be a property / field name clash. # We will issue an error later. continue + if not isinstance(node, Var): + if name in redefined_attrs and len(redefined_attrs[name]) > 1: + continue + self._api.fail( + f"Dataclass attribute '{name}' cannot be a function. " + f"Use a variable with type annotation instead.", + stmt, + ) + continue + + assert isinstance(node, Var), node + if not isinstance(node, Var): + if name in redefined_attrs and len(redefined_attrs[name]) > 1: + if name in last_def_with_type: + continue + last_def = redefined_attrs.get(name, [stmt])[-1] + if last_def.type is not None: + var = Var(name) + var.is_property = False + var.info = cls.info + var.line = last_def.line + var.column = last_def.column + var.type = self._api.anal_type(last_def.type) + cls.info.names[name] = SymbolTableNode(MDEF, var) + node = var + else: + self._api.fail( + f"Dataclass attribute '{name}' cannot be a function. " + f"Use a variable with type annotation instead.", + stmt, + ) + continue assert isinstance(node, Var), node # x: ClassVar[int] is ignored by dataclasses. diff --git a/mypy/semanal_main.py b/mypy/semanal_main.py index 21e6d9c63c5e..ba1684d86e81 100644 --- a/mypy/semanal_main.py +++ b/mypy/semanal_main.py @@ -486,6 +486,7 @@ def apply_class_plugin_hooks(graph: Graph, scc: list[str], errors: Errors) -> No """ num_passes = 0 incomplete = True + already_processed: dict[TypeInfo, set[int]] = {} # If we encounter a base class that has not been processed, we'll run another # pass. This should eventually reach a fixed point. while incomplete: @@ -498,6 +499,13 @@ def apply_class_plugin_hooks(graph: Graph, scc: list[str], errors: Errors) -> No assert tree for _, node, _ in tree.local_definitions(): if isinstance(node.node, TypeInfo): + if node.node in already_processed: + pass_count = len(already_processed[node.node]) + if pass_count >= 3 and num_passes > 3: + continue + else: + already_processed[node.node] = set() + already_processed[node.node].add(num_passes) if not apply_hooks_to_class( state.manager.semantic_analyzer, module, diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 3cc4a03ffb11..25e6021de35a 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2737,3 +2737,13 @@ class ClassB(ClassA): def value(self) -> int: return 0 [builtins fixtures/dict.pyi] + +[case testDataclassNameCollisionNoCrash] +from dataclasses import dataclass +def fn(a: int) -> int: + return a +@dataclass +class Test: + foo = fn + foo: int = 42 # E: Name "foo" already defined on line 6 +[builtins fixtures/tuple.pyi] From 01ba68ab59048198ed21f4ae61985ece1b8dbac4 Mon Sep 17 00:00:00 2001 From: Aaron Wieczorek Date: Sun, 4 Jan 2026 12:09:11 +0000 Subject: [PATCH 2/3] Fix assert that was causing some unreachable code, failing some CI tests --- mypy/plugins/dataclasses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 80f13279f437..b114974c0c16 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -631,8 +631,6 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: ) continue - assert isinstance(node, Var), node - if not isinstance(node, Var): if name in redefined_attrs and len(redefined_attrs[name]) > 1: if name in last_def_with_type: From 79839a0cb8ae64576ce78b5bb08b57cd46f5c914 Mon Sep 17 00:00:00 2001 From: Aaron Wieczorek Date: Sun, 4 Jan 2026 12:34:09 +0000 Subject: [PATCH 3/3] Defining local var inside loop, seems to be causing CI test issues --- mypy/plugins/dataclasses.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index b114974c0c16..83cbf6b4a548 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -600,13 +600,15 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: if not isinstance(lhs, NameExpr): continue - sym = cls.info.names.get(lhs.name) + attr_name = lhs.name + sym = cls.info.names.get(attr_name) if sym is None: # There was probably a semantic analysis error. continue node = sym.node - assert not isinstance(node, PlaceholderNode) + if isinstance(node, PlaceholderNode): + continue if isinstance(node, TypeAlias): self._api.fail( @@ -622,32 +624,23 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: # We will issue an error later. continue if not isinstance(node, Var): - if name in redefined_attrs and len(redefined_attrs[name]) > 1: - continue - self._api.fail( - f"Dataclass attribute '{name}' cannot be a function. " - f"Use a variable with type annotation instead.", - stmt, - ) - continue - - if not isinstance(node, Var): - if name in redefined_attrs and len(redefined_attrs[name]) > 1: - if name in last_def_with_type: + if attr_name in redefined_attrs and len(redefined_attrs[attr_name]) > 1: + if attr_name in last_def_with_type: continue - last_def = redefined_attrs.get(name, [stmt])[-1] + + last_def = redefined_attrs.get(attr_name, [stmt])[-1] if last_def.type is not None: - var = Var(name) + var = Var(attr_name) var.is_property = False var.info = cls.info var.line = last_def.line var.column = last_def.column var.type = self._api.anal_type(last_def.type) - cls.info.names[name] = SymbolTableNode(MDEF, var) + cls.info.names[attr_name] = SymbolTableNode(MDEF, var) node = var else: self._api.fail( - f"Dataclass attribute '{name}' cannot be a function. " + f"Dataclass attribute '{attr_name}' cannot be a function. " f"Use a variable with type annotation instead.", stmt, )