Last active
October 22, 2016 13:41
-
-
Save elazarg/bc9e5f326d718dcde827c13a3328a86b to your computer and use it in GitHub Desktop.
A small script that warns about functions that are not called from within the code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
from contextlib import contextmanager | |
from ast import NodeVisitor | |
import ast | |
import sys | |
import glob | |
class Flags: | |
include_strings = True | |
def is_external_or_in(s): | |
def is_external(name): | |
prefixes = ['.__', '.test_', '.visit_'] | |
if any(name.startswith(p) for p in prefixes): | |
return True | |
return False | |
return lambda x: x in s or is_external(x) | |
class Collector(NodeVisitor): | |
@classmethod | |
def collect(cls, modules_filenames, referenced=set()): | |
collector = cls() | |
collector.referenced = is_external_or_in(referenced) | |
for module, filename in modules_filenames: | |
collector.namespace = ('module ' + filename,) | |
collector.visit(module) | |
return collector.return_items() | |
@contextmanager | |
def enter_namespace(self, kind, name): | |
self.namespace += ('{} {}'.format(kind, name),) | |
yield | |
self.namespace = self.namespace[:-1] | |
def not_in_class(self): | |
return not self.namespace[-1].startswith('class ') | |
def visit_ClassDef(self, cd: ast.ClassDef): | |
with self.enter_namespace('class', cd.name): | |
self.generic_visit(cd) | |
def visit_FunctionDef(self, fd: ast.FunctionDef): | |
with self.enter_namespace('def', fd.name): | |
self.generic_visit(fd) | |
def visit_AsyncFunctionDef(self, fd: 'ast.AsyncFunctionDef'): | |
return self.visit_FunctionDef(fd) | |
def is_referenced(self, name): | |
return self.referenced('.' + name) \ | |
or self.not_in_class() and self.referenced(name) | |
class Defs(Collector): | |
def __init__(self): | |
self.defs = {} # name -> namespace | |
def visit_FunctionDef(self, fd: ast.FunctionDef): | |
if not self.is_referenced(fd.name): | |
self.defs[fd.name] = self.namespace + (str(fd.lineno),) | |
super().visit_FunctionDef(fd) | |
def return_items(self): | |
return self.defs | |
class Refs(Collector): | |
def __init__(self): | |
self.refs = set() | |
def visit_FunctionDef(self, fd: ast.FunctionDef): | |
if self.is_referenced(fd.name) or fd.name in self.refs: | |
super().visit_FunctionDef(fd) | |
def visit_Attribute(self, attr: ast.Attribute): | |
self.refs.add('.' + attr.attr) | |
def visit_Name(self, name: ast.Name): | |
if isinstance(name.ctx, ast.Load): | |
self.refs.add(name.id) | |
def visit_Str(self, st: ast.Str): | |
if Flags.include_strings: | |
self.refs.add(st.s) | |
self.refs.add('.' + st.s) | |
def return_items(self): | |
return self.refs | |
def parse_modules(filenames): | |
for filename in filenames: | |
with open(filename) as f: | |
source = f.read() | |
try: | |
module = ast.parse(source, filename=filename) | |
except SyntaxError: | |
from sys import stderr | |
print('Could not parse ' + filename, file=stderr) | |
else: | |
yield module, filename | |
def find_unused(files): | |
modules_filenames = tuple(parse_modules(files)) | |
referenced = set() | |
while True: | |
now_referenced = Refs.collect(modules_filenames, | |
referenced=referenced) | |
if now_referenced <= referenced: | |
break | |
referenced.update(now_referenced) | |
return Defs.collect(modules_filenames, referenced) | |
def print_unused(defs): | |
for item, (filename, *namespace, line) in sorted(defs.items(), key=lambda x:x[1]): | |
path = '.'.join(namespace) | |
print('{0}:{1}\t{2}\tat {3}'.format(filename[7:], line, item, path)) | |
def main(): | |
argv = sys.argv[1:] or glob.glob('*.py') | |
print_unused(find_unused(argv)) | |
if __name__ == '__main__': | |
main() | |
def unused(): pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from re import findall, IGNORECASE | |
import sys | |
def find_defs(txt): | |
return set([x[5:] for x in findall(' def [a-z][0-9a-z_]+', txt, IGNORECASE)]) | |
def find_calls(txt): | |
return set(findall('(?<!def )[a-z][0-9a-z_]+', txt, IGNORECASE)) | |
def main(files): | |
file_defs = {} | |
calls = set() | |
for file in files: | |
file_defs[file] = set() | |
with open(file) as f: | |
text = f.read() | |
file_defs[file].update(find_defs(text)) | |
calls.update(find_calls(text)) | |
for file in file_defs: | |
file_defs[file] = {x for x in file_defs[file] - calls | |
if 'visit' not in x and not x.startswith('test_')} | |
for file, defs in file_defs.items(): | |
if not defs: | |
continue | |
print(file, ':') | |
for d in defs: | |
print('\t{}'.format(d)) | |
if __name__ == '__main__': | |
main(sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment