diff options
author | Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> | 2022-03-24 16:30:02 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-24 16:30:02 +0100 |
commit | fd91d04a2f946c6fe8b31b6f9217a61caab55c7d (patch) | |
tree | cefdda722781e11603468e947f968c1e3883094e | |
parent | bb8e098f5a8c18ac68cebdae12d48138bab858b8 (diff) | |
download | pylint-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-- | ChangeLog | 5 | ||||
-rw-r--r-- | doc/user_guide/options.rst | 17 | ||||
-rw-r--r-- | doc/whatsnew/2.13.rst | 5 | ||||
-rw-r--r-- | pylint/checkers/base.py | 87 | ||||
-rw-r--r-- | pylint/constants.py | 1 | ||||
-rw-r--r-- | pylint/utils/linterstats.py | 7 | ||||
-rw-r--r-- | pylintrc | 3 | ||||
-rw-r--r-- | tests/checkers/unittest_base.py | 4 | ||||
-rw-r--r-- | tests/functional/t/typevar_naming_style_default.py | 49 | ||||
-rw-r--r-- | tests/functional/t/typevar_naming_style_default.txt | 12 | ||||
-rw-r--r-- | tests/functional/t/typevar_naming_style_rgx.py | 15 | ||||
-rw-r--r-- | tests/functional/t/typevar_naming_style_rgx.rc | 2 | ||||
-rw-r--r-- | tests/functional/t/typevar_naming_style_rgx.txt | 3 |
13 files changed, 176 insertions, 34 deletions
@@ -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 @@ -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 |