diff options
Diffstat (limited to 'pylint/checkers/exceptions.py')
-rw-r--r-- | pylint/checkers/exceptions.py | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py new file mode 100644 index 0000000..88a8f22 --- /dev/null +++ b/pylint/checkers/exceptions.py @@ -0,0 +1,332 @@ +# 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 sys + +import astroid +from astroid import YES, Instance, unpack_infer, List, Tuple +from logilab.common.compat import builtins + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import ( + is_empty, + is_raising, + check_messages, + inherit_from_std_ex, + EXCEPTIONS_MODULE, + has_known_bases, + safe_infer) +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE + + +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, (List, Tuple)): + for elt in stmt.elts: + inferred = safe_infer(elt) + if inferred and inferred is not YES: + yield elt, inferred + return + for infered in stmt.infer(context): + if infered is 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)}), + '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.'), + 'W0704': ('Except doesn\'t do anything', + 'pointless-except', + 'Used when an except clause does nothing but "pass" and there is\ + no "else" clause.'), + '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' : '<comma-separated class names>', + 'help' : 'Exceptions that will emit a warning ' + 'when being caught. Defaults to "%s"' % ( + ', '.join(OVERGENERAL_EXCEPTIONS),)} + ), + ) + + @check_messages('nonstandard-exception', + 'raising-bad-type', 'raising-non-exception', + 'notimplemented-raised', 'bad-exception-context') + def visit_raise(self, node): + """visit raise possibly inferring value""" + # ignore empty raise + if node.exc is None: + 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(unpack_infer(expr)) + except astroid.InferenceError: + return + self._check_raise_value(node, value) + + 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 = safe_infer(node.cause) + if cause in (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.Class) 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.Function))): + emit = True + if not PY3K and isinstance(expr, astroid.Tuple): + # 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 = safe_infer(first) + if isinstance(inferred, Instance): + # pylint: disable=protected-access + inferred = inferred._proxied + if (inferred is YES or + isinstance(inferred, astroid.Class) + 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.CallFunc) and + isinstance(expr.func, astroid.Name) and + expr.func.name == 'NotImplemented')): + self.add_message('notimplemented-raised', node=node) + elif isinstance(expr, (Instance, astroid.Class)): + if isinstance(expr, Instance): + # pylint: disable=protected-access + expr = expr._proxied + if (isinstance(expr, astroid.Class) and + not inherit_from_std_ex(expr)): + if expr.newstyle: + self.add_message('raising-non-exception', node=node) + else: + if has_known_bases(expr): + confidence = INFERENCE + else: + confidence = INFERENCE_FAILURE + self.add_message( + 'nonstandard-exception', node=node, + confidence=confidence) + 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 = [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.Class): + # 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.root().name != BUILTINS_NAME): + if has_known_bases(exc): + self.add_message('catching-non-exception', + node=handler.type, + args=(exc.name, )) + + @check_messages('bare-except', 'broad-except', 'pointless-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): + # single except doing nothing but "pass" without else clause + if is_empty(handler.body) and not node.orelse: + self.add_message('pointless-except', + node=handler.type or handler.body[0]) + 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 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.Class): + continue + + exc_ancestors = [anc for anc in exc.ancestors() + if isinstance(anc, astroid.Class)] + 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) + + exceptions_classes += [exc for _, exc in excs] + + +def register(linter): + """required method to auto register this checker""" + linter.register_checker(ExceptionsChecker(linter)) |