|
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 |
|
7 import collections |
|
8 import ast |
|
9 |
|
10 __version__ = '0.6.1_eric6' |
|
11 |
|
12 |
|
13 class ASTVisitor(object): |
|
14 """Performs a depth-first walk of the AST.""" |
|
15 |
|
16 def __init__(self): |
|
17 self.node = None |
|
18 self._cache = {} |
|
19 |
|
20 def default(self, node, *args): |
|
21 for child in ast.iter_child_nodes(node): |
|
22 self.dispatch(child, *args) |
|
23 |
|
24 def dispatch(self, node, *args): |
|
25 self.node = node |
|
26 klass = node.__class__ |
|
27 meth = self._cache.get(klass) |
|
28 if meth is None: |
|
29 className = klass.__name__ |
|
30 meth = getattr(self.visitor, 'visit' + className, self.default) |
|
31 self._cache[klass] = meth |
|
32 return meth(node, *args) |
|
33 |
|
34 def preorder(self, tree, visitor, *args): |
|
35 """Do preorder walk of tree using visitor""" |
|
36 self.visitor = visitor |
|
37 visitor.visit = self.dispatch |
|
38 self.dispatch(tree, *args) # XXX *args make sense? |
|
39 |
|
40 |
|
41 class PathNode(object): |
|
42 def __init__(self, name, look="circle"): |
|
43 self.name = name |
|
44 self.look = look |
|
45 |
|
46 def to_dot(self): |
|
47 print('node [shape=%s,label="%s"] %d;' % ( |
|
48 self.look, self.name, self.dot_id())) |
|
49 |
|
50 def dot_id(self): |
|
51 return id(self) |
|
52 |
|
53 |
|
54 class PathGraph(object): |
|
55 def __init__(self, name, entity, lineno, column=0): |
|
56 self.name = name |
|
57 self.entity = entity |
|
58 self.lineno = lineno |
|
59 self.column = column |
|
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:%d: %r' % (node.lineno, node.col_offset, 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, node.col_offset) |
|
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 visitAsyncFunctionDef = visitFunctionDef |
|
131 |
|
132 def visitClassDef(self, node): |
|
133 old_classname = self.classname |
|
134 self.classname += node.name + "." |
|
135 self.dispatch_list(node.body) |
|
136 self.classname = old_classname |
|
137 |
|
138 def appendPathNode(self, name): |
|
139 if not self.tail: |
|
140 return |
|
141 pathnode = PathNode(name) |
|
142 self.graph.connect(self.tail, pathnode) |
|
143 self.tail = pathnode |
|
144 return pathnode |
|
145 |
|
146 def visitSimpleStatement(self, node): |
|
147 if node.lineno is None: |
|
148 lineno = 0 |
|
149 else: |
|
150 lineno = node.lineno |
|
151 name = "Stmt %d" % lineno |
|
152 self.appendPathNode(name) |
|
153 |
|
154 def default(self, node, *args): |
|
155 if isinstance(node, ast.stmt): |
|
156 self.visitSimpleStatement(node) |
|
157 else: |
|
158 super(PathGraphingAstVisitor, self).default(node, *args) |
|
159 |
|
160 def visitLoop(self, node): |
|
161 name = "Loop %d" % node.lineno |
|
162 self._subgraph(node, name) |
|
163 |
|
164 visitAsyncFor = visitFor = visitWhile = visitLoop |
|
165 |
|
166 def visitIf(self, node): |
|
167 name = "If %d" % node.lineno |
|
168 self._subgraph(node, name) |
|
169 |
|
170 def _subgraph(self, node, name, extra_blocks=()): |
|
171 """create the subgraphs representing any `if` and `for` statements""" |
|
172 if self.graph is None: |
|
173 # global loop |
|
174 self.graph = PathGraph(name, name, node.lineno, node.col_offset) |
|
175 pathnode = PathNode(name) |
|
176 self._subgraph_parse(node, pathnode, extra_blocks) |
|
177 self.graphs["%s%s" % (self.classname, name)] = self.graph |
|
178 self.reset() |
|
179 else: |
|
180 pathnode = self.appendPathNode(name) |
|
181 self._subgraph_parse(node, pathnode, extra_blocks) |
|
182 |
|
183 def _subgraph_parse(self, node, pathnode, extra_blocks): |
|
184 """parse the body and any `else` block of `if` and `for` statements""" |
|
185 loose_ends = [] |
|
186 self.tail = pathnode |
|
187 self.dispatch_list(node.body) |
|
188 loose_ends.append(self.tail) |
|
189 for extra in extra_blocks: |
|
190 self.tail = pathnode |
|
191 self.dispatch_list(extra.body) |
|
192 loose_ends.append(self.tail) |
|
193 if node.orelse: |
|
194 self.tail = pathnode |
|
195 self.dispatch_list(node.orelse) |
|
196 loose_ends.append(self.tail) |
|
197 else: |
|
198 loose_ends.append(pathnode) |
|
199 if pathnode: |
|
200 bottom = PathNode("", look='point') |
|
201 for le in loose_ends: |
|
202 self.graph.connect(le, bottom) |
|
203 self.tail = bottom |
|
204 |
|
205 def visitTryExcept(self, node): |
|
206 name = "TryExcept %d" % node.lineno |
|
207 self._subgraph(node, name, extra_blocks=node.handlers) |
|
208 |
|
209 visitTry = visitTryExcept |
|
210 |
|
211 def visitWith(self, node): |
|
212 name = "With %d" % node.lineno |
|
213 self.appendPathNode(name) |
|
214 self.dispatch_list(node.body) |
|
215 |
|
216 visitAsyncWith = visitWith |