diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index e916ded01dd2..83cbf6b4a548 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. @@ -587,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( @@ -608,7 +623,28 @@ 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 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(attr_name, [stmt])[-1] + if last_def.type is not None: + 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[attr_name] = SymbolTableNode(MDEF, var) + node = var + else: + self._api.fail( + f"Dataclass attribute '{attr_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]