Repository: pylint-dev/pylint (Python)

Issue:
pylint should not use Class name regex for TypeVar names
### Steps to reproduce

1. Consider the following example:

```python
"""Example"""
from typing import TypeVar

T = TypeVar('T')


def identity(arg: T) -> T:
    """Identity function."""
    return arg
```

2. pylint typevar.py

### Current behavior

typevar.py:4:0: C0103: Class name "T" doesn't conform to PascalCase naming style (invalid-name)

### Expected behavior

Either:
- Test should pass
- pylint should use a whitelist of names for TypeVars that is separate from that which is used for Class names. 'T' is a perfectly canonical TypeVar annotation in functional notation, but it is a perfectly rubbish runtime Class name.

See https://docs.python.org/3/library/typing.html#typing.TypeVar for a canonical example of usage from the official python docs.

### pylint --version output

```
pylint 2.3.1
astroid 2.2.5
Python 3.7.6 (default, Dec 19 2019, 22:50:01) 
[GCC 9.2.1 20190827 (Red Hat 9.2.1-1)]
```


Discussion:
Still hurts.
Can we workaround this bug in .pylintrc or somehow by far ?
Our broad code base suffers from this.
@remort there is the PR #3401 that should fix this issue. However i'v got no answer from the author (@kaihoankalina).
In the meantime maybe you can have a look to the `class-rgx` parameter inside your `.pylintrc` file.
As I get it - changing `class-rgx` default value leads to changes in linting logic for both: Classes and module level Constants at the same time? If so - this is an undesirable solution.



Patch:
diff --git a/ChangeLog b/ChangeLog
index 33dacc56bf..9ce5147aa0 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -450,6 +450,11 @@ Release date: TBA
   Insert your changelog randomly, it will reduce merge conflicts
   (Ie. not necessarily at the end)
 
+* Improve ``invalid-name`` check for ``TypeVar`` names.
+  The accepted pattern can be customized with ``--typevar-rgx``.
+
+  Closes #3401
+
 * Added new checker ``typevar-name-missing-variance``. Emitted when a covariant
   or contravariant ``TypeVar`` does not end with  ``_co`` or ``_contra`` respectively or
   when a ``TypeVar`` is not either but has a suffix.
diff --git a/doc/user_guide/options.rst b/doc/user_guide/options.rst
index 898c7a60a7..6e1535e249 100644
--- a/doc/user_guide/options.rst
+++ b/doc/user_guide/options.rst
@@ -39,6 +39,8 @@ name is found in, and not the type of object assigned.
 +--------------------+---------------------------------------------------------------------------------------------------+
 | ``inlinevar``      | Loop variables in list comprehensions and generator expressions.                                  |
 +--------------------+---------------------------------------------------------------------------------------------------+
+| ``typevar``        | Type variable declared with ``TypeVar``.                                                          |
++--------------------+---------------------------------------------------------------------------------------------------+
 
 Default behavior
 ~~~~~~~~~~~~~~~~
@@ -82,6 +84,19 @@ Following options are exposed:
 
 .. option:: --inlinevar-naming-style=<style>
 
+Predefined Naming Patterns
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Pylint provides predefined naming patterns for some names. These patterns are often
+based on a Naming Style but there is no option to choose one of the styles mentioned above.
+The pattern can be overwritten with the options discussed below.
+
+The following type of names are checked with a predefined pattern:
+
++--------------------+---------------------------------------------------+------------------------------------------------------------+
+| Name type          | Good names                                        | Bad names                                                  |
++====================+===================================================+============================================================+
+| ``typevar``        |`T`, `_CallableT`, `_T_co`, `AnyStr`, `DeviceTypeT`| `DICT_T`, `CALLABLE_T`, `ENUM_T`, `DeviceType`, `_StrType` |
++--------------------+---------------------------------------------------+------------------------------------------------------------+
 
 Custom regular expressions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -118,6 +133,8 @@ expression will lead to an instance of ``invalid-name``.
 
 .. option:: --inlinevar-rgx=<regex>
 
+.. option:: --typevar-rgx=<regex>
+
 Multiple naming styles for custom regular expressions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst
index 96dfa518a9..fa2ac92287 100644
--- a/doc/whatsnew/2.13.rst
+++ b/doc/whatsnew/2.13.rst
@@ -334,6 +334,11 @@ Other Changes
 
   Closes #5360, #3877
 
+* Improve ``invalid-name`` check for ``TypeVar`` names.
+  The accepted pattern can be customized with ``--typevar-rgx``.
+
+  Closes #3401
+
 * Fixed a false positive (affecting unreleased development) for
   ``used-before-assignment`` involving homonyms between filtered comprehensions
   and assignments in except blocks.
diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py
index 71dabd8efc..cea9781ae4 100644
--- a/pylint/checkers/base.py
+++ b/pylint/checkers/base.py
@@ -73,7 +73,7 @@
 import itertools
 import re
 import sys
-from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Pattern, cast
+from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Pattern, Tuple, cast
 
 import astroid
 from astroid import nodes
@@ -189,6 +189,13 @@ class AnyStyle(NamingStyle):
     "any": AnyStyle,
 }
 
+# Default patterns for name types that do not have styles
+DEFAULT_PATTERNS = {
+    "typevar": re.compile(
+        r"^_{0,2}(?:[^\W\da-z_]+|(?:[^\W\da-z_][^\WA-Z_]+)+T?(?<!Type))(?:_co(?:ntra)?)?$"
+    )
+}
+
 # do not require a doc string on private/system methods
 NO_REQUIRED_DOC_RGX = re.compile("^_")
 REVERSED_PROTOCOL_METHOD = "__reversed__"
@@ -1660,7 +1667,8 @@ def visit_for(self, node: nodes.For) -> None:
         self._check_redeclared_assign_name([node.target])
 
 
-KNOWN_NAME_TYPES = {
+# Name types that have a style option
+KNOWN_NAME_TYPES_WITH_STYLE = {
     "module",
     "const",
     "class",
@@ -1674,6 +1682,12 @@ def visit_for(self, node: nodes.For) -> None:
     "inlinevar",
 }
 
+# Name types that have a 'rgx' option
+KNOWN_NAME_TYPES = {
+    *KNOWN_NAME_TYPES_WITH_STYLE,
+    "typevar",
+}
+
 DEFAULT_NAMING_STYLES = {
     "module": "snake_case",
     "const": "UPPER_CASE",
@@ -1693,28 +1707,37 @@ def _create_naming_options():
     name_options = []
     for name_type in sorted(KNOWN_NAME_TYPES):
         human_readable_name = constants.HUMAN_READABLE_TYPES[name_type]
-        default_style = DEFAULT_NAMING_STYLES[name_type]
-        name_type = name_type.replace("_", "-")
-        name_options.append(
-            (
-                f"{name_type}-naming-style",
-                {
-                    "default": default_style,
-                    "type": "choice",
-                    "choices": list(NAMING_STYLES.keys()),
-                    "metavar": "<style>",
-                    "help": f"Naming style matching correct {human_readable_name} names.",
-                },
+        name_type_hyphened = name_type.replace("_", "-")
+
+        help_msg = f"Regular expression matching correct {human_readable_name} names. "
+        if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
+            help_msg += f"Overrides {name_type_hyphened}-naming-style. "
+        help_msg += f"If left empty, {human_readable_name} names will be checked with the set naming style."
+
+        # Add style option for names that support it
+        if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
+            default_style = DEFAULT_NAMING_STYLES[name_type]
+            name_options.append(
+                (
+                    f"{name_type_hyphened}-naming-style",
+                    {
+                        "default": default_style,
+                        "type": "choice",
+                        "choices": list(NAMING_STYLES.keys()),
+                        "metavar": "<style>",
+                        "help": f"Naming style matching correct {human_readable_name} names.",
+                    },
+                )
             )
-        )
+
         name_options.append(
             (
-                f"{name_type}-rgx",
+                f"{name_type_hyphened}-rgx",
                 {
                     "default": None,
                     "type": "regexp",
                     "metavar": "<regexp>",
-                    "help": f"Regular expression matching correct {human_readable_name} names. Overrides {name_type}-naming-style.",
+                    "help": help_msg,
                 },
             )
         )
@@ -1862,15 +1885,19 @@ def open(self):
             re.compile(rgxp) for rgxp in self.config.bad_names_rgxs
         ]
 
-    def _create_naming_rules(self):
-        regexps = {}
-        hints = {}
+    def _create_naming_rules(self) -> Tuple[Dict[str, Pattern[str]], Dict[str, str]]:
+        regexps: Dict[str, Pattern[str]] = {}
+        hints: Dict[str, str] = {}
 
         for name_type in KNOWN_NAME_TYPES:
-            naming_style_option_name = f"{name_type}_naming_style"
-            naming_style_name = getattr(self.config, naming_style_option_name)
-
-            regexps[name_type] = NAMING_STYLES[naming_style_name].get_regex(name_type)
+            if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
+                naming_style_name = getattr(self.config, f"{name_type}_naming_style")
+                regexps[name_type] = NAMING_STYLES[naming_style_name].get_regex(
+                    name_type
+                )
+            else:
+                naming_style_name = "predefined"
+                regexps[name_type] = DEFAULT_PATTERNS[name_type]
 
             custom_regex_setting_name = f"{name_type}_rgx"
             custom_regex = getattr(self.config, custom_regex_setting_name, None)
@@ -2078,14 +2105,6 @@ def _name_disallowed_by_regex(self, name: str) -> bool:
     def _check_name(self, node_type, name, node, confidence=interfaces.HIGH):
         """Check for a name using the type's regexp."""
 
-        # pylint: disable=fixme
-        # TODO: move this down in the function and check TypeVar
-        # for name patterns as well.
-        # Check TypeVar names for variance suffixes
-        if node_type == "typevar":
-            self._check_typevar_variance(name, node)
-            return
-
         def _should_exempt_from_invalid_name(node):
             if node_type == "variable":
                 inferred = utils.safe_infer(node)
@@ -2111,6 +2130,10 @@ def _should_exempt_from_invalid_name(node):
         if match is None and not _should_exempt_from_invalid_name(node):
             self._raise_name_warning(None, node, node_type, name, confidence)
 
+        # Check TypeVar names for variance suffixes
+        if node_type == "typevar":
+            self._check_typevar_variance(name, node)
+
     def _check_assign_to_new_keyword_violation(self, name, node):
         keyword_first_version = self._name_became_keyword_in_version(
             name, self.KEYWORD_ONSET
diff --git a/pylint/constants.py b/pylint/constants.py
index ae87d4293d..9dd1ba7509 100644
--- a/pylint/constants.py
+++ b/pylint/constants.py
@@ -71,6 +71,7 @@ class WarningScope:
     "class_attribute": "class attribute",
     "class_const": "class constant",
     "inlinevar": "inline iteration",
+    "typevar": "type variable",
 }
 
 
diff --git a/pylint/utils/linterstats.py b/pylint/utils/linterstats.py
index 54b98d533e..deb83a2136 100644
--- a/pylint/utils/linterstats.py
+++ b/pylint/utils/linterstats.py
@@ -26,6 +26,7 @@ class BadNames(TypedDict):
     method: int
     module: int
     variable: int
+    typevar: int
 
 
 class CodeTypeCount(TypedDict):
@@ -102,6 +103,7 @@ def __init__(
             method=0,
             module=0,
             variable=0,
+            typevar=0,
         )
         self.by_module: Dict[str, ModuleStats] = by_module or {}
         self.by_msg: Dict[str, int] = by_msg or {}
@@ -171,6 +173,7 @@ def get_bad_names(
             "method",
             "module",
             "variable",
+            "typevar",
         ],
     ) -> int:
         """Get a bad names node count."""
@@ -192,6 +195,7 @@ def increase_bad_name(self, node_name: str, increase: int) -> None:
             "method",
             "module",
             "variable",
+            "typevar",
         }:
             raise ValueError("Node type not part of the bad_names stat")
 
@@ -208,6 +212,7 @@ def increase_bad_name(self, node_name: str, increase: int) -> None:
                 "method",
                 "module",
                 "variable",
+                "typevar",
             ],
             node_name,
         )
@@ -230,6 +235,7 @@ def reset_bad_names(self) -> None:
             method=0,
             module=0,
             variable=0,
+            typevar=0,
         )
 
     def get_code_count(
@@ -317,6 +323,7 @@ def merge_stats(stats: List[LinterStats]):
         merged.bad_names["method"] += stat.bad_names["method"]
         merged.bad_names["module"] += stat.bad_names["module"]
         merged.bad_names["variable"] += stat.bad_names["variable"]
+        merged.bad_names["typevar"] += stat.bad_names["typevar"]
 
         for mod_key, mod_value in stat.by_module.items():
             merged.by_module[mod_key] = mod_value
diff --git a/pylintrc b/pylintrc
index 4e093a87c5..1cacac6c71 100644
--- a/pylintrc
+++ b/pylintrc
@@ -353,6 +353,9 @@ method-rgx=[a-z_][a-z0-9_]{2,}$
 # Naming hint for method names
 method-name-hint=[a-z_][a-z0-9_]{2,}$
 
+# Regular expression which can overwrite the naming style set by typevar-naming-style.
+#typevar-rgx=
+
 # Regular expression which should only match function or class names that do
 # not require a docstring. Use ^(?!__init__$)_ to also check __init__.
 no-docstring-rgx=__.*__


Test patch:
diff --git a/tests/checkers/unittest_base.py b/tests/checkers/unittest_base.py
index fe46efcb4e..83f919c16d 100644
--- a/tests/checkers/unittest_base.py
+++ b/tests/checkers/unittest_base.py
@@ -213,13 +213,13 @@ class TestNamePresets(unittest.TestCase):
     def _test_name_is_correct_for_all_name_types(
         self, naming_style: Type[base.NamingStyle], name: str
     ) -> None:
-        for name_type in base.KNOWN_NAME_TYPES:
+        for name_type in base.KNOWN_NAME_TYPES_WITH_STYLE:
             self._test_is_correct(naming_style, name, name_type)
 
     def _test_name_is_incorrect_for_all_name_types(
         self, naming_style: Type[base.NamingStyle], name: str
     ) -> None:
-        for name_type in base.KNOWN_NAME_TYPES:
+        for name_type in base.KNOWN_NAME_TYPES_WITH_STYLE:
             self._test_is_incorrect(naming_style, name, name_type)
 
     def _test_should_always_pass(self, naming_style: Type[base.NamingStyle]) -> None:
diff --git a/tests/functional/t/typevar_naming_style_default.py b/tests/functional/t/typevar_naming_style_default.py
new file mode 100644
index 0000000000..105ddd20c5
--- /dev/null
+++ b/tests/functional/t/typevar_naming_style_default.py
@@ -0,0 +1,49 @@
+"""Test case for typevar-name-incorrect-variance with default settings"""
+# pylint: disable=too-few-public-methods
+
+from typing import TypeVar
+
+# PascalCase names with prefix
+GoodNameT = TypeVar("GoodNameT")
+_T = TypeVar("_T")
+_GoodNameT = TypeVar("_GoodNameT")
+__GoodNameT = TypeVar("__GoodNameT")
+GoodNameWithoutContra = TypeVar(  # [typevar-name-incorrect-variance]
+    "GoodNameWithoutContra", contravariant=True
+)
+GoodNameT_co = TypeVar("GoodNameT_co", covariant=True)
+GoodNameT_contra = TypeVar("GoodNameT_contra", contravariant=True)
+GoodBoundNameT = TypeVar("GoodBoundNameT", bound=int)
+
+# Some of these will create a RunTime error but serve as a regression test
+T = TypeVar(  # [typevar-name-incorrect-variance]
+    "T", covariant=True, contravariant=True
+)
+T = TypeVar("T", covariant=False, contravariant=False)
+T_co = TypeVar("T_co", covariant=True, contravariant=True)
+T_contra = TypeVar(  # [typevar-name-incorrect-variance]
+    "T_contra", covariant=True, contravariant=True
+)
+T_co = TypeVar("T_co", covariant=True, contravariant=False)
+T_contra = TypeVar("T_contra", covariant=False, contravariant=True)
+
+# PascalCase names without prefix
+AnyStr = TypeVar("AnyStr")
+DeviceTypeT = TypeVar("DeviceTypeT")
+CALLABLE_T = TypeVar("CALLABLE_T")  # [invalid-name]
+DeviceType = TypeVar("DeviceType")  # [invalid-name]
+
+# camelCase names with prefix
+badName = TypeVar("badName")  # [invalid-name]
+badName_co = TypeVar("badName_co", covariant=True)  # [invalid-name]
+badName_contra = TypeVar("badName_contra", contravariant=True)  # [invalid-name]
+
+# PascalCase names with lower letter prefix in tuple assignment
+(
+    a_BadName,  # [invalid-name]
+    a_BadNameWithoutContra,  # [invalid-name, typevar-name-incorrect-variance]
+) = TypeVar("a_BadName"), TypeVar("a_BadNameWithoutContra", contravariant=True)
+GoodName_co, a_BadName_contra = TypeVar(  # [invalid-name]
+    "GoodName_co", covariant=True
+), TypeVar("a_BadName_contra", contravariant=True)
+GoodName_co, VAR = TypeVar("GoodName_co", covariant=True), "a string"
diff --git a/tests/functional/t/typevar_naming_style_default.txt b/tests/functional/t/typevar_naming_style_default.txt
new file mode 100644
index 0000000000..a0f2257d40
--- /dev/null
+++ b/tests/functional/t/typevar_naming_style_default.txt
@@ -0,0 +1,12 @@
+typevar-name-incorrect-variance:11:0:11:21::"Type variable ""GoodNameWithoutContra"" is contravariant, use ""GoodNameWithoutContra_contra"" instead":INFERENCE
+typevar-name-incorrect-variance:19:0:19:1::"Type variable ""T"" is covariant, use ""T_co"" instead":INFERENCE
+typevar-name-incorrect-variance:24:0:24:8::"Type variable ""T_contra"" is covariant, use ""T_co"" instead":INFERENCE
+invalid-name:33:0:33:10::"Type variable name ""CALLABLE_T"" doesn't conform to predefined naming style":HIGH
+invalid-name:34:0:34:10::"Type variable name ""DeviceType"" doesn't conform to predefined naming style":HIGH
+invalid-name:37:0:37:7::"Type variable name ""badName"" doesn't conform to predefined naming style":HIGH
+invalid-name:38:0:38:10::"Type variable name ""badName_co"" doesn't conform to predefined naming style":HIGH
+invalid-name:39:0:39:14::"Type variable name ""badName_contra"" doesn't conform to predefined naming style":HIGH
+invalid-name:43:4:43:13::"Type variable name ""a_BadName"" doesn't conform to predefined naming style":HIGH
+invalid-name:44:4:44:26::"Type variable name ""a_BadNameWithoutContra"" doesn't conform to predefined naming style":HIGH
+typevar-name-incorrect-variance:44:4:44:26::"Type variable ""a_BadNameWithoutContra"" is contravariant, use ""a_BadNameWithoutContra_contra"" instead":INFERENCE
+invalid-name:46:13:46:29::"Type variable name ""a_BadName_contra"" doesn't conform to predefined naming style":HIGH
diff --git a/tests/functional/t/typevar_naming_style_rgx.py b/tests/functional/t/typevar_naming_style_rgx.py
new file mode 100644
index 0000000000..c08eb9e41d
--- /dev/null
+++ b/tests/functional/t/typevar_naming_style_rgx.py
@@ -0,0 +1,15 @@
+"""Test case for typevar-name-missing-variance with non-default settings"""
+
+from typing import TypeVar
+
+# Name set by regex pattern
+TypeVarsShouldBeLikeThis = TypeVar("TypeVarsShouldBeLikeThis")
+TypeVarsShouldBeLikeThis_contra = TypeVar(
+    "TypeVarsShouldBeLikeThis_contra", contravariant=True
+)
+TypeVarsShouldBeLikeThis_co = TypeVar("TypeVarsShouldBeLikeThis_co", covariant=True)
+
+# Name using the standard style
+GoodNameT = TypeVar("GoodNameT")  # [invalid-name]
+GoodNameT_co = TypeVar("GoodNameT_co", covariant=True)  # [invalid-name]
+GoodNameT_contra = TypeVar("GoodNameT_contra", contravariant=True)  # [invalid-name]
diff --git a/tests/functional/t/typevar_naming_style_rgx.rc b/tests/functional/t/typevar_naming_style_rgx.rc
new file mode 100644
index 0000000000..347da995e9
--- /dev/null
+++ b/tests/functional/t/typevar_naming_style_rgx.rc
@@ -0,0 +1,2 @@
+[BASIC]
+typevar-rgx=TypeVarsShouldBeLikeThis(_co(ntra)?)?$
diff --git a/tests/functional/t/typevar_naming_style_rgx.txt b/tests/functional/t/typevar_naming_style_rgx.txt
new file mode 100644
index 0000000000..0b291bbf37
--- /dev/null
+++ b/tests/functional/t/typevar_naming_style_rgx.txt
@@ -0,0 +1,3 @@
+invalid-name:13:0:13:9::"Type variable name ""GoodNameT"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH
+invalid-name:14:0:14:12::"Type variable name ""GoodNameT_co"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH
+invalid-name:15:0:15:16::"Type variable name ""GoodNameT_contra"" doesn't conform to 'TypeVarsShouldBeLikeThis(_co(ntra)?)?$' pattern":HIGH