mirror of
https://github.com/PyCQA/flake8.git
synced 2026-04-04 20:26:53 +00:00
added the mccabe metric
This commit is contained in:
parent
e720de1b33
commit
263533c9fd
3 changed files with 270 additions and 46 deletions
|
|
@ -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, '<stdin>')
|
||||
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, '<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()
|
||||
|
|
|
|||
206
flake8/mccabe.py
Normal file
206
flake8/mccabe.py
Normal file
|
|
@ -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:])
|
||||
|
|
@ -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 <johann@rocholl.net>
|
||||
#
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue