Tipragot
628be439b8
Cela permet de ne pas avoir de problèmes de compatibilité car python est dans le git.
355 lines
14 KiB
Python
355 lines
14 KiB
Python
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
|
|
# Copyright (c) https://github.com/pylint-dev/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)
|