| Viewing file:  comparison_checker.py (13.54 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
# 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
 from pylint.interfaces import HIGH
 
 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: nodes.NodeNG) -> bool:
 """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": (
 "In '%s', use '%s' when comparing constant literals not '%s' ('%s')",
 "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.",
 ),
 "R0133": (
 "Comparison between constants: '%s %s %s' has a constant value",
 "comparison-of-constants",
 "When two literals are compared with each other the result is a constant. "
 "Using the constant directly is both easier to read and more performant. "
 "Initializing 'True' and 'False' this way is not required since Python 2.3.",
 ),
 "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: nodes.NodeNG,
 right_value: nodes.NodeNG,
 root_node: nodes.Compare,
 checking_for_absence: bool = False,
 ) -> None:
 """Check if == or != is being used to compare a singleton value."""
 
 if utils.is_singleton_const(left_value):
 singleton, other_value = left_value.value, right_value
 elif utils.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: nodes.NodeNG,
 right_value: nodes.NodeNG,
 root_node: nodes.Compare,
 checking_for_absence: bool = False,
 ) -> None:
 def _is_float_nan(node: nodes.NodeNG) -> bool:
 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: nodes.NodeNG) -> bool:
 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: nodes.NodeNG) -> 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: nodes.NodeNG, node: nodes.Compare
 ) -> None:
 """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:
 incorrect_node_str = node.as_string()
 if "is not" in incorrect_node_str:
 equal_or_not_equal = "!="
 is_or_is_not = "is not"
 else:
 equal_or_not_equal = "=="
 is_or_is_not = "is"
 fixed_node_str = incorrect_node_str.replace(
 is_or_is_not, equal_or_not_equal
 )
 self.add_message(
 "literal-comparison",
 args=(
 incorrect_node_str,
 equal_or_not_equal,
 is_or_is_not,
 fixed_node_str,
 ),
 node=node,
 confidence=HIGH,
 )
 
 def _check_logical_tautology(self, node: nodes.Compare) -> None:
 """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_constants_comparison(self, node: nodes.Compare) -> None:
 """When two constants are being compared it is always a logical tautology."""
 left_operand = node.left
 if not isinstance(left_operand, nodes.Const):
 return
 
 right_operand = node.ops[0][1]
 if not isinstance(right_operand, nodes.Const):
 return
 
 operator = node.ops[0][0]
 self.add_message(
 "comparison-of-constants",
 node=node,
 args=(left_operand.value, operator, right_operand.value),
 confidence=HIGH,
 )
 
 def _check_callable_comparison(self, node: nodes.Compare) -> None:
 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.only_required_for_messages(
 "singleton-comparison",
 "unidiomatic-typecheck",
 "literal-comparison",
 "comparison-with-itself",
 "comparison-of-constants",
 "comparison-with-callable",
 "nan-comparison",
 )
 def visit_compare(self, node: nodes.Compare) -> None:
 self._check_callable_comparison(node)
 self._check_logical_tautology(node)
 self._check_unidiomatic_typecheck(node)
 self._check_constants_comparison(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: nodes.Compare) -> None:
 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: nodes.Compare,
 left: nodes.NodeNG,
 operator: str,
 right: nodes.NodeNG,
 ) -> None:
 """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)
 
 |