123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- # -*- coding: utf-8 -*-
- # Copyright (c) 2006, 2009-2010, 2012-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
- # Copyright (c) 2012, 2014 Google, Inc.
- # Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
- # Copyright (c) 2014 Arun Persaud <arun@nubati.net>
- # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
- # Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
- # Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
- # Copyright (c) 2018 Ashley Whetter <ashley@awhetter.co.uk>
-
- # 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
-
- """check for signs of poor design"""
-
- from collections import defaultdict
- import re
-
- from astroid import If, BoolOp
- from astroid import decorators
-
- from pylint.interfaces import IAstroidChecker
- from pylint.checkers import BaseChecker
- from pylint.checkers import utils as checker_utils
- from pylint.checkers.utils import check_messages
- from pylint import utils
-
-
- 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'),
- }
- SPECIAL_OBJ = re.compile('^_{2}[a-z]+_{2}$')
-
-
- 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' : '<int>',
- 'help': 'Maximum number of arguments for function / method'}
- ),
- ('max-locals',
- {'default' : 15, 'type' : 'int', 'metavar' : '<int>',
- 'help': 'Maximum number of locals for function / method body'}
- ),
- ('max-returns',
- {'default' : 6, 'type' : 'int', 'metavar' : '<int>',
- 'help': 'Maximum number of return / yield for function / '
- 'method body'}
- ),
- ('max-branches',
- {'default' : 12, 'type' : 'int', 'metavar' : '<int>',
- 'help': 'Maximum number of branch for function / method body'}
- ),
- ('max-statements',
- {'default' : 50, 'type' : 'int', 'metavar' : '<int>',
- 'help': 'Maximum number of statements in function / method '
- 'body'}
- ),
- ('max-parents',
- {'default' : 7,
- 'type' : 'int',
- 'metavar' : '<num>',
- 'help' : 'Maximum number of parents for a class (see R0901).'}
- ),
- ('max-attributes',
- {'default' : 7,
- 'type' : 'int',
- 'metavar' : '<num>',
- 'help' : 'Maximum number of attributes for a class \
- (see R0902).'}
- ),
- ('min-public-methods',
- {'default' : 2,
- 'type' : 'int',
- 'metavar' : '<num>',
- 'help' : 'Minimum number of public methods for a class \
- (see R0903).'}
- ),
- ('max-public-methods',
- {'default' : 20,
- 'type' : 'int',
- 'metavar' : '<num>',
- 'help' : 'Maximum number of public methods for a class \
- (see R0904).'}
- ),
- ('max-bool-expr',
- {'default': 5,
- 'type': 'int',
- 'metavar': '<num>',
- '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)
-
- @decorators.cachedproperty
- def _ignored_argument_names(self):
- return utils.get_global_option(self, 'ignored-argument-names', default=None)
-
- @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
- """
- 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))
-
- 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('_'))
-
- # 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' or checker_utils.is_enum_class(node):
- return
-
- all_methods = sum(1 for method in node.methods()
- if not method.name.startswith('_'))
- # Special methods count towards the number of public methods,
- # but don't count towards there being too many methods.
- for method in node.mymethods():
- if SPECIAL_OBJ.search(method.name) and method.name != '__init__':
- all_methods += 1
-
- # 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', 'keyword-arg-before-vararg')
- 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
- ignored_argument_names = self._ignored_argument_names
- if args is not None:
- ignored_args_num = 0
- if ignored_argument_names:
- ignored_args_num = sum(1 for arg in args if 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
-
- visit_asyncfunctiondef = visit_functiondef
-
- @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))
-
- leave_asyncfunctiondef = leave_functiondef
-
- 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))
|