diff --git a/doc/whatsnew/fragments/10790.false_positive b/doc/whatsnew/fragments/10790.false_positive new file mode 100644 index 0000000000..b934adfe81 --- /dev/null +++ b/doc/whatsnew/fragments/10790.false_positive @@ -0,0 +1,4 @@ +Fix a false positive for ``invalid-name`` where a dataclass field typed with ``Final`` +was evaluated against the ``class_const`` regex instead of the ``class_attribute`` regex. + +Closes #10790 diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 9b3ed0ed1f..41ccc674a1 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -552,9 +552,14 @@ def visit_assignname( # pylint: disable=too-many-branches,too-many-statements elif isinstance(frame, nodes.ClassDef) and not any( frame.local_attr_ancestors(node.name) ): - if utils.is_enum_member(node) or utils.is_assign_name_annotated_with( - node, "Final" - ): + if utils.is_assign_name_annotated_with_class_var_typing_name(node, "Final"): + self._check_name("class_const", node.name, node) + elif utils.is_assign_name_annotated_with(node, "Final"): + if frame.is_dataclass: + self._check_name("class_attribute", node.name, node) + else: + self._check_name("class_const", node.name, node) + elif utils.is_enum_member(node): self._check_name("class_const", node.name, node) else: self._check_name("class_attribute", node.name, node) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 7a45b472bd..f6dc9dfe3f 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1762,6 +1762,22 @@ def is_assign_name_annotated_with(node: nodes.AssignName, typing_name: str) -> b return False +def is_assign_name_annotated_with_class_var_typing_name( + node: nodes.AssignName, typing_name: str +) -> bool: + if not is_assign_name_annotated_with(node, "ClassVar"): + return False + annotation = node.parent.annotation + if isinstance(annotation, nodes.Subscript): + annotation = annotation.slice + if isinstance(annotation, nodes.Subscript): + annotation = annotation.value + match annotation: + case nodes.Name(name=n) | nodes.Attribute(attrname=n) if n == typing_name: + return True + return False + + def get_iterating_dictionary_name(node: nodes.For | nodes.Comprehension) -> str | None: """Get the name of the dictionary which keys are being iterated over on a ``nodes.For`` or ``nodes.Comprehension`` node. diff --git a/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py b/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py index 3d32d8a2b4..989b8bd040 100644 --- a/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py +++ b/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.py @@ -3,7 +3,7 @@ # pylint: disable=missing-class-docstring, missing-function-docstring from dataclasses import dataclass -from typing import Final +from typing import ClassVar, Final module_snake_case_constant: Final[int] = 42 # [invalid-name] MODULE_UPPER_CASE_CONSTANT: Final[int] = 42 @@ -17,8 +17,11 @@ def function() -> None: @dataclass class Class: - class_snake_case_constant: Final[int] = 42 # [invalid-name] - CLASS_UPPER_CASE_CONSTANT: Final[int] = 42 + class_snake_case_constant: ClassVar[Final[int]] = 42 # [invalid-name] + CLASS_UPPER_CASE_CONSTANT: ClassVar[Final[int]] = 42 + + field_annotated_with_final: Final[int] = 42 + FIELD_ANNOTATED_WITH_FINAL: Final[int] = 42 # this could emit invalid-name eventually. def method(self) -> None: method_snake_case_constant: Final[int] = 42 diff --git a/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.txt b/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.txt index 595b35224c..60a683a08f 100644 --- a/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.txt +++ b/tests/functional/i/invalid/invalid_name/invalid_name_with_final_typing.txt @@ -1,4 +1,4 @@ invalid-name:8:0:8:26::"Constant name ""module_snake_case_constant"" doesn't conform to UPPER_CASE naming style":HIGH invalid-name:14:4:14:32:function:"Variable name ""FUNCTION_UPPER_CASE_CONSTANT"" doesn't conform to snake_case naming style":HIGH invalid-name:20:4:20:29:Class:"Class constant name ""class_snake_case_constant"" doesn't conform to UPPER_CASE naming style":HIGH -invalid-name:25:8:25:34:Class.method:"Variable name ""METHOD_UPPER_CASE_CONSTANT"" doesn't conform to snake_case naming style":HIGH +invalid-name:28:8:28:34:Class.method:"Variable name ""METHOD_UPPER_CASE_CONSTANT"" doesn't conform to snake_case naming style":HIGH