diff --git a/flake8/mccabe.py b/flake8/mccabe.py index e93c87f..b6a5857 100644 --- a/flake8/mccabe.py +++ b/flake8/mccabe.py @@ -3,35 +3,26 @@ http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html MIT License. """ -try: - from compiler import parse # NOQA - iter_child_nodes = None # NOQA -except ImportError: - from ast import parse, iter_child_nodes # NOQA +from __future__ import with_statement import optparse import sys -from flake8.util import skip_warning from collections import defaultdict -WARNING_CODE = "W901" +from flake8.util import ast, iter_child_nodes + +version = 0.1 -class ASTVisitor: - - VERBOSE = 0 +class ASTVisitor(object): + """Performs a depth-first walk of the AST.""" def __init__(self): self.node = None self._cache = {} def default(self, node, *args): - if hasattr(node, 'getChildNodes'): - children = node.getChildNodes() - else: - children = iter_child_nodes(node) - - for child in children: + for child in iter_child_nodes(node): self.dispatch(child, *args) def dispatch(self, node, *args): @@ -42,7 +33,6 @@ class ASTVisitor: 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): @@ -52,7 +42,7 @@ class ASTVisitor: self.dispatch(tree, *args) # XXX *args make sense? -class PathNode: +class PathNode(object): def __init__(self, name, look="circle"): self.name = name self.look = look @@ -65,7 +55,7 @@ class PathNode: return id(self) -class PathGraph: +class PathGraph(object): def __init__(self, name, entity, lineno): self.name = name self.entity = entity @@ -99,7 +89,7 @@ class PathGraphingAstVisitor(ASTVisitor): """ def __init__(self): - ASTVisitor.__init__(self) + super(PathGraphingAstVisitor, self).__init__() self.classname = "" self.graphs = {} self.reset() @@ -108,7 +98,11 @@ class PathGraphingAstVisitor(ASTVisitor): self.graph = None self.tail = None - def visitFunction(self, node): + 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) @@ -121,7 +115,7 @@ class PathGraphingAstVisitor(ASTVisitor): # closure pathnode = self.appendPathNode(name) self.tail = pathnode - self.default(node) + self.dispatch_list(node.body) bottom = PathNode("", look='point') self.graph.connect(self.tail, bottom) self.graph.connect(pathnode, bottom) @@ -130,16 +124,14 @@ class PathGraphingAstVisitor(ASTVisitor): self.graph = PathGraph(name, entity, node.lineno) pathnode = PathNode(name) self.tail = pathnode - self.default(node) + self.dispatch_list(node.body) self.graphs["%s%s" % (self.classname, node.name)] = self.graph self.reset() - visitFunctionDef = visitFunction - - def visitClass(self, node): + def visitClassDef(self, node): old_classname = self.classname self.classname += node.name + "." - self.default(node) + self.dispatch_list(node.body) self.classname = old_classname def appendPathNode(self, name): @@ -158,9 +150,9 @@ class PathGraphingAstVisitor(ASTVisitor): name = "Stmt %d" % lineno self.appendPathNode(name) - visitAssert = visitAssign = visitAssTuple = visitPrint = \ - visitPrintnl = visitRaise = visitSubscript = visitDecorators = \ - visitPass = visitDiscard = visitGlobal = visitReturn = \ + visitAssert = visitAssign = visitAugAssign = visitDelete = visitPrint = \ + visitRaise = visitYield = visitImport = visitCall = visitSubscript = \ + visitPass = visitContinue = visitBreak = visitGlobal = visitReturn = \ visitSimpleStatement def visitLoop(self, node): @@ -171,101 +163,119 @@ class PathGraphingAstVisitor(ASTVisitor): self.graph = PathGraph(name, name, node.lineno) pathnode = PathNode(name) self.tail = pathnode - self.default(node) + self.dispatch_list(node.body) self.graphs["%s%s" % (self.classname, name)] = self.graph self.reset() else: pathnode = self.appendPathNode(name) self.tail = pathnode - self.default(node.body) + self.dispatch_list(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_ + # TODO: else clause in node.orelse 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.dispatch_list(node.body) + loose_ends.append(self.tail) + if node.orelse: self.tail = pathnode - self.default(n) - loose_ends.append(self.tail) - if node.else_: - self.tail = pathnode - self.default(node.else_) + self.dispatch_list(node.orelse) 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 + if 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 visitTryExcept(self, node): + name = "TryExcept %d" % node.lineno + pathnode = self.appendPathNode(name) + loose_ends = [] + self.dispatch_list(node.body) + loose_ends.append(self.tail) + for handler in node.handlers: + self.tail = pathnode + self.dispatch_list(handler.body) + loose_ends.append(self.tail) + if pathnode: + bottom = PathNode("", look='point') + for le in loose_ends: + self.graph.connect(le, bottom) + self.tail = bottom - # XXX todo: determine which ones can add to the complexity - # py2 - # TODO: visitStmt - # TODO: visitAssName - # TODO: visitCallFunc - # TODO: visitConst - - # py3 - # TODO: visitStore - # TODO: visitCall - # TODO: visitLoad - # TODO: visitNum - # TODO: visitarguments - # TODO: visitExpr + def visitWith(self, node): + name = "With %d" % node.lineno + self.appendPathNode(name) + self.dispatch_list(node.body) -def get_code_complexity(code, min=7, filename='stdin'): - complex = [] +class McCabeChecker(object): + """McCabe cyclomatic complexity checker.""" + name = 'mccabe' + version = version + _code = 'C901' + _error_tmpl = "C901 %r is too complex (%d)" + max_complexity = 0 + + def __init__(self, tree, filename): + self.tree = tree + + @classmethod + def add_options(cls, parser): + parser.add_option('--max-complexity', default=-1, action='store', + type='int', help="McCabe complexity threshold") + parser.config_options.append('max-complexity') + + @classmethod + def parse_options(cls, options): + cls.max_complexity = options.max_complexity + + def run(self): + if self.max_complexity < 0: + return + visitor = PathGraphingAstVisitor() + visitor.preorder(self.tree, visitor) + for graph in visitor.graphs.values(): + graph_complexity = graph.complexity() + if graph_complexity >= self.max_complexity: + text = self._error_tmpl % (graph.entity, graph_complexity) + yield graph.lineno, 0, text, type(self) + + +def get_code_complexity(code, min_complexity=7, filename='stdin'): try: - ast = parse(code) - except (AttributeError, SyntaxError): + tree = compile(code, filename, "exec", ast.PyCF_ONLY_AST) + except SyntaxError: e = sys.exc_info()[1] sys.stderr.write("Unable to parse %s: %s\n" % (filename, e)) return 0 - visitor = PathGraphingAstVisitor() - visitor.preorder(ast, visitor) - for graph in visitor.graphs.values(): - if graph is None: - # ? - continue - if graph.complexity() >= min: - graph.filename = filename - if not skip_warning(graph): - msg = '%s:%d:1: %s %r is too complex (%d)' % ( - filename, - graph.lineno, - WARNING_CODE, - graph.entity, - graph.complexity(), - ) - complex.append(msg) + complx = [] + McCabeChecker.max_complexity = min_complexity + for lineno, offset, text, check in McCabeChecker(tree, filename): + complx.append('%s:%d:1: %s' % (filename, lineno, text)) - if len(complex) == 0: + if len(complx) == 0: return 0 - - print('\n'.join(complex)) - return len(complex) + print('\n'.join(complx)) + return len(complx) -def get_module_complexity(module_path, min=7): +def get_module_complexity(module_path, min_complexity=7): """Returns the complexity of a module""" - code = open(module_path, "rU").read() + '\n\n' - return get_code_complexity(code, min, filename=module_path) + with open(module_path, "rU") as mod: + code = mod.read() + return get_code_complexity(code, min_complexity, filename=module_path) def main(argv): @@ -278,10 +288,11 @@ def main(argv): options, args = opar.parse_args(argv) - text = open(args[0], "rU").read() + '\n\n' - ast = parse(text) + with open(args[0], "rU") as mod: + code = mod.read() + tree = compile(code, args[0], "exec", ast.PyCF_ONLY_AST) visitor = PathGraphingAstVisitor() - visitor.preorder(ast, visitor) + visitor.preorder(tree, visitor) if options.dot: print('graph {') diff --git a/setup.py b/setup.py index 8db2373..aab1ebf 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ setup( 'console_scripts': ['flake8 = flake8.main:main'], 'flake8.extension': [ 'F = flake8._pyflakes:FlakesChecker', + 'C90 = flake8.mccabe:McCabeChecker', ], }, tests_require=['nose'],