Skip to content

Commit

Permalink
Detect unreachable code after sys.exit, quit, exit, and os._exit (#7520)
Browse files Browse the repository at this point in the history
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
Co-authored-by: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com>
  • Loading branch information
3 people committed Nov 13, 2022
1 parent 435a5a1 commit bed42ba
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 13 deletions.
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/519.false_negative
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Code following a call to ``quit``, ``exit``, ``sys.exit`` or ``os._exit`` will be marked as `unreachable`.

Refs #519
43 changes: 36 additions & 7 deletions pylint/checkers/base/basic_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from pylint import utils as lint_utils
from pylint.checkers import BaseChecker, utils
from pylint.interfaces import HIGH, INFERENCE
from pylint.interfaces import HIGH, INFERENCE, Confidence
from pylint.reporters.ureports import nodes as reporter_nodes
from pylint.utils import LinterStats

Expand Down Expand Up @@ -657,13 +657,38 @@ 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", "bad-reversed-sequence", "misplaced-format-function"
"eval-used",
"exec-used",
"bad-reversed-sequence",
"misplaced-format-function",
"unreachable",
)
def visit_call(self, node: nodes.Call) -> None:
"""Visit a Call node -> check if this is not a disallowed builtin
call and check for * or ** use.
"""
"""Visit a Call node."""
if self._is_terminating_func(node):
self._check_unreachable(node, confidence=INFERENCE)
self._check_misplaced_format_function(node)
if isinstance(node.func, nodes.Name):
name = node.func.name
Expand Down Expand Up @@ -731,7 +756,9 @@ def leave_tryfinally(self, _: nodes.TryFinally) -> None:
self._tryfinallys.pop()

def _check_unreachable(
self, node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise
self,
node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise | nodes.Call,
confidence: Confidence = HIGH,
) -> None:
"""Check unreachable code."""
unreachable_statement = node.next_sibling()
Expand All @@ -746,7 +773,9 @@ def _check_unreachable(
unreachable_statement = unreachable_statement.next_sibling()
if unreachable_statement is None:
return
self.add_message("unreachable", node=unreachable_statement, confidence=HIGH)
self.add_message(
"unreachable", node=unreachable_statement, confidence=confidence
)

def _check_not_in_finally(
self,
Expand Down
49 changes: 48 additions & 1 deletion tests/functional/u/unreachable.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# pylint: disable=missing-docstring, broad-exception-raised
# pylint: disable=missing-docstring, broad-exception-raised, too-few-public-methods, redefined-outer-name
# pylint: disable=consider-using-sys-exit, protected-access

import os
import signal
import sys

def func1():
return 1
Expand Down Expand Up @@ -32,3 +36,46 @@ def func6():
return
yield
print("unreachable") # [unreachable]

def func7():
sys.exit(1)
var = 2 + 2 # [unreachable]
print(var)

def func8():
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
print(1)
except KeyboardInterrupt:
pass

class FalseExit:
def exit(self, number):
print(f"False positive this is not sys.exit({number})")

def func_false_exit():
sys = FalseExit()
sys.exit(1)
var = 2 + 2
print(var)

def func9():
os._exit()
var = 2 + 2 # [unreachable]
print(var)

def func10():
exit()
var = 2 + 2 # [unreachable]
print(var)

def func11():
quit()
var = 2 + 2 # [unreachable]
print(var)

incognito_function = sys.exit
def func12():
incognito_function()
var = 2 + 2 # [unreachable]
print(var)
15 changes: 10 additions & 5 deletions tests/functional/u/unreachable.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
unreachable:6:4:6:24:func1:Unreachable code:HIGH
unreachable:11:8:11:28:func2:Unreachable code:HIGH
unreachable:17:8:17:28:func3:Unreachable code:HIGH
unreachable:21:4:21:16:func4:Unreachable code:HIGH
unreachable:34:4:34:24:func6:Unreachable code:HIGH
unreachable:10:4:10:24:func1:Unreachable code:HIGH
unreachable:15:8:15:28:func2:Unreachable code:HIGH
unreachable:21:8:21:28:func3:Unreachable code:HIGH
unreachable:25:4:25:16:func4:Unreachable code:HIGH
unreachable:38:4:38:24:func6:Unreachable code:HIGH
unreachable:42:4:42:15:func7:Unreachable code:INFERENCE
unreachable:64:4:64:15:func9:Unreachable code:INFERENCE
unreachable:69:4:69:15:func10:Unreachable code:INFERENCE
unreachable:74:4:74:15:func11:Unreachable code:INFERENCE
unreachable:80:4:80:15:func12:Unreachable code:INFERENCE

0 comments on commit bed42ba

Please sign in to comment.