|
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 from __future__ import with_statement |
|
7 |
|
8 import collections |
|
9 import ast |
|
10 |
|
11 __version__ = '0.3.1_eric6' |
|
12 |
|
13 |
|
14 class ASTVisitor(object): |
|
15 """Performs a depth-first walk of the AST.""" |
|
16 |
|
17 def __init__(self): |
|
18 self.node = None |
|
19 self._cache = {} |
|
20 |
|
21 def default(self, node, *args): |
|
22 for child in ast.iter_child_nodes(node): |
|
23 self.dispatch(child, *args) |
|
24 |
|
25 def dispatch(self, node, *args): |
|
26 self.node = node |
|
27 klass = node.__class__ |
|
28 meth = self._cache.get(klass) |
|
29 if meth is None: |
|
30 className = klass.__name__ |
|
31 meth = getattr(self.visitor, 'visit' + className, self.default) |
|
32 self._cache[klass] = meth |
|
33 return meth(node, *args) |
|
34 |
|
35 def preorder(self, tree, visitor, *args): |
|
36 """Do preorder walk of tree using visitor""" |
|
37 self.visitor = visitor |
|
38 visitor.visit = self.dispatch |
|
39 self.dispatch(tree, *args) # XXX *args make sense? |
|
40 |
|
41 |
|
42 class PathNode(object): |
|
43 def __init__(self, name, look="circle"): |
|
44 self.name = name |
|
45 self.look = look |
|
46 |
|
47 def to_dot(self): |
|
48 print('node [shape=%s,label="%s"] %d;' % ( |
|
49 self.look, self.name, self.dot_id())) |
|
50 |
|
51 def dot_id(self): |
|
52 return id(self) |
|
53 |
|
54 |
|
55 class PathGraph(object): |
|
56 def __init__(self, name, entity, lineno): |
|
57 self.name = name |
|
58 self.entity = entity |
|
59 self.lineno = lineno |
|
60 self.nodes = collections.defaultdict(list) |
|
61 |
|
62 def connect(self, n1, n2): |
|
63 self.nodes[n1].append(n2) |
|
64 # Ensure that the destination node is always counted. |
|
65 self.nodes[n2] = [] |
|
66 |
|
67 def to_dot(self): |
|
68 print('subgraph {') |
|
69 for node in self.nodes: |
|
70 node.to_dot() |
|
71 for node, nexts in self.nodes.items(): |
|
72 for next in nexts: |
|
73 print('%s -- %s;' % (node.dot_id(), next.dot_id())) |
|
74 print('}') |
|
75 |
|
76 def complexity(self): |
|
77 """ Return the McCabe complexity for the graph. |
|
78 V-E+2 |
|
79 """ |
|
80 num_edges = sum([len(n) for n in self.nodes.values()]) |
|
81 num_nodes = len(self.nodes) |
|
82 return num_edges - num_nodes + 2 |
|
83 |
|
84 |
|
85 class PathGraphingAstVisitor(ASTVisitor): |
|
86 """ A visitor for a parsed Abstract Syntax Tree which finds executable |
|
87 statements. |
|
88 """ |
|
89 |
|
90 def __init__(self): |
|
91 super(PathGraphingAstVisitor, self).__init__() |
|
92 self.classname = "" |
|
93 self.graphs = {} |
|
94 self.reset() |
|
95 |
|
96 def reset(self): |
|
97 self.graph = None |
|
98 self.tail = None |
|
99 |
|
100 def dispatch_list(self, node_list): |
|
101 for node in node_list: |
|
102 self.dispatch(node) |
|
103 |
|
104 def visitFunctionDef(self, node): |
|
105 |
|
106 if self.classname: |
|
107 entity = '%s%s' % (self.classname, node.name) |
|
108 else: |
|
109 entity = node.name |
|
110 |
|
111 name = '%d:1: %r' % (node.lineno, entity) |
|
112 |
|
113 if self.graph is not None: |
|
114 # closure |
|
115 pathnode = self.appendPathNode(name) |
|
116 self.tail = pathnode |
|
117 self.dispatch_list(node.body) |
|
118 bottom = PathNode("", look='point') |
|
119 self.graph.connect(self.tail, bottom) |
|
120 self.graph.connect(pathnode, bottom) |
|
121 self.tail = bottom |
|
122 else: |
|
123 self.graph = PathGraph(name, entity, node.lineno) |
|
124 pathnode = PathNode(name) |
|
125 self.tail = pathnode |
|
126 self.dispatch_list(node.body) |
|
127 self.graphs["%s%s" % (self.classname, node.name)] = self.graph |
|
128 self.reset() |
|
129 |
|
130 def visitClassDef(self, node): |
|
131 old_classname = self.classname |
|
132 self.classname += node.name + "." |
|
133 self.dispatch_list(node.body) |
|
134 self.classname = old_classname |
|
135 |
|
136 def appendPathNode(self, name): |
|
137 if not self.tail: |
|
138 return |
|
139 pathnode = PathNode(name) |
|
140 self.graph.connect(self.tail, pathnode) |
|
141 self.tail = pathnode |
|
142 return pathnode |
|
143 |
|
144 def visitSimpleStatement(self, node): |
|
145 if node.lineno is None: |
|
146 lineno = 0 |
|
147 else: |
|
148 lineno = node.lineno |
|
149 name = "Stmt %d" % lineno |
|
150 self.appendPathNode(name) |
|
151 |
|
152 visitAssert = visitAssign = visitAugAssign = visitDelete = visitPrint = \ |
|
153 visitRaise = visitYield = visitImport = visitCall = visitSubscript = \ |
|
154 visitPass = visitContinue = visitBreak = visitGlobal = visitReturn = \ |
|
155 visitSimpleStatement |
|
156 |
|
157 def visitLoop(self, node): |
|
158 name = "Loop %d" % node.lineno |
|
159 self._subgraph(node, name) |
|
160 |
|
161 visitFor = visitWhile = visitLoop |
|
162 |
|
163 def visitIf(self, node): |
|
164 name = "If %d" % node.lineno |
|
165 self._subgraph(node, name) |
|
166 |
|
167 def _subgraph(self, node, name, extra_blocks=()): |
|
168 """create the subgraphs representing any `if` and `for` statements""" |
|
169 if self.graph is None: |
|
170 # global loop |
|
171 self.graph = PathGraph(name, name, node.lineno) |
|
172 pathnode = PathNode(name) |
|
173 self._subgraph_parse(node, pathnode, extra_blocks) |
|
174 self.graphs["%s%s" % (self.classname, name)] = self.graph |
|
175 self.reset() |
|
176 else: |
|
177 pathnode = self.appendPathNode(name) |
|
178 self._subgraph_parse(node, pathnode, extra_blocks) |
|
179 |
|
180 def _subgraph_parse(self, node, pathnode, extra_blocks): |
|
181 """parse the body and any `else` block of `if` and `for` statements""" |
|
182 loose_ends = [] |
|
183 self.tail = pathnode |
|
184 self.dispatch_list(node.body) |
|
185 loose_ends.append(self.tail) |
|
186 for extra in extra_blocks: |
|
187 self.tail = pathnode |
|
188 self.dispatch_list(extra.body) |
|
189 loose_ends.append(self.tail) |
|
190 if node.orelse: |
|
191 self.tail = pathnode |
|
192 self.dispatch_list(node.orelse) |
|
193 loose_ends.append(self.tail) |
|
194 else: |
|
195 loose_ends.append(pathnode) |
|
196 if pathnode: |
|
197 bottom = PathNode("", look='point') |
|
198 for le in loose_ends: |
|
199 self.graph.connect(le, bottom) |
|
200 self.tail = bottom |
|
201 |
|
202 def visitTryExcept(self, node): |
|
203 name = "TryExcept %d" % node.lineno |
|
204 self._subgraph(node, name, extra_blocks=node.handlers) |
|
205 |
|
206 visitTry = visitTryExcept |
|
207 |
|
208 def visitWith(self, node): |
|
209 name = "With %d" % node.lineno |
|
210 self.appendPathNode(name) |
|
211 self.dispatch_list(node.body) |