# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """exceptions handling (raising, catching, exceptions classes) checker """ import inspect import sys import astroid from astroid import helpers from six.moves import builtins import six from pylint.checkers import BaseChecker from pylint.checkers.utils import ( is_raising, check_messages, inherit_from_std_ex, EXCEPTIONS_MODULE) from pylint.interfaces import IAstroidChecker def _builtin_exceptions(): def predicate(obj): return isinstance(obj, type) and issubclass(obj, BaseException) members = inspect.getmembers(six.moves.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', 'infered node'). """ if isinstance(stmt, (astroid.List, astroid.Tuple)): for elt in stmt.elts: inferred = helpers.safe_infer(elt) if inferred and inferred is not astroid.YES: yield elt, inferred return for infered in stmt.infer(context): if infered is astroid.YES: continue yield stmt, infered PY3K = sys.version_info >= (3, 0) OVERGENERAL_EXCEPTIONS = ('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 catched 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.', {'minversion': (3, 0)}), '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 BaseException: %s', 'catching-non-exception', 'Used when a class which doesn\'t inherit from \ BaseException 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.'), 'W0710': ('Exception doesn\'t inherit from standard "Exception" class', 'nonstandard-exception', 'Used when a custom exception class is raised but doesn\'t \ inherit from the builtin "Exception" class.', {'maxversion': (3, 0)}), '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):"'), } class ExceptionsChecker(BaseChecker): """checks for * excepts without exception filter * type of raise argument : string, Exceptions, other values """ __implements__ = 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(ExceptionsChecker, self).open() @check_messages('nonstandard-exception', 'misplaced-bare-raise', 'raising-bad-type', 'raising-non-exception', 'notimplemented-raised', 'bad-exception-context') def visit_raise(self, node): """visit raise possibly inferring value""" if node.exc is None: self._check_misplaced_bare_raise(node) return if PY3K and node.cause: self._check_bad_exception_context(node) expr = node.exc if self._check_raise_value(node, expr): return else: try: value = next(astroid.unpack_infer(expr)) except astroid.InferenceError: return self._check_raise_value(node, 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, astroid.TryFinally) 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): """Verify that the exception context is properly set. An exception context can be only `None` or an exception. """ cause = helpers.safe_infer(node.cause) if cause in (astroid.YES, 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 inherit_from_std_ex(cause)): self.add_message('bad-exception-context', node=node) def _check_raise_value(self, node, expr): """check for bad values, string exception and class inheritance """ value_found = True if isinstance(expr, astroid.Const): value = expr.value if not isinstance(value, str): # raising-string will be emitted from python3 porting checker. self.add_message('raising-bad-type', node=node, args=value.__class__.__name__) elif ((isinstance(expr, astroid.Name) and expr.name in ('None', 'True', 'False')) or isinstance(expr, (astroid.List, astroid.Dict, astroid.Tuple, astroid.Module, astroid.FunctionDef))): emit = True if not PY3K and isinstance(expr, astroid.Tuple) and expr.elts: # On Python 2, using the following is not an error: # raise (ZeroDivisionError, None) # raise (ZeroDivisionError, ) # What's left to do is to check that the first # argument is indeed an exception. # Verifying the other arguments is not # the scope of this check. first = expr.elts[0] inferred = helpers.safe_infer(first) if isinstance(inferred, astroid.Instance): # pylint: disable=protected-access inferred = inferred._proxied if (inferred is astroid.YES or isinstance(inferred, astroid.ClassDef) and inherit_from_std_ex(inferred)): emit = False if emit: self.add_message('raising-bad-type', node=node, args=expr.name) elif ((isinstance(expr, astroid.Name) and expr.name == 'NotImplemented') or (isinstance(expr, astroid.Call) and isinstance(expr.func, astroid.Name) and expr.func.name == 'NotImplemented')): self.add_message('notimplemented-raised', node=node) elif isinstance(expr, (astroid.Instance, astroid.ClassDef)): if isinstance(expr, astroid.Instance): # pylint: disable=protected-access expr = expr._proxied if (isinstance(expr, astroid.ClassDef) and not inherit_from_std_ex(expr) and helpers.has_known_bases(expr)): if expr.newstyle: self.add_message('raising-non-exception', node=node) else: self.add_message('nonstandard-exception', node=node) else: value_found = False else: value_found = False return value_found def _check_catching_non_exception(self, handler, exc, part): if isinstance(exc, astroid.Tuple): # Check if it is a tuple of exceptions. inferred = [helpers.safe_infer(elt) for elt in exc.elts] if any(node is astroid.YES for node in inferred): # Don't emit if we don't know every component. return if all(node and inherit_from_std_ex(node) for node in inferred): return if not isinstance(exc, astroid.ClassDef): # Don't emit the warning if the infered 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 inherit_from_std_ex(exc) and exc.name not in self.builtin_exceptions): if helpers.has_known_bases(exc): self.add_message('catching-non-exception', node=handler.type, args=(exc.name, )) @check_messages('bare-except', 'broad-except', 'binary-op-exception', 'bad-except-order', 'catching-non-exception') def visit_tryexcept(self, node): """check for empty except""" 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 a "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.YES: continue if (isinstance(exc, astroid.Instance) and 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 == 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))