diff --git a/src/flake8_pydantic/errors.py b/src/flake8_pydantic/errors.py index 9f94d8a..d52ebe6 100644 --- a/src/flake8_pydantic/errors.py +++ b/src/flake8_pydantic/errors.py @@ -48,6 +48,11 @@ class PYD005(Error): message = "Field name overrides annotation" +class PYD006(Error): + error_code = "PYD005" + message = "Duplicate field name" + + class PYD010(Error): error_code = "PYD010" message = "Usage of __pydantic_config__" diff --git a/src/flake8_pydantic/visitor.py b/src/flake8_pydantic/visitor.py index 64480ab..65bfe0f 100644 --- a/src/flake8_pydantic/visitor.py +++ b/src/flake8_pydantic/visitor.py @@ -6,7 +6,7 @@ from ._compat import TypeAlias from ._utils import extract_annotations, is_dataclass, is_function, is_name, is_pydantic_model -from .errors import PYD001, PYD002, PYD003, PYD004, PYD005, PYD010, Error +from .errors import PYD001, PYD002, PYD003, PYD004, PYD005, PYD006, PYD010, Error ClassType: TypeAlias = Literal["pydantic_model", "dataclass", "other_class"] @@ -95,6 +95,17 @@ def _check_pyd_005(self, node: ast.ClassDef) -> None: if previous_targets & extract_annotations(stmt.annotation): self.errors.append(PYD005.from_node(stmt)) + def _check_pyd_006(self, node: ast.ClassDef) -> None: + if self.current_class in {"pydantic_model", "dataclass"}: + previous_targets: set[str] = set() + + for stmt in node.body: + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + if stmt.target.id in previous_targets: + self.errors.append(PYD006.from_node(stmt)) + + previous_targets.add(stmt.target.id) + def _check_pyd_010(self, node: ast.ClassDef) -> None: if self.current_class == "other_class": for stmt in node.body: @@ -115,6 +126,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: self.enter_class(node) self._check_pyd_002(node) self._check_pyd_005(node) + self._check_pyd_006(node) self._check_pyd_010(node) self.generic_visit(node) self.leave_class() diff --git a/tests/rules/test_pyd006.py b/tests/rules/test_pyd006.py new file mode 100644 index 0000000..0a9abaa --- /dev/null +++ b/tests/rules/test_pyd006.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import ast + +import pytest + +from flake8_pydantic.errors import PYD006, Error +from flake8_pydantic.visitor import Visitor + +PYD006_1 = """ +class Model(BaseModel): + x: int + x: str = "1" +""" + +PYD006_2 = """ +class Model(BaseModel): + x: int + y: int +""" + + +@pytest.mark.parametrize( + ["source", "expected"], + [ + (PYD006_1, [PYD006(4, 4)]), + (PYD006_2, []), + ], +) +def test_pyd006(source: str, expected: list[Error]) -> None: + module = ast.parse(source) + visitor = Visitor() + visitor.visit(module) + + assert visitor.errors == expected