Skip to content

Commit 6faf5b4

Browse files
Improve code suggestion for use-implicit-booleaness-not-x checks (#10328)
Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 818b4c8 commit 6faf5b4

8 files changed

+370
-9
lines changed

doc/whatsnew/fragments/9353.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The `use-implicit-booleaness-not-x` checks now distinguish between comparisons
2+
used in boolean contexts and those that are not, enabling them to provide more accurate refactoring suggestions.
3+
4+
Closes #9353

pylint/checkers/refactoring/implicit_booleaness_checker.py

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def _check_compare_to_str_or_zero(self, node: nodes.Compare) -> None:
194194
if len(node.ops) != 1:
195195
return
196196

197+
negation_redundant_ops = {"!=", "is not"}
197198
# note: astroid.Compare has the left most operand in node.left
198199
# while the rest are a list of tuples in node.ops
199200
# the format of the tuple is ('compare operator sign', node)
@@ -219,10 +220,8 @@ def _check_compare_to_str_or_zero(self, node: nodes.Compare) -> None:
219220
original = (
220221
f"{left_operand.as_string()} {operator} {right_operand.as_string()}"
221222
)
222-
suggestion = (
223-
operand.as_string()
224-
if operator in {"!=", "is not"}
225-
else f"not {operand.as_string()}"
223+
suggestion = self._get_suggestion(
224+
node, operand.as_string(), operator, negation_redundant_ops
226225
)
227226
self.add_message(
228227
"use-implicit-booleaness-not-comparison-to-zero",
@@ -241,8 +240,8 @@ def _check_compare_to_str_or_zero(self, node: nodes.Compare) -> None:
241240
elif utils.is_empty_str_literal(right_operand):
242241
node_name = left_operand.as_string()
243242
if node_name is not None:
244-
suggestion = (
245-
f"not {node_name}" if operator in {"==", "is"} else node_name
243+
suggestion = self._get_suggestion(
244+
node, node_name, operator, negation_redundant_ops
246245
)
247246
self.add_message(
248247
"use-implicit-booleaness-not-comparison-to-string",
@@ -294,7 +293,7 @@ def _check_use_implicit_booleaness_not_comparison(
294293
self.add_message(
295294
"use-implicit-booleaness-not-comparison",
296295
args=self._implicit_booleaness_message_args(
297-
literal_node, operator, target_node
296+
node, literal_node, operator, target_node
298297
),
299298
node=node,
300299
confidence=HIGH,
@@ -309,7 +308,11 @@ def _get_node_description(self, node: nodes.NodeNG) -> str:
309308
}.get(type(node), "iterable")
310309

311310
def _implicit_booleaness_message_args(
312-
self, literal_node: nodes.NodeNG, operator: str, target_node: nodes.NodeNG
311+
self,
312+
node: nodes.Compare,
313+
literal_node: nodes.NodeNG,
314+
operator: str,
315+
target_node: nodes.NodeNG,
313316
) -> tuple[str, str, str]:
314317
"""Helper to get the right message for "use-implicit-booleaness-not-comparison"."""
315318
description = self._get_node_description(literal_node)
@@ -324,9 +327,91 @@ def _implicit_booleaness_message_args(
324327
elif isinstance(target_node, (nodes.Attribute, nodes.Name)):
325328
instance_name = target_node.as_string()
326329
original_comparison = f"{instance_name} {operator} {collection_literal}"
327-
suggestion = f"{instance_name}" if operator == "!=" else f"not {instance_name}"
330+
suggestion = self._get_suggestion(node, instance_name, operator, {"!="})
328331
return original_comparison, suggestion, description
329332

333+
def _get_suggestion(
334+
self,
335+
node: nodes.Compare,
336+
name: str,
337+
operator: str,
338+
negation_redundant_ops: set[str],
339+
) -> str:
340+
if operator in negation_redundant_ops:
341+
return f"{name}" if self._in_boolean_context(node) else f"bool({name})"
342+
return f"not {name}"
343+
344+
def _in_boolean_context(self, node: nodes.Compare) -> bool:
345+
"""
346+
Returns True if the comparison is used in a boolean context; False otherwise.
347+
348+
A comparison is considered to be in a boolean context when it appears in constructs
349+
that evaluate its truthiness directly, such as:
350+
- control flow statements (`if`, `while`, `assert`)
351+
- ternary expressions (`x if condition else y`)
352+
- logical negation (`not`)
353+
- comprehension filters (e.g. `[x for x in items if cond]`)
354+
- generator expressions passed to `all()` or `any()`
355+
- lambdas expressions passed to `filter()`
356+
- `bool()` cast
357+
- boolean operations `and`, `or` nested within any of the above
358+
359+
In contrast, a comparison is not in a boolean context when its result is used as a value,
360+
such as when it is assigned to a variable, returned from a function, passed as an argument,
361+
or used in an expression that does not depend on its truthiness.
362+
"""
363+
current, parent = node, node.parent
364+
while parent:
365+
if (
366+
isinstance(parent, (nodes.If, nodes.While, nodes.Assert))
367+
and current is parent.test
368+
):
369+
return True
370+
371+
if isinstance(parent, nodes.IfExp) and current is parent.test:
372+
return True
373+
374+
if (
375+
isinstance(parent, nodes.UnaryOp)
376+
and parent.op == "not"
377+
and current is parent.operand
378+
):
379+
return True
380+
381+
if isinstance(parent, nodes.Comprehension) and current in parent.ifs:
382+
return True
383+
384+
if (
385+
isinstance(parent, nodes.GeneratorExp)
386+
and current is parent.elt
387+
and (
388+
utils.is_call_of_name(parent.parent, "all")
389+
or utils.is_call_of_name(parent.parent, "any")
390+
)
391+
and parent in parent.parent.args
392+
):
393+
return True
394+
395+
if (
396+
isinstance(parent, nodes.Lambda)
397+
and current is parent.body
398+
and utils.is_call_of_name(parent.parent, "filter")
399+
and parent in parent.parent.args
400+
):
401+
return True
402+
403+
if utils.is_call_of_name(parent, "bool") and current in parent.args:
404+
return True
405+
406+
if isinstance(parent, nodes.BoolOp) and current in parent.values:
407+
current = parent
408+
parent = current.parent
409+
continue
410+
411+
break
412+
413+
return False
414+
330415
@staticmethod
331416
def base_names_of_instance(
332417
node: util.UninferableBase | bases.Instance,

tests/functional/u/use/use_implicit_booleaness_not_comparison.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,71 @@ def test_func():
241241
assert my_class.my_difficult_property == {}
242242
# Uninferable does not raise
243243
assert AnotherClassWithProperty().my_property == {}
244+
245+
246+
def test_in_boolean_context():
247+
"""
248+
Cases where a comparison like `x != []` is used in a boolean context.
249+
250+
It is safe and idiomatic to simplify `x != []` to just `x`.
251+
"""
252+
# pylint: disable=pointless-statement,superfluous-parens,unnecessary-negation
253+
254+
# Control flow
255+
if empty_list != []: # [use-implicit-booleaness-not-comparison]
256+
pass
257+
while empty_list != []: # [use-implicit-booleaness-not-comparison]
258+
pass
259+
assert empty_list != [] # [use-implicit-booleaness-not-comparison]
260+
261+
# Ternary
262+
_ = 1 if empty_list != [] else 2 # [use-implicit-booleaness-not-comparison]
263+
264+
# Not
265+
if not (empty_list != []): # [use-implicit-booleaness-not-comparison]
266+
pass
267+
268+
# Comprehension filters
269+
[x for x in [] if empty_list != []] # [use-implicit-booleaness-not-comparison]
270+
{x for x in [] if empty_list != []} # [use-implicit-booleaness-not-comparison]
271+
(x for x in [] if empty_list != []) # [use-implicit-booleaness-not-comparison]
272+
273+
# all() / any() with generator expressions
274+
all(empty_list != [] for _ in range(1)) # [use-implicit-booleaness-not-comparison]
275+
any(empty_list != [] for _ in range(1)) # [use-implicit-booleaness-not-comparison]
276+
277+
# filter() with lambda
278+
filter(lambda: empty_list != [], []) # [use-implicit-booleaness-not-comparison]
279+
280+
# boolean cast
281+
bool(empty_list != []) # [use-implicit-booleaness-not-comparison]
282+
283+
# Logical operators nested in boolean contexts
284+
if empty_list != [] and input(): # [use-implicit-booleaness-not-comparison]
285+
pass
286+
while input() or empty_list != []: # [use-implicit-booleaness-not-comparison]
287+
pass
288+
if (empty_list != [] or input()) and input(): # [use-implicit-booleaness-not-comparison]
289+
pass
290+
291+
292+
def test_not_in_boolean_context():
293+
"""
294+
Cases where a comparison like `x != []` is used in a non-boolean context.
295+
296+
These comparisons cannot be safely replaced with just `x`, and should be explicitly cast using `bool(x)`.
297+
"""
298+
# pylint: disable=pointless-statement
299+
_ = empty_list != [] # [use-implicit-booleaness-not-comparison]
300+
301+
_ = empty_list != [] or input() # [use-implicit-booleaness-not-comparison]
302+
303+
print(empty_list != []) # [use-implicit-booleaness-not-comparison]
304+
305+
[empty_list != [] for _ in []] # [use-implicit-booleaness-not-comparison]
306+
307+
lambda: empty_list != [] # [use-implicit-booleaness-not-comparison]
308+
309+
filter(lambda x: x, [empty_list != []]) # [use-implicit-booleaness-not-comparison]
310+
311+
return empty_list != [] # [use-implicit-booleaness-not-comparison]

tests/functional/u/use/use_implicit_booleaness_not_comparison.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,25 @@ use-implicit-booleaness-not-comparison:191:3:191:13::"""data != {}"" can be simp
3030
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
3131
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
3232
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
33+
use-implicit-booleaness-not-comparison:255:7:255:23:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
34+
use-implicit-booleaness-not-comparison:257:10:257:26:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
35+
use-implicit-booleaness-not-comparison:259:11:259:27:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
36+
use-implicit-booleaness-not-comparison:262:13:262:29:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
37+
use-implicit-booleaness-not-comparison:265:12:265:28:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
38+
use-implicit-booleaness-not-comparison:269:22:269:38:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
39+
use-implicit-booleaness-not-comparison:270:22:270:38:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
40+
use-implicit-booleaness-not-comparison:271:22:271:38:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
41+
use-implicit-booleaness-not-comparison:274:8:274:24:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
42+
use-implicit-booleaness-not-comparison:275:8:275:24:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
43+
use-implicit-booleaness-not-comparison:278:19:278:35:test_in_boolean_context.<lambda>:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
44+
use-implicit-booleaness-not-comparison:281:9:281:25:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
45+
use-implicit-booleaness-not-comparison:284:7:284:23:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
46+
use-implicit-booleaness-not-comparison:286:21:286:37:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
47+
use-implicit-booleaness-not-comparison:288:8:288:24:test_in_boolean_context:"""empty_list != []"" can be simplified to ""empty_list"", if it is strictly a sequence, as an empty list is falsey":HIGH
48+
use-implicit-booleaness-not-comparison:299:8:299:24:test_not_in_boolean_context:"""empty_list != []"" can be simplified to ""bool(empty_list)"", if it is strictly a sequence, as an empty list is falsey":HIGH
49+
use-implicit-booleaness-not-comparison:301:8:301:24:test_not_in_boolean_context:"""empty_list != []"" can be simplified to ""bool(empty_list)"", if it is strictly a sequence, as an empty list is falsey":HIGH
50+
use-implicit-booleaness-not-comparison:303:10:303:26:test_not_in_boolean_context:"""empty_list != []"" can be simplified to ""bool(empty_list)"", if it is strictly a sequence, as an empty list is falsey":HIGH
51+
use-implicit-booleaness-not-comparison:305:5:305:21:test_not_in_boolean_context:"""empty_list != []"" can be simplified to ""bool(empty_list)"", if it is strictly a sequence, as an empty list is falsey":HIGH
52+
use-implicit-booleaness-not-comparison:307:12:307:28:test_not_in_boolean_context.<lambda>:"""empty_list != []"" can be simplified to ""bool(empty_list)"", if it is strictly a sequence, as an empty list is falsey":HIGH
53+
use-implicit-booleaness-not-comparison:309:25:309:41:test_not_in_boolean_context:"""empty_list != []"" can be simplified to ""bool(empty_list)"", if it is strictly a sequence, as an empty list is falsey":HIGH
54+
use-implicit-booleaness-not-comparison:311:11:311:27:test_not_in_boolean_context:"""empty_list != []"" can be simplified to ""bool(empty_list)"", if it is strictly a sequence, as an empty list is falsey":HIGH

tests/functional/u/use/use_implicit_booleaness_not_comparison_to_string.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,72 @@
2929

3030
if X == Y == X == Y == "":
3131
pass
32+
33+
34+
def test_in_boolean_context():
35+
"""
36+
Cases where a comparison like `x != ""` is used in a boolean context.
37+
38+
It is safe and idiomatic to simplify `x != ""` to just `x`.
39+
"""
40+
# pylint: disable=pointless-statement,superfluous-parens,unnecessary-negation
41+
42+
# Control flow
43+
if X != "": # [use-implicit-booleaness-not-comparison-to-string]
44+
pass
45+
while X != "": # [use-implicit-booleaness-not-comparison-to-string]
46+
pass
47+
assert X != "" # [use-implicit-booleaness-not-comparison-to-string]
48+
49+
# Ternary
50+
_ = 1 if X != "" else 2 # [use-implicit-booleaness-not-comparison-to-string]
51+
52+
# Not
53+
if not (X != ""): # [use-implicit-booleaness-not-comparison-to-string]
54+
pass
55+
56+
# Comprehension filters
57+
[x for x in [] if X != ""] # [use-implicit-booleaness-not-comparison-to-string]
58+
{x for x in [] if X != ""} # [use-implicit-booleaness-not-comparison-to-string]
59+
(x for x in [] if X != "") # [use-implicit-booleaness-not-comparison-to-string]
60+
61+
# all() / any() with generator expressions
62+
all(X != "" for _ in range(1)) # [use-implicit-booleaness-not-comparison-to-string]
63+
any(X != "" for _ in range(1)) # [use-implicit-booleaness-not-comparison-to-string]
64+
65+
# filter() with lambda
66+
filter(lambda: X != "", []) # [use-implicit-booleaness-not-comparison-to-string]
67+
68+
# boolean cast
69+
bool(X != "") # [use-implicit-booleaness-not-comparison-to-string]
70+
71+
# Logical operators nested in boolean contexts
72+
if X != "" and input(): # [use-implicit-booleaness-not-comparison-to-string]
73+
pass
74+
while input() or X != "": # [use-implicit-booleaness-not-comparison-to-string]
75+
pass
76+
if (X != "" or input()) and input(): # [use-implicit-booleaness-not-comparison-to-string]
77+
pass
78+
79+
80+
def test_not_in_boolean_context():
81+
"""
82+
Cases where a comparison like `x != ""` is used in a non-boolean context.
83+
84+
These comparisons cannot be safely replaced with just `x`, and should be explicitly
85+
cast using `bool(x)`.
86+
"""
87+
# pylint: disable=pointless-statement
88+
_ = X != "" # [use-implicit-booleaness-not-comparison-to-string]
89+
90+
_ = X != "" or input() # [use-implicit-booleaness-not-comparison-to-string]
91+
92+
print(X != "") # [use-implicit-booleaness-not-comparison-to-string]
93+
94+
[X != "" for _ in []] # [use-implicit-booleaness-not-comparison-to-string]
95+
96+
lambda: X != "" # [use-implicit-booleaness-not-comparison-to-string]
97+
98+
filter(lambda x: x, [X != ""]) # [use-implicit-booleaness-not-comparison-to-string]
99+
100+
return X != "" # [use-implicit-booleaness-not-comparison-to-string]

0 commit comments

Comments
 (0)