diff options
-rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | doc/whatsnew/2.13.rst | 4 | ||||
-rw-r--r-- | pylint/checkers/base.py | 121 | ||||
-rw-r--r-- | tests/functional/g/generic_alias/generic_alias_side_effects.py | 2 | ||||
-rw-r--r-- | tests/functional/r/regression/regression_2443_duplicate_bases.py | 2 | ||||
-rw-r--r-- | tests/functional/t/typevar_name_incorrect_variance.py | 69 | ||||
-rw-r--r-- | tests/functional/t/typevar_name_incorrect_variance.txt | 21 |
8 files changed, 218 insertions, 7 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index eeaaa13b1..ec76f57d7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -598,6 +598,8 @@ contributors: * Kian-Meng, Ang: contributor +* Nuzula H. Yudaka (Nuzhuka): contributor + * Carli Freudenberg (CarliJoy): contributor - Fixed issue 5281, added Unicode checker - Improve non-ascii-name checker @@ -450,6 +450,10 @@ Release date: TBA Insert your changelog randomly, it will reduce merge conflicts (Ie. not necessarily at the end) +* 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. + What's New in Pylint 2.12.2? ============================ diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 1aa94d4af..96dfa518a 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -37,6 +37,10 @@ New checkers Closes #5460 +* 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. + * Add ``modified-iterating-list``, ``modified-iterating-dict``, and ``modified-iterating-set``, emitted when items are added to or removed from respectively a list, dictionary or set being iterated through. diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index e2a3ece49..71dabd8ef 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -228,6 +228,7 @@ COMPARISON_OPERATORS = frozenset(("==", "!=", "<", ">", "<=", ">=")) # List of methods which can be redefined REDEFINABLE_METHODS = frozenset(("__module__",)) TYPING_FORWARD_REF_QNAME = "typing.ForwardRef" +TYPING_TYPE_VAR_QNAME = "typing.TypeVar" def _redefines_import(node): @@ -1738,6 +1739,16 @@ class NameChecker(_BasicChecker): ] }, ), + "C0105": ( + 'Type variable "%s" is %s, use "%s" instead', + "typevar-name-incorrect-variance", + "Emitted when a TypeVar name doesn't reflect its type variance. " + "According to PEP8, it is recommended to add suffixes '_co' and " + "'_contra' to the variables used to declare covariant or " + "contravariant behaviour respectively. Invariant (default) variables " + "do not require a suffix. The message is also emitted when invariant " + "variables do have a suffix.", + ), "W0111": ( "Name %s will become a keyword in Python %s", "assign-to-new-keyword", @@ -1940,33 +1951,69 @@ class NameChecker(_BasicChecker): for name in node.names: self._check_name("const", name, node) - @utils.check_messages("disallowed-name", "invalid-name", "assign-to-new-keyword") + @utils.check_messages( + "disallowed-name", + "invalid-name", + "assign-to-new-keyword", + "typevar-name-incorrect-variance", + ) def visit_assignname(self, node: nodes.AssignName) -> None: """Check module level assigned names.""" self._check_assign_to_new_keyword_violation(node.name, node) frame = node.frame(future=True) assign_type = node.assign_type() + + # Check names defined in comprehensions if isinstance(assign_type, nodes.Comprehension): self._check_name("inlinevar", node.name, node) + + # Check names defined in module scope elif isinstance(frame, nodes.Module): + # Check names defined in Assign nodes if isinstance(assign_type, nodes.Assign): - if isinstance(utils.safe_infer(assign_type.value), nodes.ClassDef): + inferred_assign_type = utils.safe_infer(assign_type.value) + + # Check TypeVar's assigned alone or in tuple assignment + if isinstance(node.parent, nodes.Assign) and self._assigns_typevar( + assign_type.value + ): + self._check_name("typevar", assign_type.targets[0].name, node) + elif ( + isinstance(node.parent, nodes.Tuple) + and isinstance(assign_type.value, nodes.Tuple) + and self._assigns_typevar( + assign_type.value.elts[node.parent.elts.index(node)] + ) + ): + self._check_name( + "typevar", + assign_type.targets[0].elts[node.parent.elts.index(node)].name, + node, + ) + + # Check classes (TypeVar's are classes so they need to be excluded first) + elif isinstance(inferred_assign_type, nodes.ClassDef): self._check_name("class", node.name, node) - # Don't emit if the name redefines an import - # in an ImportError except handler. + + # Don't emit if the name redefines an import in an ImportError except handler. elif not _redefines_import(node) and isinstance( - utils.safe_infer(assign_type.value), nodes.Const + inferred_assign_type, nodes.Const ): self._check_name("const", node.name, node) + # Check names defined in AnnAssign nodes elif isinstance( assign_type, nodes.AnnAssign ) and utils.is_assign_name_annotated_with(node, "Final"): self._check_name("const", node.name, node) + + # Check names defined in function scopes elif isinstance(frame, nodes.FunctionDef): # global introduced variable aren't in the function locals if node.name in frame and node.name not in frame.argnames(): if not _redefines_import(node): self._check_name("variable", node.name, node) + + # Check names defined in class scopes elif isinstance(frame, nodes.ClassDef): if not list(frame.local_attr_ancestors(node.name)): for ancestor in frame.ancestors(): @@ -2031,6 +2078,14 @@ 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) @@ -2075,6 +2130,62 @@ class NameChecker(_BasicChecker): return ".".join(str(v) for v in version) return None + @staticmethod + def _assigns_typevar(node: Optional[nodes.NodeNG]) -> bool: + """Check if a node is assigning a TypeVar.""" + if isinstance(node, astroid.Call): + inferred = utils.safe_infer(node.func) + if ( + isinstance(inferred, astroid.ClassDef) + and inferred.qname() == TYPING_TYPE_VAR_QNAME + ): + return True + return False + + def _check_typevar_variance(self, name: str, node: nodes.AssignName) -> None: + """Check if a TypeVar has a variance and if it's included in the name. + + Returns the args for the message to be displayed. + """ + if isinstance(node.parent, nodes.Assign): + keywords = node.assign_type().value.keywords + elif isinstance(node.parent, nodes.Tuple): + keywords = ( + node.assign_type().value.elts[node.parent.elts.index(node)].keywords + ) + + for kw in keywords: + if kw.arg == "covariant" and kw.value.value: + if not name.endswith("_co"): + suggest_name = f"{re.sub('_contra$', '', name)}_co" + self.add_message( + "typevar-name-incorrect-variance", + node=node, + args=(name, "covariant", suggest_name), + confidence=interfaces.INFERENCE, + ) + return + + if kw.arg == "contravariant" and kw.value.value: + if not name.endswith("_contra"): + suggest_name = f"{re.sub('_co$', '', name)}_contra" + self.add_message( + "typevar-name-incorrect-variance", + node=node, + args=(name, "contravariant", suggest_name), + confidence=interfaces.INFERENCE, + ) + return + + if name.endswith("_co") or name.endswith("_contra"): + suggest_name = re.sub("_contra$|_co$", "", name) + self.add_message( + "typevar-name-incorrect-variance", + node=node, + args=(name, "invariant", suggest_name), + confidence=interfaces.INFERENCE, + ) + class DocStringChecker(_BasicChecker): msgs = { diff --git a/tests/functional/g/generic_alias/generic_alias_side_effects.py b/tests/functional/g/generic_alias/generic_alias_side_effects.py index ea7d7a9ce..344db9553 100644 --- a/tests/functional/g/generic_alias/generic_alias_side_effects.py +++ b/tests/functional/g/generic_alias/generic_alias_side_effects.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring,invalid-name,line-too-long,too-few-public-methods,use-list-literal,use-dict-literal +# pylint: disable=missing-docstring,invalid-name,line-too-long,too-few-public-methods,use-list-literal,use-dict-literal, typevar-name-incorrect-variance import typing import collections from typing import Generic, TypeVar diff --git a/tests/functional/r/regression/regression_2443_duplicate_bases.py b/tests/functional/r/regression/regression_2443_duplicate_bases.py index f32490c44..b5ea1c9f9 100644 --- a/tests/functional/r/regression/regression_2443_duplicate_bases.py +++ b/tests/functional/r/regression/regression_2443_duplicate_bases.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring, too-many-ancestors,too-few-public-methods +# pylint: disable=missing-docstring, too-many-ancestors,too-few-public-methods, typevar-name-incorrect-variance from typing import Generic, TypeVar IN = TypeVar('IN', contravariant=True) diff --git a/tests/functional/t/typevar_name_incorrect_variance.py b/tests/functional/t/typevar_name_incorrect_variance.py new file mode 100644 index 000000000..bb8a89880 --- /dev/null +++ b/tests/functional/t/typevar_name_incorrect_variance.py @@ -0,0 +1,69 @@ +"""Test case for typevar-name-incorrect-variance.""" + +from typing import TypeVar + +# Type variables without variance +T = TypeVar("T") +T_co = TypeVar("T_co") # [typevar-name-incorrect-variance] +T_contra = TypeVar("T_contra") # [typevar-name-incorrect-variance] +ScoresT_contra = TypeVar("ScoresT_contra") # [typevar-name-incorrect-variance] + +# Type variables not starting with T +N = TypeVar("N") +N_co = TypeVar("N_co", covariant=True) +N_contra = TypeVar("N_contra", contravariant=True) + +# Tests for combinations with contravariance +CT_co = TypeVar("CT_co", contravariant=True) # [typevar-name-incorrect-variance] +CT_contra = TypeVar("CT_contra") # [typevar-name-incorrect-variance] +CT_contra = TypeVar("CT_contra", contravariant=True) + +# Tests for combinations with covariance +VT = TypeVar("VT", covariant=True) # [typevar-name-incorrect-variance] +VT_contra = TypeVar("VT_contra", covariant=True) # [typevar-name-incorrect-variance] +VT_co = TypeVar("VT_co", covariant=True) + +# Tests for combinations with bound +VT = TypeVar("VT", bound=int) +VT_co = TypeVar("VT_co", bound=int) # [typevar-name-incorrect-variance] +VT_contra = TypeVar("VT_contra", bound=int) # [typevar-name-incorrect-variance] + +VT = TypeVar("VT", bound=int, covariant=True) # [typevar-name-incorrect-variance] +VT_co = TypeVar("VT_co", bound=int, covariant=True) +VT_contra = TypeVar( # [typevar-name-incorrect-variance] + "VT_contra", bound=int, covariant=True +) + +VT = TypeVar("VT", bound=int, covariant=False) +VT_co = TypeVar( # [typevar-name-incorrect-variance] + "VT_co", bound=int, covariant=False +) +VT_contra = TypeVar( # [typevar-name-incorrect-variance] + "VT_contra", bound=int, covariant=False +) + +VT = TypeVar("VT", bound=int, contravariant=True) # [typevar-name-incorrect-variance] +VT_co = TypeVar( # [typevar-name-incorrect-variance] + "VT_co", bound=int, contravariant=True +) +VT_contra = TypeVar("VT_contra", bound=int, contravariant=True) + +VT = TypeVar("VT", bound=int, contravariant=False) +VT_co = TypeVar( # [typevar-name-incorrect-variance] + "VT_co", bound=int, contravariant=False +) +VT_contra = TypeVar( # [typevar-name-incorrect-variance] + "VT_contra", bound=int, contravariant=False +) + +# Tests for combinations with tuple assignment +( + VT, # [typevar-name-incorrect-variance] + VT_contra, # [typevar-name-incorrect-variance] +) = TypeVar("VT", covariant=True), TypeVar("VT_contra", covariant=True) +VT_co, VT_contra = TypeVar( # [typevar-name-incorrect-variance] + "VT_co", covariant=True +), TypeVar("VT_contra", covariant=True) +VAR, VT_contra = "a string", TypeVar( # [typevar-name-incorrect-variance] + "VT_contra", covariant=True +) diff --git a/tests/functional/t/typevar_name_incorrect_variance.txt b/tests/functional/t/typevar_name_incorrect_variance.txt new file mode 100644 index 000000000..d4dcfd2f9 --- /dev/null +++ b/tests/functional/t/typevar_name_incorrect_variance.txt @@ -0,0 +1,21 @@ +typevar-name-incorrect-variance:7:0:7:4::"Type variable ""T_co"" is invariant, use ""T"" instead":INFERENCE +typevar-name-incorrect-variance:8:0:8:8::"Type variable ""T_contra"" is invariant, use ""T"" instead":INFERENCE +typevar-name-incorrect-variance:9:0:9:14::"Type variable ""ScoresT_contra"" is invariant, use ""ScoresT"" instead":INFERENCE +typevar-name-incorrect-variance:17:0:17:5::"Type variable ""CT_co"" is contravariant, use ""CT_contra"" instead":INFERENCE +typevar-name-incorrect-variance:18:0:18:9::"Type variable ""CT_contra"" is invariant, use ""CT"" instead":INFERENCE +typevar-name-incorrect-variance:22:0:22:2::"Type variable ""VT"" is covariant, use ""VT_co"" instead":INFERENCE +typevar-name-incorrect-variance:23:0:23:9::"Type variable ""VT_contra"" is covariant, use ""VT_co"" instead":INFERENCE +typevar-name-incorrect-variance:28:0:28:5::"Type variable ""VT_co"" is invariant, use ""VT"" instead":INFERENCE +typevar-name-incorrect-variance:29:0:29:9::"Type variable ""VT_contra"" is invariant, use ""VT"" instead":INFERENCE +typevar-name-incorrect-variance:31:0:31:2::"Type variable ""VT"" is covariant, use ""VT_co"" instead":INFERENCE +typevar-name-incorrect-variance:33:0:33:9::"Type variable ""VT_contra"" is covariant, use ""VT_co"" instead":INFERENCE +typevar-name-incorrect-variance:38:0:38:5::"Type variable ""VT_co"" is invariant, use ""VT"" instead":INFERENCE +typevar-name-incorrect-variance:41:0:41:9::"Type variable ""VT_contra"" is invariant, use ""VT"" instead":INFERENCE +typevar-name-incorrect-variance:45:0:45:2::"Type variable ""VT"" is contravariant, use ""VT_contra"" instead":INFERENCE +typevar-name-incorrect-variance:46:0:46:5::"Type variable ""VT_co"" is contravariant, use ""VT_contra"" instead":INFERENCE +typevar-name-incorrect-variance:52:0:52:5::"Type variable ""VT_co"" is invariant, use ""VT"" instead":INFERENCE +typevar-name-incorrect-variance:55:0:55:9::"Type variable ""VT_contra"" is invariant, use ""VT"" instead":INFERENCE +typevar-name-incorrect-variance:61:4:61:6::"Type variable ""VT"" is covariant, use ""VT_co"" instead":INFERENCE +typevar-name-incorrect-variance:62:4:62:13::"Type variable ""VT_contra"" is covariant, use ""VT_co"" instead":INFERENCE +typevar-name-incorrect-variance:64:7:64:16::"Type variable ""VT_contra"" is covariant, use ""VT_co"" instead":INFERENCE +typevar-name-incorrect-variance:67:5:67:14::"Type variable ""VT_contra"" is covariant, use ""VT_co"" instead":INFERENCE |