Skip to content

Commit 808880d

Browse files
Skip check in chained comparisons for use-implicit-booleaness-not-x checks (#10246) (#10248)
(cherry picked from commit 0a1044b) Co-authored-by: Zen Lee <53538590+zenlyj@users.noreply.github.com>
1 parent b9f515f commit 808880d

6 files changed

+132
-99
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix false positives for `use-implicit-booleaness-not-comparison`, `use-implicit-booleaness-not-comparison-to-string`
2+
and `use-implicit-booleaness-not-comparison-to-zero` when chained comparisons are checked.
3+
4+
Closes #10065

pylint/checkers/refactoring/implicit_booleaness_checker.py

Lines changed: 92 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -190,108 +190,115 @@ def visit_compare(self, node: nodes.Compare) -> None:
190190
self._check_compare_to_str_or_zero(node)
191191

192192
def _check_compare_to_str_or_zero(self, node: nodes.Compare) -> None:
193+
# Skip check for chained comparisons
194+
if len(node.ops) != 1:
195+
return
196+
193197
# note: astroid.Compare has the left most operand in node.left
194198
# while the rest are a list of tuples in node.ops
195199
# the format of the tuple is ('compare operator sign', node)
196200
# here we squash everything into `ops` to make it easier for processing later
197201
ops: list[tuple[str, nodes.NodeNG]] = [("", node.left), *node.ops]
198202
iter_ops = iter(ops)
199203
all_ops = list(itertools.chain(*iter_ops))
200-
for ops_idx in range(len(all_ops) - 2):
201-
op_2 = all_ops[ops_idx + 1]
202-
if op_2 not in self._operators:
203-
continue
204-
op_1 = all_ops[ops_idx]
205-
op_3 = all_ops[ops_idx + 2]
206-
if self.linter.is_message_enabled(
207-
"use-implicit-booleaness-not-comparison-to-zero"
208-
):
209-
op = None
210-
# 0 ?? X
211-
if _is_constant_zero(op_1):
212-
op = op_3
213-
# X ?? 0
214-
elif _is_constant_zero(op_3):
215-
op = op_1
216-
if op is not None:
217-
original = f"{op_1.as_string()} {op_2} {op_3.as_string()}"
218-
suggestion = (
219-
op.as_string()
220-
if op_2 in {"!=", "is not"}
221-
else f"not {op.as_string()}"
222-
)
223-
self.add_message(
224-
"use-implicit-booleaness-not-comparison-to-zero",
225-
args=(original, suggestion),
226-
node=node,
227-
confidence=HIGH,
228-
)
229-
if self.linter.is_message_enabled(
230-
"use-implicit-booleaness-not-comparison-to-str"
231-
):
232-
node_name = None
233-
# x ?? ""
234-
if utils.is_empty_str_literal(op_1):
235-
node_name = op_3.as_string()
236-
# '' ?? X
237-
elif utils.is_empty_str_literal(op_3):
238-
node_name = op_1.as_string()
239-
if node_name is not None:
240-
suggestion = (
241-
f"not {node_name}" if op_2 in {"==", "is"} else node_name
242-
)
243-
self.add_message(
244-
"use-implicit-booleaness-not-comparison-to-string",
245-
args=(node.as_string(), suggestion),
246-
node=node,
247-
confidence=HIGH,
248-
)
204+
_, left_operand, operator, right_operand = all_ops
205+
206+
if operator not in self._operators:
207+
return
208+
if self.linter.is_message_enabled(
209+
"use-implicit-booleaness-not-comparison-to-zero"
210+
):
211+
operand = None
212+
# 0 ?? X
213+
if _is_constant_zero(left_operand):
214+
operand = right_operand
215+
# X ?? 0
216+
elif _is_constant_zero(right_operand):
217+
operand = left_operand
218+
if operand is not None:
219+
original = (
220+
f"{left_operand.as_string()} {operator} {right_operand.as_string()}"
221+
)
222+
suggestion = (
223+
operand.as_string()
224+
if operator in {"!=", "is not"}
225+
else f"not {operand.as_string()}"
226+
)
227+
self.add_message(
228+
"use-implicit-booleaness-not-comparison-to-zero",
229+
args=(original, suggestion),
230+
node=node,
231+
confidence=HIGH,
232+
)
233+
if self.linter.is_message_enabled(
234+
"use-implicit-booleaness-not-comparison-to-str"
235+
):
236+
node_name = None
237+
# x ?? ""
238+
if utils.is_empty_str_literal(left_operand):
239+
node_name = right_operand.as_string()
240+
# '' ?? X
241+
elif utils.is_empty_str_literal(right_operand):
242+
node_name = left_operand.as_string()
243+
if node_name is not None:
244+
suggestion = (
245+
f"not {node_name}" if operator in {"==", "is"} else node_name
246+
)
247+
self.add_message(
248+
"use-implicit-booleaness-not-comparison-to-string",
249+
args=(node.as_string(), suggestion),
250+
node=node,
251+
confidence=HIGH,
252+
)
249253

250254
def _check_use_implicit_booleaness_not_comparison(
251255
self, node: nodes.Compare
252256
) -> None:
253257
"""Check for left side and right side of the node for empty literals."""
258+
# Skip check for chained comparisons
259+
if len(node.ops) != 1:
260+
return
261+
262+
# Check both left-hand side and right-hand side for literals
263+
operator, comparator = node.ops[0]
254264
is_left_empty_literal = utils.is_base_container(
255265
node.left
256266
) or utils.is_empty_dict_literal(node.left)
267+
is_right_empty_literal = utils.is_base_container(
268+
comparator
269+
) or utils.is_empty_dict_literal(comparator)
257270

258-
# Check both left-hand side and right-hand side for literals
259-
for operator, comparator in node.ops:
260-
is_right_empty_literal = utils.is_base_container(
261-
comparator
262-
) or utils.is_empty_dict_literal(comparator)
263-
# Using Exclusive OR (XOR) to compare between two side.
264-
# If two sides are both literal, it should be different error.
265-
if is_right_empty_literal ^ is_left_empty_literal:
266-
# set target_node to opposite side of literal
267-
target_node = node.left if is_right_empty_literal else comparator
268-
literal_node = comparator if is_right_empty_literal else node.left
269-
# Infer node to check
270-
target_instance = utils.safe_infer(target_node)
271-
if target_instance is None:
272-
continue
273-
mother_classes = self.base_names_of_instance(target_instance)
274-
is_base_comprehension_type = any(
275-
t in mother_classes for t in ("tuple", "list", "dict", "set")
276-
)
271+
# If both sides are literals/non-literals, it should be different error.
272+
if not (is_left_empty_literal ^ is_right_empty_literal):
273+
return
277274

278-
# Only time we bypass check is when target_node is not inherited by
279-
# collection literals and have its own __bool__ implementation.
280-
if not is_base_comprehension_type and self.instance_has_bool(
281-
target_instance
282-
):
283-
continue
284-
285-
# No need to check for operator when visiting compare node
286-
if operator in {"==", "!=", ">=", ">", "<=", "<"}:
287-
self.add_message(
288-
"use-implicit-booleaness-not-comparison",
289-
args=self._implicit_booleaness_message_args(
290-
literal_node, operator, target_node
291-
),
292-
node=node,
293-
confidence=HIGH,
294-
)
275+
# Set target_node to opposite side of literal
276+
target_node = node.left if is_right_empty_literal else comparator
277+
literal_node = comparator if is_right_empty_literal else node.left
278+
# Infer node to check
279+
target_instance = utils.safe_infer(target_node)
280+
if target_instance is None:
281+
return
282+
mother_classes = self.base_names_of_instance(target_instance)
283+
is_base_comprehension_type = any(
284+
t in mother_classes for t in ("tuple", "list", "dict", "set")
285+
)
286+
287+
# Only time we bypass check is when target_node is not inherited by
288+
# collection literals and have its own __bool__ implementation.
289+
if not is_base_comprehension_type and self.instance_has_bool(target_instance):
290+
return
291+
292+
# No need to check for operator when visiting compare node
293+
if operator in {"==", "!=", ">=", ">", "<=", "<"}:
294+
self.add_message(
295+
"use-implicit-booleaness-not-comparison",
296+
args=self._implicit_booleaness_message_args(
297+
literal_node, operator, target_node
298+
),
299+
node=node,
300+
confidence=HIGH,
301+
)
295302

296303
def _get_node_description(self, node: nodes.NodeNG) -> str:
297304
return {

tests/functional/u/use/use_implicit_booleaness_not_comparison.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def bad_dict_return():
5757
assert () > empty_tuple # [use-implicit-booleaness-not-comparison]
5858
assert [] >= empty_list # [use-implicit-booleaness-not-comparison]
5959

60+
assert empty_list != empty_tuple != {}
61+
assert [] == empty_list == empty_list
62+
assert empty_list != empty_dict != empty_tuple == ()
63+
6064
assert [] == []
6165
assert {} != {}
6266
assert () == ()

tests/functional/u/use/use_implicit_booleaness_not_comparison.txt

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ use-implicit-booleaness-not-comparison:55:7:55:23::"""empty_tuple < ()"" can be
1515
use-implicit-booleaness-not-comparison:56:7:56:23::"""empty_list <= []"" can be simplified to ""not empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
1616
use-implicit-booleaness-not-comparison:57:7:57:23::"""empty_tuple > ()"" can be simplified to ""not empty_tuple"", if it is strictly a sequence, as an empty tuple is falsey":HIGH
1717
use-implicit-booleaness-not-comparison:58:7:58:23::"""empty_list >= []"" can be simplified to ""not empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
18-
use-implicit-booleaness-not-comparison:83:3:83:10::"""a == []"" can be simplified to ""not a"", if it is strictly a sequence, as an empty list is falsey":HIGH
19-
use-implicit-booleaness-not-comparison:95:3:95:10::"""e == []"" can be simplified to ""not e"", if it is strictly a sequence, as an empty list is falsey":HIGH
20-
use-implicit-booleaness-not-comparison:95:15:95:22::"""f == {}"" can be simplified to ""not f"", if it is strictly a sequence, as an empty dict is falsey":HIGH
21-
use-implicit-booleaness-not-comparison:133:3:133:14::"""A.lst == []"" can be simplified to ""not A.lst"", if it is strictly a sequence, as an empty list is falsey":HIGH
18+
use-implicit-booleaness-not-comparison:87:3:87:10::"""a == []"" can be simplified to ""not a"", if it is strictly a sequence, as an empty list is falsey":HIGH
19+
use-implicit-booleaness-not-comparison:99:3:99:10::"""e == []"" can be simplified to ""not e"", if it is strictly a sequence, as an empty list is falsey":HIGH
20+
use-implicit-booleaness-not-comparison:99:15:99:22::"""f == {}"" can be simplified to ""not f"", if it is strictly a sequence, as an empty dict is falsey":HIGH
2221
use-implicit-booleaness-not-comparison:137:3:137:14::"""A.lst == []"" can be simplified to ""not A.lst"", if it is strictly a sequence, as an empty list is falsey":HIGH
23-
use-implicit-booleaness-not-comparison:141:3:141:20::"""A.test(...) == []"" can be simplified to ""not A.test(...)"", if it is strictly a sequence, as an empty list is falsey":HIGH
24-
use-implicit-booleaness-not-comparison:149:3:149:24::"""test_function(...) == []"" can be simplified to ""not test_function(...)"", if it is strictly a sequence, as an empty list is falsey":HIGH
25-
use-implicit-booleaness-not-comparison:156:3:156:20::"""numpy_array == []"" can be simplified to ""not numpy_array"", if it is strictly a sequence, as an empty list is falsey":HIGH
26-
use-implicit-booleaness-not-comparison:158:3:158:20::"""numpy_array != []"" can be simplified to ""numpy_array"", if it is strictly a sequence, as an empty list is falsey":HIGH
27-
use-implicit-booleaness-not-comparison:160:3:160:20::"""numpy_array >= ()"" can be simplified to ""not numpy_array"", if it is strictly a sequence, as an empty tuple is falsey":HIGH
28-
use-implicit-booleaness-not-comparison:185:3:185:13::"""data == {}"" can be simplified to ""not data"", if it is strictly a sequence, as an empty dict is falsey":HIGH
29-
use-implicit-booleaness-not-comparison:187:3:187:13::"""data != {}"" can be simplified to ""data"", if it is strictly a sequence, as an empty dict is falsey":HIGH
30-
use-implicit-booleaness-not-comparison:195:3:195:26::"""long_test == {}"" can be simplified to ""not long_test"", if it is strictly a sequence, as an empty dict is falsey":HIGH
31-
use-implicit-booleaness-not-comparison:233:11:233:41:test_func:"""my_class.parent_function == {}"" can be simplified to ""not my_class.parent_function"", if it is strictly a sequence, as an empty dict is falsey":HIGH
32-
use-implicit-booleaness-not-comparison:234:11:234:37:test_func:"""my_class.my_property == {}"" can be simplified to ""not my_class.my_property"", if it is strictly a sequence, as an empty dict is falsey":HIGH
22+
use-implicit-booleaness-not-comparison:141:3:141:14::"""A.lst == []"" can be simplified to ""not A.lst"", if it is strictly a sequence, as an empty list is falsey":HIGH
23+
use-implicit-booleaness-not-comparison:145:3:145:20::"""A.test(...) == []"" can be simplified to ""not A.test(...)"", if it is strictly a sequence, as an empty list is falsey":HIGH
24+
use-implicit-booleaness-not-comparison:153:3:153:24::"""test_function(...) == []"" can be simplified to ""not test_function(...)"", if it is strictly a sequence, as an empty list is falsey":HIGH
25+
use-implicit-booleaness-not-comparison:160:3:160:20::"""numpy_array == []"" can be simplified to ""not numpy_array"", if it is strictly a sequence, as an empty list is falsey":HIGH
26+
use-implicit-booleaness-not-comparison:162:3:162:20::"""numpy_array != []"" can be simplified to ""numpy_array"", if it is strictly a sequence, as an empty list is falsey":HIGH
27+
use-implicit-booleaness-not-comparison:164:3:164:20::"""numpy_array >= ()"" can be simplified to ""not numpy_array"", if it is strictly a sequence, as an empty tuple is falsey":HIGH
28+
use-implicit-booleaness-not-comparison:189:3:189:13::"""data == {}"" can be simplified to ""not data"", if it is strictly a sequence, as an empty dict is falsey":HIGH
29+
use-implicit-booleaness-not-comparison:191:3:191:13::"""data != {}"" can be simplified to ""data"", if it is strictly a sequence, as an empty dict is falsey":HIGH
30+
use-implicit-booleaness-not-comparison:199:3:199:26::"""long_test == {}"" can be simplified to ""not long_test"", if it is strictly a sequence, as an empty dict is falsey":HIGH
31+
use-implicit-booleaness-not-comparison:237:11:237:41:test_func:"""my_class.parent_function == {}"" can be simplified to ""not my_class.parent_function"", if it is strictly a sequence, as an empty dict is falsey":HIGH
32+
use-implicit-booleaness-not-comparison:238:11:238:37:test_func:"""my_class.my_property == {}"" can be simplified to ""not my_class.my_property"", if it is strictly a sequence, as an empty dict is falsey":HIGH

tests/functional/u/use/use_implicit_booleaness_not_comparison_to_string.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@
2020

2121
if '' != X: # [use-implicit-booleaness-not-comparison-to-string]
2222
pass
23+
24+
if X == Y == "":
25+
pass
26+
27+
if "" == X == Y:
28+
pass
29+
30+
if X == Y == X == Y == "":
31+
pass

tests/functional/u/use/use_implicit_booleaness_not_comparison_to_zero.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,12 @@
4444

4545
if 0 > X:
4646
pass
47+
48+
if X == Y == 0:
49+
pass
50+
51+
if 0 == X == Y:
52+
pass
53+
54+
if X == Y == X == Y == 0:
55+
pass

0 commit comments

Comments
 (0)