summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--ChangeLog4
-rw-r--r--doc/whatsnew/2.13.rst4
-rw-r--r--pylint/checkers/base.py121
-rw-r--r--tests/functional/g/generic_alias/generic_alias_side_effects.py2
-rw-r--r--tests/functional/r/regression/regression_2443_duplicate_bases.py2
-rw-r--r--tests/functional/t/typevar_name_incorrect_variance.py69
-rw-r--r--tests/functional/t/typevar_name_incorrect_variance.txt21
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
diff --git a/ChangeLog b/ChangeLog
index 6e3a338e1..33dacc56b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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