# -*- coding: utf-8 -*- # Copyright (c) 2006-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) # Copyright (c) 2011-2014 Google, Inc. # Copyright (c) 2012 Tim Hatch # Copyright (c) 2013-2020 Claudiu Popa # Copyright (c) 2014 Brett Cannon # Copyright (c) 2014 Arun Persaud # Copyright (c) 2015 Rene Zhang # Copyright (c) 2015 Florian Bruhin # Copyright (c) 2015 Steven Myint # Copyright (c) 2015 Ionel Cristian Maries # Copyright (c) 2016 Erik # Copyright (c) 2016 Jakub Wilk # Copyright (c) 2017 Łukasz Rogalski # Copyright (c) 2017 Martin von Gagern # Copyright (c) 2018 Lucas Cimon # Copyright (c) 2018 ssolanki # Copyright (c) 2018 Natalie Serebryakova # Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com> # Copyright (c) 2018 Carey Metcalfe # Copyright (c) 2018 Mike Frysinger # Copyright (c) 2018 Alexander Todorov # Copyright (c) 2018 Ville Skyttä # Copyright (c) 2019 Djailla # Copyright (c) 2019 Hugo van Kemenade # Copyright (c) 2019 Pierre Sassoulas # Copyright (c) 2020 Ram Rachum # Copyright (c) 2020 Anthony Sottile # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/COPYING """Checks for various exception related errors.""" import builtins import inspect import typing import astroid from astroid.node_classes import NodeNG from pylint import checkers, interfaces from pylint.checkers import utils 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, (astroid.List, astroid.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: typing.List) -> bool: """Return true if the given statement node raise an exception""" for node in body: if isinstance(node, astroid.Raise): return True return False OVERGENERAL_EXCEPTIONS = ("BaseException", "Exception") BUILTINS_NAME = builtins.__name__ MSGS = { "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, an instance or a " "string 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, node): # pylint: disable=unused-argument """Default implementation for all the nodes.""" class ExceptionRaiseRefVisitor(BaseVisitor): """Visit references (anything that is not an AST leaf).""" def visit_name(self, name): if name.name == "NotImplemented": self._checker.add_message("notimplemented-raised", node=self._node) def visit_call(self, call): if isinstance(call.func, astroid.Name): self.visit_name(call.func) if ( len(call.args) > 1 and isinstance(call.args[0], astroid.Const) and isinstance(call.args[0].value, str) ): msg = call.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, const): if not isinstance(const.value, str): # raising-string will be emitted from python3 porting checker. self._checker.add_message( "raising-bad-type", node=self._node, args=const.value.__class__.__name__ ) def visit_instance(self, instance): # pylint: disable=protected-access cls = instance._proxied self.visit_classdef(cls) # Exception instances have a particular class type visit_exceptioninstance = visit_instance def visit_classdef(self, cls): if not utils.inherit_from_std_ex(cls) and utils.has_known_bases(cls): if cls.newstyle: self._checker.add_message("raising-non-exception", node=self._node) def visit_tuple(self, _): self._checker.add_message("raising-bad-type", node=self._node, args="tuple") def visit_default(self, node): 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 priority = -4 options = ( ( "overgeneral-exceptions", { "default": OVERGENERAL_EXCEPTIONS, "type": "csv", "metavar": "", "help": "Exceptions that will emit a warning " '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): 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) try: inferred_value = expr.inferred()[-1] except astroid.InferenceError: pass else: if inferred_value: ExceptionRaiseLeafVisitor(self, node).visit(inferred_value) def _check_misplaced_bare_raise(self, node): # Filter out if it's present in __exit__. scope = node.scope() if ( isinstance(scope, astroid.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 = (astroid.ExceptHandler, astroid.FunctionDef) while current and not isinstance(current.parent, ignores): current = current.parent expected = (astroid.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: astroid.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, astroid.Const): if cause.value is not None: self.add_message("bad-exception-context", node=node) elif not isinstance(cause, astroid.ClassDef) and not utils.inherit_from_std_ex( cause ): self.add_message("bad-exception-context", node=node) def _check_raise_missing_from(self, node: astroid.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, astroid.Call) and isinstance( node.exc.func, astroid.Name ): # We have a `raise SomeException(whatever)`. self.add_message("raise-missing-from", node=node) elif ( isinstance(node.exc, astroid.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, astroid.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, astroid.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, astroid.Const) and exc.value is None: if ( isinstance(handler.type, astroid.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, ) -> typing.Optional[typing.List[NodeNG]]: exceptions = [] # type: typing.List[NodeNG] if handler.type: exceptions_in_handler = utils.safe_infer(handler.type) if isinstance(exceptions_in_handler, astroid.Tuple): exceptions = list( { exception for exception in exceptions_in_handler.elts if isinstance(exception, astroid.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 excs_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: bare_raise = False break if excs_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(exc_in_bare_handler), inferred_current ) for exc_in_bare_handler in excs_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 excs_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): if isinstance(node.parent, astroid.ExceptHandler): # except (V | A) suggestion = "Did you mean '(%s, %s)' instead?" % ( node.left.as_string(), node.right.as_string(), ) self.add_message("wrong-exception-operation", node=node, args=(suggestion,)) @utils.check_messages("wrong-exception-operation") def visit_compare(self, node): if isinstance(node.parent, astroid.ExceptHandler): # except (V < A) suggestion = "Did you mean '(%s, %s)' instead?" % ( node.left.as_string(), ", ".join(operand.as_string() for _, operand in node.ops), ) 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): """check for empty except""" self._check_try_except_raise(node) exceptions_classes = [] 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, astroid.BoolOp): self.add_message( "binary-op-exception", node=handler, args=handler.type.op ) else: try: excs = list(_annotated_unpack_infer(handler.type)) except astroid.InferenceError: continue for part, exc in excs: if exc is astroid.Uninferable: continue if isinstance(exc, astroid.Instance) and utils.inherit_from_std_ex( exc ): # pylint: disable=protected-access exc = exc._proxied self._check_catching_non_exception(handler, exc, part) if not isinstance(exc, astroid.ClassDef): continue exc_ancestors = [ anc for anc in exc.ancestors() if isinstance(anc, astroid.ClassDef) ] for previous_exc in exceptions_classes: if previous_exc in exc_ancestors: msg = "%s is an ancestor class of %s" % ( previous_exc.name, exc.name, ) self.add_message( "bad-except-order", node=handler.type, args=msg ) if ( exc.name in self.config.overgeneral_exceptions and exc.root().name == utils.EXCEPTIONS_MODULE and not _is_raising(handler.body) ): self.add_message( "broad-except", args=exc.name, node=handler.type ) if exc in exceptions_classes: self.add_message( "duplicate-except", args=exc.name, node=handler.type ) exceptions_classes += [exc for _, exc in excs] def register(linter): """required method to auto register this checker""" linter.register_checker(ExceptionsChecker(linter))