summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPierre Sassoulas <pierre.sassoulas@gmail.com>2022-03-24 20:06:46 +0100
committerPierre Sassoulas <pierre.sassoulas@gmail.com>2022-03-24 22:40:30 +0100
commit977b08d160e81aaecebf871d2b8ba2f9a96ef9d6 (patch)
tree0ac6fda5c3d24e2be4f155af13b9854f93329cc8
parentddfca0ca884d677e4eb0e6f53553b16e7a503157 (diff)
downloadpylint-git-977b08d160e81aaecebf871d2b8ba2f9a96ef9d6.tar.gz
[refactor] Create files for comparison checker in pylint.checker.base
-rw-r--r--pylint/checkers/base/__init__.py287
-rw-r--r--pylint/checkers/base/comparison_checker.py295
2 files changed, 297 insertions, 285 deletions
diff --git a/pylint/checkers/base/__init__.py b/pylint/checkers/base/__init__.py
index c2cb771e1..bf0be8566 100644
--- a/pylint/checkers/base/__init__.py
+++ b/pylint/checkers/base/__init__.py
@@ -16,6 +16,7 @@ from pylint import constants, interfaces
from pylint import utils as lint_utils
from pylint.checkers import utils
from pylint.checkers.base.basic_checker import _BasicChecker
+from pylint.checkers.base.comparison_checker import ComparisonChecker
from pylint.checkers.utils import (
infer_all,
is_overload_stub,
@@ -135,10 +136,7 @@ NO_REQUIRED_DOC_RGX = re.compile("^_")
REVERSED_PROTOCOL_METHOD = "__reversed__"
SEQUENCE_PROTOCOL_METHODS = ("__getitem__", "__len__")
REVERSED_METHODS = (SEQUENCE_PROTOCOL_METHODS, (REVERSED_PROTOCOL_METHOD,))
-TYPECHECK_COMPARISON_OPERATORS = frozenset(("is", "is not", "==", "!="))
-LITERAL_NODE_TYPES = (nodes.Const, nodes.Dict, nodes.List, nodes.Set)
UNITTEST_CASE = "unittest.case"
-TYPE_QNAME = "builtins.type"
ABC_METACLASSES = {"_py_abc.ABCMeta", "abc.ABCMeta"} # Python 3.7+,
# Name categories that are always consistent with all naming conventions.
@@ -165,7 +163,7 @@ DEFAULT_ARGUMENT_SYMBOLS = dict(
},
)
-COMPARISON_OPERATORS = frozenset(("==", "!=", "<", ">", "<=", ">="))
+
# List of methods which can be redefined
REDEFINABLE_METHODS = frozenset(("__module__",))
TYPING_FORWARD_REF_QNAME = "typing.ForwardRef"
@@ -2333,13 +2331,6 @@ class PassChecker(_BasicChecker):
self.add_message("unnecessary-pass", node=node)
-def _is_one_arg_pos_call(call):
- """Is this a call with exactly 1 argument,
- where that argument is positional?
- """
- return isinstance(call, nodes.Call) and len(call.args) == 1 and not call.keywords
-
-
def _infer_dunder_doc_attribute(node):
# Try to see if we have a `__doc__` attribute.
try:
@@ -2355,280 +2346,6 @@ def _infer_dunder_doc_attribute(node):
return docstring.value
-class ComparisonChecker(_BasicChecker):
- """Checks for comparisons.
-
- - singleton comparison: 'expr == True', 'expr == False' and 'expr == None'
- - yoda condition: 'const "comp" right' where comp can be '==', '!=', '<',
- '<=', '>' or '>=', and right can be a variable, an attribute, a method or
- a function
- """
-
- msgs = {
- "C0121": (
- "Comparison %s should be %s",
- "singleton-comparison",
- "Used when an expression is compared to singleton "
- "values like True, False or None.",
- ),
- "C0123": (
- "Use isinstance() rather than type() for a typecheck.",
- "unidiomatic-typecheck",
- "The idiomatic way to perform an explicit typecheck in "
- "Python is to use isinstance(x, Y) rather than "
- "type(x) == Y, type(x) is Y. Though there are unusual "
- "situations where these give different results.",
- {"old_names": [("W0154", "old-unidiomatic-typecheck")]},
- ),
- "R0123": (
- "Comparison to literal",
- "literal-comparison",
- "Used when comparing an object to a literal, which is usually "
- "what you do not want to do, since you can compare to a different "
- "literal than what was expected altogether.",
- ),
- "R0124": (
- "Redundant comparison - %s",
- "comparison-with-itself",
- "Used when something is compared against itself.",
- ),
- "W0143": (
- "Comparing against a callable, did you omit the parenthesis?",
- "comparison-with-callable",
- "This message is emitted when pylint detects that a comparison with a "
- "callable was made, which might suggest that some parenthesis were omitted, "
- "resulting in potential unwanted behaviour.",
- ),
- "W0177": (
- "Comparison %s should be %s",
- "nan-comparison",
- "Used when an expression is compared to NaN"
- "values like numpy.NaN and float('nan')",
- ),
- }
-
- def _check_singleton_comparison(
- self, left_value, right_value, root_node, checking_for_absence: bool = False
- ):
- """Check if == or != is being used to compare a singleton value."""
- singleton_values = (True, False, None)
-
- def _is_singleton_const(node) -> bool:
- return isinstance(node, nodes.Const) and any(
- node.value is value for value in singleton_values
- )
-
- if _is_singleton_const(left_value):
- singleton, other_value = left_value.value, right_value
- elif _is_singleton_const(right_value):
- singleton, other_value = right_value.value, left_value
- else:
- return
-
- singleton_comparison_example = {False: "'{} is {}'", True: "'{} is not {}'"}
-
- # True/False singletons have a special-cased message in case the user is
- # mistakenly using == or != to check for truthiness
- if singleton in {True, False}:
- suggestion_template = (
- "{} if checking for the singleton value {}, or {} if testing for {}"
- )
- truthiness_example = {False: "not {}", True: "{}"}
- truthiness_phrase = {True: "truthiness", False: "falsiness"}
-
- # Looks for comparisons like x == True or x != False
- checking_truthiness = singleton is not checking_for_absence
-
- suggestion = suggestion_template.format(
- singleton_comparison_example[checking_for_absence].format(
- left_value.as_string(), right_value.as_string()
- ),
- singleton,
- (
- "'bool({})'"
- if not utils.is_test_condition(root_node) and checking_truthiness
- else "'{}'"
- ).format(
- truthiness_example[checking_truthiness].format(
- other_value.as_string()
- )
- ),
- truthiness_phrase[checking_truthiness],
- )
- else:
- suggestion = singleton_comparison_example[checking_for_absence].format(
- left_value.as_string(), right_value.as_string()
- )
- self.add_message(
- "singleton-comparison",
- node=root_node,
- args=(f"'{root_node.as_string()}'", suggestion),
- )
-
- def _check_nan_comparison(
- self, left_value, right_value, root_node, checking_for_absence: bool = False
- ):
- def _is_float_nan(node):
- try:
- if isinstance(node, nodes.Call) and len(node.args) == 1:
- if (
- node.args[0].value.lower() == "nan"
- and node.inferred()[0].pytype() == "builtins.float"
- ):
- return True
- return False
- except AttributeError:
- return False
-
- def _is_numpy_nan(node):
- if isinstance(node, nodes.Attribute) and node.attrname == "NaN":
- if isinstance(node.expr, nodes.Name):
- return node.expr.name in {"numpy", "nmp", "np"}
- return False
-
- def _is_nan(node) -> bool:
- return _is_float_nan(node) or _is_numpy_nan(node)
-
- nan_left = _is_nan(left_value)
- if not nan_left and not _is_nan(right_value):
- return
-
- absence_text = ""
- if checking_for_absence:
- absence_text = "not "
- if nan_left:
- suggestion = f"'{absence_text}math.isnan({right_value.as_string()})'"
- else:
- suggestion = f"'{absence_text}math.isnan({left_value.as_string()})'"
- self.add_message(
- "nan-comparison",
- node=root_node,
- args=(f"'{root_node.as_string()}'", suggestion),
- )
-
- def _check_literal_comparison(self, literal, node: nodes.Compare):
- """Check if we compare to a literal, which is usually what we do not want to do."""
- is_other_literal = isinstance(literal, (nodes.List, nodes.Dict, nodes.Set))
- is_const = False
- if isinstance(literal, nodes.Const):
- if isinstance(literal.value, bool) or literal.value is None:
- # Not interested in these values.
- return
- is_const = isinstance(literal.value, (bytes, str, int, float))
-
- if is_const or is_other_literal:
- self.add_message("literal-comparison", node=node)
-
- def _check_logical_tautology(self, node: nodes.Compare):
- """Check if identifier is compared against itself.
-
- :param node: Compare node
- :Example:
- val = 786
- if val == val: # [comparison-with-itself]
- pass
- """
- left_operand = node.left
- right_operand = node.ops[0][1]
- operator = node.ops[0][0]
- if isinstance(left_operand, nodes.Const) and isinstance(
- right_operand, nodes.Const
- ):
- left_operand = left_operand.value
- right_operand = right_operand.value
- elif isinstance(left_operand, nodes.Name) and isinstance(
- right_operand, nodes.Name
- ):
- left_operand = left_operand.name
- right_operand = right_operand.name
-
- if left_operand == right_operand:
- suggestion = f"{left_operand} {operator} {right_operand}"
- self.add_message("comparison-with-itself", node=node, args=(suggestion,))
-
- def _check_callable_comparison(self, node):
- operator = node.ops[0][0]
- if operator not in COMPARISON_OPERATORS:
- return
-
- bare_callables = (nodes.FunctionDef, astroid.BoundMethod)
- left_operand, right_operand = node.left, node.ops[0][1]
- # this message should be emitted only when there is comparison of bare callable
- # with non bare callable.
- number_of_bare_callables = 0
- for operand in left_operand, right_operand:
- inferred = utils.safe_infer(operand)
- # Ignore callables that raise, as well as typing constants
- # implemented as functions (that raise via their decorator)
- if (
- isinstance(inferred, bare_callables)
- and "typing._SpecialForm" not in inferred.decoratornames()
- and not any(isinstance(x, nodes.Raise) for x in inferred.body)
- ):
- number_of_bare_callables += 1
- if number_of_bare_callables == 1:
- self.add_message("comparison-with-callable", node=node)
-
- @utils.check_messages(
- "singleton-comparison",
- "unidiomatic-typecheck",
- "literal-comparison",
- "comparison-with-itself",
- "comparison-with-callable",
- )
- def visit_compare(self, node: nodes.Compare) -> None:
- self._check_callable_comparison(node)
- self._check_logical_tautology(node)
- self._check_unidiomatic_typecheck(node)
- # NOTE: this checker only works with binary comparisons like 'x == 42'
- # but not 'x == y == 42'
- if len(node.ops) != 1:
- return
-
- left = node.left
- operator, right = node.ops[0]
-
- if operator in {"==", "!="}:
- self._check_singleton_comparison(
- left, right, node, checking_for_absence=operator == "!="
- )
-
- if operator in {"==", "!=", "is", "is not"}:
- self._check_nan_comparison(
- left, right, node, checking_for_absence=operator in {"!=", "is not"}
- )
- if operator in {"is", "is not"}:
- self._check_literal_comparison(right, node)
-
- def _check_unidiomatic_typecheck(self, node):
- operator, right = node.ops[0]
- if operator in TYPECHECK_COMPARISON_OPERATORS:
- left = node.left
- if _is_one_arg_pos_call(left):
- self._check_type_x_is_y(node, left, operator, right)
-
- def _check_type_x_is_y(self, node, left, operator, right):
- """Check for expressions like type(x) == Y."""
- left_func = utils.safe_infer(left.func)
- if not (
- isinstance(left_func, nodes.ClassDef) and left_func.qname() == TYPE_QNAME
- ):
- return
-
- if operator in {"is", "is not"} and _is_one_arg_pos_call(right):
- right_func = utils.safe_infer(right.func)
- if (
- isinstance(right_func, nodes.ClassDef)
- and right_func.qname() == TYPE_QNAME
- ):
- # type(x) == type(a)
- right_arg = utils.safe_infer(right.args[0])
- if not isinstance(right_arg, LITERAL_NODE_TYPES):
- # not e.g. type(x) == type([])
- return
- self.add_message("unidiomatic-typecheck", node=node)
-
-
def register(linter: "PyLinter") -> None:
linter.register_checker(BasicErrorChecker(linter))
linter.register_checker(BasicChecker(linter))
diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py
new file mode 100644
index 000000000..1db50761b
--- /dev/null
+++ b/pylint/checkers/base/comparison_checker.py
@@ -0,0 +1,295 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Comparison checker from the basic checker."""
+
+import astroid
+from astroid import nodes
+
+from pylint.checkers import utils
+from pylint.checkers.base.basic_checker import _BasicChecker
+
+LITERAL_NODE_TYPES = (nodes.Const, nodes.Dict, nodes.List, nodes.Set)
+COMPARISON_OPERATORS = frozenset(("==", "!=", "<", ">", "<=", ">="))
+TYPECHECK_COMPARISON_OPERATORS = frozenset(("is", "is not", "==", "!="))
+TYPE_QNAME = "builtins.type"
+
+
+def _is_one_arg_pos_call(call):
+ """Is this a call with exactly 1 positional argument ?"""
+ return isinstance(call, nodes.Call) and len(call.args) == 1 and not call.keywords
+
+
+class ComparisonChecker(_BasicChecker):
+ """Checks for comparisons.
+
+ - singleton comparison: 'expr == True', 'expr == False' and 'expr == None'
+ - yoda condition: 'const "comp" right' where comp can be '==', '!=', '<',
+ '<=', '>' or '>=', and right can be a variable, an attribute, a method or
+ a function
+ """
+
+ msgs = {
+ "C0121": (
+ "Comparison %s should be %s",
+ "singleton-comparison",
+ "Used when an expression is compared to singleton "
+ "values like True, False or None.",
+ ),
+ "C0123": (
+ "Use isinstance() rather than type() for a typecheck.",
+ "unidiomatic-typecheck",
+ "The idiomatic way to perform an explicit typecheck in "
+ "Python is to use isinstance(x, Y) rather than "
+ "type(x) == Y, type(x) is Y. Though there are unusual "
+ "situations where these give different results.",
+ {"old_names": [("W0154", "old-unidiomatic-typecheck")]},
+ ),
+ "R0123": (
+ "Comparison to literal",
+ "literal-comparison",
+ "Used when comparing an object to a literal, which is usually "
+ "what you do not want to do, since you can compare to a different "
+ "literal than what was expected altogether.",
+ ),
+ "R0124": (
+ "Redundant comparison - %s",
+ "comparison-with-itself",
+ "Used when something is compared against itself.",
+ ),
+ "W0143": (
+ "Comparing against a callable, did you omit the parenthesis?",
+ "comparison-with-callable",
+ "This message is emitted when pylint detects that a comparison with a "
+ "callable was made, which might suggest that some parenthesis were omitted, "
+ "resulting in potential unwanted behaviour.",
+ ),
+ "W0177": (
+ "Comparison %s should be %s",
+ "nan-comparison",
+ "Used when an expression is compared to NaN"
+ "values like numpy.NaN and float('nan')",
+ ),
+ }
+
+ def _check_singleton_comparison(
+ self, left_value, right_value, root_node, checking_for_absence: bool = False
+ ):
+ """Check if == or != is being used to compare a singleton value."""
+ singleton_values = (True, False, None)
+
+ def _is_singleton_const(node) -> bool:
+ return isinstance(node, nodes.Const) and any(
+ node.value is value for value in singleton_values
+ )
+
+ if _is_singleton_const(left_value):
+ singleton, other_value = left_value.value, right_value
+ elif _is_singleton_const(right_value):
+ singleton, other_value = right_value.value, left_value
+ else:
+ return
+
+ singleton_comparison_example = {False: "'{} is {}'", True: "'{} is not {}'"}
+
+ # True/False singletons have a special-cased message in case the user is
+ # mistakenly using == or != to check for truthiness
+ if singleton in {True, False}:
+ suggestion_template = (
+ "{} if checking for the singleton value {}, or {} if testing for {}"
+ )
+ truthiness_example = {False: "not {}", True: "{}"}
+ truthiness_phrase = {True: "truthiness", False: "falsiness"}
+
+ # Looks for comparisons like x == True or x != False
+ checking_truthiness = singleton is not checking_for_absence
+
+ suggestion = suggestion_template.format(
+ singleton_comparison_example[checking_for_absence].format(
+ left_value.as_string(), right_value.as_string()
+ ),
+ singleton,
+ (
+ "'bool({})'"
+ if not utils.is_test_condition(root_node) and checking_truthiness
+ else "'{}'"
+ ).format(
+ truthiness_example[checking_truthiness].format(
+ other_value.as_string()
+ )
+ ),
+ truthiness_phrase[checking_truthiness],
+ )
+ else:
+ suggestion = singleton_comparison_example[checking_for_absence].format(
+ left_value.as_string(), right_value.as_string()
+ )
+ self.add_message(
+ "singleton-comparison",
+ node=root_node,
+ args=(f"'{root_node.as_string()}'", suggestion),
+ )
+
+ def _check_nan_comparison(
+ self, left_value, right_value, root_node, checking_for_absence: bool = False
+ ):
+ def _is_float_nan(node):
+ try:
+ if isinstance(node, nodes.Call) and len(node.args) == 1:
+ if (
+ node.args[0].value.lower() == "nan"
+ and node.inferred()[0].pytype() == "builtins.float"
+ ):
+ return True
+ return False
+ except AttributeError:
+ return False
+
+ def _is_numpy_nan(node):
+ if isinstance(node, nodes.Attribute) and node.attrname == "NaN":
+ if isinstance(node.expr, nodes.Name):
+ return node.expr.name in {"numpy", "nmp", "np"}
+ return False
+
+ def _is_nan(node) -> bool:
+ return _is_float_nan(node) or _is_numpy_nan(node)
+
+ nan_left = _is_nan(left_value)
+ if not nan_left and not _is_nan(right_value):
+ return
+
+ absence_text = ""
+ if checking_for_absence:
+ absence_text = "not "
+ if nan_left:
+ suggestion = f"'{absence_text}math.isnan({right_value.as_string()})'"
+ else:
+ suggestion = f"'{absence_text}math.isnan({left_value.as_string()})'"
+ self.add_message(
+ "nan-comparison",
+ node=root_node,
+ args=(f"'{root_node.as_string()}'", suggestion),
+ )
+
+ def _check_literal_comparison(self, literal, node: nodes.Compare):
+ """Check if we compare to a literal, which is usually what we do not want to do."""
+ is_other_literal = isinstance(literal, (nodes.List, nodes.Dict, nodes.Set))
+ is_const = False
+ if isinstance(literal, nodes.Const):
+ if isinstance(literal.value, bool) or literal.value is None:
+ # Not interested in these values.
+ return
+ is_const = isinstance(literal.value, (bytes, str, int, float))
+
+ if is_const or is_other_literal:
+ self.add_message("literal-comparison", node=node)
+
+ def _check_logical_tautology(self, node: nodes.Compare):
+ """Check if identifier is compared against itself.
+
+ :param node: Compare node
+ :Example:
+ val = 786
+ if val == val: # [comparison-with-itself]
+ pass
+ """
+ left_operand = node.left
+ right_operand = node.ops[0][1]
+ operator = node.ops[0][0]
+ if isinstance(left_operand, nodes.Const) and isinstance(
+ right_operand, nodes.Const
+ ):
+ left_operand = left_operand.value
+ right_operand = right_operand.value
+ elif isinstance(left_operand, nodes.Name) and isinstance(
+ right_operand, nodes.Name
+ ):
+ left_operand = left_operand.name
+ right_operand = right_operand.name
+
+ if left_operand == right_operand:
+ suggestion = f"{left_operand} {operator} {right_operand}"
+ self.add_message("comparison-with-itself", node=node, args=(suggestion,))
+
+ def _check_callable_comparison(self, node):
+ operator = node.ops[0][0]
+ if operator not in COMPARISON_OPERATORS:
+ return
+
+ bare_callables = (nodes.FunctionDef, astroid.BoundMethod)
+ left_operand, right_operand = node.left, node.ops[0][1]
+ # this message should be emitted only when there is comparison of bare callable
+ # with non bare callable.
+ number_of_bare_callables = 0
+ for operand in left_operand, right_operand:
+ inferred = utils.safe_infer(operand)
+ # Ignore callables that raise, as well as typing constants
+ # implemented as functions (that raise via their decorator)
+ if (
+ isinstance(inferred, bare_callables)
+ and "typing._SpecialForm" not in inferred.decoratornames()
+ and not any(isinstance(x, nodes.Raise) for x in inferred.body)
+ ):
+ number_of_bare_callables += 1
+ if number_of_bare_callables == 1:
+ self.add_message("comparison-with-callable", node=node)
+
+ @utils.check_messages(
+ "singleton-comparison",
+ "unidiomatic-typecheck",
+ "literal-comparison",
+ "comparison-with-itself",
+ "comparison-with-callable",
+ )
+ def visit_compare(self, node: nodes.Compare) -> None:
+ self._check_callable_comparison(node)
+ self._check_logical_tautology(node)
+ self._check_unidiomatic_typecheck(node)
+ # NOTE: this checker only works with binary comparisons like 'x == 42'
+ # but not 'x == y == 42'
+ if len(node.ops) != 1:
+ return
+
+ left = node.left
+ operator, right = node.ops[0]
+
+ if operator in {"==", "!="}:
+ self._check_singleton_comparison(
+ left, right, node, checking_for_absence=operator == "!="
+ )
+
+ if operator in {"==", "!=", "is", "is not"}:
+ self._check_nan_comparison(
+ left, right, node, checking_for_absence=operator in {"!=", "is not"}
+ )
+ if operator in {"is", "is not"}:
+ self._check_literal_comparison(right, node)
+
+ def _check_unidiomatic_typecheck(self, node):
+ operator, right = node.ops[0]
+ if operator in TYPECHECK_COMPARISON_OPERATORS:
+ left = node.left
+ if _is_one_arg_pos_call(left):
+ self._check_type_x_is_y(node, left, operator, right)
+
+ def _check_type_x_is_y(self, node, left, operator, right):
+ """Check for expressions like type(x) == Y."""
+ left_func = utils.safe_infer(left.func)
+ if not (
+ isinstance(left_func, nodes.ClassDef) and left_func.qname() == TYPE_QNAME
+ ):
+ return
+
+ if operator in {"is", "is not"} and _is_one_arg_pos_call(right):
+ right_func = utils.safe_infer(right.func)
+ if (
+ isinstance(right_func, nodes.ClassDef)
+ and right_func.qname() == TYPE_QNAME
+ ):
+ # type(x) == type(a)
+ right_arg = utils.safe_infer(right.args[0])
+ if not isinstance(right_arg, LITERAL_NODE_TYPES):
+ # not e.g. type(x) == type([])
+ return
+ self.add_message("unidiomatic-typecheck", node=node)