summaryrefslogtreecommitdiff
path: root/pylint/checkers/base/comparison_checker.py
blob: ffbd273744797ae751e11686c98d6a5acf9ea85b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# 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)