diff options
-rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
-rw-r--r-- | pylint/checkers/base.py | 122 | ||||
-rw-r--r-- | pylint/test/functional/too_many_nested_blocks.py | 95 | ||||
-rw-r--r-- | pylint/test/functional/too_many_nested_blocks.txt | 2 |
4 files changed, 218 insertions, 3 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 7dc35ab..f4d66b8 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -74,4 +74,4 @@ Order doesn't matter (not that much, at least ;) * Dmitry Pribysh: multiple-imports, not-iterable, not-a-mapping, various patches. * Laura Medioni (Logilab, on behalf of the CNES): misplaced-comparison-constant, - no-classmethod-decorator, no-staticmethod-decorator + no-classmethod-decorator, no-staticmethod-decorator, too-many-nested-blocks diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 8d860ac..bd8a348 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -30,10 +30,11 @@ import astroid.scoped_nodes from astroid import are_exclusive, InferenceError from astroid import helpers -from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH +from pylint.interfaces import (IAstroidChecker, ITokenChecker, INFERENCE, + INFERENCE_FAILURE, HIGH) from pylint.utils import EmptyReport, deprecated_option from pylint.reporters import diff_string -from pylint.checkers import BaseChecker +from pylint.checkers import BaseChecker, BaseTokenChecker from pylint.checkers.utils import ( check_messages, clobber_in_except, @@ -1633,6 +1634,122 @@ class ComparisonChecker(_BasicChecker): self.add_message('unidiomatic-typecheck', node=node) +class ElifChecker(BaseTokenChecker): + """Checks needing to distinguish "else if" from "elif" + + This checker mixes the astroid and the token approaches in order to create + knowledge about whether a "else if" node is a true "else if" node, or a + "elif" node. + + The following checks depend on this implementation: + - check for too many nested blocks (if/elif structures aren't considered + as nested) + - to be continued + """ + __implements__ = (ITokenChecker, IAstroidChecker) + name = 'elif' + msgs = {'R0101': ('Too many nested blocks (%s/%s)', + 'too-many-nested-blocks', + 'Used when a function or a method has too many nested ' + 'blocks. This makes the code less understandable and ' + 'maintainable.'), + } + options = (('max-nested-blocks', + {'default' : 5, 'type' : 'int', 'metavar' : '<int>', + 'help': 'Maximum number of nested blocks for function / ' + 'method body'} + ),) + + def __init__(self, linter=None): + BaseTokenChecker.__init__(self, linter) + self._init() + + def _init(self): + self._nested_blocks = [] + self._elifs = [] + self._if_counter = 0 + self._nested_blocks_msg = None + + def process_tokens(self, tokens): + # Process tokens and look for 'if' or 'elif' + for _, token, _, _, _ in tokens: + if token == 'elif': + self._elifs.append(True) + elif token == 'if': + self._elifs.append(False) + + def leave_module(self, node): + self._init() + + @check_messages('too-many-nested-blocks') + def visit_tryexcept(self, node): + self._check_nested_blocks(node) + + visit_tryfinally = visit_tryexcept + visit_while = visit_tryexcept + visit_for = visit_while + + def visit_ifexp(self, node): + self._if_counter += 1 + + def visit_comprehension(self, node): + for if_test in node.ifs: + self._if_counter += 1 + + @check_messages('too-many-nested-blocks') + def visit_if(self, node): + self._check_nested_blocks(node) + self._if_counter += 1 + + @check_messages('too-many-nested-blocks') + def leave_functiondef(self, node): + # new scope = reinitialize the stack of nested blocks + self._nested_blocks = [] + # if there is a waiting message left, send it + if self._nested_blocks_msg: + self.add_message('too-many-nested-blocks', + node=self._nested_blocks_msg[0], + args=self._nested_blocks_msg[1]) + self._nested_blocks_msg = None + + def _check_nested_blocks(self, node): + """Update and check the number of nested blocks + """ + # only check block levels inside functions or methods + if not isinstance(node.scope(), astroid.FunctionDef): + return + # messages are triggered on leaving the nested block. Here we save the + # stack in case the current node isn't nested in the previous one + nested_blocks = self._nested_blocks[:] + if node.parent == node.scope(): + self._nested_blocks = [node] + else: + # go through ancestors from the most nested to the less + for ancestor_node in reversed(self._nested_blocks): + if ancestor_node == node.parent: + break + self._nested_blocks.pop() + # if the node is a elif, this should not be another nesting level + if isinstance(node, astroid.If) and self._elifs[self._if_counter]: + if self._nested_blocks: + self._nested_blocks.pop() + self._nested_blocks.append(node) + # send message only once per group of nested blocks + if len(nested_blocks) > self.config.max_nested_blocks: + if len(nested_blocks) > len(self._nested_blocks): + self.add_message('too-many-nested-blocks', node=nested_blocks[0], + args=(len(nested_blocks), + self.config.max_nested_blocks)) + self._nested_blocks_msg = None + else: + # if time has not come yet to send the message (ie the stack of + # nested nodes is still increasing), save it in case the + # current node is the last one of the function + self._nested_blocks_msg = (self._nested_blocks[0], + (len(self._nested_blocks), + self.config.max_nested_blocks)) + + def register(linter): """required method to auto register this checker""" linter.register_checker(BasicErrorChecker(linter)) @@ -1643,3 +1760,4 @@ def register(linter): linter.register_checker(LambdaForComprehensionChecker(linter)) linter.register_checker(ComparisonChecker(linter)) linter.register_checker(RecommandationChecker(linter)) + linter.register_checker(ElifChecker(linter)) diff --git a/pylint/test/functional/too_many_nested_blocks.py b/pylint/test/functional/too_many_nested_blocks.py new file mode 100644 index 0000000..8d371dd --- /dev/null +++ b/pylint/test/functional/too_many_nested_blocks.py @@ -0,0 +1,95 @@ +"""Checks the maximum block level is smaller than 6 in function definitions""" + +#pylint: disable=using-constant-test, missing-docstring, too-many-return-statements + +def my_function(): + if 1: # [too-many-nested-blocks] + for i in range(10): + if i == 2: + while True: + try: + if True: + i += 1 + except IOError: + pass + + if 1: + for i in range(10): + if i == 2: + while True: + try: + i += 1 + except IOError: + pass + + def nested_func(): + if True: + for i in range(10): + while True: + if True: + if True: + print i + + nested_func() + +def more_complex_function(): + attr1 = attr2 = attr3 = [1, 2, 3] + if attr1: + for i in attr1: + if attr2: + return i + else: + return 'duh' + elif attr2: + for i in attr2: + if attr2: + return i + else: + return 'duh' + else: + for i in range(15): + if attr3: + return i + else: + return 'doh' + return None + +def elif_function(): + arg = None + if arg == 1: + return 1 + elif arg == 2: + return 2 + elif arg == 3: + return 3 + elif arg == 4: + return 4 + elif arg == 5: + return 5 + elif arg == 6: + return 6 + elif arg == 7: + return 7 + +def else_if_function(): + arg = None + if arg == 1: # [too-many-nested-blocks] + return 1 + else: + if arg == 2: + return 2 + else: + if arg == 3: + return 3 + else: + if arg == 4: + return 4 + else: + if arg == 5: + return 5 + else: + if arg == 6: + return 6 + else: + if arg == 7: + return 7 diff --git a/pylint/test/functional/too_many_nested_blocks.txt b/pylint/test/functional/too_many_nested_blocks.txt new file mode 100644 index 0000000..dcee9d2 --- /dev/null +++ b/pylint/test/functional/too_many_nested_blocks.txt @@ -0,0 +1,2 @@ +too-many-nested-blocks:6:my_function:Too many nested blocks (6/5) +too-many-nested-blocks:76:else_if_function:Too many nested blocks (7/5) |