mccabe.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. """ Meager code path measurement tool.
  2. Ned Batchelder
  3. http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
  4. MIT License.
  5. """
  6. try:
  7. from compiler import parse # NOQA
  8. iter_child_nodes = None # NOQA
  9. except ImportError:
  10. from ast import parse, iter_child_nodes # NOQA
  11. import optparse
  12. import sys
  13. from collections import defaultdict
  14. WARNING_CODE = "W901"
  15. class ASTVisitor:
  16. VERBOSE = 0
  17. def __init__(self):
  18. self.node = None
  19. self._cache = {}
  20. def default(self, node, *args):
  21. if hasattr(node, 'getChildNodes'):
  22. children = node.getChildNodes()
  23. else:
  24. children = iter_child_nodes(node)
  25. for child in children:
  26. self.dispatch(child, *args)
  27. def dispatch(self, node, *args):
  28. self.node = node
  29. klass = node.__class__
  30. meth = self._cache.get(klass)
  31. if meth is None:
  32. className = klass.__name__
  33. meth = getattr(self.visitor, 'visit' + className, self.default)
  34. self._cache[klass] = meth
  35. return meth(node, *args)
  36. def preorder(self, tree, visitor, *args):
  37. """Do preorder walk of tree using visitor"""
  38. self.visitor = visitor
  39. visitor.visit = self.dispatch
  40. self.dispatch(tree, *args) # XXX *args make sense?
  41. class PathNode:
  42. def __init__(self, name, look="circle"):
  43. self.name = name
  44. self.look = look
  45. def to_dot(self):
  46. print('node [shape=%s,label="%s"] %d;' % \
  47. (self.look, self.name, self.dot_id()))
  48. def dot_id(self):
  49. return id(self)
  50. class PathGraph:
  51. def __init__(self, name, entity, lineno):
  52. self.name = name
  53. self.entity = entity
  54. self.lineno = lineno
  55. self.nodes = defaultdict(list)
  56. def connect(self, n1, n2):
  57. self.nodes[n1].append(n2)
  58. def to_dot(self):
  59. print('subgraph {')
  60. for node in self.nodes:
  61. node.to_dot()
  62. for node, nexts in self.nodes.items():
  63. for next in nexts:
  64. print('%s -- %s;' % (node.dot_id(), next.dot_id()))
  65. print('}')
  66. def complexity(self):
  67. """ Return the McCabe complexity for the graph.
  68. V-E+2
  69. """
  70. num_edges = sum([len(n) for n in self.nodes.values()])
  71. num_nodes = len(self.nodes)
  72. return num_edges - num_nodes + 2
  73. class PathGraphingAstVisitor(ASTVisitor):
  74. """ A visitor for a parsed Abstract Syntax Tree which finds executable
  75. statements.
  76. """
  77. def __init__(self):
  78. ASTVisitor.__init__(self)
  79. self.classname = ""
  80. self.graphs = {}
  81. self.reset()
  82. def reset(self):
  83. self.graph = None
  84. self.tail = None
  85. def visitFunction(self, node):
  86. if self.classname:
  87. entity = '%s%s' % (self.classname, node.name)
  88. else:
  89. entity = node.name
  90. name = '%d:1: %r' % (node.lineno, entity)
  91. if self.graph is not None:
  92. # closure
  93. pathnode = self.appendPathNode(name)
  94. self.tail = pathnode
  95. self.default(node)
  96. bottom = PathNode("", look='point')
  97. self.graph.connect(self.tail, bottom)
  98. self.graph.connect(pathnode, bottom)
  99. self.tail = bottom
  100. else:
  101. self.graph = PathGraph(name, entity, node.lineno)
  102. pathnode = PathNode(name)
  103. self.tail = pathnode
  104. self.default(node)
  105. self.graphs["%s%s" % (self.classname, node.name)] = self.graph
  106. self.reset()
  107. visitFunctionDef = visitFunction
  108. def visitClass(self, node):
  109. old_classname = self.classname
  110. self.classname += node.name + "."
  111. self.default(node)
  112. self.classname = old_classname
  113. def appendPathNode(self, name):
  114. if not self.tail:
  115. return
  116. pathnode = PathNode(name)
  117. self.graph.connect(self.tail, pathnode)
  118. self.tail = pathnode
  119. return pathnode
  120. def visitSimpleStatement(self, node):
  121. if node.lineno is None:
  122. lineno = 0
  123. else:
  124. lineno = node.lineno
  125. name = "Stmt %d" % lineno
  126. self.appendPathNode(name)
  127. visitAssert = visitAssign = visitAssTuple = visitPrint = \
  128. visitPrintnl = visitRaise = visitSubscript = visitDecorators = \
  129. visitPass = visitDiscard = visitGlobal = visitReturn = \
  130. visitSimpleStatement
  131. def visitLoop(self, node):
  132. name = "Loop %d" % node.lineno
  133. if self.graph is None:
  134. # global loop
  135. self.graph = PathGraph(name, name, node.lineno)
  136. pathnode = PathNode(name)
  137. self.tail = pathnode
  138. self.default(node)
  139. self.graphs["%s%s" % (self.classname, name)] = self.graph
  140. self.reset()
  141. else:
  142. pathnode = self.appendPathNode(name)
  143. self.tail = pathnode
  144. self.default(node.body)
  145. bottom = PathNode("", look='point')
  146. self.graph.connect(self.tail, bottom)
  147. self.graph.connect(pathnode, bottom)
  148. self.tail = bottom
  149. # TODO: else clause in node.else_
  150. visitFor = visitWhile = visitLoop
  151. def visitIf(self, node):
  152. name = "If %d" % node.lineno
  153. pathnode = self.appendPathNode(name)
  154. if not pathnode:
  155. return # TODO: figure out what to do with if's outside def's.
  156. loose_ends = []
  157. for t, n in node.tests:
  158. self.tail = pathnode
  159. self.default(n)
  160. loose_ends.append(self.tail)
  161. if node.else_:
  162. self.tail = pathnode
  163. self.default(node.else_)
  164. loose_ends.append(self.tail)
  165. else:
  166. loose_ends.append(pathnode)
  167. bottom = PathNode("", look='point')
  168. for le in loose_ends:
  169. self.graph.connect(le, bottom)
  170. self.tail = bottom
  171. # TODO: visitTryExcept
  172. # TODO: visitTryFinally
  173. # TODO: visitWith
  174. # XXX todo: determine which ones can add to the complexity
  175. # py2
  176. # TODO: visitStmt
  177. # TODO: visitAssName
  178. # TODO: visitCallFunc
  179. # TODO: visitConst
  180. # py3
  181. # TODO: visitStore
  182. # TODO: visitCall
  183. # TODO: visitLoad
  184. # TODO: visitNum
  185. # TODO: visitarguments
  186. # TODO: visitExpr
  187. def get_code_complexity(code, min=7, filename='stdin'):
  188. complex = []
  189. try:
  190. ast = parse(code)
  191. except AttributeError:
  192. e = sys.exc_info()[1]
  193. sys.stderr.write("Unable to parse %s: %s\n" % (filename, e))
  194. return 0
  195. visitor = PathGraphingAstVisitor()
  196. visitor.preorder(ast, visitor)
  197. for graph in visitor.graphs.values():
  198. if graph is None:
  199. # ?
  200. continue
  201. if graph.complexity() >= min:
  202. complex.append(dict(
  203. type = 'W',
  204. lnum = graph.lineno,
  205. text = '%s %r is too complex (%d)' % (
  206. WARNING_CODE,
  207. graph.entity,
  208. graph.complexity(),
  209. )
  210. ))
  211. return complex
  212. def get_module_complexity(module_path, min=7):
  213. """Returns the complexity of a module"""
  214. code = open(module_path, "rU").read() + '\n\n'
  215. return get_code_complexity(code, min, filename=module_path)
  216. def main(argv):
  217. opar = optparse.OptionParser()
  218. opar.add_option("-d", "--dot", dest="dot",
  219. help="output a graphviz dot file", action="store_true")
  220. opar.add_option("-m", "--min", dest="min",
  221. help="minimum complexity for output", type="int",
  222. default=2)
  223. options, args = opar.parse_args(argv)
  224. text = open(args[0], "rU").read() + '\n\n'
  225. ast = parse(text)
  226. visitor = PathGraphingAstVisitor()
  227. visitor.preorder(ast, visitor)
  228. if options.dot:
  229. print('graph {')
  230. for graph in visitor.graphs.values():
  231. if graph.complexity() >= options.min:
  232. graph.to_dot()
  233. print('}')
  234. else:
  235. for graph in visitor.graphs.values():
  236. if graph.complexity() >= options.min:
  237. print(graph.name, graph.complexity())
  238. if __name__ == '__main__':
  239. main(sys.argv[1:])