Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/7563.false_positive
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix ``used-before-assignment`` false positive when else branch calls ``sys.exit`` or similar terminating functions.

Closes #7563
23 changes: 1 addition & 22 deletions pylint/checkers/base/basic_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,27 +668,6 @@ def _check_misplaced_format_function(self, call_node: nodes.Call) -> None:
):
self.add_message("misplaced-format-function", node=call_node)

@staticmethod
def _is_terminating_func(node: nodes.Call) -> bool:
"""Detect call to exit(), quit(), os._exit(), or sys.exit()."""
if (
not isinstance(node.func, nodes.Attribute)
and not (isinstance(node.func, nodes.Name))
or isinstance(node.parent, nodes.Lambda)
):
return False

qnames = {"_sitebuiltins.Quitter", "sys.exit", "posix._exit", "nt._exit"}

try:
for inferred in node.func.infer():
if hasattr(inferred, "qname") and inferred.qname() in qnames:
return True
except (StopIteration, astroid.InferenceError):
pass

return False

@utils.only_required_for_messages(
"eval-used",
"exec-used",
Expand All @@ -698,7 +677,7 @@ def _is_terminating_func(node: nodes.Call) -> bool:
)
def visit_call(self, node: nodes.Call) -> None:
"""Visit a Call node."""
if self._is_terminating_func(node):
if utils.is_terminating_func(node):
self._check_unreachable(node, confidence=INFERENCE)
self._check_misplaced_format_function(node)
if isinstance(node.func, nodes.Name):
Expand Down
21 changes: 21 additions & 0 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2138,3 +2138,24 @@ def is_singleton_const(node: nodes.NodeNG) -> bool:
return isinstance(node, nodes.Const) and any(
node.value is value for value in SINGLETON_VALUES
)


def is_terminating_func(node: nodes.Call) -> bool:
"""Detect call to exit(), quit(), os._exit(), or sys.exit()."""
if (
not isinstance(node.func, nodes.Attribute)
and not (isinstance(node.func, nodes.Name))
or isinstance(node.parent, nodes.Lambda)
):
return False

qnames = {"_sitebuiltins.Quitter", "sys.exit", "posix._exit", "nt._exit"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: This could be a global, maybe even a frozenset. That would be a small performance improvement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


try:
for inferred in node.func.infer():
if hasattr(inferred, "qname") and inferred.qname() in qnames:
return True
except (StopIteration, astroid.InferenceError):
pass

return False
9 changes: 8 additions & 1 deletion pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,14 @@ def _uncertain_nodes_in_except_blocks(
isinstance(else_statement, nodes.Return)
for else_statement in closest_try_except.orelse
)
if try_block_returns or else_block_returns:
else_block_exits = any(
isinstance(else_statement, nodes.Expr)
and isinstance(else_statement.value, nodes.Call)
and utils.is_terminating_func(else_statement.value)
for else_statement in closest_try_except.orelse
)

if try_block_returns or else_block_returns or else_block_exits:
# Exception: if this node is in the final block of the other_node_statement,
# it will execute before returning. Assume the except statements are uncertain.
if (
Expand Down
15 changes: 14 additions & 1 deletion tests/functional/u/used/used_before_assignment_else_return.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""If the else block returns, it is generally safe to rely on assignments in the except."""

# pylint: disable=missing-function-docstring, invalid-name
import sys

def valid():
"""https://github.com/PyCQA/pylint/issues/6790"""
Expand Down Expand Up @@ -59,3 +60,15 @@ def invalid_4():
else:
print(error) # [used-before-assignment]
return

def valid_exit():
try:
pass
except SystemExit as e:
lint_result = e.code
else:
sys.exit("Bad")
if lint_result != 0:
sys.exit("Error is 0.")

print(lint_result)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
used-before-assignment:25:14:25:19:invalid:Using variable 'error' before assignment:CONTROL_FLOW
used-before-assignment:38:14:38:19:invalid_2:Using variable 'error' before assignment:CONTROL_FLOW
used-before-assignment:50:14:50:19:invalid_3:Using variable 'error' before assignment:CONTROL_FLOW
used-before-assignment:60:14:60:19:invalid_4:Using variable 'error' before assignment:CONTROL_FLOW
used-before-assignment:26:14:26:19:invalid:Using variable 'error' before assignment:CONTROL_FLOW
used-before-assignment:39:14:39:19:invalid_2:Using variable 'error' before assignment:CONTROL_FLOW
used-before-assignment:51:14:51:19:invalid_3:Using variable 'error' before assignment:CONTROL_FLOW
used-before-assignment:61:14:61:19:invalid_4:Using variable 'error' before assignment:CONTROL_FLOW