# 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. """check for signs of poor design""" import re from collections import defaultdict from astroid import If, BoolOp from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker from pylint.checkers.utils import check_messages # regexp for ignored argument name IGNORED_ARGUMENT_NAMES = re.compile('_.*') MSGS = { 'R0901': ('Too many ancestors (%s/%s)', 'too-many-ancestors', 'Used when class has too many parent classes, try to reduce \ this to get a simpler (and so easier to use) class.'), 'R0902': ('Too many instance attributes (%s/%s)', 'too-many-instance-attributes', 'Used when class has too many instance attributes, try to reduce \ this to get a simpler (and so easier to use) class.'), 'R0903': ('Too few public methods (%s/%s)', 'too-few-public-methods', 'Used when class has too few public methods, so be sure it\'s \ really worth it.'), 'R0904': ('Too many public methods (%s/%s)', 'too-many-public-methods', 'Used when class has too many public methods, try to reduce \ this to get a simpler (and so easier to use) class.'), 'R0911': ('Too many return statements (%s/%s)', 'too-many-return-statements', 'Used when a function or method has too many return statement, \ making it hard to follow.'), 'R0912': ('Too many branches (%s/%s)', 'too-many-branches', 'Used when a function or method has too many branches, \ making it hard to follow.'), 'R0913': ('Too many arguments (%s/%s)', 'too-many-arguments', 'Used when a function or method takes too many arguments.'), 'R0914': ('Too many local variables (%s/%s)', 'too-many-locals', 'Used when a function or method has too many local variables.'), 'R0915': ('Too many statements (%s/%s)', 'too-many-statements', 'Used when a function or method has too many statements. You \ should then split it in smaller functions / methods.'), 'R0916': ('Too many boolean expressions in if statement (%s/%s)', 'too-many-boolean-expressions', 'Used when a if statement contains too many boolean ' 'expressions'), } def _count_boolean_expressions(bool_op): """Counts the number of boolean expressions in BoolOp `bool_op` (recursive) example: a and (b or c or (d and e)) ==> 5 boolean expressions """ nb_bool_expr = 0 for bool_expr in bool_op.get_children(): if isinstance(bool_expr, BoolOp): nb_bool_expr += _count_boolean_expressions(bool_expr) else: nb_bool_expr += 1 return nb_bool_expr class MisdesignChecker(BaseChecker): """checks for sign of poor/misdesign: * number of methods, attributes, local variables... * size, complexity of functions, methods """ __implements__ = (IAstroidChecker,) # configuration section name name = 'design' # messages msgs = MSGS priority = -2 # configuration options options = (('max-args', {'default' : 5, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of arguments for function / method'} ), ('ignored-argument-names', {'default' : IGNORED_ARGUMENT_NAMES, 'type' :'regexp', 'metavar' : '', 'help' : 'Argument names that match this expression will be ' 'ignored. Default to name with leading underscore'} ), ('max-locals', {'default' : 15, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of locals for function / method body'} ), ('max-returns', {'default' : 6, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of return / yield for function / ' 'method body'} ), ('max-branches', {'default' : 12, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of branch for function / method body'} ), ('max-statements', {'default' : 50, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of statements in function / method ' 'body'} ), ('max-parents', {'default' : 7, 'type' : 'int', 'metavar' : '', 'help' : 'Maximum number of parents for a class (see R0901).'} ), ('max-attributes', {'default' : 7, 'type' : 'int', 'metavar' : '', 'help' : 'Maximum number of attributes for a class \ (see R0902).'} ), ('min-public-methods', {'default' : 2, 'type' : 'int', 'metavar' : '', 'help' : 'Minimum number of public methods for a class \ (see R0903).'} ), ('max-public-methods', {'default' : 20, 'type' : 'int', 'metavar' : '', 'help' : 'Maximum number of public methods for a class \ (see R0904).'} ), ('max-bool-expr', {'default': 5, 'type': 'int', 'metavar': '', 'help': 'Maximum number of boolean expressions in a if ' 'statement'} ), ) def __init__(self, linter=None): BaseChecker.__init__(self, linter) self.stats = None self._returns = None self._branches = None self._stmts = 0 def open(self): """initialize visit variables""" self.stats = self.linter.add_stats() self._returns = [] self._branches = defaultdict(int) @check_messages('too-many-ancestors', 'too-many-instance-attributes', 'too-few-public-methods', 'too-many-public-methods') def visit_classdef(self, node): """check size of inheritance hierarchy and number of instance attributes """ # Is the total inheritance hierarchy is 7 or less? nb_parents = len(list(node.ancestors())) if nb_parents > self.config.max_parents: self.add_message('too-many-ancestors', node=node, args=(nb_parents, self.config.max_parents)) # Does the class contain less than 20 attributes for # non-GUI classes (40 for GUI)? # FIXME detect gui classes if len(node.instance_attrs) > self.config.max_attributes: self.add_message('too-many-instance-attributes', node=node, args=(len(node.instance_attrs), self.config.max_attributes)) @check_messages('too-few-public-methods', 'too-many-public-methods') def leave_classdef(self, node): """check number of public methods""" my_methods = sum(1 for method in node.mymethods() if not method.name.startswith('_')) all_methods = sum(1 for method in node.methods() if not method.name.startswith('_')) # Does the class contain less than n public methods ? # This checks only the methods defined in the current class, # since the user might not have control over the classes # from the ancestors. It avoids some false positives # for classes such as unittest.TestCase, which provides # a lot of assert methods. It doesn't make sense to warn # when the user subclasses TestCase to add his own tests. if my_methods > self.config.max_public_methods: self.add_message('too-many-public-methods', node=node, args=(my_methods, self.config.max_public_methods)) # stop here for exception, metaclass and interface classes if node.type != 'class': return # Does the class contain more than n public methods ? # This checks all the methods defined by ancestors and # by the current class. if all_methods < self.config.min_public_methods: self.add_message('too-few-public-methods', node=node, args=(all_methods, self.config.min_public_methods)) @check_messages('too-many-return-statements', 'too-many-branches', 'too-many-arguments', 'too-many-locals', 'too-many-statements') def visit_functiondef(self, node): """check function name, docstring, arguments, redefinition, variable names, max locals """ # init branch and returns counters self._returns.append(0) # check number of arguments args = node.args.args if args is not None: ignored_args_num = len( [arg for arg in args if self.config.ignored_argument_names.match(arg.name)]) argnum = len(args) - ignored_args_num if argnum > self.config.max_args: self.add_message('too-many-arguments', node=node, args=(len(args), self.config.max_args)) else: ignored_args_num = 0 # check number of local variables locnum = len(node.locals) - ignored_args_num if locnum > self.config.max_locals: self.add_message('too-many-locals', node=node, args=(locnum, self.config.max_locals)) # init statements counter self._stmts = 1 @check_messages('too-many-return-statements', 'too-many-branches', 'too-many-arguments', 'too-many-locals', 'too-many-statements') def leave_functiondef(self, node): """most of the work is done here on close: checks for max returns, branch, return in __init__ """ returns = self._returns.pop() if returns > self.config.max_returns: self.add_message('too-many-return-statements', node=node, args=(returns, self.config.max_returns)) branches = self._branches[node] if branches > self.config.max_branches: self.add_message('too-many-branches', node=node, args=(branches, self.config.max_branches)) # check number of statements if self._stmts > self.config.max_statements: self.add_message('too-many-statements', node=node, args=(self._stmts, self.config.max_statements)) def visit_return(self, _): """count number of returns""" if not self._returns: return # return outside function, reported by the base checker self._returns[-1] += 1 def visit_default(self, node): """default visit method -> increments the statements counter if necessary """ if node.is_statement: self._stmts += 1 def visit_tryexcept(self, node): """increments the branches counter""" branches = len(node.handlers) if node.orelse: branches += 1 self._inc_branch(node, branches) self._stmts += branches def visit_tryfinally(self, node): """increments the branches counter""" self._inc_branch(node, 2) self._stmts += 2 @check_messages('too-many-boolean-expressions') def visit_if(self, node): """increments the branches counter and checks boolean expressions""" self._check_boolean_expressions(node) branches = 1 # don't double count If nodes coming from some 'elif' if node.orelse and (len(node.orelse) > 1 or not isinstance(node.orelse[0], If)): branches += 1 self._inc_branch(node, branches) self._stmts += branches def _check_boolean_expressions(self, node): """Go through "if" node `node` and counts its boolean expressions if the "if" node test is a BoolOp node """ condition = node.test if not isinstance(condition, BoolOp): return nb_bool_expr = _count_boolean_expressions(condition) if nb_bool_expr > self.config.max_bool_expr: self.add_message('too-many-boolean-expressions', node=condition, args=(nb_bool_expr, self.config.max_bool_expr)) def visit_while(self, node): """increments the branches counter""" branches = 1 if node.orelse: branches += 1 self._inc_branch(node, branches) visit_for = visit_while def _inc_branch(self, node, branchesnum=1): """increments the branches counter""" self._branches[node.scope()] += branchesnum def register(linter): """required method to auto register this checker """ linter.register_checker(MisdesignChecker(linter))