|
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.6.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, column=0): |
|
57 self.name = name |
|
58 self.entity = entity |
|
59 self.lineno = lineno |
|
60 self.column = column |
|
61 self.nodes = collections.defaultdict(list) |
|
62 |
|
63 def connect(self, n1, n2): |
|
64 self.nodes[n1].append(n2) |
|
65 # Ensure that the destination node is always counted. |
|
66 self.nodes[n2] = [] |
|
67 |
|
68 def to_dot(self): |
|
69 print('subgraph {') |
|
70 for node in self.nodes: |
|
71 node.to_dot() |
|
72 for node, nexts in self.nodes.items(): |
|
73 for next in nexts: |
|
74 print('%s -- %s;' % (node.dot_id(), next.dot_id())) |
|
75 print('}') |
|
76 |
|
77 def complexity(self): |
|
78 """ Return the McCabe complexity for the graph. |
|
79 V-E+2 |
|
80 """ |
|
81 num_edges = sum([len(n) for n in self.nodes.values()]) |
|
82 num_nodes = len(self.nodes) |
|
83 return num_edges - num_nodes + 2 |
|
84 |
|
85 |
|
86 class PathGraphingAstVisitor(ASTVisitor): |
|
87 """ A visitor for a parsed Abstract Syntax Tree which finds executable |
|
88 statements. |
|
89 """ |
|
90 |
|
91 def __init__(self): |
|
92 super(PathGraphingAstVisitor, self).__init__() |
|
93 self.classname = "" |
|
94 self.graphs = {} |
|
95 self.reset() |
|
96 |
|
97 def reset(self): |
|
98 self.graph = None |
|
99 self.tail = None |
|
100 |
|
101 def dispatch_list(self, node_list): |
|
102 for node in node_list: |
|
103 self.dispatch(node) |
|
104 |
|
105 def visitFunctionDef(self, node): |
|
106 |
|
107 if self.classname: |
|
108 entity = '%s%s' % (self.classname, node.name) |
|
109 else: |
|
110 entity = node.name |
|
111 |
|
112 name = '%d:%d: %r' % (node.lineno, node.col_offset, entity) |
|
113 |
|
114 if self.graph is not None: |
|
115 # closure |
|
116 pathnode = self.appendPathNode(name) |
|
117 self.tail = pathnode |
|
118 self.dispatch_list(node.body) |
|
119 bottom = PathNode("", look='point') |
|
120 self.graph.connect(self.tail, bottom) |
|
121 self.graph.connect(pathnode, bottom) |
|
122 self.tail = bottom |
|
123 else: |
|
124 self.graph = PathGraph(name, entity, node.lineno, node.col_offset) |
|
125 pathnode = PathNode(name) |
|
126 self.tail = pathnode |
|
127 self.dispatch_list(node.body) |
|
128 self.graphs["%s%s" % (self.classname, node.name)] = self.graph |
|
129 self.reset() |
|
130 |
|
131 visitAsyncFunctionDef = visitFunctionDef |
|
132 |
|
133 def visitClassDef(self, node): |
|
134 old_classname = self.classname |
|
135 self.classname += node.name + "." |
|
136 self.dispatch_list(node.body) |
|
137 self.classname = old_classname |
|
138 |
|
139 def appendPathNode(self, name): |
|
140 if not self.tail: |
|
141 return |
|
142 pathnode = PathNode(name) |
|
143 self.graph.connect(self.tail, pathnode) |
|
144 self.tail = pathnode |
|
145 return pathnode |
|
146 |
|
147 def visitSimpleStatement(self, node): |
|
148 if node.lineno is None: |
|
149 lineno = 0 |
|
150 else: |
|
151 lineno = node.lineno |
|
152 name = "Stmt %d" % lineno |
|
153 self.appendPathNode(name) |
|
154 |
|
155 def default(self, node, *args): |
|
156 if isinstance(node, ast.stmt): |
|
157 self.visitSimpleStatement(node) |
|
158 else: |
|
159 super(PathGraphingAstVisitor, self).default(node, *args) |
|
160 |
|
161 def visitLoop(self, node): |
|
162 name = "Loop %d" % node.lineno |
|
163 self._subgraph(node, name) |
|
164 |
|
165 visitAsyncFor = visitFor = visitWhile = visitLoop |
|
166 |
|
167 def visitIf(self, node): |
|
168 name = "If %d" % node.lineno |
|
169 self._subgraph(node, name) |
|
170 |
|
171 def _subgraph(self, node, name, extra_blocks=()): |
|
172 """create the subgraphs representing any `if` and `for` statements""" |
|
173 if self.graph is None: |
|
174 # global loop |
|
175 self.graph = PathGraph(name, name, node.lineno, node.col_offset) |
|
176 pathnode = PathNode(name) |
|
177 self._subgraph_parse(node, pathnode, extra_blocks) |
|
178 self.graphs["%s%s" % (self.classname, name)] = self.graph |
|
179 self.reset() |
|
180 else: |
|
181 pathnode = self.appendPathNode(name) |
|
182 self._subgraph_parse(node, pathnode, extra_blocks) |
|
183 |
|
184 def _subgraph_parse(self, node, pathnode, extra_blocks): |
|
185 """parse the body and any `else` block of `if` and `for` statements""" |
|
186 loose_ends = [] |
|
187 self.tail = pathnode |
|
188 self.dispatch_list(node.body) |
|
189 loose_ends.append(self.tail) |
|
190 for extra in extra_blocks: |
|
191 self.tail = pathnode |
|
192 self.dispatch_list(extra.body) |
|
193 loose_ends.append(self.tail) |
|
194 if node.orelse: |
|
195 self.tail = pathnode |
|
196 self.dispatch_list(node.orelse) |
|
197 loose_ends.append(self.tail) |
|
198 else: |
|
199 loose_ends.append(pathnode) |
|
200 if pathnode: |
|
201 bottom = PathNode("", look='point') |
|
202 for le in loose_ends: |
|
203 self.graph.connect(le, bottom) |
|
204 self.tail = bottom |
|
205 |
|
206 def visitTryExcept(self, node): |
|
207 name = "TryExcept %d" % node.lineno |
|
208 self._subgraph(node, name, extra_blocks=node.handlers) |
|
209 |
|
210 visitTry = visitTryExcept |
|
211 |
|
212 def visitWith(self, node): |
|
213 name = "With %d" % node.lineno |
|
214 self.appendPathNode(name) |
|
215 self.dispatch_list(node.body) |
|
216 |
|
217 visitAsyncWith = visitWith |
|
218 |
|
219 # |
|
220 # eflag: noqa = M702 |