Repository: pylint-dev/pylint

Issue:
# Fix FP for `invalid-name` with `typing.Final` on dataclass fields

## Type of Changes
|     | Type                   |
| --- | ---------------------- |
| ✓   | :bug: Bug fix          |
## Description
Before, dataclass fields annotated with `Final` were treated like all other class variables annotated with `Final` and required to be uppercase. But that's not how dataclasses work -- you still want lower cased fields even if marked with `Final`. An exception would be `ClassVar[Final]`, which doesn't give you a field.

Closes #10790


Implementation patch:
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 7536658209..90c63922f6 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.


Test patch:
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..f0f28687f2 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
 
     def method(self) -> None:
         method_snake_case_constant: Final[int] = 42