From bed42badfe6be6317e6dc6406433218287ded3b9 Mon Sep 17 00:00:00 2001 From: Dani Alcala <112832187+clavedeluna@users.noreply.github.com> Date: Sun, 13 Nov 2022 19:35:30 -0300 Subject: [PATCH] Detect unreachable code after sys.exit, quit, exit, and os._exit (#7520) Co-authored-by: Pierre Sassoulas Co-authored-by: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> --- doc/whatsnew/fragments/519.false_negative | 3 ++ pylint/checkers/base/basic_checker.py | 43 ++++++++++++++++---- tests/functional/u/unreachable.py | 49 ++++++++++++++++++++++- tests/functional/u/unreachable.txt | 15 ++++--- 4 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 doc/whatsnew/fragments/519.false_negative diff --git a/doc/whatsnew/fragments/519.false_negative b/doc/whatsnew/fragments/519.false_negative new file mode 100644 index 0000000000..7c8a0f45ce --- /dev/null +++ b/doc/whatsnew/fragments/519.false_negative @@ -0,0 +1,3 @@ +Code following a call to ``quit``, ``exit``, ``sys.exit`` or ``os._exit`` will be marked as `unreachable`. + +Refs #519 diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index 6ea7197b30..077fc37513 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -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 @@ -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 @@ -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() @@ -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, diff --git a/tests/functional/u/unreachable.py b/tests/functional/u/unreachable.py index 0bb35b20d4..0211a61366 100644 --- a/tests/functional/u/unreachable.py +++ b/tests/functional/u/unreachable.py @@ -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 @@ -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) diff --git a/tests/functional/u/unreachable.txt b/tests/functional/u/unreachable.txt index 491912d99c..82f9797aa1 100644 --- a/tests/functional/u/unreachable.txt +++ b/tests/functional/u/unreachable.txt @@ -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