eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/mccabe.py

changeset 8168
bdb0258faf42
parent 7639
422fd05e9c91
child 8218
7c09585bd960
equal deleted inserted replaced
8167:cdc1b6692766 8168:bdb0258faf42
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

eric ide

mercurial