# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt """Checks for various exception related errors.""" import builtins import inspect from typing import TYPE_CHECKING, Any, List, Optional import astroid from astroid import nodes, objects from pylint import checkers, interfaces from pylint.checkers import utils if TYPE_CHECKING: from pylint.lint import PyLinter def _builtin_exceptions(): def predicate(obj): return isinstance(obj, type) and issubclass(obj, BaseException) members = inspect.getmembers(builtins, predicate) return {exc.__name__ for (_, exc) in members} def _annotated_unpack_infer(stmt, context=None): """Recursively generate nodes inferred by the given statement. If the inferred value is a list or a tuple, recurse on the elements. Returns an iterator which yields tuples in the format ('original node', 'inferred node'). """ if isinstance(stmt, (nodes.List, nodes.Tuple)): for elt in stmt.elts: inferred = utils.safe_infer(elt) if inferred and inferred is not astroid.Uninferable: yield elt, inferred return for inferred in stmt.infer(context): if inferred is astroid.Uninferable: continue yield stmt, inferred def _is_raising(body: List) -> bool: """Return whether the given statement node raises an exception.""" return any(isinstance(node, nodes.Raise) for node in body) OVERGENERAL_EXCEPTIONS = ("BaseException", "Exception") MSGS = { # pylint: disable=consider-using-namedtuple-or-dataclass "E0701": ( "Bad except clauses order (%s)", "bad-except-order", "Used when except clauses are not in the correct order (from the " "more specific to the more generic). If you don't fix the order, " "some exceptions may not be caught by the most specific handler.", ), "E0702": ( "Raising %s while only classes or instances are allowed", "raising-bad-type", "Used when something which is neither a class nor an instance " "is raised (i.e. a `TypeError` will be raised).", ), "E0703": ( "Exception context set to something which is not an exception, nor None", "bad-exception-context", 'Used when using the syntax "raise ... from ...", ' "where the exception context is not an exception, " "nor None.", ), "E0704": ( "The raise statement is not inside an except clause", "misplaced-bare-raise", "Used when a bare raise is not used inside an except clause. " "This generates an error, since there are no active exceptions " "to be reraised. An exception to this rule is represented by " "a bare raise inside a finally clause, which might work, as long " "as an exception is raised inside the try block, but it is " "nevertheless a code smell that must not be relied upon.", ), "E0710": ( "Raising a new style class which doesn't inherit from BaseException", "raising-non-exception", "Used when a new style class which doesn't inherit from " "BaseException is raised.", ), "E0711": ( "NotImplemented raised - should raise NotImplementedError", "notimplemented-raised", "Used when NotImplemented is raised instead of NotImplementedError", ), "E0712": ( "Catching an exception which doesn't inherit from Exception: %s", "catching-non-exception", "Used when a class which doesn't inherit from " "Exception is used as an exception in an except clause.", ), "W0702": ( "No exception type(s) specified", "bare-except", "Used when an except clause doesn't specify exceptions type to catch.", ), "W0703": ( "Catching too general exception %s", "broad-except", "Used when an except catches a too general exception, " "possibly burying unrelated errors.", ), "W0705": ( "Catching previously caught exception type %s", "duplicate-except", "Used when an except catches a type that was already caught by " "a previous handler.", ), "W0706": ( "The except handler raises immediately", "try-except-raise", "Used when an except handler uses raise as its first or only " "operator. This is useless because it raises back the exception " "immediately. Remove the raise operator or the entire " "try-except-raise block!", ), "W0707": ( "Consider explicitly re-raising using the 'from' keyword", "raise-missing-from", "Python 3's exception chaining means it shows the traceback of the " "current exception, but also the original exception. Not using `raise " "from` makes the traceback inaccurate, because the message implies " "there is a bug in the exception-handling code itself, which is a " "separate situation than wrapping an exception.", ), "W0711": ( 'Exception to catch is the result of a binary "%s" operation', "binary-op-exception", "Used when the exception to catch is of the form " '"except A or B:". If intending to catch multiple, ' 'rewrite as "except (A, B):"', ), "W0715": ( "Exception arguments suggest string formatting might be intended", "raising-format-tuple", "Used when passing multiple arguments to an exception " "constructor, the first of them a string literal containing what " "appears to be placeholders intended for formatting", ), "W0716": ( "Invalid exception operation. %s", "wrong-exception-operation", "Used when an operation is done against an exception, but the operation " "is not valid for the exception in question. Usually emitted when having " "binary operations between exceptions in except handlers.", ), } class BaseVisitor: """Base class for visitors defined in this module.""" def __init__(self, checker, node): self._checker = checker self._node = node def visit(self, node): name = node.__class__.__name__.lower() dispatch_meth = getattr(self, "visit_" + name, None) if dispatch_meth: dispatch_meth(node) else: self.visit_default(node) def visit_default(self, _: nodes.NodeNG) -> None: """Default implementation for all the nodes.""" class ExceptionRaiseRefVisitor(BaseVisitor): """Visit references (anything that is not an AST leaf).""" def visit_name(self, node: nodes.Name) -> None: if node.name == "NotImplemented": self._checker.add_message("notimplemented-raised", node=self._node) def visit_call(self, node: nodes.Call) -> None: if isinstance(node.func, nodes.Name): self.visit_name(node.func) if ( len(node.args) > 1 and isinstance(node.args[0], nodes.Const) and isinstance(node.args[0].value, str) ): msg = node.args[0].value if "%" in msg or ("{" in msg and "}" in msg): self._checker.add_message("raising-format-tuple", node=self._node) class ExceptionRaiseLeafVisitor(BaseVisitor): """Visitor for handling leaf kinds of a raise value.""" def visit_const(self, node: nodes.Const) -> None: self._checker.add_message( "raising-bad-type", node=self._node, args=node.value.__class__.__name__ ) def visit_instance(self, instance: objects.ExceptionInstance) -> None: cls = instance._proxied self.visit_classdef(cls) # Exception instances have a particular class type visit_exceptioninstance = visit_instance def visit_classdef(self, node: nodes.ClassDef) -> None: if not utils.inherit_from_std_ex(node) and utils.has_known_bases(node): if node.newstyle: self._checker.add_message("raising-non-exception", node=self._node) def visit_tuple(self, _: nodes.Tuple) -> None: self._checker.add_message("raising-bad-type", node=self._node, args="tuple") def visit_default(self, node: nodes.NodeNG) -> None: name = getattr(node, "name", node.__class__.__name__) self._checker.add_message("raising-bad-type", node=self._node, args=name) class ExceptionsChecker(checkers.BaseChecker): """Exception related checks.""" __implements__ = interfaces.IAstroidChecker name = "exceptions" msgs = MSGS options = ( ( "overgeneral-exceptions", { "default": OVERGENERAL_EXCEPTIONS, "type": "csv", "metavar": "", "help": "Exceptions that will emit a warning " # pylint: disable=consider-using-f-string 'when being caught. Defaults to "%s".' % (", ".join(OVERGENERAL_EXCEPTIONS),), }, ), ) def open(self): self._builtin_exceptions = _builtin_exceptions() super().open() @utils.check_messages( "misplaced-bare-raise", "raising-bad-type", "raising-non-exception", "notimplemented-raised", "bad-exception-context", "raising-format-tuple", "raise-missing-from", ) def visit_raise(self, node: nodes.Raise) -> None: if node.exc is None: self._check_misplaced_bare_raise(node) return if node.cause is None: self._check_raise_missing_from(node) else: self._check_bad_exception_context(node) expr = node.exc ExceptionRaiseRefVisitor(self, node).visit(expr) inferred = utils.safe_infer(expr) if inferred is None or inferred is astroid.Uninferable: return ExceptionRaiseLeafVisitor(self, node).visit(inferred) def _check_misplaced_bare_raise(self, node): # Filter out if it's present in __exit__. scope = node.scope() if ( isinstance(scope, nodes.FunctionDef) and scope.is_method() and scope.name == "__exit__" ): return current = node # Stop when a new scope is generated or when the raise # statement is found inside a TryFinally. ignores = (nodes.ExceptHandler, nodes.FunctionDef) while current and not isinstance(current.parent, ignores): current = current.parent expected = (nodes.ExceptHandler,) if not current or not isinstance(current.parent, expected): self.add_message("misplaced-bare-raise", node=node) def _check_bad_exception_context(self, node: nodes.Raise) -> None: """Verify that the exception context is properly set. An exception context can be only `None` or an exception. """ cause = utils.safe_infer(node.cause) if cause in (astroid.Uninferable, None): return if isinstance(cause, nodes.Const): if cause.value is not None: self.add_message("bad-exception-context", node=node) elif not isinstance(cause, nodes.ClassDef) and not utils.inherit_from_std_ex( cause ): self.add_message("bad-exception-context", node=node) def _check_raise_missing_from(self, node: nodes.Raise) -> None: if node.exc is None: # This is a plain `raise`, raising the previously-caught exception. No need for a # cause. return # We'd like to check whether we're inside an `except` clause: containing_except_node = utils.find_except_wrapper_node_in_scope(node) if not containing_except_node: return # We found a surrounding `except`! We're almost done proving there's a # `raise-missing-from` here. The only thing we need to protect against is that maybe # the `raise` is raising the exception that was caught, possibly with some shenanigans # like `exc.with_traceback(whatever)`. We won't analyze these, we'll just assume # there's a violation on two simple cases: `raise SomeException(whatever)` and `raise # SomeException`. if containing_except_node.name is None: # The `except` doesn't have an `as exception:` part, meaning there's no way that # the `raise` is raising the same exception. self.add_message("raise-missing-from", node=node) elif isinstance(node.exc, nodes.Call) and isinstance(node.exc.func, nodes.Name): # We have a `raise SomeException(whatever)`. self.add_message("raise-missing-from", node=node) elif ( isinstance(node.exc, nodes.Name) and node.exc.name != containing_except_node.name.name ): # We have a `raise SomeException`. self.add_message("raise-missing-from", node=node) def _check_catching_non_exception(self, handler, exc, part): if isinstance(exc, nodes.Tuple): # Check if it is a tuple of exceptions. inferred = [utils.safe_infer(elt) for elt in exc.elts] if any(node is astroid.Uninferable for node in inferred): # Don't emit if we don't know every component. return if all( node and (utils.inherit_from_std_ex(node) or not utils.has_known_bases(node)) for node in inferred ): return if not isinstance(exc, nodes.ClassDef): # Don't emit the warning if the inferred stmt # is None, but the exception handler is something else, # maybe it was redefined. if isinstance(exc, nodes.Const) and exc.value is None: if ( isinstance(handler.type, nodes.Const) and handler.type.value is None ) or handler.type.parent_of(exc): # If the exception handler catches None or # the exception component, which is None, is # defined by the entire exception handler, then # emit a warning. self.add_message( "catching-non-exception", node=handler.type, args=(part.as_string(),), ) else: self.add_message( "catching-non-exception", node=handler.type, args=(part.as_string(),), ) return if ( not utils.inherit_from_std_ex(exc) and exc.name not in self._builtin_exceptions ): if utils.has_known_bases(exc): self.add_message( "catching-non-exception", node=handler.type, args=(exc.name,) ) def _check_try_except_raise(self, node): def gather_exceptions_from_handler( handler, ) -> Optional[List[nodes.NodeNG]]: exceptions: List[nodes.NodeNG] = [] if handler.type: exceptions_in_handler = utils.safe_infer(handler.type) if isinstance(exceptions_in_handler, nodes.Tuple): exceptions = list( { exception for exception in exceptions_in_handler.elts if isinstance(exception, nodes.Name) } ) elif exceptions_in_handler: exceptions = [exceptions_in_handler] else: # Break when we cannot infer anything reliably. return None return exceptions bare_raise = False handler_having_bare_raise = None exceptions_in_bare_handler = [] for handler in node.handlers: if bare_raise: # check that subsequent handler is not parent of handler which had bare raise. # since utils.safe_infer can fail for bare except, check it before. # also break early if bare except is followed by bare except. excs_in_current_handler = gather_exceptions_from_handler(handler) if not excs_in_current_handler: break if exceptions_in_bare_handler is None: # It can be `None` when the inference failed break for exc_in_current_handler in excs_in_current_handler: inferred_current = utils.safe_infer(exc_in_current_handler) if any( utils.is_subclass_of(utils.safe_infer(e), inferred_current) for e in exceptions_in_bare_handler ): bare_raise = False break # `raise` as the first operator inside the except handler if _is_raising([handler.body[0]]): # flags when there is a bare raise if handler.body[0].exc is None: bare_raise = True handler_having_bare_raise = handler exceptions_in_bare_handler = gather_exceptions_from_handler(handler) else: if bare_raise: self.add_message("try-except-raise", node=handler_having_bare_raise) @utils.check_messages("wrong-exception-operation") def visit_binop(self, node: nodes.BinOp) -> None: if isinstance(node.parent, nodes.ExceptHandler): # except (V | A) suggestion = f"Did you mean '({node.left.as_string()}, {node.right.as_string()})' instead?" self.add_message("wrong-exception-operation", node=node, args=(suggestion,)) @utils.check_messages("wrong-exception-operation") def visit_compare(self, node: nodes.Compare) -> None: if isinstance(node.parent, nodes.ExceptHandler): # except (V < A) suggestion = f"Did you mean '({node.left.as_string()}, {', '.join(operand.as_string() for _, operand in node.ops)})' instead?" self.add_message("wrong-exception-operation", node=node, args=(suggestion,)) @utils.check_messages( "bare-except", "broad-except", "try-except-raise", "binary-op-exception", "bad-except-order", "catching-non-exception", "duplicate-except", ) def visit_tryexcept(self, node: nodes.TryExcept) -> None: """Check for empty except.""" self._check_try_except_raise(node) exceptions_classes: List[Any] = [] nb_handlers = len(node.handlers) for index, handler in enumerate(node.handlers): if handler.type is None: if not _is_raising(handler.body): self.add_message("bare-except", node=handler) # check if an "except:" is followed by some other # except if index < (nb_handlers - 1): msg = "empty except clause should always appear last" self.add_message("bad-except-order", node=node, args=msg) elif isinstance(handler.type, nodes.BoolOp): self.add_message( "binary-op-exception", node=handler, args=handler.type.op ) else: try: exceptions = list(_annotated_unpack_infer(handler.type)) except astroid.InferenceError: continue for part, exception in exceptions: if exception is astroid.Uninferable: continue if isinstance( exception, astroid.Instance ) and utils.inherit_from_std_ex(exception): exception = exception._proxied self._check_catching_non_exception(handler, exception, part) if not isinstance(exception, nodes.ClassDef): continue exc_ancestors = [ anc for anc in exception.ancestors() if isinstance(anc, nodes.ClassDef) ] for previous_exc in exceptions_classes: if previous_exc in exc_ancestors: msg = f"{previous_exc.name} is an ancestor class of {exception.name}" self.add_message( "bad-except-order", node=handler.type, args=msg ) if ( exception.name in self.linter.namespace.overgeneral_exceptions and exception.root().name == utils.EXCEPTIONS_MODULE and not _is_raising(handler.body) ): self.add_message( "broad-except", args=exception.name, node=handler.type ) if exception in exceptions_classes: self.add_message( "duplicate-except", args=exception.name, node=handler.type ) exceptions_classes += [exc for _, exc in exceptions] def register(linter: "PyLinter") -> None: linter.register_checker(ExceptionsChecker(linter))