Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merging of use-implicit-booleaness-like checkers for consistency and performance #6870

Merged

This file was deleted.

This file was deleted.

This file was deleted.

30 changes: 18 additions & 12 deletions doc/user_guide/checkers/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -893,14 +893,21 @@ Refactoring checker Messages
Emitted when a single "return" or "return None" statement is found at the end
of function or method definition. This statement can safely be removed
because Python will implicitly return None
:use-implicit-booleaness-not-comparison-to-zero (C1805): *"%s" can be simplified to "%s" as 0 is falsey*
Used when Pylint detects comparison to a 0 constant.
:use-implicit-booleaness-not-comparison-to-string (C1804): *"%s" can be simplified to "%s" as an empty string is falsey*
Used when Pylint detects comparison to an empty string constant.
:use-implicit-booleaness-not-comparison (C1803): *'%s' can be simplified to '%s' as an empty %s is falsey*
Used when Pylint detects that collection literal comparison is being used to
check for emptiness; Use implicit booleaness instead of a collection classes;
empty collections are considered as false
:use-implicit-booleaness-not-comparison-to-string (C1804): *"%s" can be simplified to "%s", if it is striclty a string, as an empty string is falsey*
Empty string are considered false in a boolean context. Following this check
blindly in weakly typed code base can create hard to debug issues. If the
value can be something else that is falsey but not a string (for example
``None``, an empty sequence, or ``0``) the code will not be equivalent.
:use-implicit-booleaness-not-comparison (C1803): *"%s" can be simplified to "%s", if it is strictly a sequence, as an empty %s is falsey*
Empty sequences are considered false in a boolean context. Following this
check blindly in weakly typed code base can create hard to debug issues. If
the value can be something else that is falsey but not a sequence (for
example ``None``, an empty string, or ``0``) the code will not be equivalent.
:use-implicit-booleaness-not-comparison-to-zero (C1805): *"%s" can be simplified to "%s", if it is strictly an int, as 0 is falsey*
0 is considered false in a boolean context. Following this check blindly in
weakly typed code base can create hard to debug issues. If the value can be
something else that is falsey but not an int (for example ``None``, an empty
string, or an empty sequence) the code will not be equivalent.
:unneeded-not (C0113): *Consider changing "%s" to "%s"*
Used when a boolean expression contains an unneeded negation.
:consider-iterating-dictionary (C0201): *Consider iterating the dictionary directly instead of calling .keys()*
Expand All @@ -916,10 +923,9 @@ Refactoring checker Messages
Emitted when code that iterates with range and len is encountered. Such code
can be simplified by using the enumerate builtin.
:use-implicit-booleaness-not-len (C1802): *Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty*
Used when Pylint detects that len(sequence) is being used without explicit
comparison inside a condition to determine if a sequence is empty. Instead of
coercing the length to a boolean, either rely on the fact that empty
sequences are false or compare the length against a scalar.
Empty sequences are considered false in a boolean context. You can either
remove the call to 'len' (``if not x``) or compare the length against ascalar
(``if len(x) > 1``).
:consider-using-f-string (C0209): *Formatting a regular string which could be an f-string*
Used when we detect a string that is being formatted with format() or % which
could potentially be an f-string. The use of f-strings is preferred. Requires
Expand Down
4 changes: 3 additions & 1 deletion doc/whatsnew/fragments/6871.user_action
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ and they now need to be enabled explicitly.
The `pylint.extensions.emptystring`` and ``pylint.extensions.compare-to-zero`` extensions
no longer exists and needs to be removed from the ``load-plugins`` option.

Messages related to implicit booleaness were made more explicit and actionable.

This permits to make their likeness explicit and will provide better performance as they share most of their
conditions to be raised.

Refs #6871
Closes #6871
169 changes: 82 additions & 87 deletions pylint/checkers/refactoring/implicit_booleaness_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,38 +66,47 @@ class ImplicitBooleanessChecker(checkers.BaseChecker):
"C1802": (
"Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty",
"use-implicit-booleaness-not-len",
"Used when Pylint detects that len(sequence) is being used "
"without explicit comparison inside a condition to determine if a sequence is empty. "
"Instead of coercing the length to a boolean, either "
"rely on the fact that empty sequences are false or "
"compare the length against a scalar.",
"Empty sequences are considered false in a boolean context. You can either"
" remove the call to 'len' (``if not x``) or compare the length against a"
"scalar (``if len(x) > 1``).",
{"old_names": [("C1801", "len-as-condition")]},
),
"C1803": (
"'%s' can be simplified to '%s' as an empty %s is falsey",
'"%s" can be simplified to "%s", if it is strictly a sequence, as an empty %s is falsey',
"use-implicit-booleaness-not-comparison",
"Used when Pylint detects that collection literal comparison is being "
"used to check for emptiness; Use implicit booleaness instead "
"of a collection classes; empty collections are considered as false",
"Empty sequences are considered false in a boolean context. Following this"
" check blindly in weakly typed code base can create hard to debug issues."
" If the value can be something else that is falsey but not a sequence (for"
" example ``None``, an empty string, or ``0``) the code will not be "
"equivalent.",
),
"C1804": (
'"%s" can be simplified to "%s" as an empty string is falsey',
'"%s" can be simplified to "%s", if it is striclty a string, as an empty string is falsey',
"use-implicit-booleaness-not-comparison-to-string",
"Used when Pylint detects comparison to an empty string constant.",
"Empty string are considered false in a boolean context. Following this"
" check blindly in weakly typed code base can create hard to debug issues."
" If the value can be something else that is falsey but not a string (for"
" example ``None``, an empty sequence, or ``0``) the code will not be "
"equivalent.",
{
"default_enabled": False,
"old_names": [("C1901", "compare-to-empty-string")],
},
),
"C1805": (
'"%s" can be simplified to "%s" as 0 is falsey',
'"%s" can be simplified to "%s", if it is strictly an int, as 0 is falsey',
"use-implicit-booleaness-not-comparison-to-zero",
"Used when Pylint detects comparison to a 0 constant.",
"0 is considered false in a boolean context. Following this"
" check blindly in weakly typed code base can create hard to debug issues."
" If the value can be something else that is falsey but not an int (for"
" example ``None``, an empty string, or an empty sequence) the code will not be "
"equivalent.",
{"default_enabled": False, "old_names": [("C2001", "compare-to-zero")]},
),
}

options = ()
_operators = {"!=", "==", "is not", "is"}

@utils.only_required_for_messages("use-implicit-booleaness-not-len")
def visit_call(self, node: nodes.Call) -> None:
Expand Down Expand Up @@ -177,91 +186,77 @@ def visit_unaryop(self, node: nodes.UnaryOp) -> None:
"use-implicit-booleaness-not-comparison-to-zero",
)
def visit_compare(self, node: nodes.Compare) -> None:
self._check_use_implicit_booleaness_not_comparison(node)
self._check_compare_to_zero(node)
self._check_compare_to_string(node)
if self.linter.is_message_enabled("use-implicit-booleaness-not-comparison"):
self._check_use_implicit_booleaness_not_comparison(node)
if self.linter.is_message_enabled(
"use-implicit-booleaness-not-comparison-to-zero"
) or self.linter.is_message_enabled(
"use-implicit-booleaness-not-comparison-to-str"
):
self._check_compare_to_str_or_zero(node)

def _check_compare_to_zero(self, node: nodes.Compare) -> None:
# pylint: disable=duplicate-code
_operators = ["!=", "==", "is not", "is"]
def _check_compare_to_str_or_zero(self, node: nodes.Compare) -> None:
# note: astroid.Compare has the left most operand in node.left
# while the rest are a list of tuples in node.ops
# the format of the tuple is ('compare operator sign', node)
# here we squash everything into `ops` to make it easier for processing later
ops: list[tuple[str, nodes.NodeNG]] = [("", node.left)]
ops.extend(node.ops)
ops: list[tuple[str, nodes.NodeNG]] = [("", node.left), *node.ops]
iter_ops = iter(ops)
all_ops = list(itertools.chain(*iter_ops))

for ops_idx in range(len(all_ops) - 2):
op_1 = all_ops[ops_idx]
op_2 = all_ops[ops_idx + 1]
if op_2 not in self._operators:
continue
op_1 = all_ops[ops_idx]
op_3 = all_ops[ops_idx + 2]
error_detected = False

# 0 ?? X
if _is_constant_zero(op_1) and op_2 in _operators:
error_detected = True
op = op_3
# X ?? 0
elif op_2 in _operators and _is_constant_zero(op_3):
error_detected = True
op = op_1

if error_detected:
original = f"{op_1.as_string()} {op_2} {op_3.as_string()}"
suggestion = (
op.as_string()
if op_2 in {"!=", "is not"}
else f"not {op.as_string()}"
)
self.add_message(
"compare-to-zero",
args=(original, suggestion),
node=node,
confidence=HIGH,
)

def _check_compare_to_string(self, node: nodes.Compare) -> None:
"""Checks for comparisons to empty string.

Most of the time you should use the fact that empty strings are false.
An exception to this rule is when an empty string value is allowed in the program
and has a different meaning than None!
"""
_operators = {"!=", "==", "is not", "is"}
# note: astroid.Compare has the left most operand in node.left while the rest
# are a list of tuples in node.ops the format of the tuple is
# ('compare operator sign', node) here we squash everything into `ops`
# to make it easier for processing later
ops: list[tuple[str, nodes.NodeNG | None]] = [("", node.left)]
ops.extend(node.ops)
iter_ops = iter(ops)
ops = list(itertools.chain(*iter_ops)) # type: ignore[arg-type]
for ops_idx in range(len(ops) - 2):
op_1: nodes.NodeNG | None = ops[ops_idx]
op_2: str = ops[ops_idx + 1] # type: ignore[assignment]
op_3: nodes.NodeNG | None = ops[ops_idx + 2]
error_detected = False
if op_1 is None or op_3 is None or op_2 not in _operators:
continue
node_name = ""
# x ?? ""
if utils.is_empty_str_literal(op_1):
error_detected = True
node_name = op_3.as_string()
# '' ?? X
elif utils.is_empty_str_literal(op_3):
error_detected = True
node_name = op_1.as_string()
if error_detected:
suggestion = f"not {node_name}" if op_2 in {"==", "is"} else node_name
self.add_message(
"compare-to-empty-string",
args=(node.as_string(), suggestion),
node=node,
confidence=HIGH,
)
if self.linter.is_message_enabled(
"use-implicit-booleaness-not-comparison-to-zero"
):
# 0 ?? X
if _is_constant_zero(op_1):
error_detected = True
op = op_3
# X ?? 0
elif _is_constant_zero(op_3):
error_detected = True
op = op_1
if error_detected:
original = f"{op_1.as_string()} {op_2} {op_3.as_string()}"
suggestion = (
op.as_string()
if op_2 in {"!=", "is not"}
else f"not {op.as_string()}"
)
self.add_message(
"use-implicit-booleaness-not-comparison-to-zero",
args=(original, suggestion),
node=node,
confidence=HIGH,
)
error_detected = False
if self.linter.is_message_enabled(
"use-implicit-booleaness-not-comparison-to-str"
):
node_name = ""
# x ?? ""
if utils.is_empty_str_literal(op_1):
error_detected = True
node_name = op_3.as_string()
# '' ?? X
elif utils.is_empty_str_literal(op_3):
error_detected = True
node_name = op_1.as_string()
if error_detected:
suggestion = (
f"not {node_name}" if op_2 in {"==", "is"} else node_name
)
self.add_message(
"use-implicit-booleaness-not-comparison-to-string",
args=(node.as_string(), suggestion),
node=node,
confidence=HIGH,
)

def _check_use_implicit_booleaness_not_comparison(
self, node: nodes.Compare
Expand Down
Loading