--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/mccabe.py Mon Mar 22 19:22:11 2021 +0100 @@ -0,0 +1,216 @@ +""" Meager code path measurement tool. + Ned Batchelder + http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html + MIT License. +""" + +import collections +import ast + +__version__ = '0.6.1_eric6' + + +class ASTVisitor(object): + """Performs a depth-first walk of the AST.""" + + def __init__(self): + self.node = None + self._cache = {} + + def default(self, node, *args): + for child in ast.iter_child_nodes(node): + self.dispatch(child, *args) + + def dispatch(self, node, *args): + self.node = node + klass = node.__class__ + meth = self._cache.get(klass) + if meth is None: + className = klass.__name__ + meth = getattr(self.visitor, 'visit' + className, self.default) + self._cache[klass] = meth + return meth(node, *args) + + def preorder(self, tree, visitor, *args): + """Do preorder walk of tree using visitor""" + self.visitor = visitor + visitor.visit = self.dispatch + self.dispatch(tree, *args) # XXX *args make sense? + + +class PathNode(object): + def __init__(self, name, look="circle"): + self.name = name + self.look = look + + def to_dot(self): + print('node [shape=%s,label="%s"] %d;' % ( + self.look, self.name, self.dot_id())) + + def dot_id(self): + return id(self) + + +class PathGraph(object): + def __init__(self, name, entity, lineno, column=0): + self.name = name + self.entity = entity + self.lineno = lineno + self.column = column + self.nodes = collections.defaultdict(list) + + def connect(self, n1, n2): + self.nodes[n1].append(n2) + # Ensure that the destination node is always counted. + self.nodes[n2] = [] + + def to_dot(self): + print('subgraph {') + for node in self.nodes: + node.to_dot() + for node, nexts in self.nodes.items(): + for next in nexts: + print('%s -- %s;' % (node.dot_id(), next.dot_id())) + print('}') + + def complexity(self): + """ Return the McCabe complexity for the graph. + V-E+2 + """ + num_edges = sum([len(n) for n in self.nodes.values()]) + num_nodes = len(self.nodes) + return num_edges - num_nodes + 2 + + +class PathGraphingAstVisitor(ASTVisitor): + """ A visitor for a parsed Abstract Syntax Tree which finds executable + statements. + """ + + def __init__(self): + super(PathGraphingAstVisitor, self).__init__() + self.classname = "" + self.graphs = {} + self.reset() + + def reset(self): + self.graph = None + self.tail = None + + def dispatch_list(self, node_list): + for node in node_list: + self.dispatch(node) + + def visitFunctionDef(self, node): + + if self.classname: + entity = '%s%s' % (self.classname, node.name) + else: + entity = node.name + + name = '%d:%d: %r' % (node.lineno, node.col_offset, entity) + + if self.graph is not None: + # closure + pathnode = self.appendPathNode(name) + self.tail = pathnode + self.dispatch_list(node.body) + bottom = PathNode("", look='point') + self.graph.connect(self.tail, bottom) + self.graph.connect(pathnode, bottom) + self.tail = bottom + else: + self.graph = PathGraph(name, entity, node.lineno, node.col_offset) + pathnode = PathNode(name) + self.tail = pathnode + self.dispatch_list(node.body) + self.graphs["%s%s" % (self.classname, node.name)] = self.graph + self.reset() + + visitAsyncFunctionDef = visitFunctionDef + + def visitClassDef(self, node): + old_classname = self.classname + self.classname += node.name + "." + self.dispatch_list(node.body) + self.classname = old_classname + + def appendPathNode(self, name): + if not self.tail: + return + pathnode = PathNode(name) + self.graph.connect(self.tail, pathnode) + self.tail = pathnode + return pathnode + + def visitSimpleStatement(self, node): + if node.lineno is None: + lineno = 0 + else: + lineno = node.lineno + name = "Stmt %d" % lineno + self.appendPathNode(name) + + def default(self, node, *args): + if isinstance(node, ast.stmt): + self.visitSimpleStatement(node) + else: + super(PathGraphingAstVisitor, self).default(node, *args) + + def visitLoop(self, node): + name = "Loop %d" % node.lineno + self._subgraph(node, name) + + visitAsyncFor = visitFor = visitWhile = visitLoop + + def visitIf(self, node): + name = "If %d" % node.lineno + self._subgraph(node, name) + + def _subgraph(self, node, name, extra_blocks=()): + """create the subgraphs representing any `if` and `for` statements""" + if self.graph is None: + # global loop + self.graph = PathGraph(name, name, node.lineno, node.col_offset) + pathnode = PathNode(name) + self._subgraph_parse(node, pathnode, extra_blocks) + self.graphs["%s%s" % (self.classname, name)] = self.graph + self.reset() + else: + pathnode = self.appendPathNode(name) + self._subgraph_parse(node, pathnode, extra_blocks) + + def _subgraph_parse(self, node, pathnode, extra_blocks): + """parse the body and any `else` block of `if` and `for` statements""" + loose_ends = [] + self.tail = pathnode + self.dispatch_list(node.body) + loose_ends.append(self.tail) + for extra in extra_blocks: + self.tail = pathnode + self.dispatch_list(extra.body) + loose_ends.append(self.tail) + if node.orelse: + self.tail = pathnode + self.dispatch_list(node.orelse) + loose_ends.append(self.tail) + else: + loose_ends.append(pathnode) + if pathnode: + bottom = PathNode("", look='point') + for le in loose_ends: + self.graph.connect(le, bottom) + self.tail = bottom + + def visitTryExcept(self, node): + name = "TryExcept %d" % node.lineno + self._subgraph(node, name, extra_blocks=node.handlers) + + visitTry = visitTryExcept + + def visitWith(self, node): + name = "With %d" % node.lineno + self.appendPathNode(name) + self.dispatch_list(node.body) + + visitAsyncWith = visitWith