Skip to content

Commit 30aa974

Browse files
Work in progress merging of use-implicit-booleaness checker
1 parent 99e05cc commit 30aa974

20 files changed

+182
-184
lines changed

doc/whatsnew/fragments/6871.internal

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
* The compare to empty string checker (``pylint.extensions.emptystring``) has been removed
2+
as it is now part of the implicit booleaness checker, a default check.
3+
4+
- ``compare-to-zero`` was renamed ``use-implicit-booleaness-not-comparison-to-zero`` but is still an extension.
5+
- ``compare-to-empty-string`` was merged with ``use-implicit-booleaness-not-comparison``.
6+
- Messages related to implicit booleaness were made more explicit and actionable.
7+
8+
Closes #6871

pylint/checkers/refactoring/implicit_booleaness_checker.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from __future__ import annotations
66

7+
import itertools
8+
79
import astroid
810
from astroid import bases, nodes
911

@@ -50,7 +52,6 @@ class ImplicitBooleanessChecker(checkers.BaseChecker):
5052
* comparison such as variable != empty_literal:
5153
"""
5254

53-
# configuration section name
5455
name = "refactoring"
5556
msgs = {
5657
"C1802": (
@@ -64,11 +65,16 @@ class ImplicitBooleanessChecker(checkers.BaseChecker):
6465
{"old_names": [("C1801", "len-as-condition")]},
6566
),
6667
"C1803": (
67-
"'%s' can be simplified to '%s' as an empty sequence is falsey",
68+
"'%s' can be simplified to '%s' as %s is falsey",
6869
"use-implicit-booleaness-not-comparison",
6970
"Used when Pylint detects that collection literal comparison is being "
7071
"used to check for emptiness; Use implicit booleaness instead "
7172
"of a collection classes; empty collections are considered as false",
73+
{
74+
"old_names": [
75+
("C1901", "compare-to-empty-string"),
76+
]
77+
},
7278
),
7379
}
7480

@@ -139,6 +145,52 @@ def visit_unaryop(self, node: nodes.UnaryOp) -> None:
139145
@utils.only_required_for_messages("use-implicit-booleaness-not-comparison")
140146
def visit_compare(self, node: nodes.Compare) -> None:
141147
self._check_use_implicit_booleaness_not_comparison(node)
148+
self._check_compare_to_empty_string(node)
149+
150+
def _check_compare_to_empty_string(self, node: nodes.Compare) -> None:
151+
"""Checks for comparisons to empty string.
152+
153+
Most of the time you should use the fact that empty strings are false.
154+
An exception to this rule is when an empty string value is allowed in the program
155+
and has a different meaning than None!
156+
"""
157+
_operators = {"!=", "==", "is not", "is"}
158+
# note: astroid.Compare has the left most operand in node.left
159+
# while the rest are a list of tuples in node.ops
160+
# the format of the tuple is ('compare operator sign', node)
161+
# here we squash everything into `ops` to make it easier for processing later
162+
ops: list[tuple[str, nodes.NodeNG | None]] = [("", node.left)]
163+
ops.extend(node.ops)
164+
iter_ops = iter(ops)
165+
ops = list(itertools.chain(*iter_ops)) # type: ignore[arg-type]
166+
for ops_idx in range(len(ops) - 2):
167+
op_1: nodes.NodeNG | None = ops[ops_idx]
168+
op_2: str = ops[ops_idx + 1] # type: ignore[assignment]
169+
op_3: nodes.NodeNG | None = ops[ops_idx + 2]
170+
error_detected = False
171+
if op_1 is None or op_3 is None or op_2 not in _operators:
172+
continue
173+
if isinstance(node.parent, nodes.Assert) and op_2 == "==":
174+
# Comparing equality to an empty string directly make more
175+
# sense in tests where you want to make sure you don't just
176+
# have a falsey value but an empty string
177+
continue
178+
# x ?? ""
179+
if utils.is_empty_str_literal(op_1):
180+
error_detected = True
181+
node_name = op_3.as_string()
182+
# '' ?? X
183+
elif utils.is_empty_str_literal(op_3):
184+
error_detected = True
185+
node_name = op_1.as_string()
186+
if error_detected:
187+
suggestion = f"not {node_name}" if op_2 in {"==", "is"} else node_name
188+
189+
self.add_message(
190+
"compare-to-empty-string",
191+
args=(node.as_string(), suggestion, "an empty string"),
192+
node=node,
193+
)
142194

143195
def _check_use_implicit_booleaness_not_comparison(
144196
self, node: nodes.Compare
@@ -177,11 +229,11 @@ def _check_use_implicit_booleaness_not_comparison(
177229

178230
# No need to check for operator when visiting compare node
179231
if operator in {"==", "!=", ">=", ">", "<=", "<"}:
180-
collection_literal = "{}"
232+
collection_literal, sequence_type = "{}", "dict"
181233
if isinstance(literal_node, nodes.List):
182-
collection_literal = "[]"
234+
collection_literal, sequence_type = "[]", "list"
183235
if isinstance(literal_node, nodes.Tuple):
184-
collection_literal = "()"
236+
collection_literal, sequence_type = "()", "tuple"
185237

186238
instance_name = "x"
187239
if isinstance(target_node, nodes.Call) and target_node.func:
@@ -202,6 +254,7 @@ def _check_use_implicit_booleaness_not_comparison(
202254
args=(
203255
original_comparison,
204256
suggestion,
257+
f"an empty {sequence_type}",
205258
),
206259
node=node,
207260
)

pylint/checkers/strings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def _check_new_format(self, node: nodes.Call, func: bases.BoundMethod) -> None:
494494

495495
check_args = False
496496
# Consider "{[0]} {[1]}" as num_args.
497-
num_args += sum(1 for field in named_fields if field == "")
497+
num_args += sum(1 for field in named_fields if not field)
498498
if named_fields:
499499
for field in named_fields:
500500
if field and field not in named_arguments:
@@ -509,7 +509,7 @@ def _check_new_format(self, node: nodes.Call, func: bases.BoundMethod) -> None:
509509
# num_args can be 0 if manual_pos is not.
510510
num_args = num_args or manual_pos
511511
if positional_arguments or num_args:
512-
empty = any(field == "" for field in named_fields)
512+
empty = not all(field for field in named_fields)
513513
if named_arguments or empty:
514514
# Verify the required number of positional arguments
515515
# only if the .format got at least one keyword argument.
@@ -546,7 +546,7 @@ def _check_new_format_specifiers(
546546
for key, specifiers in fields:
547547
# Obtain the argument. If it can't be obtained
548548
# or inferred, skip this check.
549-
if key == "":
549+
if not key:
550550
# {[0]} will have an unnamed argument, defaulting
551551
# to 0. It will not be present in `named`, so use the value
552552
# 0 for it.

pylint/config/_pylint_config/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def validate_yes_no(question: str, default: Literal["yes", "no"] | None) -> bool
8383
# pylint: disable-next=bad-builtin
8484
answer = input(question).lower()
8585

86-
if answer == "" and default:
86+
if not answer and default:
8787
answer = default
8888

8989
if answer not in YES_NO_ANSWERS:

pylint/extensions/comparetozero.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ class CompareToZeroChecker(checkers.BaseChecker):
3434
# configuration section name
3535
name = "compare-to-zero"
3636
msgs = {
37-
"C2001": (
37+
"C2002": (
3838
"Avoid comparisons to zero",
39-
"compare-to-zero",
39+
"use-implicit-booleaness-not-comparison-to-zero",
4040
"Used when Pylint detects comparison to a 0 constant.",
41+
{
42+
"old_names": [
43+
("C2001", "compare-to-zero"),
44+
]
45+
},
4146
)
4247
}
4348

pylint/extensions/emptystring.py

Lines changed: 0 additions & 71 deletions
This file was deleted.

pylint/testutils/_primer/primer_compare_command.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,14 @@ def _create_comment(
6363
comment += self._create_comment_for_package(
6464
package, new_messages, missing_messages
6565
)
66-
if comment == "":
67-
comment = (
66+
comment = (
67+
f"🤖 **Effect of this PR on checked open source code:** 🤖\n\n{comment}"
68+
if comment
69+
else (
6870
"🤖 According to the primer, this change has **no effect** on the"
6971
" checked open source code. 🤖🎉\n\n"
7072
)
71-
else:
72-
comment = (
73-
f"🤖 **Effect of this PR on checked open source code:** 🤖\n\n{comment}"
74-
)
73+
)
7574
return self._truncate_comment(comment)
7675

7776
def _create_comment_for_package(

pylint/testutils/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def create_files(paths: list[str], chroot: str = ".") -> None:
9393
path = os.path.join(chroot, path)
9494
filename = os.path.basename(path)
9595
# path is a directory path
96-
if filename == "":
96+
if not filename:
9797
dirs.add(path)
9898
# path is a filename path
9999
else:

script/fix_documentation.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,7 @@ def changelog_insert_empty_lines(file_content: str, subtitle_text: str) -> str:
3636
for i, line in enumerate(lines):
3737
if line.startswith(subtitle_text):
3838
subtitle_count += 1
39-
if (
40-
subtitle_count == 1
41-
or i < 2
42-
or lines[i - 1] == ""
43-
and lines[i - 2] == ""
44-
):
39+
if subtitle_count == 1 or i < 2 or not lines[i - 1] and not lines[i - 2]:
4540
continue
4641
lines.insert(i, "")
4742
return "\n".join(lines)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# pylint: disable=literal-comparison,missing-docstring
2+
3+
X = 123
4+
Y = len('test')
5+
6+
if X is 0: # [use-implicit-booleaness-not-comparison-to-zero]
7+
pass
8+
9+
if Y is not 0: # [use-implicit-booleaness-not-comparison-to-zero]
10+
pass
11+
12+
if X == 0: # [use-implicit-booleaness-not-comparison-to-zero]
13+
pass
14+
15+
if Y != 0: # [use-implicit-booleaness-not-comparison-to-zero]
16+
pass
17+
18+
if X > 0:
19+
pass
20+
21+
if X < 0:
22+
pass
23+
24+
if 0 < X:
25+
pass
26+
27+
if 0 > X:
28+
pass
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
use-implicit-booleaness-not-comparison-to-zero:6:3:6:9::Avoid comparisons to zero:UNDEFINED
2+
use-implicit-booleaness-not-comparison-to-zero:9:3:9:13::Avoid comparisons to zero:UNDEFINED
3+
use-implicit-booleaness-not-comparison-to-zero:12:3:12:9::Avoid comparisons to zero:UNDEFINED
4+
use-implicit-booleaness-not-comparison-to-zero:15:3:15:9::Avoid comparisons to zero:UNDEFINED

tests/functional/ext/comparetozero/comparetozero.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

tests/functional/ext/comparetozero/comparetozero.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.

tests/functional/ext/emptystring/empty_string_comparison.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

tests/functional/ext/emptystring/empty_string_comparison.rc

Lines changed: 0 additions & 2 deletions
This file was deleted.

tests/functional/ext/emptystring/empty_string_comparison.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)