summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>2022-03-24 16:30:02 +0100
committerGitHub <noreply@github.com>2022-03-24 16:30:02 +0100
commitfd91d04a2f946c6fe8b31b6f9217a61caab55c7d (patch)
treecefdda722781e11603468e947f968c1e3883094e
parentbb8e098f5a8c18ac68cebdae12d48138bab858b8 (diff)
downloadpylint-git-fd91d04a2f946c6fe8b31b6f9217a61caab55c7d.tar.gz
Create a ``TypeVar`` style for ``invalid-name`` (#5894)
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
-rw-r--r--ChangeLog5
-rw-r--r--doc/user_guide/options.rst17
-rw-r--r--doc/whatsnew/2.13.rst5
-rw-r--r--pylint/checkers/base.py87
-rw-r--r--pylint/constants.py1
-rw-r--r--pylint/utils/linterstats.py7
-rw-r--r--pylintrc3
-rw-r--r--tests/checkers/unittest_base.py4
-rw-r--r--tests/functional/t/typevar_naming_style_default.py49
-rw-r--r--tests/functional/t/typevar_naming_style_default.txt12
-rw-r--r--tests/functional/t/typevar_naming_style_rgx.py15
-rw-r--r--tests/functional/t/typevar_naming_style_rgx.rc2
-rw-r--r--tests/functional/t/typevar_naming_style_rgx.txt3
13 files changed, 176 insertions, 34 deletions
diff --git a/ChangeLog b/ChangeLog
index b75fe9515..22cd0aed3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -522,6 +522,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 898c7a60a..6e1535e24 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 47e78e520..c03335247 100644
--- a/doc/whatsnew/2.13.rst
+++ b/doc/whatsnew/2.13.rst
@@ -398,6 +398,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 38791c05e..43076d669 100644
--- a/pylint/checkers/base.py
+++ b/pylint/checkers/base.py
@@ -7,7 +7,7 @@ import collections
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
@@ -122,6 +122,13 @@ NAMING_STYLES = {
"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__"
@@ -1601,7 +1608,8 @@ class BasicChecker(_BasicChecker):
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",
@@ -1615,6 +1623,12 @@ KNOWN_NAME_TYPES = {
"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",
@@ -1634,28 +1648,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,
},
)
)
@@ -1803,15 +1826,19 @@ class NameChecker(_BasicChecker):
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)
@@ -2019,14 +2046,6 @@ class NameChecker(_BasicChecker):
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)
@@ -2052,6 +2071,10 @@ class NameChecker(_BasicChecker):
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 a6c9aa63d..2ecbd9778 100644
--- a/pylint/constants.py
+++ b/pylint/constants.py
@@ -73,6 +73,7 @@ HUMAN_READABLE_TYPES = {
"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 f7ca05f61..82895ec87 100644
--- a/pylint/utils/linterstats.py
+++ b/pylint/utils/linterstats.py
@@ -27,6 +27,7 @@ class BadNames(TypedDict):
method: int
module: int
variable: int
+ typevar: int
class CodeTypeCount(TypedDict):
@@ -103,6 +104,7 @@ class LinterStats:
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 {}
@@ -172,6 +174,7 @@ class LinterStats:
"method",
"module",
"variable",
+ "typevar",
],
) -> int:
"""Get a bad names node count."""
@@ -193,6 +196,7 @@ class LinterStats:
"method",
"module",
"variable",
+ "typevar",
}:
raise ValueError("Node type not part of the bad_names stat")
@@ -209,6 +213,7 @@ class LinterStats:
"method",
"module",
"variable",
+ "typevar",
],
node_name,
)
@@ -231,6 +236,7 @@ class LinterStats:
method=0,
module=0,
variable=0,
+ typevar=0,
)
def get_code_count(
@@ -318,6 +324,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 4e093a87c..1cacac6c7 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=__.*__
diff --git a/tests/checkers/unittest_base.py b/tests/checkers/unittest_base.py
index 198eb174e..b8eb2f350 100644
--- a/tests/checkers/unittest_base.py
+++ b/tests/checkers/unittest_base.py
@@ -190,13 +190,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 000000000..105ddd20c
--- /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 000000000..a0f2257d4
--- /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 000000000..c08eb9e41
--- /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 000000000..347da995e
--- /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 000000000..0b291bbf3
--- /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