diff options
author | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2022-03-24 20:06:46 +0100 |
---|---|---|
committer | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2022-03-24 22:40:30 +0100 |
commit | 977b08d160e81aaecebf871d2b8ba2f9a96ef9d6 (patch) | |
tree | 0ac6fda5c3d24e2be4f155af13b9854f93329cc8 | |
parent | ddfca0ca884d677e4eb0e6f53553b16e7a503157 (diff) | |
download | pylint-git-977b08d160e81aaecebf871d2b8ba2f9a96ef9d6.tar.gz |
[refactor] Create files for comparison checker in pylint.checker.base
-rw-r--r-- | pylint/checkers/base/__init__.py | 287 | ||||
-rw-r--r-- | pylint/checkers/base/comparison_checker.py | 295 |
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) |