Plugins/CheckerPlugins/CodeStyleChecker/mccabe.py

changeset 4423
14f3ca3dcef6
child 4465
2fbb5bdb8ddf
equal deleted inserted replaced
4419:66be66ec3afc 4423:14f3ca3dcef6
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.3.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):
57 self.name = name
58 self.entity = entity
59 self.lineno = lineno
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:1: %r' % (node.lineno, 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)
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 def visitClassDef(self, node):
131 old_classname = self.classname
132 self.classname += node.name + "."
133 self.dispatch_list(node.body)
134 self.classname = old_classname
135
136 def appendPathNode(self, name):
137 if not self.tail:
138 return
139 pathnode = PathNode(name)
140 self.graph.connect(self.tail, pathnode)
141 self.tail = pathnode
142 return pathnode
143
144 def visitSimpleStatement(self, node):
145 if node.lineno is None:
146 lineno = 0
147 else:
148 lineno = node.lineno
149 name = "Stmt %d" % lineno
150 self.appendPathNode(name)
151
152 visitAssert = visitAssign = visitAugAssign = visitDelete = visitPrint = \
153 visitRaise = visitYield = visitImport = visitCall = visitSubscript = \
154 visitPass = visitContinue = visitBreak = visitGlobal = visitReturn = \
155 visitSimpleStatement
156
157 def visitLoop(self, node):
158 name = "Loop %d" % node.lineno
159 self._subgraph(node, name)
160
161 visitFor = visitWhile = visitLoop
162
163 def visitIf(self, node):
164 name = "If %d" % node.lineno
165 self._subgraph(node, name)
166
167 def _subgraph(self, node, name, extra_blocks=()):
168 """create the subgraphs representing any `if` and `for` statements"""
169 if self.graph is None:
170 # global loop
171 self.graph = PathGraph(name, name, node.lineno)
172 pathnode = PathNode(name)
173 self._subgraph_parse(node, pathnode, extra_blocks)
174 self.graphs["%s%s" % (self.classname, name)] = self.graph
175 self.reset()
176 else:
177 pathnode = self.appendPathNode(name)
178 self._subgraph_parse(node, pathnode, extra_blocks)
179
180 def _subgraph_parse(self, node, pathnode, extra_blocks):
181 """parse the body and any `else` block of `if` and `for` statements"""
182 loose_ends = []
183 self.tail = pathnode
184 self.dispatch_list(node.body)
185 loose_ends.append(self.tail)
186 for extra in extra_blocks:
187 self.tail = pathnode
188 self.dispatch_list(extra.body)
189 loose_ends.append(self.tail)
190 if node.orelse:
191 self.tail = pathnode
192 self.dispatch_list(node.orelse)
193 loose_ends.append(self.tail)
194 else:
195 loose_ends.append(pathnode)
196 if pathnode:
197 bottom = PathNode("", look='point')
198 for le in loose_ends:
199 self.graph.connect(le, bottom)
200 self.tail = bottom
201
202 def visitTryExcept(self, node):
203 name = "TryExcept %d" % node.lineno
204 self._subgraph(node, name, extra_blocks=node.handlers)
205
206 visitTry = visitTryExcept
207
208 def visitWith(self, node):
209 name = "With %d" % node.lineno
210 self.appendPathNode(name)
211 self.dispatch_list(node.body)

eric ide

mercurial