From 263533c9fd781faf31f4316e6b4647d3e6ea7e55 Mon Sep 17 00:00:00 2001 From: Tarek Ziade Date: Sun, 13 Feb 2011 11:43:19 +0100 Subject: [PATCH] added the mccabe metric --- flake8/__init__.py | 109 ++++++++++++++---------- flake8/mccabe.py | 206 +++++++++++++++++++++++++++++++++++++++++++++ flake8/pep8.py | 1 + 3 files changed, 270 insertions(+), 46 deletions(-) create mode 100644 flake8/mccabe.py diff --git a/flake8/__init__.py b/flake8/__init__.py index 4ce9a3b..e9407cb 100644 --- a/flake8/__init__.py +++ b/flake8/__init__.py @@ -2,11 +2,13 @@ """ Implementation of the command-line I{flake8} tool. """ -import re import sys import os import _ast import pep8 +import mccabe +import re + checker = __import__('flake8.checker').checker @@ -69,26 +71,10 @@ def check(codeString, filename): def _noqa(warning): # XXX quick dirty hack, just need to keep the line in the warning - line = open(warning.filename).readlines()[warning.lineno - 1] + line = open(warning.filename).readlines()[warning.lineno-1] return line.strip().lower().endswith('# noqa') -_NOQA = re.compile(r'^# flake8: noqa', re.I | re.M) - - -def skip_file(path): - """Returns True if this header is found in path - - # -*- flake8: noqa -*- - """ - f = open(path) - try: - content = f.read() - finally: - f.close() - return _NOQA.match(content) is not None - - def checkPath(filename): """ Check the given path, printing out any warnings detected. @@ -102,52 +88,80 @@ def checkPath(filename): return 1 +def check_file(path, complexity=10): + warnings = checkPath(path) + warnings += pep8.input_file(path) + warnings += mccabe.get_module_complexity(path, complexity) + return warnings + + +def check_code(code, complexity=10): + warnings = check(code, '') + warnings += mccabe.get_code_complexity(code, complexity) + return warnings + + +_NOQA = re.compile(r'^# flake8: noqa', re.I | re.M) + + +def skip_file(path): + """Returns True if this header is found in path + + # flake8: noqa + """ + f = open(path) + try: + content = f.read() + finally: + f.close() + return _NOQA.match(content) is not None + + +def _get_python_files(paths): + for path in paths: + if os.path.isdir(path): + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + if not filename.endswith('.py'): + continue + fullpath = os.path.join(dirpath, filename) + if not skip_file(fullpath): + yield fullpath + + else: + if not skip_file(path): + yield path + + def main(): pep8.process_options() - warnings = 0 args = sys.argv[1:] if args: - for arg in args: - if os.path.isdir(arg): - for dirpath, dirnames, filenames in os.walk(arg): - for filename in filenames: - if not filename.endswith('.py'): - continue - fullpath = os.path.join(dirpath, filename) - if skip_file(fullpath): - continue - warnings += checkPath(fullpath) - warnings += pep8.input_file(fullpath) - else: - if skip_file(arg): - continue - warnings += checkPath(arg) - warnings += pep8.input_file(arg) - + for path in _get_python_files(args): + warnings += check_file(path) else: stdin = sys.stdin.read() - warnings += check(stdin, '') + warnings += check_code(stdin) raise SystemExit(warnings > 0) -def hg_hook(ui, repo, **kwargs): - pep8.process_options() - warnings = 0 - files = [] +def _get_files(repo, **kwargs): for rev in xrange(repo[kwargs['node']], len(repo)): for file_ in repo[rev].files(): if not file_.endswith('.py'): continue if skip_file(file_): continue - if file_ not in files: - files.append(file_) + yield file_ - for file_ in files: - warnings += checkPath(file_) - warnings += pep8.input_file(file_) + +def hg_hook(ui, repo, **kwargs): + pep8.process_options() + warnings = 0 + for file_ in _get_files(repo, **kwargs): + warnings += check_file(file_) strict = ui.config('flake8', 'strict') if strict is None: @@ -157,3 +171,6 @@ def hg_hook(ui, repo, **kwargs): return warnings return 0 + +if __name__ == '__main__': + main() diff --git a/flake8/mccabe.py b/flake8/mccabe.py new file mode 100644 index 0000000..767326e --- /dev/null +++ b/flake8/mccabe.py @@ -0,0 +1,206 @@ +""" Meager code path measurement tool. + Ned Batchelder + http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html + MIT License. +""" + +import compiler, optparse, sys + + +class PathNode: + 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: + def __init__(self, name): + self.name = name + self.nodes = {} + + def add_node(self, n): + assert n + self.nodes.setdefault(n, []) + + def connect(self, n1, n2): + assert n1 + assert n2 + self.nodes.setdefault(n1, []).append(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(compiler.visitor.ASTVisitor): + """ A visitor for a parsed Abstract Syntax Tree which finds executable + statements. + """ + + def __init__(self): + compiler.visitor.ASTVisitor.__init__(self) + self.classname = "" + self.graphs = {} + self.reset() + + def reset(self): + self.graph = None + self.tail = None + + def visitFunction(self, node): + if self.classname: + entity = '%s%s' % (self.classname, node.name) + else: + entity = node.name + + name = '%d:1: %r' % (node.lineno, entity) + self.graph = PathGraph(name) + pathnode = PathNode(name) + self.tail = pathnode + self.default(node) + self.graphs["%s%s" % (self.classname, node.name)] = self.graph + self.reset() + + def visitClass(self, node): + old_classname = self.classname + self.classname += node.name + "." + self.default(node) + self.classname = old_classname + + def appendPathNode(self, name): + if not self.tail: + return + pathnode = PathNode(name) + self.graph.add_node(pathnode) + self.graph.connect(self.tail, pathnode) + self.tail = pathnode + return pathnode + + def visitSimpleStatement(self, node): + name = "Stmt %d" % node.lineno + self.appendPathNode(name) + + visitAssert = visitAssign = visitAssTuple = visitPrint = \ + visitPrintnl = visitRaise = visitSubscript = visitDecorators = \ + visitPass = visitDiscard = visitGlobal = visitReturn = \ + visitSimpleStatement + + def visitLoop(self, node): + name = "Loop %d" % node.lineno + pathnode = self.appendPathNode(name) + self.tail = pathnode + self.default(node.body) + bottom = PathNode("", look='point') + self.graph.connect(self.tail, bottom) + self.graph.connect(pathnode, bottom) + self.tail = bottom + # TODO: else clause in node.else_ + + visitFor = visitWhile = visitLoop + + def visitIf(self, node): + name = "If %d" % node.lineno + pathnode = self.appendPathNode(name) + if not pathnode: + return # TODO: figure out what to do with if's outside def's. + loose_ends = [] + for t, n in node.tests: + self.tail = pathnode + self.default(n) + loose_ends.append(self.tail) + if node.else_: + self.tail = pathnode + self.default(node.else_) + loose_ends.append(self.tail) + else: + loose_ends.append(pathnode) + bottom = PathNode("", look='point') + for le in loose_ends: + self.graph.connect(le, bottom) + self.tail = bottom + + # TODO: visitTryExcept + # TODO: visitTryFinally + # TODO: visitWith + + +def get_code_complexity(code, min=7, filename='stdin'): + complex = [] + ast = compiler.parse(code) + visitor = PathGraphingAstVisitor() + try: + visitor.preorder(ast, visitor) + except AttributeError: + print('McCabe: Could not parse code') + return -1 + + for graph in visitor.graphs.values(): + if graph is None: + # ? + continue + if graph.complexity() >= min: + msg = '%s:%s is too complex (%d)' % (filename, + graph.name, graph.complexity()) + complex.append(msg) + + if len(complex) == 0: + return 0 + + print('\n'.join(complex)) + return len(complex) + + +def get_module_complexity(module_path, min=7): + """Returns the complexity of a module""" + code = open(module_path, "rU").read() + '\n\n' + res = get_code_complexity(code, min, filename=module_path) + if res == -1: + print('McCabe: Problem with %s' % module_path) + return res + + +def main(argv): + opar = optparse.OptionParser() + opar.add_option("-d", "--dot", dest="dot", help="output a graphviz dot file", action="store_true") + opar.add_option("-m", "--min", dest="min", help="minimum complexity for output", type="int", default=2) + + options, args = opar.parse_args(argv) + + text = open(args[0], "rU").read()+'\n\n' + ast = compiler.parse(text) + visitor = PathGraphingAstVisitor() + visitor.preorder(ast, visitor) + + if options.dot: + print 'graph {' + for graph in visitor.graphs.values(): + if graph.complexity() >= options.min: + graph.to_dot() + print '}' + else: + for graph in visitor.graphs.values(): + if graph.complexity() >= options.min: + print graph.name, graph.complexity() + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/flake8/pep8.py b/flake8/pep8.py index ee661d8..fad2429 100644 --- a/flake8/pep8.py +++ b/flake8/pep8.py @@ -1,4 +1,5 @@ #!/usr/bin/python +# flake8: noqa # pep8.py - Check Python source code formatting, according to PEP 8 # Copyright (C) 2006 Johann C. Rocholl #