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