mirror of
https://github.com/PyCQA/flake8.git
synced 2026-04-16 17:19:52 +00:00
Upgrade McCabe checker as an extension; now it uses the AST
This commit is contained in:
parent
740e8b9ad6
commit
1c6ed0526a
2 changed files with 105 additions and 93 deletions
187
flake8/mccabe.py
187
flake8/mccabe.py
|
|
@ -3,35 +3,26 @@
|
||||||
http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
|
http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
|
||||||
MIT License.
|
MIT License.
|
||||||
"""
|
"""
|
||||||
try:
|
from __future__ import with_statement
|
||||||
from compiler import parse # NOQA
|
|
||||||
iter_child_nodes = None # NOQA
|
|
||||||
except ImportError:
|
|
||||||
from ast import parse, iter_child_nodes # NOQA
|
|
||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
import sys
|
import sys
|
||||||
from flake8.util import skip_warning
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
WARNING_CODE = "W901"
|
from flake8.util import ast, iter_child_nodes
|
||||||
|
|
||||||
|
version = 0.1
|
||||||
|
|
||||||
|
|
||||||
class ASTVisitor:
|
class ASTVisitor(object):
|
||||||
|
"""Performs a depth-first walk of the AST."""
|
||||||
VERBOSE = 0
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.node = None
|
self.node = None
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
|
|
||||||
def default(self, node, *args):
|
def default(self, node, *args):
|
||||||
if hasattr(node, 'getChildNodes'):
|
for child in iter_child_nodes(node):
|
||||||
children = node.getChildNodes()
|
|
||||||
else:
|
|
||||||
children = iter_child_nodes(node)
|
|
||||||
|
|
||||||
for child in children:
|
|
||||||
self.dispatch(child, *args)
|
self.dispatch(child, *args)
|
||||||
|
|
||||||
def dispatch(self, node, *args):
|
def dispatch(self, node, *args):
|
||||||
|
|
@ -42,7 +33,6 @@ class ASTVisitor:
|
||||||
className = klass.__name__
|
className = klass.__name__
|
||||||
meth = getattr(self.visitor, 'visit' + className, self.default)
|
meth = getattr(self.visitor, 'visit' + className, self.default)
|
||||||
self._cache[klass] = meth
|
self._cache[klass] = meth
|
||||||
|
|
||||||
return meth(node, *args)
|
return meth(node, *args)
|
||||||
|
|
||||||
def preorder(self, tree, visitor, *args):
|
def preorder(self, tree, visitor, *args):
|
||||||
|
|
@ -52,7 +42,7 @@ class ASTVisitor:
|
||||||
self.dispatch(tree, *args) # XXX *args make sense?
|
self.dispatch(tree, *args) # XXX *args make sense?
|
||||||
|
|
||||||
|
|
||||||
class PathNode:
|
class PathNode(object):
|
||||||
def __init__(self, name, look="circle"):
|
def __init__(self, name, look="circle"):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.look = look
|
self.look = look
|
||||||
|
|
@ -65,7 +55,7 @@ class PathNode:
|
||||||
return id(self)
|
return id(self)
|
||||||
|
|
||||||
|
|
||||||
class PathGraph:
|
class PathGraph(object):
|
||||||
def __init__(self, name, entity, lineno):
|
def __init__(self, name, entity, lineno):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
|
|
@ -99,7 +89,7 @@ class PathGraphingAstVisitor(ASTVisitor):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
ASTVisitor.__init__(self)
|
super(PathGraphingAstVisitor, self).__init__()
|
||||||
self.classname = ""
|
self.classname = ""
|
||||||
self.graphs = {}
|
self.graphs = {}
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
@ -108,7 +98,11 @@ class PathGraphingAstVisitor(ASTVisitor):
|
||||||
self.graph = None
|
self.graph = None
|
||||||
self.tail = 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:
|
if self.classname:
|
||||||
entity = '%s%s' % (self.classname, node.name)
|
entity = '%s%s' % (self.classname, node.name)
|
||||||
|
|
@ -121,7 +115,7 @@ class PathGraphingAstVisitor(ASTVisitor):
|
||||||
# closure
|
# closure
|
||||||
pathnode = self.appendPathNode(name)
|
pathnode = self.appendPathNode(name)
|
||||||
self.tail = pathnode
|
self.tail = pathnode
|
||||||
self.default(node)
|
self.dispatch_list(node.body)
|
||||||
bottom = PathNode("", look='point')
|
bottom = PathNode("", look='point')
|
||||||
self.graph.connect(self.tail, bottom)
|
self.graph.connect(self.tail, bottom)
|
||||||
self.graph.connect(pathnode, bottom)
|
self.graph.connect(pathnode, bottom)
|
||||||
|
|
@ -130,16 +124,14 @@ class PathGraphingAstVisitor(ASTVisitor):
|
||||||
self.graph = PathGraph(name, entity, node.lineno)
|
self.graph = PathGraph(name, entity, node.lineno)
|
||||||
pathnode = PathNode(name)
|
pathnode = PathNode(name)
|
||||||
self.tail = pathnode
|
self.tail = pathnode
|
||||||
self.default(node)
|
self.dispatch_list(node.body)
|
||||||
self.graphs["%s%s" % (self.classname, node.name)] = self.graph
|
self.graphs["%s%s" % (self.classname, node.name)] = self.graph
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
visitFunctionDef = visitFunction
|
def visitClassDef(self, node):
|
||||||
|
|
||||||
def visitClass(self, node):
|
|
||||||
old_classname = self.classname
|
old_classname = self.classname
|
||||||
self.classname += node.name + "."
|
self.classname += node.name + "."
|
||||||
self.default(node)
|
self.dispatch_list(node.body)
|
||||||
self.classname = old_classname
|
self.classname = old_classname
|
||||||
|
|
||||||
def appendPathNode(self, name):
|
def appendPathNode(self, name):
|
||||||
|
|
@ -158,9 +150,9 @@ class PathGraphingAstVisitor(ASTVisitor):
|
||||||
name = "Stmt %d" % lineno
|
name = "Stmt %d" % lineno
|
||||||
self.appendPathNode(name)
|
self.appendPathNode(name)
|
||||||
|
|
||||||
visitAssert = visitAssign = visitAssTuple = visitPrint = \
|
visitAssert = visitAssign = visitAugAssign = visitDelete = visitPrint = \
|
||||||
visitPrintnl = visitRaise = visitSubscript = visitDecorators = \
|
visitRaise = visitYield = visitImport = visitCall = visitSubscript = \
|
||||||
visitPass = visitDiscard = visitGlobal = visitReturn = \
|
visitPass = visitContinue = visitBreak = visitGlobal = visitReturn = \
|
||||||
visitSimpleStatement
|
visitSimpleStatement
|
||||||
|
|
||||||
def visitLoop(self, node):
|
def visitLoop(self, node):
|
||||||
|
|
@ -171,101 +163,119 @@ class PathGraphingAstVisitor(ASTVisitor):
|
||||||
self.graph = PathGraph(name, name, node.lineno)
|
self.graph = PathGraph(name, name, node.lineno)
|
||||||
pathnode = PathNode(name)
|
pathnode = PathNode(name)
|
||||||
self.tail = pathnode
|
self.tail = pathnode
|
||||||
self.default(node)
|
self.dispatch_list(node.body)
|
||||||
self.graphs["%s%s" % (self.classname, name)] = self.graph
|
self.graphs["%s%s" % (self.classname, name)] = self.graph
|
||||||
self.reset()
|
self.reset()
|
||||||
else:
|
else:
|
||||||
pathnode = self.appendPathNode(name)
|
pathnode = self.appendPathNode(name)
|
||||||
self.tail = pathnode
|
self.tail = pathnode
|
||||||
self.default(node.body)
|
self.dispatch_list(node.body)
|
||||||
bottom = PathNode("", look='point')
|
bottom = PathNode("", look='point')
|
||||||
self.graph.connect(self.tail, bottom)
|
self.graph.connect(self.tail, bottom)
|
||||||
self.graph.connect(pathnode, bottom)
|
self.graph.connect(pathnode, bottom)
|
||||||
self.tail = bottom
|
self.tail = bottom
|
||||||
|
|
||||||
# TODO: else clause in node.else_
|
# TODO: else clause in node.orelse
|
||||||
|
|
||||||
visitFor = visitWhile = visitLoop
|
visitFor = visitWhile = visitLoop
|
||||||
|
|
||||||
def visitIf(self, node):
|
def visitIf(self, node):
|
||||||
name = "If %d" % node.lineno
|
name = "If %d" % node.lineno
|
||||||
pathnode = self.appendPathNode(name)
|
pathnode = self.appendPathNode(name)
|
||||||
if not pathnode:
|
|
||||||
return # TODO: figure out what to do with if's outside def's.
|
|
||||||
loose_ends = []
|
loose_ends = []
|
||||||
for t, n in node.tests:
|
self.dispatch_list(node.body)
|
||||||
self.tail = pathnode
|
|
||||||
self.default(n)
|
|
||||||
loose_ends.append(self.tail)
|
loose_ends.append(self.tail)
|
||||||
if node.else_:
|
if node.orelse:
|
||||||
self.tail = pathnode
|
self.tail = pathnode
|
||||||
self.default(node.else_)
|
self.dispatch_list(node.orelse)
|
||||||
loose_ends.append(self.tail)
|
loose_ends.append(self.tail)
|
||||||
else:
|
else:
|
||||||
loose_ends.append(pathnode)
|
loose_ends.append(pathnode)
|
||||||
|
if pathnode:
|
||||||
bottom = PathNode("", look='point')
|
bottom = PathNode("", look='point')
|
||||||
for le in loose_ends:
|
for le in loose_ends:
|
||||||
self.graph.connect(le, bottom)
|
self.graph.connect(le, bottom)
|
||||||
self.tail = bottom
|
self.tail = bottom
|
||||||
|
|
||||||
# TODO: visitTryExcept
|
def visitTryExcept(self, node):
|
||||||
# TODO: visitTryFinally
|
name = "TryExcept %d" % node.lineno
|
||||||
# TODO: visitWith
|
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
|
def visitWith(self, node):
|
||||||
# py2
|
name = "With %d" % node.lineno
|
||||||
# TODO: visitStmt
|
self.appendPathNode(name)
|
||||||
# TODO: visitAssName
|
self.dispatch_list(node.body)
|
||||||
# TODO: visitCallFunc
|
|
||||||
# TODO: visitConst
|
|
||||||
|
|
||||||
# py3
|
|
||||||
# TODO: visitStore
|
|
||||||
# TODO: visitCall
|
|
||||||
# TODO: visitLoad
|
|
||||||
# TODO: visitNum
|
|
||||||
# TODO: visitarguments
|
|
||||||
# TODO: visitExpr
|
|
||||||
|
|
||||||
|
|
||||||
def get_code_complexity(code, min=7, filename='stdin'):
|
class McCabeChecker(object):
|
||||||
complex = []
|
"""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:
|
try:
|
||||||
ast = parse(code)
|
tree = compile(code, filename, "exec", ast.PyCF_ONLY_AST)
|
||||||
except (AttributeError, SyntaxError):
|
except SyntaxError:
|
||||||
e = sys.exc_info()[1]
|
e = sys.exc_info()[1]
|
||||||
sys.stderr.write("Unable to parse %s: %s\n" % (filename, e))
|
sys.stderr.write("Unable to parse %s: %s\n" % (filename, e))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
visitor = PathGraphingAstVisitor()
|
complx = []
|
||||||
visitor.preorder(ast, visitor)
|
McCabeChecker.max_complexity = min_complexity
|
||||||
for graph in visitor.graphs.values():
|
for lineno, offset, text, check in McCabeChecker(tree, filename):
|
||||||
if graph is None:
|
complx.append('%s:%d:1: %s' % (filename, lineno, text))
|
||||||
# ?
|
|
||||||
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)
|
|
||||||
|
|
||||||
if len(complex) == 0:
|
if len(complx) == 0:
|
||||||
return 0
|
return 0
|
||||||
|
print('\n'.join(complx))
|
||||||
print('\n'.join(complex))
|
return len(complx)
|
||||||
return len(complex)
|
|
||||||
|
|
||||||
|
|
||||||
def get_module_complexity(module_path, min=7):
|
def get_module_complexity(module_path, min_complexity=7):
|
||||||
"""Returns the complexity of a module"""
|
"""Returns the complexity of a module"""
|
||||||
code = open(module_path, "rU").read() + '\n\n'
|
with open(module_path, "rU") as mod:
|
||||||
return get_code_complexity(code, min, filename=module_path)
|
code = mod.read()
|
||||||
|
return get_code_complexity(code, min_complexity, filename=module_path)
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
|
|
@ -278,10 +288,11 @@ def main(argv):
|
||||||
|
|
||||||
options, args = opar.parse_args(argv)
|
options, args = opar.parse_args(argv)
|
||||||
|
|
||||||
text = open(args[0], "rU").read() + '\n\n'
|
with open(args[0], "rU") as mod:
|
||||||
ast = parse(text)
|
code = mod.read()
|
||||||
|
tree = compile(code, args[0], "exec", ast.PyCF_ONLY_AST)
|
||||||
visitor = PathGraphingAstVisitor()
|
visitor = PathGraphingAstVisitor()
|
||||||
visitor.preorder(ast, visitor)
|
visitor.preorder(tree, visitor)
|
||||||
|
|
||||||
if options.dot:
|
if options.dot:
|
||||||
print('graph {')
|
print('graph {')
|
||||||
|
|
|
||||||
1
setup.py
1
setup.py
|
|
@ -36,6 +36,7 @@ setup(
|
||||||
'console_scripts': ['flake8 = flake8.main:main'],
|
'console_scripts': ['flake8 = flake8.main:main'],
|
||||||
'flake8.extension': [
|
'flake8.extension': [
|
||||||
'F = flake8._pyflakes:FlakesChecker',
|
'F = flake8._pyflakes:FlakesChecker',
|
||||||
|
'C90 = flake8.mccabe:McCabeChecker',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
tests_require=['nose'],
|
tests_require=['nose'],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue