design_analysis.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. # Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE).
  2. # http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3. #
  4. # This program is free software; you can redistribute it and/or modify it under
  5. # the terms of the GNU General Public License as published by the Free Software
  6. # Foundation; either version 2 of the License, or (at your option) any later
  7. # version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License along with
  14. # this program; if not, write to the Free Software Foundation, Inc.,
  15. # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  16. """check for signs of poor design
  17. see http://intranet.logilab.fr/jpl/view?rql=Any%20X%20where%20X%20eid%201243
  18. FIXME: missing 13, 15, 16
  19. """
  20. from logilab.astng import Function, If, InferenceError
  21. from pylint.interfaces import IASTNGChecker
  22. from pylint.checkers import BaseChecker
  23. import re
  24. # regexp for ignored argument name
  25. IGNORED_ARGUMENT_NAMES = re.compile('_.*')
  26. def class_is_abstract(klass):
  27. """return true if the given class node should be considered as an abstract
  28. class
  29. """
  30. for attr in klass.values():
  31. if isinstance(attr, Function):
  32. if attr.is_abstract(pass_is_abstract=False):
  33. return True
  34. return False
  35. MSGS = {
  36. 'R0901': ('Too many ancestors (%s/%s)',
  37. 'Used when class has too many parent classes, try to reduce \
  38. this to get a more simple (and so easier to use) class.'),
  39. 'R0902': ('Too many instance attributes (%s/%s)',
  40. 'Used when class has too many instance attributes, try to reduce \
  41. this to get a more simple (and so easier to use) class.'),
  42. 'R0903': ('Too few public methods (%s/%s)',
  43. 'Used when class has too few public methods, so be sure it\'s \
  44. really worth it.'),
  45. 'R0904': ('Too many public methods (%s/%s)',
  46. 'Used when class has too many public methods, try to reduce \
  47. this to get a more simple (and so easier to use) class.'),
  48. 'R0911': ('Too many return statements (%s/%s)',
  49. 'Used when a function or method has too many return statement, \
  50. making it hard to follow.'),
  51. 'R0912': ('Too many branches (%s/%s)',
  52. 'Used when a function or method has too many branches, \
  53. making it hard to follow.'),
  54. 'R0913': ('Too many arguments (%s/%s)',
  55. 'Used when a function or method takes too many arguments.'),
  56. 'R0914': ('Too many local variables (%s/%s)',
  57. 'Used when a function or method has too many local variables.'),
  58. 'R0915': ('Too many statements (%s/%s)',
  59. 'Used when a function or method has too many statements. You \
  60. should then split it in smaller functions / methods.'),
  61. 'R0921': ('Abstract class not referenced',
  62. 'Used when an abstract class is not used as ancestor anywhere.'),
  63. 'R0922': ('Abstract class is only referenced %s times',
  64. 'Used when an abstract class is used less than X times as \
  65. ancestor.'),
  66. 'R0923': ('Interface not implemented',
  67. 'Used when an interface class is not implemented anywhere.'),
  68. }
  69. class MisdesignChecker(BaseChecker):
  70. """checks for sign of poor/misdesign:
  71. * number of methods, attributes, local variables...
  72. * size, complexity of functions, methods
  73. """
  74. __implements__ = (IASTNGChecker,)
  75. # configuration section name
  76. name = 'design'
  77. # messages
  78. msgs = MSGS
  79. priority = -2
  80. # configuration options
  81. options = (('max-args',
  82. {'default' : 5, 'type' : 'int', 'metavar' : '<int>',
  83. 'help': 'Maximum number of arguments for function / method'}
  84. ),
  85. ('ignored-argument-names',
  86. {'default' : IGNORED_ARGUMENT_NAMES,
  87. 'type' :'regexp', 'metavar' : '<regexp>',
  88. 'help' : 'Argument names that match this expression will be '
  89. 'ignored. Default to name with leading underscore'}
  90. ),
  91. ('max-locals',
  92. {'default' : 15, 'type' : 'int', 'metavar' : '<int>',
  93. 'help': 'Maximum number of locals for function / method body'}
  94. ),
  95. ('max-returns',
  96. {'default' : 6, 'type' : 'int', 'metavar' : '<int>',
  97. 'help': 'Maximum number of return / yield for function / '
  98. 'method body'}
  99. ),
  100. ('max-branchs',
  101. {'default' : 12, 'type' : 'int', 'metavar' : '<int>',
  102. 'help': 'Maximum number of branch for function / method body'}
  103. ),
  104. ('max-statements',
  105. {'default' : 50, 'type' : 'int', 'metavar' : '<int>',
  106. 'help': 'Maximum number of statements in function / method '
  107. 'body'}
  108. ),
  109. ('max-parents',
  110. {'default' : 7,
  111. 'type' : 'int',
  112. 'metavar' : '<num>',
  113. 'help' : 'Maximum number of parents for a class (see R0901).'}
  114. ),
  115. ('max-attributes',
  116. {'default' : 7,
  117. 'type' : 'int',
  118. 'metavar' : '<num>',
  119. 'help' : 'Maximum number of attributes for a class \
  120. (see R0902).'}
  121. ),
  122. ('min-public-methods',
  123. {'default' : 2,
  124. 'type' : 'int',
  125. 'metavar' : '<num>',
  126. 'help' : 'Minimum number of public methods for a class \
  127. (see R0903).'}
  128. ),
  129. ('max-public-methods',
  130. {'default' : 20,
  131. 'type' : 'int',
  132. 'metavar' : '<num>',
  133. 'help' : 'Maximum number of public methods for a class \
  134. (see R0904).'}
  135. ),
  136. )
  137. def __init__(self, linter=None):
  138. BaseChecker.__init__(self, linter)
  139. self.stats = None
  140. self._returns = None
  141. self._branchs = None
  142. self._used_abstracts = None
  143. self._used_ifaces = None
  144. self._abstracts = None
  145. self._ifaces = None
  146. self._stmts = 0
  147. def open(self):
  148. """initialize visit variables"""
  149. self.stats = self.linter.add_stats()
  150. self._returns = []
  151. self._branchs = []
  152. self._used_abstracts = {}
  153. self._used_ifaces = {}
  154. self._abstracts = []
  155. self._ifaces = []
  156. def close(self):
  157. """check that abstract/interface classes are used"""
  158. for abstract in self._abstracts:
  159. if not abstract in self._used_abstracts:
  160. self.add_message('R0921', node=abstract)
  161. elif self._used_abstracts[abstract] < 2:
  162. self.add_message('R0922', node=abstract,
  163. args=self._used_abstracts[abstract])
  164. for iface in self._ifaces:
  165. if not iface in self._used_ifaces:
  166. self.add_message('R0923', node=iface)
  167. def visit_class(self, node):
  168. """check size of inheritance hierarchy and number of instance attributes
  169. """
  170. self._inc_branch()
  171. # Is the total inheritance hierarchy is 7 or less?
  172. nb_parents = len(list(node.ancestors()))
  173. if nb_parents > self.config.max_parents:
  174. self.add_message('R0901', node=node,
  175. args=(nb_parents, self.config.max_parents))
  176. # Does the class contain less than 20 attributes for
  177. # non-GUI classes (40 for GUI)?
  178. # FIXME detect gui classes
  179. if len(node.instance_attrs) > self.config.max_attributes:
  180. self.add_message('R0902', node=node,
  181. args=(len(node.instance_attrs),
  182. self.config.max_attributes))
  183. # update abstract / interface classes structures
  184. if class_is_abstract(node):
  185. self._abstracts.append(node)
  186. elif node.type == 'interface' and node.name != 'Interface':
  187. self._ifaces.append(node)
  188. for parent in node.ancestors(False):
  189. if parent.name == 'Interface':
  190. continue
  191. self._used_ifaces[parent] = 1
  192. try:
  193. for iface in node.interfaces():
  194. self._used_ifaces[iface] = 1
  195. except InferenceError:
  196. # XXX log ?
  197. pass
  198. for parent in node.ancestors():
  199. try:
  200. self._used_abstracts[parent] += 1
  201. except KeyError:
  202. self._used_abstracts[parent] = 1
  203. def leave_class(self, node):
  204. """check number of public methods"""
  205. nb_public_methods = 0
  206. for method in node.methods():
  207. if not method.name.startswith('_'):
  208. nb_public_methods += 1
  209. # Does the class contain less than 20 public methods ?
  210. if nb_public_methods > self.config.max_public_methods:
  211. self.add_message('R0904', node=node,
  212. args=(nb_public_methods,
  213. self.config.max_public_methods))
  214. # stop here for exception, metaclass and interface classes
  215. if node.type != 'class':
  216. return
  217. # Does the class contain more than 5 public methods ?
  218. if nb_public_methods < self.config.min_public_methods:
  219. self.add_message('R0903', node=node,
  220. args=(nb_public_methods,
  221. self.config.min_public_methods))
  222. def visit_function(self, node):
  223. """check function name, docstring, arguments, redefinition,
  224. variable names, max locals
  225. """
  226. self._inc_branch()
  227. # init branch and returns counters
  228. self._returns.append(0)
  229. self._branchs.append(0)
  230. # check number of arguments
  231. args = node.args.args
  232. if args is not None:
  233. ignored_args_num = len(
  234. [arg for arg in args
  235. if self.config.ignored_argument_names.match(arg.name)])
  236. argnum = len(args) - ignored_args_num
  237. if argnum > self.config.max_args:
  238. self.add_message('R0913', node=node,
  239. args=(len(args), self.config.max_args))
  240. else:
  241. ignored_args_num = 0
  242. # check number of local variables
  243. locnum = len(node.locals) - ignored_args_num
  244. if locnum > self.config.max_locals:
  245. self.add_message('R0914', node=node,
  246. args=(locnum, self.config.max_locals))
  247. # init statements counter
  248. self._stmts = 1
  249. def leave_function(self, node):
  250. """most of the work is done here on close:
  251. checks for max returns, branch, return in __init__
  252. """
  253. returns = self._returns.pop()
  254. if returns > self.config.max_returns:
  255. self.add_message('R0911', node=node,
  256. args=(returns, self.config.max_returns))
  257. branchs = self._branchs.pop()
  258. if branchs > self.config.max_branchs:
  259. self.add_message('R0912', node=node,
  260. args=(branchs, self.config.max_branchs))
  261. # check number of statements
  262. if self._stmts > self.config.max_statements:
  263. self.add_message('R0915', node=node,
  264. args=(self._stmts, self.config.max_statements))
  265. def visit_return(self, _):
  266. """count number of returns"""
  267. if not self._returns:
  268. return # return outside function, reported by the base checker
  269. self._returns[-1] += 1
  270. def visit_default(self, node):
  271. """default visit method -> increments the statements counter if
  272. necessary
  273. """
  274. if node.is_statement:
  275. self._stmts += 1
  276. def visit_tryexcept(self, node):
  277. """increments the branchs counter"""
  278. branchs = len(node.handlers)
  279. if node.orelse:
  280. branchs += 1
  281. self._inc_branch(branchs)
  282. self._stmts += branchs
  283. def visit_tryfinally(self, _):
  284. """increments the branchs counter"""
  285. self._inc_branch(2)
  286. self._stmts += 2
  287. def visit_if(self, node):
  288. """increments the branchs counter"""
  289. branchs = 1
  290. # don't double count If nodes coming from some 'elif'
  291. if node.orelse and (len(node.orelse)>1 or
  292. not isinstance(node.orelse[0], If)):
  293. branchs += 1
  294. self._inc_branch(branchs)
  295. self._stmts += branchs
  296. def visit_while(self, node):
  297. """increments the branchs counter"""
  298. branchs = 1
  299. if node.orelse:
  300. branchs += 1
  301. self._inc_branch(branchs)
  302. visit_for = visit_while
  303. def _inc_branch(self, branchsnum=1):
  304. """increments the branchs counter"""
  305. branchs = self._branchs
  306. for i in xrange(len(branchs)):
  307. branchs[i] += branchsnum
  308. # FIXME: make a nice report...
  309. def register(linter):
  310. """required method to auto register this checker """
  311. linter.register_checker(MisdesignChecker(linter))