Index: third_party/pylint/checkers/design_analysis.py |
diff --git a/third_party/pylint/checkers/design_analysis.py b/third_party/pylint/checkers/design_analysis.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..0deb6c79684ef3256e1714970820679b94521b95 |
--- /dev/null |
+++ b/third_party/pylint/checkers/design_analysis.py |
@@ -0,0 +1,344 @@ |
+# Copyright (c) 2003-2006 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., |
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
+"""check for signs of poor design |
+ |
+ |
+ see http://intranet.logilab.fr/jpl/view?rql=Any%20X%20where%20X%20eid%201243 |
+ FIXME: missing 13, 15, 16 |
+""" |
+ |
+from logilab.astng import Function, If, InferenceError |
+ |
+from pylint.interfaces import IASTNGChecker |
+from pylint.checkers import BaseChecker |
+ |
+import re |
+ |
+# regexp for ignored argument name |
+IGNORED_ARGUMENT_NAMES = re.compile('_.*') |
+ |
+def class_is_abstract(klass): |
+ """return true if the given class node should be considered as an abstract |
+ class |
+ """ |
+ for attr in klass.values(): |
+ if isinstance(attr, Function): |
+ if attr.is_abstract(pass_is_abstract=False): |
+ return True |
+ return False |
+ |
+ |
+MSGS = { |
+ 'R0901': ('Too many ancestors (%s/%s)', |
+ 'Used when class has too many parent classes, try to reduce \ |
+ this to get a more simple (and so easier to use) class.'), |
+ 'R0902': ('Too many instance attributes (%s/%s)', |
+ 'Used when class has too many instance attributes, try to reduce \ |
+ this to get a more simple (and so easier to use) class.'), |
+ 'R0903': ('Too few public methods (%s/%s)', |
+ 'Used when class has too few public methods, so be sure it\'s \ |
+ really worth it.'), |
+ 'R0904': ('Too many public methods (%s/%s)', |
+ 'Used when class has too many public methods, try to reduce \ |
+ this to get a more simple (and so easier to use) class.'), |
+ |
+ 'R0911': ('Too many return statements (%s/%s)', |
+ 'Used when a function or method has too many return statement, \ |
+ making it hard to follow.'), |
+ 'R0912': ('Too many branches (%s/%s)', |
+ 'Used when a function or method has too many branches, \ |
+ making it hard to follow.'), |
+ 'R0913': ('Too many arguments (%s/%s)', |
+ 'Used when a function or method takes too many arguments.'), |
+ 'R0914': ('Too many local variables (%s/%s)', |
+ 'Used when a function or method has too many local variables.'), |
+ 'R0915': ('Too many statements (%s/%s)', |
+ 'Used when a function or method has too many statements. You \ |
+ should then split it in smaller functions / methods.'), |
+ |
+ 'R0921': ('Abstract class not referenced', |
+ 'Used when an abstract class is not used as ancestor anywhere.'), |
+ 'R0922': ('Abstract class is only referenced %s times', |
+ 'Used when an abstract class is used less than X times as \ |
+ ancestor.'), |
+ 'R0923': ('Interface not implemented', |
+ 'Used when an interface class is not implemented anywhere.'), |
+ } |
+ |
+ |
+class MisdesignChecker(BaseChecker): |
+ """checks for sign of poor/misdesign: |
+ * number of methods, attributes, local variables... |
+ * size, complexity of functions, methods |
+ """ |
+ |
+ __implements__ = (IASTNGChecker,) |
+ |
+ # 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'} |
+ ), |
+ ('ignored-argument-names', |
+ {'default' : IGNORED_ARGUMENT_NAMES, |
+ 'type' :'regexp', 'metavar' : '<regexp>', |
+ 'help' : 'Argument names that match this expression will be ' |
+ 'ignored. Default to name with leading underscore'} |
+ ), |
+ ('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-branchs', |
+ {'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).'} |
+ ), |
+ ) |
+ |
+ def __init__(self, linter=None): |
+ BaseChecker.__init__(self, linter) |
+ self.stats = None |
+ self._returns = None |
+ self._branchs = None |
+ self._used_abstracts = None |
+ self._used_ifaces = None |
+ self._abstracts = None |
+ self._ifaces = None |
+ self._stmts = 0 |
+ |
+ def open(self): |
+ """initialize visit variables""" |
+ self.stats = self.linter.add_stats() |
+ self._returns = [] |
+ self._branchs = [] |
+ self._used_abstracts = {} |
+ self._used_ifaces = {} |
+ self._abstracts = [] |
+ self._ifaces = [] |
+ |
+ def close(self): |
+ """check that abstract/interface classes are used""" |
+ for abstract in self._abstracts: |
+ if not abstract in self._used_abstracts: |
+ self.add_message('R0921', node=abstract) |
+ elif self._used_abstracts[abstract] < 2: |
+ self.add_message('R0922', node=abstract, |
+ args=self._used_abstracts[abstract]) |
+ for iface in self._ifaces: |
+ if not iface in self._used_ifaces: |
+ self.add_message('R0923', node=iface) |
+ |
+ def visit_class(self, node): |
+ """check size of inheritance hierarchy and number of instance attributes |
+ """ |
+ self._inc_branch() |
+ # 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('R0901', 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('R0902', node=node, |
+ args=(len(node.instance_attrs), |
+ self.config.max_attributes)) |
+ # update abstract / interface classes structures |
+ if class_is_abstract(node): |
+ self._abstracts.append(node) |
+ elif node.type == 'interface' and node.name != 'Interface': |
+ self._ifaces.append(node) |
+ for parent in node.ancestors(False): |
+ if parent.name == 'Interface': |
+ continue |
+ self._used_ifaces[parent] = 1 |
+ try: |
+ for iface in node.interfaces(): |
+ self._used_ifaces[iface] = 1 |
+ except InferenceError: |
+ # XXX log ? |
+ pass |
+ for parent in node.ancestors(): |
+ try: |
+ self._used_abstracts[parent] += 1 |
+ except KeyError: |
+ self._used_abstracts[parent] = 1 |
+ |
+ def leave_class(self, node): |
+ """check number of public methods""" |
+ nb_public_methods = 0 |
+ for method in node.methods(): |
+ if not method.name.startswith('_'): |
+ nb_public_methods += 1 |
+ # Does the class contain less than 20 public methods ? |
+ if nb_public_methods > self.config.max_public_methods: |
+ self.add_message('R0904', node=node, |
+ args=(nb_public_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 5 public methods ? |
+ if nb_public_methods < self.config.min_public_methods: |
+ self.add_message('R0903', node=node, |
+ args=(nb_public_methods, |
+ self.config.min_public_methods)) |
+ |
+ |
+ def visit_function(self, node): |
+ """check function name, docstring, arguments, redefinition, |
+ variable names, max locals |
+ """ |
+ self._inc_branch() |
+ # init branch and returns counters |
+ self._returns.append(0) |
+ self._branchs.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('R0913', 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('R0914', node=node, |
+ args=(locnum, self.config.max_locals)) |
+ # init statements counter |
+ self._stmts = 1 |
+ |
+ def leave_function(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('R0911', node=node, |
+ args=(returns, self.config.max_returns)) |
+ branchs = self._branchs.pop() |
+ if branchs > self.config.max_branchs: |
+ self.add_message('R0912', node=node, |
+ args=(branchs, self.config.max_branchs)) |
+ # check number of statements |
+ if self._stmts > self.config.max_statements: |
+ self.add_message('R0915', 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 branchs counter""" |
+ branchs = len(node.handlers) |
+ if node.orelse: |
+ branchs += 1 |
+ self._inc_branch(branchs) |
+ self._stmts += branchs |
+ |
+ def visit_tryfinally(self, _): |
+ """increments the branchs counter""" |
+ self._inc_branch(2) |
+ self._stmts += 2 |
+ |
+ def visit_if(self, node): |
+ """increments the branchs counter""" |
+ branchs = 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)): |
+ branchs += 1 |
+ self._inc_branch(branchs) |
+ self._stmts += branchs |
+ |
+ def visit_while(self, node): |
+ """increments the branchs counter""" |
+ branchs = 1 |
+ if node.orelse: |
+ branchs += 1 |
+ self._inc_branch(branchs) |
+ |
+ visit_for = visit_while |
+ |
+ def _inc_branch(self, branchsnum=1): |
+ """increments the branchs counter""" |
+ branchs = self._branchs |
+ for i in xrange(len(branchs)): |
+ branchs[i] += branchsnum |
+ |
+ # FIXME: make a nice report... |
+ |
+def register(linter): |
+ """required method to auto register this checker """ |
+ linter.register_checker(MisdesignChecker(linter)) |