src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/mccabe.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8990
ca8e477c590c
child 9653
e67609152c5e
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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

eric ide

mercurial